test: lock release snippet guard negative fixtures

This commit is contained in:
2026-05-01 02:27:49 -04:00
parent 0d91eb1866
commit c4155b432c
+120 -41
View File
@@ -123,6 +123,7 @@ TRAINING_M41_RELEASE_INDEX_GUARD_TEST_INVENTORY = (
"test_training_m59_release_index_required_snippet_inventory_source_shape_preserves_literal_order",
"test_training_m60_release_index_source_shape_fixture_guard_inventory_self_lock_is_current",
"test_training_m61_release_index_source_shape_fixture_guard_methods_constant_source_shape_is_locked",
"test_training_m62_release_index_source_shape_fixture_guard_methods_reject_dynamic_constant_sources",
"test_training_m53_release_index_doc_category_coverage_is_exact",
"test_training_case_product_doc_reflects_current_m40_maturity",
)
@@ -589,6 +590,9 @@ class DocumentationConsistencyTests(unittest.TestCase):
"required_snippet_inventory_source_shape_fixture_constant_source_lock": (
"test_training_m61_release_index_source_shape_fixture_guard_methods_constant_source_shape_is_locked"
),
"required_snippet_inventory_source_shape_fixture_constant_negative_sources": (
"test_training_m62_release_index_source_shape_fixture_guard_methods_reject_dynamic_constant_sources"
),
"per_doc_helper_exact_category_coverage": (
"test_training_m53_release_index_doc_category_coverage_is_exact"
),
@@ -746,6 +750,7 @@ class DocumentationConsistencyTests(unittest.TestCase):
"test_training_m59_release_index_required_snippet_inventory_source_shape_preserves_literal_order",
"test_training_m60_release_index_source_shape_fixture_guard_inventory_self_lock_is_current",
"test_training_m61_release_index_source_shape_fixture_guard_methods_constant_source_shape_is_locked",
"test_training_m62_release_index_source_shape_fixture_guard_methods_reject_dynamic_constant_sources",
"test_training_m53_release_index_doc_category_coverage_is_exact",
"test_training_case_product_doc_reflects_current_m40_maturity",
)
@@ -824,18 +829,11 @@ class DocumentationConsistencyTests(unittest.TestCase):
self.assertEqual(loader.errors, [])
self.assertEqual(suite.countTestCases(), 1)
def test_training_m61_release_index_source_shape_fixture_guard_methods_constant_source_shape_is_locked(
self,
) -> None:
@staticmethod
def _release_index_source_shape_fixture_guard_method_names_from_source(
module_source: str,
) -> tuple[str, ...]:
constant_name = "TRAINING_M41_RELEASE_INDEX_SOURCE_SHAPE_FIXTURE_GUARD_METHODS"
expected_source_shape_fixture_guards = (
"test_training_m55_release_index_required_snippet_inventory_source_shape_is_locked",
"test_training_m56_release_index_required_snippet_inventory_source_shape_rejects_dynamic_fixtures",
"test_training_m57_release_index_required_snippet_inventory_source_shape_diagnostics_are_locked",
"test_training_m58_release_index_required_snippet_inventory_source_shape_accepts_literal_fixtures",
"test_training_m59_release_index_required_snippet_inventory_source_shape_preserves_literal_order",
)
module_source = Path(__file__).read_text(encoding="utf-8")
module_tree = ast.parse(module_source)
constant_assignments = tuple(
@@ -847,43 +845,124 @@ class DocumentationConsistencyTests(unittest.TestCase):
and node.targets[0].id == constant_name
)
self.assertEqual(len(constant_assignments), 1)
if len(constant_assignments) != 1:
raise AssertionError(
"source shape fixture guard methods must have one assignment"
)
constant_source = constant_assignments[0].value
self.assertIsInstance(constant_source, ast.Tuple)
self.assertTrue(constant_source.elts)
self.assertTrue(
all(
isinstance(element, ast.Constant)
and isinstance(element.value, str)
for element in constant_source.elts
)
disallowed_source_nodes = (
ast.Call,
ast.DictComp,
ast.GeneratorExp,
ast.List,
ast.ListComp,
ast.Name,
ast.SetComp,
ast.Starred,
)
self.assertIsNone(
next(
(
node
for node in ast.walk(constant_source)
if isinstance(
node,
(
ast.Call,
ast.DictComp,
ast.GeneratorExp,
ast.List,
ast.ListComp,
ast.Name,
ast.SetComp,
),
)
),
None,
)
dynamic_source_node = next(
(
node
for node in ast.walk(constant_source)
if isinstance(node, disallowed_source_nodes)
),
None,
)
if dynamic_source_node is not None:
dynamic_node = type(dynamic_source_node).__name__
raise AssertionError(
"source shape fixture guard methods use dynamic source node "
f"{dynamic_node}"
)
if not isinstance(constant_source, ast.Tuple):
raise AssertionError(
"source shape fixture guard methods must be a literal tuple assignment"
)
if not constant_source.elts or not all(
isinstance(element, ast.Constant) and isinstance(element.value, str)
for element in constant_source.elts
):
raise AssertionError(
"source shape fixture guard methods must use literal method names"
)
try:
method_names = ast.literal_eval(constant_source)
except (SyntaxError, ValueError) as exc:
raise AssertionError(
"source shape fixture guard methods must use literal method names"
) from exc
if not all(isinstance(method_name, str) for method_name in method_names):
raise AssertionError(
"source shape fixture guard methods must use literal method names"
)
return method_names
def test_training_m61_release_index_source_shape_fixture_guard_methods_constant_source_shape_is_locked(
self,
) -> None:
expected_source_shape_fixture_guards = (
"test_training_m55_release_index_required_snippet_inventory_source_shape_is_locked",
"test_training_m56_release_index_required_snippet_inventory_source_shape_rejects_dynamic_fixtures",
"test_training_m57_release_index_required_snippet_inventory_source_shape_diagnostics_are_locked",
"test_training_m58_release_index_required_snippet_inventory_source_shape_accepts_literal_fixtures",
"test_training_m59_release_index_required_snippet_inventory_source_shape_preserves_literal_order",
)
module_source = Path(__file__).read_text(encoding="utf-8")
self.assertEqual(
ast.literal_eval(constant_source),
self._release_index_source_shape_fixture_guard_method_names_from_source(
module_source
),
expected_source_shape_fixture_guards,
)
def test_training_m62_release_index_source_shape_fixture_guard_methods_reject_dynamic_constant_sources(
self,
) -> None:
constant_name = "TRAINING_M41_RELEASE_INDEX_SOURCE_SHAPE_FIXTURE_GUARD_METHODS"
first_method = (
"test_training_m55_release_index_required_snippet_inventory_source_shape_is_locked"
)
second_method = (
"test_training_m56_release_index_required_snippet_inventory_source_shape_rejects_dynamic_fixtures"
)
invalid_sources = {
"tuple_call": (
f"{constant_name} = tuple([{first_method!r}, {second_method!r}])",
"dynamic source node Call",
),
"sorted_call": (
f"{constant_name} = sorted(({first_method!r}, {second_method!r}))",
"dynamic source node Call",
),
"name_alias": (
f"fixture_guard_methods = ({first_method!r}, {second_method!r})\n"
f"{constant_name} = fixture_guard_methods",
"dynamic source node Name",
),
"list_literal": (
f"{constant_name} = [{first_method!r}, {second_method!r}]",
"dynamic source node List",
),
"list_comprehension": (
f"{constant_name} = [method for method in ({first_method!r}, {second_method!r})]",
"dynamic source node ListComp",
),
}
for fixture_name, (source, error_snippet) in invalid_sources.items():
with self.subTest(fixture_name=fixture_name):
with self.assertRaisesRegex(AssertionError, re.escape(error_snippet)):
self._release_index_source_shape_fixture_guard_method_names_from_source(
source
)
def test_training_m47_release_index_per_doc_helper_inventory_self_lock_is_current(self) -> None:
helper_name = "_release_index_missing_required_snippets_by_doc"
docs_guard_name = "test_training_readmes_expose_m1_m40_release_index_without_raw_test_modules"