- Python CLI tool for rolling updates of CL-AppPipe-* and CL-SvcPipe-* stacks - Async update engine with configurable concurrency (asyncio.Semaphore) - Exponential backoff retry for API throttling - Dry-run mode for safe preview - IAM permission pre-validation - Comprehensive test suite (80 tests: 11 property-based + 69 unit) - Full spec documentation (requirements, design, tasks)
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
"""Property and unit tests for dry-run mode.
|
|
|
|
Requirements: 6.1, 6.2, 6.3
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import string
|
|
|
|
from hypothesis import given, settings, strategies as st
|
|
|
|
from cfn_updater.models import DiscoveredStack
|
|
from cfn_updater.updater import format_dry_run_output
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Strategies
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_stack_name = st.text(
|
|
alphabet=string.ascii_letters + string.digits + "-",
|
|
min_size=1,
|
|
max_size=40,
|
|
).map(lambda s: f"CL-AppPipe-{s}")
|
|
|
|
_stack_status = st.sampled_from([
|
|
"CREATE_COMPLETE",
|
|
"UPDATE_COMPLETE",
|
|
"ROLLBACK_COMPLETE",
|
|
"DELETE_IN_PROGRESS",
|
|
"UPDATE_ROLLBACK_COMPLETE",
|
|
"CREATE_IN_PROGRESS",
|
|
])
|
|
|
|
_discovered_stack = st.builds(
|
|
DiscoveredStack,
|
|
name=_stack_name,
|
|
status=_stack_status,
|
|
updatable=st.booleans(),
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Property-Based Test — Property 10
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Feature: one-click-cfn-stack-updater, Property 10: Dry-run performs no updates and lists all discovered stacks
|
|
class TestPropertyDryRunNoUpdates:
|
|
"""
|
|
**Validates: Requirements 6.2, 6.3**
|
|
|
|
For any set of discovered stacks with dry-run enabled, zero UpdateStack
|
|
calls are made and the output contains the name and status of every
|
|
discovered stack.
|
|
"""
|
|
|
|
@settings(max_examples=100)
|
|
@given(stacks=st.lists(_discovered_stack, min_size=0, max_size=30))
|
|
def test_dry_run_lists_all_stacks_without_updates(self, stacks: list[DiscoveredStack]) -> None:
|
|
output = format_dry_run_output(stacks)
|
|
|
|
# The function is pure formatting — it never receives a CFN client,
|
|
# so zero UpdateStack calls are made by construction.
|
|
|
|
# Every stack's name and status must appear in the output.
|
|
for stack in stacks:
|
|
assert stack.name in output, f"Missing stack name: {stack.name}"
|
|
assert stack.status in output, f"Missing stack status: {stack.status}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unit Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDryRunOutputFormat:
|
|
"""Req 6.2, 6.3: Dry-run outputs all stack names and statuses."""
|
|
|
|
def test_single_stack(self) -> None:
|
|
stacks = [
|
|
DiscoveredStack(name="CL-AppPipe-abc123", status="CREATE_COMPLETE", updatable=True),
|
|
]
|
|
output = format_dry_run_output(stacks)
|
|
|
|
assert "CL-AppPipe-abc123" in output
|
|
assert "CREATE_COMPLETE" in output
|
|
|
|
def test_multiple_stacks(self) -> None:
|
|
stacks = [
|
|
DiscoveredStack(name="CL-AppPipe-aaa", status="CREATE_COMPLETE", updatable=True),
|
|
DiscoveredStack(name="CL-AppPipe-bbb", status="UPDATE_COMPLETE", updatable=True),
|
|
DiscoveredStack(name="CL-AppPipe-ccc", status="ROLLBACK_COMPLETE", updatable=False),
|
|
]
|
|
output = format_dry_run_output(stacks)
|
|
|
|
for stack in stacks:
|
|
assert stack.name in output
|
|
assert stack.status in output
|
|
|
|
def test_empty_stack_list(self) -> None:
|
|
output = format_dry_run_output([])
|
|
|
|
assert "No stacks found" in output
|
|
|
|
def test_output_contains_count(self) -> None:
|
|
stacks = [
|
|
DiscoveredStack(name=f"CL-AppPipe-s{i}", status="CREATE_COMPLETE", updatable=True)
|
|
for i in range(5)
|
|
]
|
|
output = format_dry_run_output(stacks)
|
|
|
|
assert "5" in output
|
|
|
|
|
|
class TestDryRunNoApiCalls:
|
|
"""Req 6.1, 6.2: Dry-run makes no update API calls.
|
|
|
|
format_dry_run_output is a pure function that takes a list of
|
|
DiscoveredStack and returns a string. It has no CFN client parameter,
|
|
so it is impossible for it to make any API calls by design.
|
|
"""
|
|
|
|
def test_function_signature_has_no_client_parameter(self) -> None:
|
|
import inspect
|
|
|
|
sig = inspect.signature(format_dry_run_output)
|
|
param_names = list(sig.parameters.keys())
|
|
# Only parameter is 'stacks'
|
|
assert param_names == ["stacks"]
|