from __future__ import annotations import ast import hashlib import re from pathlib import Path APP_ROOT = Path(__file__).resolve().parents[1] ORIGINAL_ROOT = APP_ROOT.parent FRONTEND_ROOT = APP_ROOT / "frontend" ORIGINAL_STATIC = ORIGINAL_ROOT / "static" SOURCE_RANGE = re.compile( r"/\* PRESERVATION-SOURCE-BEGIN app\.js:(\d+)-(\d+) \*/\n" r"(.*?)" r"/\* PRESERVATION-SOURCE-END app\.js:\1-\2 \*/\n?", re.DOTALL, ) # These exact original app.js line ranges were retired in slice 11 after the # definition-only symbols passed static, runtime, and compatibility review. RETIRED_FRONTEND_SOURCE_RANGES = ( (3537, 3540), (4139, 4145), (4973, 4989), (9022, 9027), (9052, 9055), ) AUDITED_FRONTEND_SOURCE_LINE_COUNT = 9283 def sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def function_contract(path: Path, name: str) -> tuple[str, str]: tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) function = next( node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == name ) body = ast.Module(body=function.body, type_ignores=[]) return ( ast.dump(function.args, include_attributes=False), ast.dump(body, include_attributes=False), ) def module_contract( path: Path, *, excluded_definitions: set[str] | None = None, excluded_import_modules: set[str] | None = None, exclude_imports: bool = False, ) -> str: excluded_definitions = excluded_definitions or set() excluded_import_modules = excluded_import_modules or set() tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) tree.body = [ node for node in tree.body if not (exclude_imports and isinstance(node, (ast.Import, ast.ImportFrom))) and not ( isinstance(node, ast.ImportFrom) and node.module in excluded_import_modules ) and getattr(node, "name", None) not in excluded_definitions ] return ast.dump(tree, include_attributes=False) def reassembled_frontend_runtime() -> str: chunks: dict[tuple[int, int], str] = {} for path in FRONTEND_ROOT.rglob("*.js"): for match in SOURCE_RANGE.finditer(path.read_text(encoding="utf-8")): source_range = (int(match.group(1)), int(match.group(2))) if source_range in chunks: raise AssertionError(f"duplicate app.js source range: {source_range}") chunks[source_range] = match.group(3) assembled: list[str] = [] next_line = 1 for (start, end), content in sorted(chunks.items()): if start != next_line: raise AssertionError( f"app.js source coverage gap: expected line {next_line}, got {start}" ) assembled.append(content) next_line = end + 1 if next_line != AUDITED_FRONTEND_SOURCE_LINE_COUNT + 1: raise AssertionError( "app.js source coverage ended at " f"{next_line - 1}, expected {AUDITED_FRONTEND_SOURCE_LINE_COUNT}" ) return "".join(assembled) def original_runtime_after_audited_retirements() -> str: lines = (ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8").splitlines( keepends=True ) retired = { line_number for start, end in RETIRED_FRONTEND_SOURCE_RANGES for line_number in range(start, end + 1) } return "".join( line for line_number, line in enumerate(lines, start=1) if line_number not in retired ) def assert_frontend_runtime_matches_audited_baseline(testcase) -> None: testcase.assertEqual( reassembled_frontend_runtime(), original_runtime_after_audited_retirements(), ) def assert_moved_asset_matches( testcase, original_relative: str, frontend_relative: str | None = None, ) -> None: target_relative = frontend_relative or original_relative testcase.assertEqual( sha256(FRONTEND_ROOT / target_relative), sha256(ORIGINAL_STATIC / original_relative), original_relative, ) def assert_page_prefix_matches(testcase, page_relative: str) -> None: original = (ORIGINAL_STATIC / page_relative).read_text(encoding="utf-8") migrated = (FRONTEND_ROOT / page_relative).read_text(encoding="utf-8") testcase.assertTrue( migrated.startswith(original.rstrip("\n") + "\n\n"), page_relative, )