"""Shared helpers for semantic validation fixture suites. Provides fixture loading, lookup, and rule-map construction used by Behave steps, Robot Framework helpers, and ASV benchmarks. Centralises the boilerplate so that adding a new rule or fixture file requires a single update instead of three. """ from __future__ import annotations import json from pathlib import Path from typing import Any from cleveragents.application.services.semantic_validation_rules import ( APIMisuseRule, BrokenReferenceRule, DuplicateImportRule, MissingImportRule, MissingSymbolRule, SemanticCheckResult, SyntaxCheckRule, ) from cleveragents.application.services.semantic_validation_service import ( SemanticValidationRule, ) # --------------------------------------------------------------------------- # Fixture directory # --------------------------------------------------------------------------- FIXTURE_DIR: Path = Path(__file__).resolve().parent # --------------------------------------------------------------------------- # Canonical fixture filenames # --------------------------------------------------------------------------- PORTING_FILENAME = "language_porting_mismatches.json" DEPENDENCY_FILENAME = "dependency_graph_violations.json" API_FILENAME = "api_surface_changes.json" CROSS_FILE_FILENAME = "cross_file_symbols.json" CIRCULAR_FILENAME = "circular_import_detection.json" ALL_FIXTURE_FILENAMES: list[str] = [ PORTING_FILENAME, DEPENDENCY_FILENAME, API_FILENAME, CROSS_FILE_FILENAME, CIRCULAR_FILENAME, ] # --------------------------------------------------------------------------- # Rule map (Protocol-typed) # --------------------------------------------------------------------------- RULE_MAP: dict[str, SemanticValidationRule] = { "syntax_error": SyntaxCheckRule(), "missing_import": MissingImportRule(), "broken_reference": BrokenReferenceRule(), "duplicate_import": DuplicateImportRule(), "api_misuse": APIMisuseRule(), "missing_symbol": MissingSymbolRule(), } # --------------------------------------------------------------------------- # Fixture loading & lookup # --------------------------------------------------------------------------- def load_fixtures(filename: str) -> list[dict[str, Any]]: """Load fixtures from a JSON file in the validation fixtures directory.""" path = FIXTURE_DIR / filename with open(path, encoding="utf-8") as fh: data: dict[str, Any] = json.load(fh) result: list[dict[str, Any]] = data["fixtures"] return result def find_fixture(fixtures: list[dict[str, Any]], name: str) -> dict[str, Any]: """Find a fixture by name in a fixture list. Raises ``ValueError`` if not found. """ for f in fixtures: if f["name"] == name: return f msg = f"Fixture '{name}' not found" raise ValueError(msg) # --------------------------------------------------------------------------- # Fixture runner # --------------------------------------------------------------------------- def run_fixture(fixture: dict[str, Any]) -> SemanticCheckResult: """Run a single fixture through its expected rule and return the result.""" rule_name: str = fixture["expected_rule"] rule = RULE_MAP[rule_name] source: str = fixture["source"] filename: str = fixture["filename"] return rule.check(source, filename) def run_all_fixtures(fixtures: list[dict[str, Any]]) -> list[SemanticCheckResult]: """Run all fixtures through their expected rules.""" return [run_fixture(f) for f in fixtures] def verify_fixture_result( fixture: dict[str, Any], result: SemanticCheckResult, ) -> None: """Verify a fixture result against expectations. Checks ``passed``, optional ``expected_message_contains``, and optional ``expected_data_contains``. Raises ``AssertionError`` on mismatch. """ expected_passed: bool = fixture["expected_passed"] assert result.passed is expected_passed, ( f"Fixture '{fixture['name']}': expected passed={expected_passed}, " f"got passed={result.passed}, message='{result.message}'" ) expected_msg: str | None = fixture.get("expected_message_contains") if expected_msg is not None: assert expected_msg in result.message, ( f"Fixture '{fixture['name']}': expected message containing " f"'{expected_msg}', got '{result.message}'" ) expected_data: dict[str, object] | None = fixture.get("expected_data_contains") if expected_data is not None: assert result.data is not None, ( f"Fixture '{fixture['name']}': expected data containing " f"{expected_data}, but result.data is None" ) for key, value in expected_data.items(): assert key in result.data, ( f"Fixture '{fixture['name']}': expected key '{key}' in " f"result.data, got keys {list(result.data.keys())}" ) if isinstance(value, str): assert value in str(result.data[key]), ( f"Fixture '{fixture['name']}': expected data['{key}'] " f"to contain '{value}', got '{result.data[key]}'" ) def validate_fixture_schema(fixture: dict[str, Any]) -> None: """Validate that a fixture dict has the required keys. Raises ``AssertionError`` if any required key is missing. """ required_keys = { "name", "description", "source", "filename", "expected_rule", "expected_passed", } actual_keys = set(fixture.keys()) missing = required_keys - actual_keys assert not missing, ( f"Fixture '{fixture.get('name', '')}' is missing " f"required keys: {missing}" ) assert fixture["expected_rule"] in RULE_MAP, ( f"Fixture '{fixture['name']}' references unknown rule " f"'{fixture['expected_rule']}'" )