diff --git a/.github/workflows/internals-distcheck-ci.yml b/.github/workflows/internals-distcheck-ci.yml index 558026f4b..b4bc31a66 100644 --- a/.github/workflows/internals-distcheck-ci.yml +++ b/.github/workflows/internals-distcheck-ci.yml @@ -9,6 +9,8 @@ on: - 'internals/distcheck/src/**' - 'internals/distcheck/tests/**' - 'internals/distcheck/pyproject.toml' + # The published-target table this lane's platform lookup reads. + - 'packages/ts/src/platforms.json' # This lane passes no `config:`, so the reusable workflow's default # applies and the ROOT testing-conventions.toml governs it -- an unknown # key there fails this lane too. diff --git a/.github/workflows/platforms-mirror-check.yml b/.github/workflows/platforms-mirror-check.yml deleted file mode 100644 index c643158a5..000000000 --- a/.github/workflows/platforms-mirror-check.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Platforms Mirror Check - -# `packages/ts/src/platforms.ts` is the release source of truth for the published -# `@dirsql/lib-*` sub-packages; the node distcheck flow keeps a hand-maintained -# Python subset of it. Nothing checked the correspondence, so it drifted in both -# directions -- a field on the Python side justified as mirroring a TypeScript -# property that never existed, and TypeScript-only fields with no counterpart. -# This holds the two tables to the subset on every change to either, so a new -# published target cannot be half-added. -# -# Deliberately its own workflow rather than a job in internals-distcheck-ci.yml: -# that lane's path filter does not watch packages/ts, which is exactly where the -# half-added-target case starts. - -on: - pull_request: - branches: [main] - paths: - - 'packages/ts/src/platforms.ts' - - 'internals/distcheck/src/distcheck/node_flow/platforms.py' - - 'internals/checks/src/checks/platforms_mirror/**' - - '.github/workflows/platforms-mirror-check.yml' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - platforms-mirror: - name: Platforms mirror - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.13" - - - name: Assert the two platform tables agree on the fields they share - run: uv run --project internals/checks dirsql-checks platforms-mirror diff --git a/internals/checks/e2e-attestations/claude-1074-platform-table-single-source.json b/internals/checks/e2e-attestations/claude-1074-platform-table-single-source.json new file mode 100644 index 000000000..b0b55bccc --- /dev/null +++ b/internals/checks/e2e-attestations/claude-1074-platform-table-single-source.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e -x -q", + "ran_at": 1788609669, + "exit_code": 0, + "commit": "c92fd606e9cbc936828378f5481e0c0dbdda98c0", + "branch": "claude/1074-platform-table-single-source" +} diff --git a/internals/checks/src/checks/cli.py b/internals/checks/src/checks/cli.py index bd451a785..977cc120b 100644 --- a/internals/checks/src/checks/cli.py +++ b/internals/checks/src/checks/cli.py @@ -12,7 +12,6 @@ from checks.attestation_guard.cli import cli as attestation_guard from checks.changelog_gate.cli import cli as changelog_gate from checks.declared_deps.cli import cli as declared_deps -from checks.platforms_mirror.cli import cli as platforms_mirror from checks.preflight.cli import cli as preflight from checks.pytest_gate.cli import cli as pytest_gate from checks.release_globs.cli import cli as release_globs @@ -28,7 +27,6 @@ def main() -> None: main.add_command(attestation_guard, name="attestation-guard") main.add_command(changelog_gate, name="changelog-gate") main.add_command(declared_deps, name="declared-deps") -main.add_command(platforms_mirror, name="platforms-mirror") main.add_command(preflight, name="preflight") main.add_command(pytest_gate, name="pytest-gate") main.add_command(release_globs, name="release-globs") diff --git a/internals/checks/src/checks/cli_test.py b/internals/checks/src/checks/cli_test.py index 6d65cc7a1..84676efc8 100644 --- a/internals/checks/src/checks/cli_test.py +++ b/internals/checks/src/checks/cli_test.py @@ -30,10 +30,6 @@ def test_declared_deps_is_registered(): assert "declared-deps" in main.commands -def test_platforms_mirror_is_registered(): - assert "platforms-mirror" in main.commands - - def test_preflight_is_registered(): assert "preflight" in main.commands diff --git a/internals/checks/src/checks/platforms_mirror/__init__.py b/internals/checks/src/checks/platforms_mirror/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/internals/checks/src/checks/platforms_mirror/as_json.py b/internals/checks/src/checks/platforms_mirror/as_json.py deleted file mode 100644 index 9b290d016..000000000 --- a/internals/checks/src/checks/platforms_mirror/as_json.py +++ /dev/null @@ -1,15 +0,0 @@ -"""A comment-free object literal as JSON. - -TypeScript's bare keys and trailing commas are the only two things left between -a stripped `PLATFORMS` array and something `json` will decode. -""" - -from __future__ import annotations - -import re - - -def as_json(text: str) -> str: - """`text` with bare object keys quoted and trailing commas dropped.""" - keyed = re.sub(r"([{,]\s*)([A-Za-z_$][\w$]*)\s*:", r'\1"\2":', text) - return re.sub(r",(\s*[}\]])", r"\1", keyed) diff --git a/internals/checks/src/checks/platforms_mirror/as_json_test.py b/internals/checks/src/checks/platforms_mirror/as_json_test.py deleted file mode 100644 index 3dae05eda..000000000 --- a/internals/checks/src/checks/platforms_mirror/as_json_test.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Colocated unit tests for the JSON normalizer (isolation -- pure text in, -text out). -""" - -from checks.platforms_mirror.as_json import as_json - - -def describe_as_json(): - def it_quotes_a_bare_object_key(): - assert as_json('{nodePlatform: "linux"}') == '{"nodePlatform": "linux"}' - - def it_quotes_every_key_after_a_comma(): - assert as_json('{a: 1, b: 2}') == '{"a": 1, "b": 2}' - - def it_quotes_a_key_holding_a_dollar_sign_or_underscore(): - assert as_json("{_a$b: 1}") == '{"_a$b": 1}' - - def it_drops_a_trailing_comma_before_a_brace(): - assert as_json('{"a": 1,}') == '{"a": 1}' - - def it_drops_a_trailing_comma_before_a_bracket(): - assert as_json('["a",]') == '["a"]' - - def it_keeps_the_whitespace_that_followed_a_dropped_comma(): - assert as_json('{"a": 1,\n}') == '{"a": 1\n}' - - def it_leaves_an_already_quoted_key_alone(): - assert as_json('{"a": 1}') == '{"a": 1}' diff --git a/internals/checks/src/checks/platforms_mirror/assigned_names.py b/internals/checks/src/checks/platforms_mirror/assigned_names.py deleted file mode 100644 index a3c6b2de8..000000000 --- a/internals/checks/src/checks/platforms_mirror/assigned_names.py +++ /dev/null @@ -1,16 +0,0 @@ -"""The module-level names a statement binds.""" - -from __future__ import annotations - -import ast - - -def assigned_names(node: ast.stmt) -> list[str]: - """The module-level names ``node`` binds, empty when it binds none.""" - if isinstance(node, ast.AnnAssign): - targets = [node.target] - elif isinstance(node, ast.Assign): - targets = node.targets - else: - return [] - return [target.id for target in targets if isinstance(target, ast.Name)] diff --git a/internals/checks/src/checks/platforms_mirror/assigned_names_test.py b/internals/checks/src/checks/platforms_mirror/assigned_names_test.py deleted file mode 100644 index 4c41cf4e3..000000000 --- a/internals/checks/src/checks/platforms_mirror/assigned_names_test.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Colocated unit tests for the names a module-level statement binds (isolation --- an `ast` node in, names out). -""" - -import ast - -from checks.platforms_mirror.assigned_names import assigned_names - - -def names(source: str) -> list[str]: - return assigned_names(ast.parse(source).body[0]) - - -def describe_assigned_names(): - def it_reads_an_annotated_assignment(): - assert names("PLATFORMS: tuple = ()\n") == ["PLATFORMS"] - - def it_reads_a_plain_assignment(): - assert names("PLATFORMS = ()\n") == ["PLATFORMS"] - - def it_reads_every_name_of_a_chained_assignment(): - assert names("OTHERS = PLATFORMS = ()\n") == ["OTHERS", "PLATFORMS"] - - def it_ignores_a_binding_that_is_not_a_plain_name(): - assert names("holder.PLATFORMS = ()\n") == [] - - def it_binds_nothing_for_a_statement_that_assigns_nothing(): - assert names("import os\n") == [] diff --git a/internals/checks/src/checks/platforms_mirror/call_name.py b/internals/checks/src/checks/platforms_mirror/call_name.py deleted file mode 100644 index 82d81264a..000000000 --- a/internals/checks/src/checks/platforms_mirror/call_name.py +++ /dev/null @@ -1,12 +0,0 @@ -"""The name of a table entry's call, when it has one.""" - -from __future__ import annotations - -import ast - - -def call_name(element: ast.expr): - """The name of a plain `Name(...)` call, else ``None``.""" - if isinstance(element, ast.Call) and isinstance(element.func, ast.Name): - return element.func.id - return None diff --git a/internals/checks/src/checks/platforms_mirror/call_name_test.py b/internals/checks/src/checks/platforms_mirror/call_name_test.py deleted file mode 100644 index fcaa03018..000000000 --- a/internals/checks/src/checks/platforms_mirror/call_name_test.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Colocated unit tests for reading a table entry's call name (isolation -- an -`ast` node in, a name out). -""" - -import ast - -from checks.platforms_mirror.call_name import call_name - - -def entry(source: str) -> ast.expr: - return ast.parse(source, mode="eval").body - - -def describe_call_name(): - def it_names_a_plain_call(): - assert call_name(entry("Platform('linux')")) == "Platform" - - def it_names_the_callee_rather_than_its_arguments(): - assert call_name(entry("Machine(Platform('linux'))")) == "Machine" - - def it_has_no_name_for_a_dotted_call(): - assert call_name(entry("platforms.Platform('linux')")) is None - - def it_has_no_name_for_something_that_is_not_a_call(): - assert call_name(entry("1")) is None diff --git a/internals/checks/src/checks/platforms_mirror/cli.py b/internals/checks/src/checks/platforms_mirror/cli.py deleted file mode 100644 index 46fbd28ad..000000000 --- a/internals/checks/src/checks/platforms_mirror/cli.py +++ /dev/null @@ -1,29 +0,0 @@ -"""The platforms-mirror check -- repo-only (#1004). - -Backs `dirsql-checks platforms-mirror`: the node distcheck flow's platform table -and the TypeScript release source of truth must agree on the fields they share. -""" - -from __future__ import annotations - -import click - -from checks.platforms_mirror.vocabulary import PYTHON_FILE, TYPESCRIPT_FILE -from checks.platforms_mirror.gate import run - - -@click.command() -@click.option( - "--python", - "python_path", - default=PYTHON_FILE, - help="The node distcheck flow's platform table.", -) -@click.option( - "--typescript", - "typescript_path", - default=TYPESCRIPT_FILE, - help="The release source of truth for published sub-packages.", -) -def cli(python_path: str, typescript_path: str) -> None: - raise SystemExit(run(python_path, typescript_path)) diff --git a/internals/checks/src/checks/platforms_mirror/cli_test.py b/internals/checks/src/checks/platforms_mirror/cli_test.py deleted file mode 100644 index 480aa5a91..000000000 --- a/internals/checks/src/checks/platforms_mirror/cli_test.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Colocated unit tests for the platforms-mirror command (isolation -- no -`CliRunner`). Driven through `.callback`; `run` is mocked at its import site. -""" - -from unittest import mock - -import pytest - -from checks.platforms_mirror.cli import PYTHON_FILE, TYPESCRIPT_FILE, cli - - -def invoke(**kwargs): - with mock.patch("checks.platforms_mirror.cli.run", **kwargs) as run: - with pytest.raises(SystemExit) as exc_info: - cli.callback(python_path="p.py", typescript_path="t.ts") - return run, exc_info.value.code - - -def test_exits_with_runs_return_code(): - run, code = invoke(return_value=0) - run.assert_called_once_with("p.py", "t.ts") - assert code == 0 - - -def test_propagates_a_drifted_mirror_as_exit_one(): - assert invoke(return_value=1)[1] == 1 - - -def test_defaults_to_the_two_committed_platform_tables(): - python_path, typescript_path = cli.params - assert python_path.default == PYTHON_FILE - assert typescript_path.default == TYPESCRIPT_FILE - assert PYTHON_FILE == "internals/distcheck/src/distcheck/node_flow/platforms.py" - assert TYPESCRIPT_FILE == "packages/ts/src/platforms.ts" diff --git a/internals/checks/src/checks/platforms_mirror/dataclass_fields.py b/internals/checks/src/checks/platforms_mirror/dataclass_fields.py deleted file mode 100644 index 753909fa4..000000000 --- a/internals/checks/src/checks/platforms_mirror/dataclass_fields.py +++ /dev/null @@ -1,28 +0,0 @@ -"""The mirrored field list, read off the `Platform` dataclass annotations.""" - -from __future__ import annotations - -import ast - -from .parse import CLASS_NAME, ParseError - - -def dataclass_fields(tree: ast.Module) -> list[str]: - classes = {node.name: node for node in tree.body if isinstance(node, ast.ClassDef)} - declaration = classes.get(CLASS_NAME) - if declaration is None: - raise ParseError( - f"no `class {CLASS_NAME}` at module level; this check reads the mirrored " - f"field list off its annotations." - ) - fields = [ - statement.target.id - for statement in declaration.body - if isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name) - ] - if not fields: - raise ParseError( - f"class {CLASS_NAME} declares no annotated fields; this check reads the " - f"mirrored field list off them." - ) - return fields diff --git a/internals/checks/src/checks/platforms_mirror/dataclass_fields_test.py b/internals/checks/src/checks/platforms_mirror/dataclass_fields_test.py deleted file mode 100644 index 085fa0455..000000000 --- a/internals/checks/src/checks/platforms_mirror/dataclass_fields_test.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Colocated unit tests for the mirrored field list (isolation -- a parsed -module in, field names out; nothing here touches the repo's real platforms.py). -""" - -import ast - -import pytest - -from checks.platforms_mirror.dataclass_fields import ParseError, dataclass_fields - - -def fields(source: str) -> list[str]: - return dataclass_fields(ast.parse(source)) - - -def describe_dataclass_fields(): - def it_reads_the_annotations_in_declaration_order(): - source = "class Platform:\n node_platform: str\n node_arch: str\n slug: str\n" - assert fields(source) == ["node_platform", "node_arch", "slug"] - - def it_ignores_a_member_with_no_annotation(): - source = "class Platform:\n slug: str\n EXTRA = 1\n" - assert fields(source) == ["slug"] - - @pytest.mark.parametrize("name", ["Machine", "Target"]) - def it_rejects_a_module_whose_only_class_is_named_otherwise(name): - with pytest.raises(ParseError, match="no `class Platform`"): - fields(f"class {name}:\n slug: str\n") - - def it_rejects_a_module_with_no_class_at_all(): - with pytest.raises(ParseError, match="no `class Platform`"): - fields("PLATFORMS = ()\n") - - def it_rejects_a_platform_class_with_no_annotated_fields(): - with pytest.raises(ParseError, match="declares no annotated fields"): - fields("class Platform:\n pass\n") diff --git a/internals/checks/src/checks/platforms_mirror/decide.py b/internals/checks/src/checks/platforms_mirror/decide.py deleted file mode 100644 index a168c876f..000000000 --- a/internals/checks/src/checks/platforms_mirror/decide.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Python fields the mirror cannot source from TypeScript. - -A field outside `SHARED` has nowhere to read its value from, so it is drift by -construction: either it was never published, or the TypeScript interface grew a -property nobody added to the shared vocabulary. -""" - -from __future__ import annotations - -from .vocabulary import SHARED, TYPESCRIPT_FILE - - -def unmirrored_fields(fields) -> list[str]: - """One message per dataclass field with no TypeScript source.""" - return [ - f"Platform.{field} has no counterpart in {TYPESCRIPT_FILE}. platforms.py " - f"holds a deliberate subset of the published target ({', '.join(SHARED)}), " - f"so a field the mirror cannot source is drift: delete it, or add the " - f"property to the TypeScript `Platform` interface and to SHARED here." - for field in fields - if field not in SHARED - ] diff --git a/internals/checks/src/checks/platforms_mirror/decide_test.py b/internals/checks/src/checks/platforms_mirror/decide_test.py deleted file mode 100644 index 66b96fb23..000000000 --- a/internals/checks/src/checks/platforms_mirror/decide_test.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Colocated unit tests for the unmirrored-field verdict (isolation -- a field -list in, messages out). -""" - -from checks.platforms_mirror.decide import SHARED, unmirrored_fields - -FIELDS = list(SHARED) - - -def describe_unmirrored_fields(): - def it_passes_the_declared_subset(): - assert unmirrored_fields(FIELDS) == [] - - def it_names_a_field_with_no_typescript_source(): - (message,) = unmirrored_fields([*FIELDS, "exe"]) - assert message.startswith("Platform.exe has no counterpart in ") - assert "packages/ts/src/platforms.ts" in message - - def it_lists_the_subset_the_mirror_can_source(): - (message,) = unmirrored_fields(["exe"]) - assert "(node_platform, node_arch, slug, os, cpu)" in message - - def it_reports_every_unmirrored_field(): - assert len(unmirrored_fields(["exe", "dev"])) == 2 diff --git a/internals/checks/src/checks/platforms_mirror/field_problems.py b/internals/checks/src/checks/platforms_mirror/field_problems.py deleted file mode 100644 index 24140d981..000000000 --- a/internals/checks/src/checks/platforms_mirror/field_problems.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Per-row field comparison for the platforms-mirror check (#1004). - -Every field in `vocabulary.py`'s SHARED must hold the same value on both sides -of a target that exists in both tables. -""" - -from __future__ import annotations - -from .typescript_value import typescript_value -from .vocabulary import PYTHON_FILE, SHARED, key - - -def field_problems(python_row: dict, typescript_row: dict) -> list[str]: - """One message per shared field whose two values disagree.""" - problems = [] - for field in SHARED: - found = python_row.get(field) - expected = typescript_value(field, typescript_row) - if found != expected: - problems.append( - f"{key(python_row['node_platform'], python_row['node_arch'])}: " - f"{field} is {found!r} in platforms.py, {expected!r} in " - f"platforms.ts. platforms.ts is the release source of truth -- " - f"change {PYTHON_FILE} to match." - ) - return problems diff --git a/internals/checks/src/checks/platforms_mirror/field_problems_test.py b/internals/checks/src/checks/platforms_mirror/field_problems_test.py deleted file mode 100644 index a94620044..000000000 --- a/internals/checks/src/checks/platforms_mirror/field_problems_test.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Colocated unit tests for the platforms-mirror field comparison (isolation -- -pure dicts in, messages out). -""" - -from checks.platforms_mirror.field_problems import field_problems - -PY_LINUX = { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], -} -TS_LINUX = { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "libc": ["glibc"], -} - - -def describe_field_problems(): - def it_passes_a_row_that_agrees(): - assert field_problems(PY_LINUX, TS_LINUX) == [] - - def it_names_the_field_and_both_values(): - (message,) = field_problems({**PY_LINUX, "cpu": ["arm64"]}, TS_LINUX) - assert message == ( - "linux-x64: cpu is ['arm64'] in platforms.py, ['x64'] in platforms.ts. " - "platforms.ts is the release source of truth -- change " - "internals/distcheck/src/distcheck/node_flow/platforms.py to match." - ) - - def it_derives_the_slug_before_comparing_it(): - (message,) = field_problems(PY_LINUX, {**TS_LINUX, "libName": "@dirsql/lib-other"}) - assert message.startswith("linux-x64: slug is 'linux-x64-gnu' in platforms.py, 'other'") - - def it_reports_every_disagreeing_field(): - drifted = {**PY_LINUX, "cpu": ["arm64"], "slug": "nope"} - assert len(field_problems(drifted, TS_LINUX)) == 2 - - def it_reports_a_value_that_sorts_after_the_typescript_one(): - # The pair above drifts *below* platforms.ts; this one drifts above, so - # neither ordering can stand in for the inequality. - (message,) = field_problems({**PY_LINUX, "slug": "zzz"}, TS_LINUX) - assert message.startswith("linux-x64: slug is 'zzz' in platforms.py") diff --git a/internals/checks/src/checks/platforms_mirror/gate.py b/internals/checks/src/checks/platforms_mirror/gate.py deleted file mode 100644 index f7c8223e4..000000000 --- a/internals/checks/src/checks/platforms_mirror/gate.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Orchestration for the platforms-mirror check (#1004). - -Reads both platform tables off disk and holds them to the subset invariant in -`vocabulary.py`. A table this cannot parse fails the check rather than passing -on an empty read: the whole point is that nothing silently stops comparing. -""" - -from __future__ import annotations - -from collections.abc import Callable - -from .parse import ParseError -from .problems import problems -from .python_table import python_table -from .read import read -from .typescript_table import typescript_table - - -def run( - python_path: str, - typescript_path: str, - source: Callable[[str], str] = read, - echo: Callable[[str], None] = print, -) -> int: - try: - fields, python_rows = python_table(source(python_path)) - typescript_rows = typescript_table(source(typescript_path)) - except ParseError as error: - echo(f"::error::platforms-mirror could not read a platform table: {error}") - return 1 - found = problems(fields, python_rows, typescript_rows) - for problem in found: - echo(f"::error::{problem}") - if found: - echo( - f"platforms-mirror: {len(found)} problem(s). platforms.ts is the release " - f"source of truth for the published sub-packages; platforms.py mirrors the " - f"subset named in " - f"internals/checks/src/checks/platforms_mirror/vocabulary.py's SHARED." - ) - return 1 - echo( - f"ok platforms-mirror: {python_path} mirrors the shared fields of " - f"{typescript_path} across {len(python_rows)} published target(s)." - ) - return 0 diff --git a/internals/checks/src/checks/platforms_mirror/gate_test.py b/internals/checks/src/checks/platforms_mirror/gate_test.py deleted file mode 100644 index 27a524976..000000000 --- a/internals/checks/src/checks/platforms_mirror/gate_test.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Colocated unit tests for the platforms-mirror gate (#1004). - -Isolation: the reader is injected, so nothing here touches the repo's real -platforms.py / platforms.ts. The table readers and the verdict in -`problems.py` run for real, since they are pure text-in / messages-out. -""" - -import inspect - -from checks.platforms_mirror.gate import run - -PYTHON = '''\ -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Platform: - node_platform: str - node_arch: str - slug: str - os: list[str] - cpu: list[str] -{extra} - -PLATFORMS: tuple[Platform, ...] = ( - Platform("linux", "x64", "linux-x64-gnu", ["linux"], ["x64"]{value}), -) -''' - -TYPESCRIPT = """\ -export const PLATFORMS: readonly Platform[] = [ - { - triple: "x86_64-unknown-linux-gnu", - nodePlatform: "linux", - nodeArch: "x64", - libName: "@dirsql/lib-linux-x64-gnu", - os: ["linux"], - cpu: ["x64"], - libc: ["glibc"], - }, -]; -""" - - -def sources(python=None, typescript=TYPESCRIPT): - text = { - "p.py": PYTHON.format(extra="", value="") if python is None else python, - "t.ts": typescript, - } - return lambda path: text[path] - - -def invoke(**kwargs): - lines = [] - code = run("p.py", "t.ts", source=sources(**kwargs), echo=lines.append) - return code, "\n".join(lines) - - -def describe_run(): - def it_reports_the_target_count_when_the_subset_agrees(): - code, out = invoke() - assert code == 0 - assert out == ( - "ok platforms-mirror: p.py mirrors the shared fields of t.ts across " - "1 published target(s)." - ) - - def it_fails_on_a_field_with_no_typescript_counterpart(): - code, out = invoke(python=PYTHON.format(extra=" exe: bool\n", value=", False")) - assert code == 1 - assert "::error::Platform.exe has no counterpart" in out - assert "platforms-mirror: 1 problem(s)." in out - - def it_names_vocabulary_py_so_the_shared_subset_can_be_found(): - code, out = invoke(python=PYTHON.format(extra=" exe: bool\n", value=", False")) - assert code == 1 - assert "platforms_mirror/vocabulary.py's SHARED" in out - - def it_counts_every_problem_it_found(): - code, out = invoke( - python=PYTHON.format(extra=" exe: bool\n dev: bool\n", value=", False, True") - ) - assert code == 1 - assert "platforms-mirror: 2 problem(s)." in out - - def it_fails_when_the_python_table_cannot_be_read(): - code, out = invoke(python="x = 1\n") - assert code == 1 - assert "::error::platforms-mirror could not read a platform table:" in out - assert "no `class Platform`" in out - - def it_fails_when_the_typescript_table_cannot_be_read(): - code, out = invoke(typescript="export const OTHER = [];\n") - assert code == 1 - assert "could not read a platform table:" in out - assert "no `PLATFORMS" in out - - def it_defaults_to_the_reader_in_its_own_module(): - default = inspect.signature(run).parameters["source"].default - assert default.__module__ == "checks.platforms_mirror.read" diff --git a/internals/checks/src/checks/platforms_mirror/literal.py b/internals/checks/src/checks/platforms_mirror/literal.py deleted file mode 100644 index 6165bc3fb..000000000 --- a/internals/checks/src/checks/platforms_mirror/literal.py +++ /dev/null @@ -1,18 +0,0 @@ -"""One `Platform(...)` argument, read rather than evaluated. - -A row is only as trustworthy as its arguments: anything `ast` cannot constant- -fold is a `ParseError`, not a value the check would go on comparing. -""" - -from __future__ import annotations - -import ast - -from .parse import CLASS_NAME, ParseError - - -def literal(node: ast.expr): - try: - return ast.literal_eval(node) - except ValueError as error: - raise ParseError(f"a {CLASS_NAME}(...) argument is not a literal: {error}") from error diff --git a/internals/checks/src/checks/platforms_mirror/literal_test.py b/internals/checks/src/checks/platforms_mirror/literal_test.py deleted file mode 100644 index cd4fefc24..000000000 --- a/internals/checks/src/checks/platforms_mirror/literal_test.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Colocated unit tests for reading one `Platform(...)` argument (isolation -- -an `ast` node in, a value out). -""" - -import ast - -import pytest - -from checks.platforms_mirror.literal import ParseError, literal - - -def value(source: str) -> ast.expr: - return ast.parse(source, mode="eval").body - - -def describe_literal(): - def it_reads_a_string(): - assert literal(value("'linux-x64-gnu'")) == "linux-x64-gnu" - - def it_reads_a_list_of_strings(): - assert literal(value("['linux', 'darwin']")) == ["linux", "darwin"] - - def it_rejects_a_name(): - with pytest.raises(ParseError, match="a Platform\\(...\\) argument is not a literal"): - literal(value("slug")) - - def it_carries_the_reason_ast_gave(): - with pytest.raises(ParseError, match="malformed node or string"): - literal(value("slug")) diff --git a/internals/checks/src/checks/platforms_mirror/missing_rows.py b/internals/checks/src/checks/platforms_mirror/missing_rows.py deleted file mode 100644 index b11767b20..000000000 --- a/internals/checks/src/checks/platforms_mirror/missing_rows.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Published targets with no row on the Python side.""" - -from __future__ import annotations - -from .vocabulary import PYTHON_FILE, key, slug - - -def missing_rows(python_keys, typescript_rows) -> list[str]: - """One message per published target with no row in platforms.py.""" - return [ - f"{key(row['nodePlatform'], row['nodeArch'])} is published by platforms.ts " - f"but has no row in platforms.py. Add " - f"Platform({row['nodePlatform']!r}, {row['nodeArch']!r}, " - f"{slug(str(row.get('libName', '')))!r}, {row.get('os')!r}, " - f"{row.get('cpu')!r}) to PLATFORMS in {PYTHON_FILE}, or the node distcheck " - f"flow cannot resolve the new target on that host." - for row in typescript_rows - if key(row["nodePlatform"], row["nodeArch"]) not in python_keys - ] diff --git a/internals/checks/src/checks/platforms_mirror/missing_rows_test.py b/internals/checks/src/checks/platforms_mirror/missing_rows_test.py deleted file mode 100644 index e1f26129f..000000000 --- a/internals/checks/src/checks/platforms_mirror/missing_rows_test.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Colocated unit tests for the unpublished-target verdict (isolation -- pure -dicts in, messages out). -""" - -from checks.platforms_mirror.missing_rows import missing_rows - -TS_LINUX = { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "libc": ["glibc"], -} - - -def describe_missing_rows(): - def it_passes_when_every_published_target_has_a_row(): - assert missing_rows({"linux-x64"}, [TS_LINUX]) == [] - - def it_suggests_the_row_to_add(): - (message,) = missing_rows(set(), [TS_LINUX]) - assert message.startswith("linux-x64 is published by platforms.ts") - assert "Platform('linux', 'x64', 'linux-x64-gnu', ['linux'], ['x64'])" in message - - def it_points_at_the_python_table_it_would_be_added_to(): - (message,) = missing_rows(set(), [TS_LINUX]) - assert "internals/distcheck/src/distcheck/node_flow/platforms.py" in message diff --git a/internals/checks/src/checks/platforms_mirror/parse.py b/internals/checks/src/checks/platforms_mirror/parse.py deleted file mode 100644 index 2572daf62..000000000 --- a/internals/checks/src/checks/platforms_mirror/parse.py +++ /dev/null @@ -1,20 +0,0 @@ -"""What both platform tables are called, and how an unreadable one fails (#1004). - -Neither side is imported: `internals/checks` depends on neither -`internals/distcheck` nor a node toolchain, and a check that executes the file -it audits can be fooled by the file. `python_table.py` reads its side out of -`ast`; `typescript_table.py` strips comments, normalizes to JSON, and hands the -result to `json`'s own decoder. - -Every shape those readers cannot read raises `ParseError` rather than returning -an empty table -- a mirror check that silently sees no rows passes forever. -""" - -from __future__ import annotations - -CLASS_NAME = "Platform" -TABLE_NAME = "PLATFORMS" - - -class ParseError(Exception): - """A platform table that could not be read in the shape this check expects.""" diff --git a/internals/checks/src/checks/platforms_mirror/parse_test.py b/internals/checks/src/checks/platforms_mirror/parse_test.py deleted file mode 100644 index 56166e3ed..000000000 --- a/internals/checks/src/checks/platforms_mirror/parse_test.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Colocated unit tests for the shared platform-table vocabulary (#1004).""" - -from checks.platforms_mirror.parse import CLASS_NAME, ParseError, TABLE_NAME - - -def describe_names(): - def it_names_the_dataclass_the_python_table_declares(): - assert CLASS_NAME == "Platform" - - def it_names_the_binding_both_sides_hold_the_table_under(): - assert TABLE_NAME == "PLATFORMS" - - -def describe_ParseError(): - def it_carries_the_message_a_reader_raised_it_with(): - assert str(ParseError("no `class Platform`")) == "no `class Platform`" - - def it_is_an_ordinary_exception(): - assert issubclass(ParseError, Exception) diff --git a/internals/checks/src/checks/platforms_mirror/prefix_problems.py b/internals/checks/src/checks/platforms_mirror/prefix_problems.py deleted file mode 100644 index cebb1e656..000000000 --- a/internals/checks/src/checks/platforms_mirror/prefix_problems.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Published targets whose `libName` no `slug` can be derived from.""" - -from __future__ import annotations - -from .vocabulary import LIB_PREFIX, TYPESCRIPT_FILE, key - - -def prefix_problems(rows) -> list[str]: - """One message per `libName` that `librarySlug()` would throw on.""" - return [ - f"{key(row.get('nodePlatform'), row.get('nodeArch'))}: libName " - f"{row.get('libName')!r} does not start with {LIB_PREFIX!r}, so " - f"`librarySlug()` throws on it and the Python `slug` cannot be derived. " - f"Fix the name in {TYPESCRIPT_FILE}." - for row in rows - if not str(row.get("libName", "")).startswith(LIB_PREFIX) - ] diff --git a/internals/checks/src/checks/platforms_mirror/prefix_problems_test.py b/internals/checks/src/checks/platforms_mirror/prefix_problems_test.py deleted file mode 100644 index a124d1d13..000000000 --- a/internals/checks/src/checks/platforms_mirror/prefix_problems_test.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Colocated unit tests for the `libName` prefix verdict (isolation -- pure -dicts in, messages out). -""" - -from checks.platforms_mirror.prefix_problems import prefix_problems - -TS_LINUX = { - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", -} -TS_WIN = { - "nodePlatform": "win32", - "nodeArch": "x64", - "libName": "@dirsql/lib-win32-x64-msvc", -} - - -def describe_prefix_problems(): - def it_passes_well_formed_names(): - assert prefix_problems([TS_LINUX, TS_WIN]) == [] - - def it_names_a_lib_name_library_slug_would_throw_on(): - (message,) = prefix_problems([{**TS_WIN, "libName": "@dirsql/win32-x64-msvc"}]) - assert message.startswith("win32-x64: libName '@dirsql/win32-x64-msvc'") - assert "@dirsql/lib-" in message - - def it_points_at_the_typescript_table_for_the_fix(): - (message,) = prefix_problems([{**TS_WIN, "libName": "nope"}]) - assert "packages/ts/src/platforms.ts" in message - - def it_names_a_row_with_no_lib_name_at_all(): - assert len(prefix_problems([{"nodePlatform": "win32", "nodeArch": "x64"}])) == 1 diff --git a/internals/checks/src/checks/platforms_mirror/problems.py b/internals/checks/src/checks/platforms_mirror/problems.py deleted file mode 100644 index e2e9d810f..000000000 --- a/internals/checks/src/checks/platforms_mirror/problems.py +++ /dev/null @@ -1,29 +0,0 @@ -"""The full platforms-mirror verdict (#1004). - -Structural disagreements first -- an unmirrored field, a malformed `libName`, a -target on only one side -- then the per-field comparison for the targets both -tables carry. -""" - -from __future__ import annotations - -from .decide import unmirrored_fields -from .field_problems import field_problems -from .missing_rows import missing_rows -from .prefix_problems import prefix_problems -from .stray_rows import stray_rows -from .vocabulary import key - - -def problems(fields, python_rows, typescript_rows) -> list[str]: - """Every way the two tables disagree, most structural first.""" - found = unmirrored_fields(fields) + prefix_problems(typescript_rows) - by_key = {key(row["nodePlatform"], row["nodeArch"]): row for row in typescript_rows} - python_keys = {key(row["node_platform"], row["node_arch"]) for row in python_rows} - found += missing_rows(python_keys, typescript_rows) - found += stray_rows(set(by_key), python_rows) - for row in python_rows: - counterpart = by_key.get(key(row["node_platform"], row["node_arch"])) - if counterpart is not None: - found += field_problems(row, counterpart) - return found diff --git a/internals/checks/src/checks/platforms_mirror/problems_test.py b/internals/checks/src/checks/platforms_mirror/problems_test.py deleted file mode 100644 index 8c7ee0cc7..000000000 --- a/internals/checks/src/checks/platforms_mirror/problems_test.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Colocated unit tests for the full platforms-mirror verdict (isolation -- pure -dicts in, messages out). -""" - -from checks.platforms_mirror.problems import problems - -FIELDS = ["node_platform", "node_arch", "slug", "os", "cpu"] - -PY_LINUX = { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], -} -TS_LINUX = { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "libc": ["glibc"], -} -PY_WIN = { - "node_platform": "win32", - "node_arch": "x64", - "slug": "win32-x64-msvc", - "os": ["win32"], - "cpu": ["x64"], -} -TS_WIN = { - "nodePlatform": "win32", - "nodeArch": "x64", - "libName": "@dirsql/lib-win32-x64-msvc", - "os": ["win32"], - "cpu": ["x64"], -} - - -def describe_problems(): - def it_passes_two_tables_that_mirror(): - assert problems(FIELDS, [PY_LINUX, PY_WIN], [TS_LINUX, TS_WIN]) == [] - - def it_reports_the_structural_problems_before_the_field_ones(): - found = problems( - [*FIELDS, "exe"], - [PY_LINUX, {**PY_WIN, "cpu": ["arm64"]}], - [TS_LINUX, TS_WIN], - ) - assert found[0].startswith("Platform.exe") - assert found[-1].startswith("win32-x64: cpu") - - def it_reports_a_lib_name_no_slug_can_be_derived_from(): - found = problems(FIELDS, [PY_LINUX], [{**TS_LINUX, "libName": "@dirsql/linux-x64-gnu"}]) - assert found[0].startswith("linux-x64: libName '@dirsql/linux-x64-gnu'") - - def it_compares_only_the_rows_present_on_both_sides(): - found = problems(FIELDS, [PY_LINUX, PY_WIN], [TS_LINUX]) - assert len(found) == 1 - assert found[0].startswith("win32-x64 has a row in platforms.py") diff --git a/internals/checks/src/checks/platforms_mirror/python_table.py b/internals/checks/src/checks/platforms_mirror/python_table.py deleted file mode 100644 index 3f7397e43..000000000 --- a/internals/checks/src/checks/platforms_mirror/python_table.py +++ /dev/null @@ -1,21 +0,0 @@ -"""The `Platform` fields and `PLATFORMS` rows of a `platforms.py`-shaped module. - -The field list comes off the dataclass annotations and the rows off the -module-level assignment, both through `ast`: the module is never imported, so a -table that builds itself at import time is a `ParseError` rather than a read. -""" - -from __future__ import annotations - -import ast - -from .dataclass_fields import dataclass_fields -from .row import row -from .table_elements import table_elements - - -def python_table(source: str) -> tuple[list[str], list[dict]]: - """`(field names, rows)` from a `platforms.py`-shaped module.""" - tree = ast.parse(source) - fields = dataclass_fields(tree) - return fields, [row(element, fields) for element in table_elements(tree)] diff --git a/internals/checks/src/checks/platforms_mirror/python_table_test.py b/internals/checks/src/checks/platforms_mirror/python_table_test.py deleted file mode 100644 index 921987325..000000000 --- a/internals/checks/src/checks/platforms_mirror/python_table_test.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Colocated unit tests for the Python-table reader (isolation -- pure text in, -data out; nothing here touches the repo's real platforms.py). -""" - -from checks.platforms_mirror.python_table import python_table - -PYTHON = '''\ -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Platform: - node_platform: str - node_arch: str - slug: str - - -PLATFORMS: tuple[Platform, ...] = ( - Platform("linux", "x64", "linux-x64-gnu"), - Platform("win32", "x64", slug="win32-x64-msvc", node_arch="x64"), -) -''' - - -def describe_python_table(): - def it_reads_the_annotated_fields_and_the_rows(): - fields, rows = python_table(PYTHON) - assert fields == ["node_platform", "node_arch", "slug"] - assert rows[0] == {"node_platform": "linux", "node_arch": "x64", "slug": "linux-x64-gnu"} - - def it_reads_every_row_of_the_table(): - assert len(python_table(PYTHON)[1]) == 2 - - def it_names_each_positional_after_the_field_in_that_position(): - # The field list is what turns a row's positionals into names, so the - # two readers have to be composed in that order. - _, rows = python_table(PYTHON) - assert rows[1] == { - "node_platform": "win32", - "node_arch": "x64", - "slug": "win32-x64-msvc", - } diff --git a/internals/checks/src/checks/platforms_mirror/read.py b/internals/checks/src/checks/platforms_mirror/read.py deleted file mode 100644 index e9e8c72dc..000000000 --- a/internals/checks/src/checks/platforms_mirror/read.py +++ /dev/null @@ -1,12 +0,0 @@ -"""The default reader for the platforms-mirror check (#1004). - -Split from the gate so the orchestration holds no I/O: the gate takes this as -an injected default and unit-tests against text rather than the repo's files. -""" - -from __future__ import annotations - - -def read(path: str) -> str: - with open(path, encoding="utf-8") as handle: - return handle.read() diff --git a/internals/checks/src/checks/platforms_mirror/read_test.py b/internals/checks/src/checks/platforms_mirror/read_test.py deleted file mode 100644 index d868fe66f..000000000 --- a/internals/checks/src/checks/platforms_mirror/read_test.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Colocated unit test for the platforms-mirror reader (isolation -- a scratch -file, never the repo's real platforms.py / platforms.ts). -""" - -from checks.platforms_mirror.read import read - - -def describe_read(): - def it_reads_a_file_as_utf8_text(tmp_path): - path = tmp_path / "platforms.ts" - path.write_text("// caffè\n", encoding="utf-8") - assert read(str(path)) == "// caffè\n" - - def it_reads_a_file_whole(tmp_path): - path = tmp_path / "platforms.ts" - path.write_text("one\ntwo\n", encoding="utf-8") - assert read(str(path)) == "one\ntwo\n" diff --git a/internals/checks/src/checks/platforms_mirror/requoted.py b/internals/checks/src/checks/platforms_mirror/requoted.py deleted file mode 100644 index 53e3076b1..000000000 --- a/internals/checks/src/checks/platforms_mirror/requoted.py +++ /dev/null @@ -1,16 +0,0 @@ -"""A TypeScript string literal as a JSON one. - -Single-quoted and template forms carry the same characters JSON spells with -double quotes, so re-quoting is what lets `json` read the table as data. -""" - -from __future__ import annotations - -import json - - -def requoted(text: str) -> str: - """A TypeScript string literal as a JSON one.""" - if text.startswith('"'): - return text - return json.dumps(text[1:-1].replace('\\"', '"').replace("\\'", "'")) diff --git a/internals/checks/src/checks/platforms_mirror/requoted_test.py b/internals/checks/src/checks/platforms_mirror/requoted_test.py deleted file mode 100644 index dd9cc4d09..000000000 --- a/internals/checks/src/checks/platforms_mirror/requoted_test.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Colocated unit tests for the string re-quoter (isolation -- pure text in, -text out). -""" - -from checks.platforms_mirror.requoted import requoted - - -def describe_requoted(): - def it_keeps_a_double_quoted_literal_verbatim(): - assert requoted('"linux"') == '"linux"' - - def it_rewrites_a_single_quoted_literal_as_json(): - assert requoted("'linux'") == '"linux"' - - def it_rewrites_a_template_literal_as_json(): - assert requoted("`linux`") == '"linux"' - - def it_keeps_the_contents_of_a_single_quoted_literal_whole(): - assert requoted("'aba'") == '"aba"' - - def it_unescapes_an_escaped_single_quote(): - assert requoted(r"'it\'s'") == '"it\'s"' - - def it_unescapes_an_escaped_double_quote_inside_a_template(): - assert requoted(r"`a\"b`") == '"a\\"b"' - - def it_escapes_a_double_quote_that_was_bare_in_a_single_quoted_literal(): - assert requoted("'a\"b'") == '"a\\"b"' diff --git a/internals/checks/src/checks/platforms_mirror/row.py b/internals/checks/src/checks/platforms_mirror/row.py deleted file mode 100644 index 33e59cec8..000000000 --- a/internals/checks/src/checks/platforms_mirror/row.py +++ /dev/null @@ -1,32 +0,0 @@ -"""One `Platform(...)` entry of the Python table, as a dict (#1004). - -Read statically rather than evaluated, so a row built by anything other than a -literal call -- a splat, a name, a computed value -- is a `ParseError` instead -of a row the check would go on comparing. -""" - -from __future__ import annotations - -import ast - -from .call_name import call_name -from .literal import literal -from .parse import CLASS_NAME, ParseError, TABLE_NAME - -ROW_CALLS = frozenset({CLASS_NAME}) - - -def row(element: ast.expr, fields: list[str]) -> dict: - if call_name(element) not in ROW_CALLS: - raise ParseError(f"every {TABLE_NAME} entry must be a literal `{CLASS_NAME}(...)` call.") - if len(element.args) > len(fields): - raise ParseError( - f"a {CLASS_NAME}(...) row passes {len(element.args)} positional arguments but " - f"{CLASS_NAME} declares {len(fields)} fields." - ) - values = dict(zip(fields, (literal(argument) for argument in element.args))) - for keyword in element.keywords: - if keyword.arg is None: - raise ParseError(f"a {CLASS_NAME}(...) row splats **kwargs; read it statically.") - values[keyword.arg] = literal(keyword.value) - return values diff --git a/internals/checks/src/checks/platforms_mirror/row_test.py b/internals/checks/src/checks/platforms_mirror/row_test.py deleted file mode 100644 index 0c9439a80..000000000 --- a/internals/checks/src/checks/platforms_mirror/row_test.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Colocated unit tests for reading one `Platform(...)` entry (isolation -- an -`ast` node in, a dict out; nothing here touches the repo's real platforms.py). -""" - -import ast - -import pytest - -from checks.platforms_mirror.row import ParseError, row - -FIELDS = ["node_platform", "node_arch", "slug"] - - -def entry(source: str) -> ast.expr: - """A table entry, as `ast` hands it over from the PLATFORMS assignment.""" - return ast.parse(source, mode="eval").body - - -def describe_row(): - def it_names_every_positional_after_the_field_in_that_position(): - assert row(entry('Platform("linux", "x64", "linux-x64-gnu")'), FIELDS) == { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - } - - def it_lets_keywords_override_positionals(): - source = 'Platform("win32", "ia32", slug="win32-x64-msvc", node_arch="x64")' - assert row(entry(source), FIELDS) == { - "node_platform": "win32", - "node_arch": "x64", - "slug": "win32-x64-msvc", - } - - def it_reads_a_list_valued_keyword(): - assert row(entry('Platform(os=["linux", "android"])'), ["os"]) == { - "os": ["linux", "android"] - } - - def it_reads_a_row_that_fills_fewer_fields_than_the_class_declares(): - assert row(entry('Platform("linux")'), FIELDS) == {"node_platform": "linux"} - - @pytest.mark.parametrize( - "source", - ["1", "Machine('a')", "Target('a')", "platforms.Platform('a')"], - ids=["not-a-call", "machine", "target", "dotted"], - ) - def it_rejects_an_entry_that_is_not_a_literal_platform_call(source): - with pytest.raises(ParseError, match="every PLATFORMS entry must be a literal `Platform"): - row(entry(source), FIELDS) - - def it_counts_the_positionals_and_the_fields_when_there_are_too_many(): - with pytest.raises( - ParseError, match="passes 2 positional arguments but Platform declares 1 fields" - ): - row(entry('Platform("a", "b")'), ["slug"]) - - def it_rejects_a_row_that_splats_keywords(): - with pytest.raises(ParseError, match="splats"): - row(entry("Platform(**other)"), ["slug"]) - - def it_rejects_a_non_literal_positional(): - with pytest.raises(ParseError, match="not a literal"): - row(entry("Platform(slug)"), ["slug"]) - - def it_rejects_a_non_literal_keyword_value(): - with pytest.raises(ParseError, match="not a literal"): - row(entry("Platform(slug=name)"), ["slug"]) diff --git a/internals/checks/src/checks/platforms_mirror/stray_rows.py b/internals/checks/src/checks/platforms_mirror/stray_rows.py deleted file mode 100644 index 00356e9df..000000000 --- a/internals/checks/src/checks/platforms_mirror/stray_rows.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Python rows for targets the release never publishes.""" - -from __future__ import annotations - -from .vocabulary import PYTHON_FILE, TYPESCRIPT_FILE, key - - -def stray_rows(typescript_keys, python_rows) -> list[str]: - """One message per platforms.py row that is not a published target.""" - return [ - f"{key(row['node_platform'], row['node_arch'])} has a row in platforms.py " - f"but is not a published target in platforms.ts. Either add the target to " - f"PLATFORMS in {TYPESCRIPT_FILE} or drop the row from {PYTHON_FILE}: " - f"distcheck must not resolve a sub-package the release never publishes." - for row in python_rows - if key(row["node_platform"], row["node_arch"]) not in typescript_keys - ] diff --git a/internals/checks/src/checks/platforms_mirror/stray_rows_test.py b/internals/checks/src/checks/platforms_mirror/stray_rows_test.py deleted file mode 100644 index 890fdb3c6..000000000 --- a/internals/checks/src/checks/platforms_mirror/stray_rows_test.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Colocated unit tests for the unpublished-row verdict (isolation -- pure dicts -in, messages out). -""" - -from checks.platforms_mirror.stray_rows import stray_rows - -PY_LINUX = { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], -} - - -def describe_stray_rows(): - def it_passes_when_every_row_is_published(): - assert stray_rows({"linux-x64"}, [PY_LINUX]) == [] - - def it_names_a_row_the_release_never_publishes(): - (message,) = stray_rows(set(), [PY_LINUX]) - assert message.startswith("linux-x64 has a row in platforms.py") - assert "packages/ts/src/platforms.ts" in message - - def it_names_both_files_the_disagreement_can_be_settled_in(): - (message,) = stray_rows(set(), [PY_LINUX]) - assert "internals/distcheck/src/distcheck/node_flow/platforms.py" in message diff --git a/internals/checks/src/checks/platforms_mirror/table_elements.py b/internals/checks/src/checks/platforms_mirror/table_elements.py deleted file mode 100644 index 5e3171bbd..000000000 --- a/internals/checks/src/checks/platforms_mirror/table_elements.py +++ /dev/null @@ -1,25 +0,0 @@ -"""The row expressions of a `platforms.py`-shaped module's `PLATFORMS` table. - -Read statically off the module-level binding, so a table that builds itself at -import time is a `ParseError` rather than a read. -""" - -from __future__ import annotations - -import ast - -from .assigned_names import assigned_names -from .parse import ParseError, TABLE_NAME - - -def table_elements(tree: ast.Module) -> list[ast.expr]: - bindings = {name: node for node in tree.body for name in assigned_names(node)} - assignment = bindings.get(TABLE_NAME) - if assignment is None: - raise ParseError(f"no module-level `{TABLE_NAME} = (...)` assignment.") - if not isinstance(assignment.value, (ast.Tuple, ast.List)): - raise ParseError( - f"{TABLE_NAME} is not a tuple or list literal; this check reads its rows " - f"statically and cannot evaluate a computed table." - ) - return list(assignment.value.elts) diff --git a/internals/checks/src/checks/platforms_mirror/table_elements_test.py b/internals/checks/src/checks/platforms_mirror/table_elements_test.py deleted file mode 100644 index 136688e57..000000000 --- a/internals/checks/src/checks/platforms_mirror/table_elements_test.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Colocated unit tests for the PLATFORMS table reader (isolation -- pure text -in, AST nodes out; nothing here touches the repo's real platforms.py). -""" - -import ast - -import pytest - -from checks.platforms_mirror.table_elements import ParseError, table_elements - - -def elements(source: str): - return table_elements(ast.parse(source)) - - -def describe_table_elements(): - def it_reads_the_rows_of_an_annotated_assignment(): - assert len(elements("PLATFORMS: tuple = (Platform('a'), Platform('b'))\n")) == 2 - - def it_reads_a_plain_assignment_without_an_annotation(): - assert len(elements("PLATFORMS = (Platform('a'),)\n")) == 1 - - def it_reads_a_list_literal(): - assert len(elements("PLATFORMS = [Platform('a')]\n")) == 1 - - def it_reads_a_table_bound_alongside_another_name(): - assert len(elements("OTHERS = PLATFORMS = (Platform('a'),)\n")) == 1 - - def it_hands_back_the_row_expressions_themselves(): - (element,) = elements("PLATFORMS = (Platform('a'),)\n") - assert isinstance(element, ast.Call) - - @pytest.mark.parametrize("name", ["OTHERS", "TARGETS"]) - def it_rejects_a_table_bound_under_another_name(name): - with pytest.raises(ParseError, match="no module-level"): - elements(f"{name} = ()\n") - - def it_rejects_a_module_that_binds_no_names_at_all(): - with pytest.raises(ParseError, match="no module-level"): - elements("import os\n") - - def it_rejects_a_computed_table(): - with pytest.raises(ParseError, match="not a tuple or list literal"): - elements("PLATFORMS = tuple(rows)\n") - - def it_names_the_table_it_could_not_find(): - with pytest.raises(ParseError, match="`PLATFORMS = "): - elements("OTHERS = ()\n") diff --git a/internals/checks/src/checks/platforms_mirror/typescript_table.py b/internals/checks/src/checks/platforms_mirror/typescript_table.py deleted file mode 100644 index 0f804c6b3..000000000 --- a/internals/checks/src/checks/platforms_mirror/typescript_table.py +++ /dev/null @@ -1,36 +0,0 @@ -"""The `PLATFORMS` rows of a `platforms.ts`-shaped module. - -No node toolchain and no evaluation: the source is stripped of comments, -normalized to JSON, and handed to `json`'s own decoder, which finds the end of -the array so nothing here counts brackets. -""" - -from __future__ import annotations - -import json -import re - -from .as_json import as_json -from .parse import ParseError, TABLE_NAME -from .without_comments import without_comments - - -def typescript_table(source: str) -> list[dict]: - """Rows of the `PLATFORMS` array in a `platforms.ts`-shaped module.""" - cleaned = without_comments(source) - marker = re.search(rf"\b{TABLE_NAME}\b[^=;]*=\s*(\[)", cleaned) - if marker is None: - raise ParseError(f"no `{TABLE_NAME} = [...]` assignment in the TypeScript source.") - try: - # `raw_decode` stops at the end of the array, so nothing here has to find - # the closing bracket or care what follows it. - rows, _ = json.JSONDecoder().raw_decode(as_json(cleaned[marker.start(1) :])) - except json.JSONDecodeError as error: - raise ParseError( - f"`{TABLE_NAME}` is not a plain array of object literals ({error}); this check " - f"reads it as data and cannot evaluate spreads, computed keys or identifiers, " - f"and cannot read an unterminated array, string or comment." - ) from error - if not all(isinstance(row, dict) for row in rows): - raise ParseError(f"every `{TABLE_NAME}` entry must be an object literal.") - return rows diff --git a/internals/checks/src/checks/platforms_mirror/typescript_table_test.py b/internals/checks/src/checks/platforms_mirror/typescript_table_test.py deleted file mode 100644 index 78dd277cd..000000000 --- a/internals/checks/src/checks/platforms_mirror/typescript_table_test.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Colocated unit tests for the TypeScript-table reader (isolation -- pure text -in, data out; nothing here touches the repo's real platforms.ts). -""" - -from unittest import mock - -import pytest - -from checks.platforms_mirror.typescript_table import ParseError, typescript_table - -TYPESCRIPT = """\ -// Leading comment with a "quoted" brace { and a stray ]. -export const PLATFORMS: readonly Platform[] = [ - { - /* block comment */ - triple: "x86_64-unknown-linux-gnu", - nodePlatform: "linux", - os: ["linux"], // trailing comment - }, -]; -""" - - -def describe_typescript_table(): - def it_reads_the_array_through_comments_and_trailing_commas(): - assert typescript_table(TYPESCRIPT) == [ - { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "os": ["linux"], - } - ] - - def it_normalizes_single_quoted_strings(): - assert typescript_table("const PLATFORMS = [{ os: ['linux'] }];") == [{"os": ["linux"]}] - - def it_unwraps_a_single_quoted_string_without_trimming_its_contents(): - assert typescript_table("const PLATFORMS = [{ os: ['aba'] }];") == [{"os": ["aba"]}] - - def it_unwraps_a_template_literal(): - assert typescript_table("const PLATFORMS = [{ os: [`aba`] }];") == [{"os": ["aba"]}] - - def it_keeps_a_double_quoted_string_verbatim(): - assert typescript_table('const PLATFORMS = [{ os: ["aba"] }];') == [{"os": ["aba"]}] - - def it_finds_the_array_when_a_comment_mentions_a_bracket(): - source = "// see PLATFORMS = [ elsewhere\nconst PLATFORMS = [{ os: ['a'] }];" - assert typescript_table(source) == [{"os": ["a"]}] - - def it_keeps_an_escape_sequence_inside_a_string(): - assert typescript_table(r'const PLATFORMS = [{ os: "a\"b" }];') == [{"os": 'a"b'}] - - def it_rejects_a_source_with_no_table(): - with pytest.raises(ParseError, match="no `PLATFORMS"): - typescript_table("export const OTHER = [];\n") - - @pytest.mark.parametrize( - "source", - [ - "const PLATFORMS = [{ os: [] }\n", - "const PLATFORMS = [ /* open\n", - 'const PLATFORMS = [{ os: "open\n', - 'const PLATFORMS = [{ os: "open\\', - "const PLATFORMS = [ // open", - ], - ids=["unclosed-array", "unclosed-block-comment", "unclosed-string", "trailing-backslash", "comment-to-eof"], - ) - def it_rejects_a_table_it_cannot_read_to_the_end(source): - with pytest.raises(ParseError, match="not a plain array"): - typescript_table(source) - - def it_rejects_an_entry_that_is_not_an_object_literal(): - with pytest.raises(ParseError, match="object literal"): - typescript_table('const PLATFORMS = ["linux"];') - - def it_rejects_a_table_it_cannot_read_as_data(): - with pytest.raises(ParseError, match="not a plain array"): - typescript_table("const PLATFORMS = [{ ...spread }];") - - def it_strips_comments_through_the_helper_in_its_own_module(): - with mock.patch( - "checks.platforms_mirror.typescript_table.without_comments", - return_value='PLATFORMS = [{"os": ["a"]}]', - ) as stripper: - assert typescript_table("// only the stripper sees this") == [{"os": ["a"]}] - stripper.assert_called_once_with("// only the stripper sees this") - - def it_normalizes_to_json_through_the_helper_in_its_own_module(): - with mock.patch( - "checks.platforms_mirror.typescript_table.as_json", - return_value='[{"os": ["a"]}]', - ) as normalizer: - assert typescript_table("const PLATFORMS = [{ os: ['b'] }];") == [{"os": ["a"]}] - normalizer.assert_called_once_with("""[{ os: ["b"] }];""") diff --git a/internals/checks/src/checks/platforms_mirror/typescript_value.py b/internals/checks/src/checks/platforms_mirror/typescript_value.py deleted file mode 100644 index 594554d89..000000000 --- a/internals/checks/src/checks/platforms_mirror/typescript_value.py +++ /dev/null @@ -1,14 +0,0 @@ -"""One TypeScript row's value for a Python field name.""" - -from __future__ import annotations - -from .vocabulary import DERIVE, SHARED - - -def typescript_value(field: str, row: dict): - """``row``'s value for a Python field name.""" - value = row.get(SHARED[field]) - derive = DERIVE.get(field) - if derive is None or not isinstance(value, str): - return value - return derive(value) diff --git a/internals/checks/src/checks/platforms_mirror/typescript_value_test.py b/internals/checks/src/checks/platforms_mirror/typescript_value_test.py deleted file mode 100644 index 9d91fc7b3..000000000 --- a/internals/checks/src/checks/platforms_mirror/typescript_value_test.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Colocated unit tests for reading a Python field off a TypeScript row -(isolation -- a dict in, a value out). -""" - -from checks.platforms_mirror.typescript_value import DERIVE, typescript_value - -TS_LINUX = { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "libc": ["glibc"], -} - - -def describe_typescript_value(): - def it_strips_the_sub_package_prefix_off_the_slug(): - assert typescript_value("slug", TS_LINUX) == "linux-x64-gnu" - - def it_reads_other_fields_straight_through(): - assert typescript_value("cpu", TS_LINUX) == ["x64"] - - def it_leaves_a_lib_name_without_the_prefix_alone(): - assert typescript_value("slug", {"libName": "@dirsql/nope"}) == "@dirsql/nope" - - def it_derives_only_the_fields_that_need_it(): - assert set(DERIVE) == {"slug"} - - def it_does_not_strip_a_prefix_off_a_straight_read_field(): - row = {"nodePlatform": "@dirsql/lib-linux"} - assert typescript_value("node_platform", row) == "@dirsql/lib-linux" - - def it_returns_none_for_a_missing_property(): - assert typescript_value("slug", {}) is None diff --git a/internals/checks/src/checks/platforms_mirror/vocabulary.py b/internals/checks/src/checks/platforms_mirror/vocabulary.py deleted file mode 100644 index b73541814..000000000 --- a/internals/checks/src/checks/platforms_mirror/vocabulary.py +++ /dev/null @@ -1,46 +0,0 @@ -"""What the two platform tables share, and where each Python field comes from. - -`internals/distcheck`'s `Platform` dataclass is a hand-maintained subset of -`packages/ts/src/platforms.ts`, which is the release source of truth for the -published `@dirsql/lib-*` sub-packages. The subset framing is deliberate -- -distcheck needs only enough of each target to reconstruct the host's -sub-package and find its staged addon -- but it was asserted by a comment and -checked by nothing, and it drifted both ways: `exe` sat on the Python side -justified as mirroring TypeScript, which has never had the field. - -So the invariant is the subset, stated as data: `SHARED` names every Python -field and where its value comes from, both tables carry the same set of -targets, and a Python field outside `SHARED` is drift by construction. -`triple` and `libc` staying TypeScript-only is the subset working as intended, -not a gap. -""" - -from __future__ import annotations - -PYTHON_FILE = "internals/distcheck/src/distcheck/node_flow/platforms.py" -TYPESCRIPT_FILE = "packages/ts/src/platforms.ts" - -# Python field -> the TypeScript property it is sourced from. `slug` is the -# sub-package name minus its prefix, which is what `librarySlug()` returns. -SHARED = { - "node_platform": "nodePlatform", - "node_arch": "nodeArch", - "slug": "libName", - "os": "os", - "cpu": "cpu", -} -LIB_PREFIX = "@dirsql/lib-" - - -def slug(lib_name: str) -> str: - return lib_name.removeprefix(LIB_PREFIX) - - -# Python field -> how its TypeScript value is derived, for the fields that are -# not a straight read. Keyed rather than branched so the field name is never -# compared, only looked up. -DERIVE = {"slug": slug} - - -def key(node_platform: str, node_arch: str) -> str: - return f"{node_platform}-{node_arch}" diff --git a/internals/checks/src/checks/platforms_mirror/vocabulary_test.py b/internals/checks/src/checks/platforms_mirror/vocabulary_test.py deleted file mode 100644 index 90004be8b..000000000 --- a/internals/checks/src/checks/platforms_mirror/vocabulary_test.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Colocated unit tests for the shared platform vocabulary (isolation -- names -and pure lookups, no collaborators). -""" - -from checks.platforms_mirror.vocabulary import ( - DERIVE, - LIB_PREFIX, - PYTHON_FILE, - SHARED, - TYPESCRIPT_FILE, - key, - slug, -) - - -def describe_shared(): - def it_names_every_mirrored_field_and_its_typescript_source(): - assert SHARED == { - "node_platform": "nodePlatform", - "node_arch": "nodeArch", - "slug": "libName", - "os": "os", - "cpu": "cpu", - } - - def it_leaves_the_typescript_only_fields_out_of_the_subset(): - assert "triple" not in SHARED - assert "libc" not in SHARED - assert "exe" not in SHARED - - -def describe_derive(): - def it_derives_only_the_slug(): - assert set(DERIVE) == {"slug"} - assert DERIVE["slug"] is slug - - -def describe_slug(): - def it_strips_the_sub_package_prefix(): - assert slug("@dirsql/lib-darwin-arm64") == "darwin-arm64" - - def it_leaves_a_name_without_the_prefix_alone(): - assert slug("@dirsql/darwin-arm64") == "@dirsql/darwin-arm64" - - def it_reads_the_prefix_the_release_publishes_under(): - assert LIB_PREFIX == "@dirsql/lib-" - - -def describe_key(): - def it_joins_the_platform_and_the_arch_with_a_dash(): - assert key("linux", "x64") == "linux-x64" - - def it_keeps_the_platform_first(): - assert key("x64", "linux") == "x64-linux" - - -def describe_the_mirrored_files(): - def it_names_both_sides_of_the_mirror(): - assert PYTHON_FILE == "internals/distcheck/src/distcheck/node_flow/platforms.py" - assert TYPESCRIPT_FILE == "packages/ts/src/platforms.ts" diff --git a/internals/checks/src/checks/platforms_mirror/without_comments.py b/internals/checks/src/checks/platforms_mirror/without_comments.py deleted file mode 100644 index debd8d4b6..000000000 --- a/internals/checks/src/checks/platforms_mirror/without_comments.py +++ /dev/null @@ -1,27 +0,0 @@ -"""A `platforms.ts`-shaped source with its comments stripped and strings requoted. - -One alternation of "things a naive scan would cut in half": the three string -forms come first, so a `//` inside a string is matched as string rather than as -the comment that follows it. -""" - -from __future__ import annotations - -import re - -from .requoted import requoted - -_TOKENS = re.compile( - r'"(?:\\.|[^"\\])*"' r"|'(?:\\.|[^'\\])*'" r"|`(?:\\.|[^`\\])*`" r"|//[^\n]*" r"|/\*.*?\*/", - re.S, -) - - -def without_comments(source: str) -> str: - """`source` with comments removed and every string literal JSON-quoted.""" - - def keep(match: re.Match) -> str: - text = match.group(0) - return "" if text.startswith(("//", "/*")) else requoted(text) - - return _TOKENS.sub(keep, source) diff --git a/internals/checks/src/checks/platforms_mirror/without_comments_test.py b/internals/checks/src/checks/platforms_mirror/without_comments_test.py deleted file mode 100644 index dfe905fc8..000000000 --- a/internals/checks/src/checks/platforms_mirror/without_comments_test.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Colocated unit tests for the comment stripper (isolation -- pure text in, -text out; nothing here touches the repo's real platforms.ts). -""" - -from checks.platforms_mirror.without_comments import without_comments - - -def describe_without_comments(): - def it_strips_a_line_comment(): - assert without_comments("a // gone\nb") == "a \nb" - - def it_strips_a_line_comment_that_runs_to_end_of_file(): - assert without_comments("a // gone") == "a " - - def it_strips_a_block_comment(): - assert without_comments("a /* gone */ b") == "a b" - - def it_strips_a_block_comment_spanning_lines(): - assert without_comments("a /* one\ntwo */ b") == "a b" - - def it_keeps_a_double_quoted_string_verbatim(): - assert without_comments('x = "linux"') == 'x = "linux"' - - def it_requotes_a_single_quoted_string(): - assert without_comments("x = 'linux'") == 'x = "linux"' - - def it_requotes_a_template_literal(): - assert without_comments("x = `linux`") == 'x = "linux"' - - def it_does_not_read_a_line_comment_marker_inside_a_string(): - assert without_comments('x = "a // b"') == 'x = "a // b"' - - def it_does_not_read_a_block_comment_marker_inside_a_string(): - assert without_comments("x = 'a /* b'") == 'x = "a /* b"' - - def it_keeps_an_escaped_quote_inside_a_string(): - assert without_comments(r'x = "a\"b"') == r'x = "a\"b"' - - def it_leaves_source_with_neither_comment_nor_string_alone(): - assert without_comments("[1, 2]") == "[1, 2]" diff --git a/internals/checks/tests/e2e/platforms_mirror_test.py b/internals/checks/tests/e2e/platforms_mirror_test.py deleted file mode 100644 index e953e5430..000000000 --- a/internals/checks/tests/e2e/platforms_mirror_test.py +++ /dev/null @@ -1,98 +0,0 @@ -"""E2E test for `dirsql-checks platforms-mirror` through the real CLI. - -No mocking of any kind: spawns the packaged `dirsql-checks` console script as a -subprocess, once against real scratch files and once against the repo's own -`platforms.py` / `platforms.ts` through the command's default paths. -""" - -from __future__ import annotations - -import shutil -import subprocess -from pathlib import Path - -REPO = Path(__file__).resolve().parents[4] - -PYTHON_SOURCE = '''\ -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Platform: - node_platform: str - node_arch: str - slug: str - os: list[str] - cpu: list[str] -{extra} - -PLATFORMS: tuple[Platform, ...] = ( - Platform("linux", "x64", "linux-x64-gnu", ["linux"], ["x64"]{value}), -) -''' - -TYPESCRIPT_SOURCE = """\ -export const PLATFORMS: readonly Platform[] = [ - { - triple: "x86_64-unknown-linux-gnu", - nodePlatform: "linux", - nodeArch: "x64", - libName: "@dirsql/lib-linux-x64-gnu", - os: ["linux"], - cpu: ["x64"], - libc: ["glibc"], - }, -]; -""" - - -def _cli() -> str: - dirsql_checks = shutil.which("dirsql-checks") - assert dirsql_checks is not None, ( - "`dirsql-checks` console script not on PATH -- run " - "`uv run --project internals/checks pytest tests/e2e` " - "or `uv sync --project internals/checks`" - ) - return dirsql_checks - - -def _write(tmp_path, *, stray_field: bool): - python = tmp_path / "platforms.py" - python.write_text( - PYTHON_SOURCE.format( - extra=" exe: bool\n" if stray_field else "", - value=", False" if stray_field else "", - ) - ) - typescript = tmp_path / "platforms.ts" - typescript.write_text(TYPESCRIPT_SOURCE) - return ["--python", str(python), "--typescript", str(typescript)] - - -def _invoke(arguments, cwd=None): - return subprocess.run( - [_cli(), "platforms-mirror", *arguments], - capture_output=True, - text=True, - timeout=60, - cwd=cwd, - ) - - -def describe_dirsql_checks_platforms_mirror(): - def it_exits_zero_when_the_subset_agrees(tmp_path): - proc = _invoke(_write(tmp_path, stray_field=False)) - - assert proc.returncode == 0, f"stdout={proc.stdout!r} stderr={proc.stderr!r}" - assert "ok platforms-mirror" in proc.stdout - - def it_exits_nonzero_and_names_the_unmirrored_field(tmp_path): - proc = _invoke(_write(tmp_path, stray_field=True)) - - assert proc.returncode == 1, f"stdout={proc.stdout!r} stderr={proc.stderr!r}" - assert "Platform.exe" in proc.stdout - - def it_holds_the_repos_own_files_through_the_default_paths(): - proc = _invoke([], cwd=REPO) - - assert proc.returncode == 0, f"stdout={proc.stdout!r} stderr={proc.stderr!r}" diff --git a/internals/checks/tests/integration/platforms_mirror_test.py b/internals/checks/tests/integration/platforms_mirror_test.py deleted file mode 100644 index c680d275e..000000000 --- a/internals/checks/tests/integration/platforms_mirror_test.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Integration tests for the platforms-mirror check against real files. - -Exercises `gate.run` with its default collaborators -- a real `ast` parse of a -real `platforms.py` and a real parse of a real `platforms.ts` -- rather than the -packaged `dirsql-checks` CLI. - -The reproduction is #1004 exactly: `Platform.exe` sat on the Python dataclass for -the stated reason that it "mirrors packages/ts/src/platforms.ts", while the -TypeScript source of truth has never carried the field. A comment asserted the -correspondence and nothing checked it, which is the same mechanism behind #972 -and #996. The last case here is the one that matters day to day: the repo's own -two files, held to the invariant on every run. -""" - -from __future__ import annotations - -from pathlib import Path - -from checks.platforms_mirror.gate import run - -REPO = Path(__file__).resolve().parents[4] -PYTHON = REPO / "internals" / "distcheck" / "src" / "distcheck" / "node_flow" / "platforms.py" -TYPESCRIPT = REPO / "packages" / "ts" / "src" / "platforms.ts" - -ANNOTATIONS = { - "node_platform": "str", - "node_arch": "str", - "slug": "str", - "os": "list[str]", - "cpu": "list[str]", - "exe": "bool", -} - -SUBSET = ("node_platform", "node_arch", "slug", "os", "cpu") - -LINUX = { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "exe": False, -} -WINDOWS = { - "node_platform": "win32", - "node_arch": "x64", - "slug": "win32-x64-msvc", - "os": ["win32"], - "cpu": ["x64"], - "exe": True, -} - -PYTHON_SOURCE = '''\ -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Platform: -{fields} - - @property - def name(self) -> str: - return f"@dirsql/lib-{{self.slug}}" - - -PLATFORMS: tuple[Platform, ...] = ( -{rows} -) -''' - -TYPESCRIPT_SOURCE = """\ -export interface Platform {{ - triple: string; - nodePlatform: NodeJS.Platform; - nodeArch: NodeJS.Architecture; - libName: string; - os: string[]; - cpu: string[]; - libc?: string[]; -}} - -export const PLATFORMS: readonly Platform[] = [ -{rows} -]; -""" - - -def write(tmp_path, python_rows, typescript_rows, fields=SUBSET): - python = tmp_path / "platforms.py" - python.write_text( - PYTHON_SOURCE.format( - fields="\n".join(f" {name}: {ANNOTATIONS[name]}" for name in fields), - rows="\n".join( - f" Platform({', '.join(repr(row[name]) for name in fields)})," - for row in python_rows - ), - ) - ) - typescript = tmp_path / "platforms.ts" - typescript.write_text( - TYPESCRIPT_SOURCE.format( - rows="\n".join( - " {\n" - f' triple: "{row["node_arch"]}-{row["node_platform"]}",\n' - f' nodePlatform: "{row["node_platform"]}",\n' - f' nodeArch: "{row["node_arch"]}",\n' - f' libName: "{row.get("lib_name", "@dirsql/lib-" + row["slug"])}",\n' - f' os: {list(row["os"])!r},\n' - f' cpu: {list(row["cpu"])!r},\n' - " }," - for row in typescript_rows - ).replace("'", '"') - ) - ) - return str(python), str(typescript) - - -def describe_run_against_real_platform_files(): - def it_passes_when_the_declared_subset_agrees(tmp_path, capsys): - assert run(*write(tmp_path, [LINUX, WINDOWS], [LINUX, WINDOWS])) == 0 - assert "ok platforms-mirror" in capsys.readouterr().out - - def it_rejects_a_python_field_with_no_typescript_counterpart(tmp_path, capsys): - # #1004: `exe` justified by a mirror that never had it. - code = run(*write(tmp_path, [LINUX, WINDOWS], [LINUX, WINDOWS], fields=(*SUBSET, "exe"))) - - assert code == 1 - out = capsys.readouterr().out - assert "::error::Platform.exe" in out - assert "no counterpart in packages/ts/src/platforms.ts" in out - - def it_rejects_a_published_target_missing_from_the_python_rows(tmp_path, capsys): - # A new target half-added: published by the TS source of truth, invisible - # to the node distcheck flow. - code = run(*write(tmp_path, [LINUX], [LINUX, WINDOWS])) - - assert code == 1 - assert "::error::win32-x64 is published by platforms.ts" in capsys.readouterr().out - - def it_rejects_a_python_row_that_is_not_a_published_target(tmp_path, capsys): - code = run(*write(tmp_path, [LINUX, WINDOWS], [LINUX])) - - assert code == 1 - assert "::error::win32-x64 has a row in platforms.py" in capsys.readouterr().out - - def it_rejects_a_shared_field_whose_values_disagree(tmp_path, capsys): - drifted = {**WINDOWS, "cpu": ["arm64"]} - code = run(*write(tmp_path, [LINUX, drifted], [LINUX, WINDOWS])) - - assert code == 1 - out = capsys.readouterr().out - assert "::error::win32-x64: cpu is ['arm64'] in platforms.py" in out - assert "['x64'] in platforms.ts" in out - - def it_rejects_a_lib_name_that_does_not_carry_the_sub_package_prefix(tmp_path, capsys): - stray = {**WINDOWS, "lib_name": "@dirsql/win32-x64-msvc"} - code = run(*write(tmp_path, [LINUX, WINDOWS], [LINUX, stray])) - - assert code == 1 - assert "@dirsql/lib-" in capsys.readouterr().out - - -def describe_run_against_the_repos_own_files(): - def it_holds_the_committed_platform_tables_to_the_invariant(capsys): - assert run(str(PYTHON), str(TYPESCRIPT)) == 0, capsys.readouterr().out diff --git a/internals/distcheck/src/distcheck/node_flow/platforms.py b/internals/distcheck/src/distcheck/node_flow/platforms.py index 2f20d18e6..3044c80f9 100644 --- a/internals/distcheck/src/distcheck/node_flow/platforms.py +++ b/internals/distcheck/src/distcheck/node_flow/platforms.py @@ -1,17 +1,22 @@ """Host-platform detection for the node distcheck flow (#520). -A Python port of the host-row lookup in `packages/ts/src/platforms.ts`: enough -of each published target to reconstruct the host's `@dirsql/lib-` -sub-package (name, os/cpu constraints, addon filename) and locate its staged -addon. Pure -- callers pass the detected `sys.platform` / `platform.machine()`, -so the lookup is unit-testable. +Reads the published-target table from `packages/ts/src/platforms.json`, the one +declarative copy shared with the TypeScript SDK, and keeps the fields this flow +needs to reconstruct the host's `@dirsql/lib-` sub-package (name, os/cpu +constraints, addon filename) and locate its staged addon. The lookup is pure -- +callers pass the detected `sys.platform` / `platform.machine()`. """ from __future__ import annotations +import json from dataclasses import dataclass +from pathlib import Path from .arch import to_node_arch +_LIB_PREFIX = "@dirsql/lib-" +_TABLE = Path(__file__).parents[5] / "packages" / "ts" / "src" / "platforms.json" + @dataclass(frozen=True) class Platform: @@ -36,16 +41,15 @@ def addon_name(self) -> str: return f"dirsql.{self.slug}.node" -# The subset of PLATFORMS in packages/ts/src/platforms.ts this flow needs -- that -# file is the release source of truth for the published sub-packages, and its -# `triple` and `libc` reconstruct nothing here. `dirsql-checks platforms-mirror` -# holds the two tables to that subset, in both directions. -PLATFORMS: tuple[Platform, ...] = ( - Platform("linux", "x64", "linux-x64-gnu", ["linux"], ["x64"]), - Platform("linux", "arm64", "linux-arm64-gnu", ["linux"], ["arm64"]), - Platform("darwin", "x64", "darwin-x64", ["darwin"], ["x64"]), - Platform("darwin", "arm64", "darwin-arm64", ["darwin"], ["arm64"]), - Platform("win32", "x64", "win32-x64-msvc", ["win32"], ["x64"]), +PLATFORMS: tuple[Platform, ...] = tuple( + Platform( + row["nodePlatform"], + row["nodeArch"], + row["libName"].removeprefix(_LIB_PREFIX), + row["os"], + row["cpu"], + ) + for row in json.loads(_TABLE.read_text(encoding="utf-8")) ) _BY_KEY = {(p.node_platform, p.node_arch): p for p in PLATFORMS} @@ -56,7 +60,7 @@ def find_host_platform(node_platform: str, node_arch: str) -> Platform: if platform is None: raise ValueError( f"unsupported host {node_platform}-{node_arch}; " - "add a row to PLATFORMS in packages/ts/src/platforms.ts (and here)" + "add a row to packages/ts/src/platforms.json" ) return platform diff --git a/internals/distcheck/src/distcheck/node_flow/platforms_test.py b/internals/distcheck/src/distcheck/node_flow/platforms_test.py index ac76e8bb6..2fbf5806a 100644 --- a/internals/distcheck/src/distcheck/node_flow/platforms_test.py +++ b/internals/distcheck/src/distcheck/node_flow/platforms_test.py @@ -43,3 +43,15 @@ def test_platform_is_immutable(): p = find_host_platform("linux", "x64") with pytest.raises(AttributeError): p.slug = "mutated" + + +def test_platforms_covers_every_published_target(): + # PLATFORMS is read from packages/ts/src/platforms.json and `slug` is the + # one field derived rather than carried; pin the whole set. + assert sorted(p.slug for p in PLATFORMS) == [ + "darwin-arm64", + "darwin-x64", + "linux-arm64-gnu", + "linux-x64-gnu", + "win32-x64-msvc", + ] diff --git a/packages/ts/changelog.d/2026-09-05-platform-table-single-source.md b/packages/ts/changelog.d/2026-09-05-platform-table-single-source.md new file mode 100644 index 000000000..5be4c05ed --- /dev/null +++ b/packages/ts/changelog.d/2026-09-05-platform-table-single-source.md @@ -0,0 +1,6 @@ +**Changed** — the published-target table moved out of `src/platforms.ts` into +`src/platforms.json`, the one declarative copy of the data. `platforms.ts` types +and re-exports it; the node distcheck flow reads the same file instead of +hand-maintaining a Python subset, so the `platforms-mirror` check that policed +the two copies is gone. `PLATFORMS`, `libTriples()` and `librarySlug()` keep +their shapes, and the JSON ships in `dist/` alongside the compiled modules. diff --git a/packages/ts/e2e-attestations/claude-1074-platform-table-single-source.json b/packages/ts/e2e-attestations/claude-1074-platform-table-single-source.json new file mode 100644 index 000000000..2ca3014eb --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1074-platform-table-single-source.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1788609659, + "exit_code": 0, + "commit": "98aa58f2bbd95a4bd448c4274dc3b798e344ba51", + "branch": "claude/1074-platform-table-single-source" +} diff --git a/packages/ts/package.json b/packages/ts/package.json index f6381bc77..ecf2a46e4 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -49,7 +49,12 @@ }, "tsc": { "command": "tsc -p tsconfig.build.json", - "files": ["src/**/*.ts", "tsconfig.json", "tsconfig.build.json"], + "files": [ + "src/**/*.ts", + "src/**/*.json", + "tsconfig.json", + "tsconfig.build.json" + ], "output": ["dist/**"] }, "stage:platform": { @@ -58,6 +63,7 @@ "files": [ "tools/stage-platform.ts", "src/platforms.ts", + "src/platforms.json", "napi/src/**/*.rs", "napi/build.rs", "napi/Cargo.toml", @@ -77,6 +83,7 @@ "napi/Cargo.toml", "../rust/src/**/*.rs", "src/**/*.ts", + "src/**/*.json", "tsconfig.json", "tsconfig.build.json" ], @@ -87,6 +94,7 @@ "dependencies": ["napi:build", "tsc"], "files": [ "src/**/*.ts", + "src/**/*.json", "tools/**/*.ts", "tests/**/*.ts", "vitest.config.ts" @@ -96,13 +104,14 @@ "test:unit": { "command": "vitest run --dir src", "dependencies": ["napi:build"], - "files": ["src/**/*.ts", "vitest.config.ts"], + "files": ["src/**/*.ts", "src/**/*.json", "vitest.config.ts"], "output": [] }, "test:integration": { "command": "vitest run --dir tests/integration/hermetic --passWithNoTests", "files": [ "src/**/*.ts", + "src/**/*.json", "tests/integration/hermetic/**/*.ts", "vitest.config.ts" ], @@ -113,6 +122,7 @@ "dependencies": ["napi:build", "tsc"], "files": [ "src/**/*.ts", + "src/**/*.json", "tools/**/*.ts", "tests/integration/binding/**/*.ts", "tests/exists.ts", @@ -125,6 +135,7 @@ "dependencies": ["build"], "files": [ "src/**/*.ts", + "src/**/*.json", "tools/**/*.ts", "tests/e2e/**/*.ts", "vitest.config.ts", @@ -138,6 +149,7 @@ "dependencies": ["napi:build"], "files": [ "src/**/*.ts", + "src/**/*.json", "tools/**/*.ts", "tests/integration/**/*.ts", "tests/exists.ts", diff --git a/packages/ts/src/platforms.json b/packages/ts/src/platforms.json new file mode 100644 index 000000000..c19b559a0 --- /dev/null +++ b/packages/ts/src/platforms.json @@ -0,0 +1,44 @@ +[ + { + "triple": "x86_64-unknown-linux-gnu", + "nodePlatform": "linux", + "nodeArch": "x64", + "libName": "@dirsql/lib-linux-x64-gnu", + "os": ["linux"], + "cpu": ["x64"], + "libc": ["glibc"] + }, + { + "triple": "aarch64-unknown-linux-gnu", + "nodePlatform": "linux", + "nodeArch": "arm64", + "libName": "@dirsql/lib-linux-arm64-gnu", + "os": ["linux"], + "cpu": ["arm64"], + "libc": ["glibc"] + }, + { + "triple": "x86_64-apple-darwin", + "nodePlatform": "darwin", + "nodeArch": "x64", + "libName": "@dirsql/lib-darwin-x64", + "os": ["darwin"], + "cpu": ["x64"] + }, + { + "triple": "aarch64-apple-darwin", + "nodePlatform": "darwin", + "nodeArch": "arm64", + "libName": "@dirsql/lib-darwin-arm64", + "os": ["darwin"], + "cpu": ["arm64"] + }, + { + "triple": "x86_64-pc-windows-msvc", + "nodePlatform": "win32", + "nodeArch": "x64", + "libName": "@dirsql/lib-win32-x64-msvc", + "os": ["win32"], + "cpu": ["x64"] + } +] diff --git a/packages/ts/src/platforms.test.ts b/packages/ts/src/platforms.test.ts index 2c20fb0ae..07c107b79 100644 --- a/packages/ts/src/platforms.test.ts +++ b/packages/ts/src/platforms.test.ts @@ -44,6 +44,21 @@ describe("PLATFORMS", () => { }); }); + describe("the node vocabulary the JSON cast cannot type", () => { + it("pairs a real `process.platform` with a real `process.arch` on every row", () => { + // `PLATFORMS` is a cast over platforms.json, so nothing at the type + // level stops a hand-edited row carrying a value node never reports. + const keys = PLATFORMS.map((p) => `${p.nodePlatform}-${p.nodeArch}`); + expect(keys.sort()).toEqual([ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "win32-x64", + ]); + }); + }); + describe("libTriples()", () => { it("maps every `${platform}-${arch}` key to its lib-* sub-package", () => { const map = libTriples(); diff --git a/packages/ts/src/platforms.ts b/packages/ts/src/platforms.ts index e09e2d18b..c78a8f1fb 100644 --- a/packages/ts/src/platforms.ts +++ b/packages/ts/src/platforms.ts @@ -1,7 +1,11 @@ -// Single source of truth for the target platforms `dirsql` publishes. +// The target platforms `dirsql` publishes. The rows live in `platforms.json`, +// the one declarative copy of the table: this module types it, and the node +// distcheck flow (`internals/distcheck/.../node_flow/platforms.py`) reads the +// same file. The JSON sits under `src/` so `tsc` emits it into `dist/` and it +// ships with the package -- `loadNativeCore()` needs the map at runtime. // // Every target triple generates ONE npm sub-package: `@dirsql/lib-`, -// holding the napi-rs `.node` addon. It backs both layers — the TypeScript +// holding the napi-rs `.node` addon. It backs both layers -- the TypeScript // SDK loads it via `loadNativeCore()`, and since #739 the `dirsql` CLI runs // in-process through its `runCli` export rather than spawning a binary. The // second family, `@dirsql/cli-`, shipped a redundant copy of the same @@ -13,6 +17,8 @@ // `libTriples()` returns a `${process.platform}-${process.arch}` → // sub-package-name map. +import rows from "./platforms.json" with { type: "json" }; + export interface Platform { /** Rust target triple — the name cargo-dist uses for archives. */ triple: string; @@ -30,50 +36,10 @@ export interface Platform { libc?: string[]; } -export const PLATFORMS: readonly Platform[] = [ - { - triple: "x86_64-unknown-linux-gnu", - nodePlatform: "linux", - nodeArch: "x64", - libName: "@dirsql/lib-linux-x64-gnu", - os: ["linux"], - cpu: ["x64"], - libc: ["glibc"], - }, - { - triple: "aarch64-unknown-linux-gnu", - nodePlatform: "linux", - nodeArch: "arm64", - libName: "@dirsql/lib-linux-arm64-gnu", - os: ["linux"], - cpu: ["arm64"], - libc: ["glibc"], - }, - { - triple: "x86_64-apple-darwin", - nodePlatform: "darwin", - nodeArch: "x64", - libName: "@dirsql/lib-darwin-x64", - os: ["darwin"], - cpu: ["x64"], - }, - { - triple: "aarch64-apple-darwin", - nodePlatform: "darwin", - nodeArch: "arm64", - libName: "@dirsql/lib-darwin-arm64", - os: ["darwin"], - cpu: ["arm64"], - }, - { - triple: "x86_64-pc-windows-msvc", - nodePlatform: "win32", - nodeArch: "x64", - libName: "@dirsql/lib-win32-x64-msvc", - os: ["win32"], - cpu: ["x64"], - }, -]; +// JSON widens `nodePlatform` / `nodeArch` to `string`, which no data file can +// carry as `NodeJS.Platform` / `NodeJS.Architecture`. The cast is the only +// place the two vocabularies meet. +export const PLATFORMS = rows as readonly Platform[]; /** Node `${platform}-${arch}` → `@dirsql/lib-*` napi sub-package name. */ export function libTriples(): Record { diff --git a/packages/ts/stryker-setup-10.js b/packages/ts/stryker-setup-10.js new file mode 100644 index 000000000..aecfacbe4 --- /dev/null +++ b/packages/ts/stryker-setup-10.js @@ -0,0 +1,71 @@ +import { beforeEach, afterAll, beforeAll, afterEach, inject, } from 'vitest'; +// This file is copied to the sandbox dir, don't import anything local! +// See https://github.com/stryker-mutator/stryker-js/issues/5305 +const globalNamespace = inject('globalNamespace'); +const mutantActivation = inject('mutantActivation'); +const mode = inject('mode'); +const ns = globalThis[globalNamespace] || (globalThis[globalNamespace] = {}); +ns.hitLimit = inject('hitLimit'); +const isGreaterThanVitest4Point1 = inject('isGreaterThanVitest4Point1'); +if (mode === 'mutant') { + beforeAll(() => { + ns.hitCount = 0; + }); + if (mutantActivation === 'static') { + ns.activeMutant = inject('activeMutant'); + } + else { + beforeAll(() => { + ns.activeMutant = inject('activeMutant'); + }); + } + if (isGreaterThanVitest4Point1) { + // Vitest's hooks API requires this empty destructure to allow access to suite.meta + // eslint-disable-next-line no-empty-pattern + afterAll(({}, suite) => { + suite.meta.hitCount = ns.hitCount; + }); + } + else { + // @ts-expect-error This was changed in Vitest v4.1 + afterAll(({ meta }) => { + meta.hitCount = ns.hitCount; + }); + } +} +else { + ns.activeMutant = undefined; + beforeEach(({ task }) => { + ns.currentTestId = toRawTestId(task); + }); + afterEach(() => { + ns.currentTestId = undefined; + }); + if (isGreaterThanVitest4Point1) { + // Vitest's hooks API requires this empty destructure to allow access to suite.meta + // eslint-disable-next-line no-empty-pattern + afterAll(({}, suite) => { + suite.meta.mutantCoverage = ns.mutantCoverage; + }); + } + else { + // @ts-expect-error This was changed in Vitest v4.1 + afterAll(({ meta }) => { + meta.mutantCoverage = ns.mutantCoverage; + }); + } +} +// Stryker disable all: this file is copied to the sandbox dir +function collectTestName({ name, suite, }) { + const nameParts = [name]; + let currentSuite = suite; + while (currentSuite) { + nameParts.unshift(currentSuite.name); + currentSuite = currentSuite.suite; + } + return nameParts.join(' ').trim(); +} +function toRawTestId(test) { + return `${test.file?.filepath ?? 'unknown.js'}#${collectTestName(test)}`; +} +//# sourceMappingURL=stryker-setup.js.map \ No newline at end of file diff --git a/packages/ts/tools/stage-platform.ts b/packages/ts/tools/stage-platform.ts index 1aac15a0c..aed9275b7 100644 --- a/packages/ts/tools/stage-platform.ts +++ b/packages/ts/tools/stage-platform.ts @@ -37,7 +37,7 @@ export function findHostPlatform( ); if (!p) { throw new Error( - `unsupported host ${key}; add a row to PLATFORMS in src/platforms.ts`, + `unsupported host ${key}; add a row to src/platforms.json`, ); } return p; diff --git a/packages/ts/tsconfig.json b/packages/ts/tsconfig.json index 4688441f5..badbf2328 100644 --- a/packages/ts/tsconfig.json +++ b/packages/ts/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", + "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true,