179 lines
5.9 KiB
Python
179 lines
5.9 KiB
Python
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
|
|
|
|
# CR-12 removes only declarations that are repeated verbatim by a later
|
|
# stylesheet layer under the same top-level cascade context. Keeping the exact
|
|
# source fragments here makes every other byte of the accepted CSS baseline a
|
|
# preservation requirement.
|
|
AUDITED_CSS_RETIREMENTS = {
|
|
"styles.css": (
|
|
".workspace-view.active-view {\n display: block;\n}\n\n",
|
|
"body.sidebar-collapsed {\n grid-template-columns: 64px minmax(0, 1fr);\n}\n\n",
|
|
".rotation-day:last-child { border-right: 0; }\n\n",
|
|
(
|
|
"#screenerView .probability-value strong,\n"
|
|
"#screenerView .probability-value small {\n"
|
|
" display: block;\n"
|
|
"}\n\n"
|
|
),
|
|
),
|
|
"renovation.css": (
|
|
".overview-strip .sentiment-block { padding-left: 0; }\n",
|
|
(
|
|
'.overview-strip[data-overview-expanded="true"] .metric-value '
|
|
"{ font-size: 18px; }\n\n"
|
|
),
|
|
),
|
|
"redesign-v2.css": (
|
|
"#dragonView .dragon-operation-table .dragon-col-reason { width: auto; }\n",
|
|
),
|
|
}
|
|
|
|
|
|
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
|
|
if original_relative in AUDITED_CSS_RETIREMENTS:
|
|
original = (ORIGINAL_STATIC / original_relative).read_text(encoding="utf-8")
|
|
for retired in AUDITED_CSS_RETIREMENTS[original_relative]:
|
|
testcase.assertEqual(original.count(retired), 1, retired)
|
|
original = original.replace(retired, "", 1)
|
|
testcase.assertEqual(
|
|
(FRONTEND_ROOT / target_relative).read_text(encoding="utf-8"),
|
|
original,
|
|
original_relative,
|
|
)
|
|
return
|
|
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,
|
|
)
|