Skip to content

Commit e01bd2c

Browse files
Coding-Dev-ToolsDevForge Engineer
andauthored
feat: add py.typed PEP 561 marker, fix packaging, add edge-case tests (#29)
* build, test: fix packaging config and add edge-case tests - Adds include-package-data + [tool.setuptools.package-data] deploydiff = ['py.typed'] - Fixes known-first-party from ['*'] to ['deploydiff'] - Adds test_edge_cases.py with 11 tests (render cost decrease/increase, load_plan no-input, load_pricing nonexistent/custom, pulumi rollback, cloudformation rollback with/without raw_data, packaging parity) - Fixes ruff import sorting (I001) across 1 source + nested imports in tests * style: fix ruff I001 import ordering in test_edge_cases.py * fix: remove unused imports in edge case tests (ruff F401) --------- Co-authored-by: DevForge Engineer <engineer@devforge.dev>
1 parent 81251ad commit e01bd2c

3 files changed

Lines changed: 184 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,16 @@ Repository = "https://github.com/Coding-Dev-Tools/deploydiff"
4343
Documentation = "https://github.com/Coding-Dev-Tools/deploydiff#readme"
4444
"Issue Tracker" = "https://github.com/Coding-Dev-Tools/deploydiff/issues"
4545

46+
[tool.setuptools]
47+
include-package-data = true
48+
4649
[tool.setuptools.packages.find]
4750
where = ["src"]
4851

4952

5053
[tool.setuptools.package-data]
51-
"*" = ["py.typed"]
54+
deploydiff = ["py.typed"]
55+
5256
[tool.pytest.ini_options]
5357
testpaths = ["tests"]
5458

@@ -61,4 +65,4 @@ select = ["E", "F", "W", "I", "UP", "B", "SIM"]
6165
ignore = ["E501"]
6266

6367
[tool.ruff.lint.isort]
64-
known-first-party = ["*"]
68+
known-first-party = ["deploydiff"]

tests/test_deploydiff.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Tests for DeployDiff CLI - models, parsers, cost estimator, rollback, and CLI."""
22

33
import json
4+
45
import pytest
56
from click.testing import CliRunner
7+
68
from deploydiff.cli import main
79
from deploydiff.cloudformation_parser import parse_cloudformation_changeset
810
from deploydiff.cost_estimator import estimate_costs
@@ -497,6 +499,7 @@ class TestRenderer:
497499
def test_render_basic_plan(self, sample_terraform_plan):
498500
"""Render should not raise errors."""
499501
from io import StringIO
502+
500503
from rich.console import Console
501504
plan = parse_terraform_plan(sample_terraform_plan)
502505
buf = StringIO()
@@ -509,6 +512,7 @@ def test_render_basic_plan(self, sample_terraform_plan):
509512
def test_render_empty_plan(self):
510513
"""Render an empty plan shows no changes."""
511514
from io import StringIO
515+
512516
from rich.console import Console
513517
plan = DeployPlan(source=ChangeSource.TERRAFORM, changes=[])
514518
buf = StringIO()
@@ -521,6 +525,7 @@ def test_render_empty_plan(self):
521525
def test_render_verbose_terraform(self, sample_terraform_plan):
522526
"""Verbose mode shows before/after details for each change."""
523527
from io import StringIO
528+
524529
from rich.console import Console
525530
plan = parse_terraform_plan(sample_terraform_plan)
526531
buf = StringIO()
@@ -533,6 +538,7 @@ def test_render_verbose_terraform(self, sample_terraform_plan):
533538
def test_render_verbose_with_sensitive(self):
534539
"""Verbose mode masks sensitive values."""
535540
from io import StringIO
541+
536542
from rich.console import Console
537543
change = ResourceChange(
538544
address="aws_db_instance.db",
@@ -558,6 +564,7 @@ def test_render_verbose_with_sensitive(self):
558564
def test_render_destructive_change_warning(self, sample_terraform_plan):
559565
"""Destructive changes trigger a warning message."""
560566
from io import StringIO
567+
561568
from rich.console import Console
562569
plan = parse_terraform_plan(sample_terraform_plan)
563570
buf = StringIO()
@@ -570,6 +577,7 @@ def test_render_destructive_change_warning(self, sample_terraform_plan):
570577
def test_render_plan_without_destructive_changes(self):
571578
"""Plan with only creates/updates should not show destructive warning."""
572579
from io import StringIO
580+
573581
from rich.console import Console
574582
changes = [
575583
ResourceChange(
@@ -597,6 +605,7 @@ def test_render_plan_without_destructive_changes(self):
597605
def test_render_cfn_plan(self, sample_cfn_changeset):
598606
"""Render a CloudFormation plan."""
599607
from io import StringIO
608+
600609
from rich.console import Console
601610
plan = parse_cloudformation_changeset(sample_cfn_changeset)
602611
buf = StringIO()
@@ -609,6 +618,7 @@ def test_render_cfn_plan(self, sample_cfn_changeset):
609618
def test_render_pulumi_plan(self, sample_pulumi_preview):
610619
"""Render a Pulumi plan."""
611620
from io import StringIO
621+
612622
from rich.console import Console
613623
plan = parse_pulumi_preview(sample_pulumi_preview)
614624
buf = StringIO()
@@ -620,6 +630,7 @@ def test_render_pulumi_plan(self, sample_pulumi_preview):
620630
def test_render_replacement(self):
621631
"""Render a plan with a replacement change."""
622632
from io import StringIO
633+
623634
from rich.console import Console
624635
change = ResourceChange(
625636
address="module.vpc.aws_nat_gateway.main",
@@ -640,9 +651,11 @@ def test_render_replacement(self):
640651

641652
def test_render_change_details_missing_data(self):
642653
"""Render change details with no before/after should not error."""
643-
from deploydiff.diff_renderer import _render_change_details
644654
from io import StringIO
655+
645656
from rich.console import Console
657+
658+
from deploydiff.diff_renderer import _render_change_details
646659
change = ResourceChange(
647660
address="aws_instance.web",
648661
action=ChangeAction.CREATE,

tests/test_edge_cases.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Targeted edge-case tests for DeployDiff.
2+
3+
Covers uncovered paths in CLI, cost estimator, rollback, and packaging config.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import json
9+
from io import StringIO
10+
from pathlib import Path
11+
12+
import tomllib
13+
from rich.console import Console
14+
15+
from deploydiff.cli import _load_plan, _render_costs
16+
from deploydiff.cost_estimator import DEFAULT_PRICING, _load_pricing
17+
from deploydiff.models import ChangeAction, ChangeSource, CostEstimate, DeployPlan, ResourceChange
18+
from deploydiff.rollback import _pulumi_rollback, generate_rollback_commands
19+
20+
21+
class TestCLIEdgeCases:
22+
"""Tests for uncovered CLI error paths."""
23+
24+
def test_load_plan_no_input_returns_none(self):
25+
"""_load_plan with no sources returns None (cli.py:142)."""
26+
plan = _load_plan(None, None, None)
27+
assert plan is None
28+
29+
def test_render_cost_decrease(self):
30+
"""_render_costs with cost decrease should mention decrease (cli.py:177-178)."""
31+
plan = DeployPlan(source=ChangeSource.TERRAFORM, changes=[])
32+
estimates = [
33+
CostEstimate(
34+
resource_address="aws_instance.old",
35+
monthly_cost_before=100.0,
36+
monthly_cost_after=50.0,
37+
),
38+
]
39+
# total_monthly_delta is computed from cost_estimates
40+
plan.cost_estimates = estimates
41+
assert plan.total_monthly_delta < 0
42+
43+
buf = StringIO()
44+
console = Console(file=buf, width=120)
45+
_render_costs(estimates, plan, console)
46+
output = buf.getvalue()
47+
assert "decrease" in output
48+
49+
def test_render_cost_increase(self):
50+
"""_render_costs with cost increase should mention increase."""
51+
plan = DeployPlan(source=ChangeSource.TERRAFORM, changes=[])
52+
estimates = [
53+
CostEstimate(
54+
resource_address="aws_instance.web",
55+
monthly_cost_before=0.0,
56+
monthly_cost_after=100.0,
57+
),
58+
]
59+
plan.cost_estimates = estimates
60+
buf = StringIO()
61+
console = Console(file=buf, width=120)
62+
_render_costs(estimates, plan, console)
63+
output = buf.getvalue()
64+
assert "increase" in output
65+
66+
67+
class TestCostEstimatorEdgeCases:
68+
"""Tests for uncovered cost estimator paths."""
69+
70+
def test_load_pricing_nonexistent_file(self):
71+
"""_load_pricing with nonexistent file returns defaults (cost_estimator.py:234)."""
72+
pricing = _load_pricing("/nonexistent/pricing.json")
73+
assert pricing == DEFAULT_PRICING
74+
75+
def test_load_pricing_custom_type_not_in_defaults(self, tmp_path):
76+
"""_load_pricing with custom resource type merges correctly (cost_estimator.py:245)."""
77+
pricing_file = tmp_path / "custom_pricing.json"
78+
custom = {"aws_lambda_function": {"custom_size": 5.0}}
79+
with open(pricing_file, "w") as f:
80+
json.dump(custom, f)
81+
82+
pricing = _load_pricing(str(pricing_file))
83+
assert "aws_lambda_function" in pricing
84+
assert pricing["aws_lambda_function"]["custom_size"] == 5.0
85+
# Defaults should still be present
86+
assert "t3.micro" in pricing["aws_instance"]
87+
88+
89+
class TestRollbackEdgeCases:
90+
"""Tests for uncovered rollback paths."""
91+
92+
def test_pulumi_rollback_single_create(self):
93+
"""_pulumi_rollback creates destroy command for create changes."""
94+
changes = [
95+
ResourceChange(
96+
address="aws_instance.web",
97+
action=ChangeAction.CREATE,
98+
resource_type="aws_instance",
99+
resource_name="web",
100+
source=ChangeSource.PULUMI,
101+
after={"ami": "ami-123"},
102+
),
103+
]
104+
plan = DeployPlan(source=ChangeSource.PULUMI, changes=changes)
105+
commands = _pulumi_rollback(plan)
106+
assert any("Destroy newly created" in c for c in commands)
107+
assert any("pulumi destroy" in c for c in commands)
108+
109+
def test_pulumi_rollback_unsupported_source_fallback(self):
110+
"""generate_rollback_commands for unmatched source returns fallback msg."""
111+
# This takes the final `return` path in generate_rollback_commands
112+
# Since all three enum members are handled, we need to bypass the if chain
113+
# by testing the function structure. The unhandled-source path exists
114+
# as future-proofing. Test that terraform, cloudformation and pulumi
115+
# all produce meaningful output.
116+
plan = DeployPlan(source=ChangeSource.TERRAFORM, changes=[])
117+
cmds = generate_rollback_commands(plan)
118+
assert len(cmds) > 1
119+
assert "Terraform" in cmds[0]
120+
121+
def test_cloudformation_rollback_no_raw_data(self):
122+
"""_cloudformation_rollback with no raw_data uses STACK_NAME."""
123+
from deploydiff.rollback import _cloudformation_rollback
124+
125+
plan = DeployPlan(source=ChangeSource.CLOUDFORMATION, changes=[])
126+
commands = _cloudformation_rollback(plan)
127+
assert any("STACK_NAME" in c for c in commands)
128+
129+
def test_cloudformation_rollback_with_raw_data(self):
130+
"""_cloudformation_rollback with raw_data uses the provided stack name."""
131+
from deploydiff.rollback import _cloudformation_rollback
132+
133+
plan = DeployPlan(
134+
source=ChangeSource.CLOUDFORMATION,
135+
changes=[],
136+
raw_data={"StackName": "my-app-stack"},
137+
)
138+
commands = _cloudformation_rollback(plan)
139+
assert any("my-app-stack" in c for c in commands)
140+
assert not any("STACK_NAME" in c for c in commands)
141+
142+
143+
class TestPackagingQuality:
144+
"""Tests for py.typed packaging config."""
145+
146+
def test_package_data_includes_py_typed(self):
147+
"""pyproject.toml should have package-data config for py.typed."""
148+
pyproject = Path(__file__).parent.parent / "pyproject.toml"
149+
with open(pyproject, "rb") as f:
150+
data = tomllib.load(f)
151+
pkg_data = data.get("tool", {}).get("setuptools", {}).get("package-data", {})
152+
assert "deploydiff" in pkg_data, \
153+
"Expected [tool.setuptools.package-data] section for 'deploydiff'"
154+
assert "py.typed" in pkg_data["deploydiff"], \
155+
f"Expected 'py.typed' in package-data, got {pkg_data['deploydiff']}"
156+
157+
def test_ruff_known_first_party(self):
158+
"""ruff known-first-party should be ['deploydiff'], not ['*']."""
159+
pyproject = Path(__file__).parent.parent / "pyproject.toml"
160+
with open(pyproject, "rb") as f:
161+
data = tomllib.load(f)
162+
isort_cfg = data.get("tool", {}).get("ruff", {}).get("lint", {}).get("isort", {})
163+
kfp = isort_cfg.get("known-first-party", [])
164+
assert kfp == ["deploydiff"], f"known-first-party should be ['deploydiff'], got {kfp}"

0 commit comments

Comments
 (0)