The 30-file tree moves under atlas as its third rung (D-243), ten verbs fronting it. Each verb restates its module's options so `--help` describes something; tooling/test_planet_router.py hands every declared option to the module's own argparse and fails on drift, and now runs in make test-tooling. The 2026-09-02 half of this move had converted the top-level imports and the repo roots. Finishing it found what the half-move left: - Lazy in-function imports, and all of sol_data/, still named siblings bare. They resolved only through sys.path.insert hacks, so under reach the first globe render in generate, batch or sol-import would have raised ModuleNotFoundError. Qualified; the hacks are gone. - 247 print() calls and a stdout progress writer that fired once per 8 KB block. Report verbs (audit, quality) write through console.out, progress through console.event, and download progress is throttled to 10% steps so a job log is not tens of thousands of lines. - Every error exit raises ReachError with a fix. Two checks that could not fail: - batch --verify-determinism printed a warning and exited 0 on a mismatch. - import-provinces exited 0 with errors > 0. Both now raise. The 271-body bake is only safe to re-run because the first one holds. sol-import --body is action="append" in the module but the router took one value, so --body GJ0d --body GJ0e kept one. Now repeatable, and _flags repeats list options. test_conformance walked one level, so a nested group was reported as a verb missing @command and its ten verbs were never checked. It recurses now; proven by stripping @command from `planet quality` and watching it fail. Stray PNGs from the 2026-09-03 runaway router-test run are parked in .cache/t1288-stray-pngs/, not committed. Their reliefmaps differ from HEAD while the heightmap regenerated byte-identical — filed as T-1291. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
7.3 KiB
Python
184 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""`atlas planet` declares its options twice — this stops them drifting (T-1288).
|
|
|
|
The router restates ten argument surfaces that already exist in the modules'
|
|
own argparse parsers. That duplication buys real `--help` for an agent, which
|
|
a passthrough could not, but it creates the obvious failure: the router grows
|
|
an option the module has never heard of, and the mismatch only shows up when
|
|
someone runs the command with that flag.
|
|
|
|
So every option the router declares is handed to the module's own parser here.
|
|
A parser rejects an unknown option with SystemExit(2), which is exactly the
|
|
signal wanted — no output comparison, no fixtures, no running the generators.
|
|
|
|
Nothing here executes a generator. Each parser is invoked directly, so a full
|
|
pass costs milliseconds and never touches the atlas DB.
|
|
|
|
Run: python3 tooling/test_planet_router.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import contextlib
|
|
import io
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
import typer # noqa: E402
|
|
|
|
from tooling.domains.atlas.planet import router as planet_router # noqa: E402
|
|
|
|
# verb -> (module attribute, positional arguments the parser requires)
|
|
IMPLEMENTATIONS = {
|
|
"generate": ("generate", ["body.json"]),
|
|
"batch": ("batch", []),
|
|
"scaffold": ("scaffold_bodies", ["index.json"]),
|
|
"import-heightmaps": ("import_heightmaps", []),
|
|
"import-provinces": ("import_province_boundaries", []),
|
|
"terrain-reference": ("populate_terrain_reference", []),
|
|
"sol-import": ("sol_import", []),
|
|
"sol-name-fixes": ("sol_name_fixes", []),
|
|
"audit": ("atlas_cohesion_audit", []),
|
|
"quality": ("atlas_quality_analysis", []),
|
|
}
|
|
|
|
# A representative value per click param type, so the parser sees a well-formed
|
|
# pair. Keyed on the type's `name` because these are ParamType INSTANCES, not
|
|
# classes — keying on the class matches nothing and every int option then gets
|
|
# the string "x" and reads as a parser rejection.
|
|
SAMPLE = {"int": "4", "str": "x", "path": "x", "filename": "x", "float": "1.0"}
|
|
|
|
# Options whose module-side parser restricts the value. The router cannot send
|
|
# a placeholder to these, so the test sends a real one.
|
|
CONSTRAINED = {
|
|
("generate", "--render-mode"): "cartographic",
|
|
("sol-import", "--render-mode"): "cartographic",
|
|
}
|
|
|
|
|
|
def _router_options(verb: str) -> list[tuple[str, type]]:
|
|
"""The long options the router declares for a verb, with their types."""
|
|
command = typer.main.get_command(planet_router.app).commands[verb] # type: ignore[attr-defined]
|
|
found: list[tuple[str, type]] = []
|
|
for param in command.params:
|
|
for opt in getattr(param, "opts", []):
|
|
if opt.startswith("--") and opt != "--help":
|
|
found.append((opt, param.type))
|
|
return found
|
|
|
|
|
|
class _Parsed(BaseException):
|
|
"""Raised the instant a parser accepts, to stop before any work happens."""
|
|
|
|
|
|
def _parser_accepts(module, argv: list[str]) -> tuple[bool, str]:
|
|
"""Feed argv to the module's own parser, and stop the moment it accepts.
|
|
|
|
`main()` builds its parser and then does the work, so simply calling it
|
|
would generate planets. Patching `parse_args` lets the module construct its
|
|
real parser — the thing under test — and aborts on the line after it
|
|
succeeds. A rejection still raises SystemExit(2) from inside argparse.
|
|
|
|
Found the hard way: the first version of this test called `main()` and let
|
|
it run, and spent two minutes generating bodies for GJ_1005A before it was
|
|
stopped.
|
|
"""
|
|
real = argparse.ArgumentParser.parse_args
|
|
|
|
def stop_after_parsing(self, args=None, namespace=None):
|
|
real(self, args, namespace)
|
|
raise _Parsed
|
|
|
|
buffer = io.StringIO()
|
|
argparse.ArgumentParser.parse_args = stop_after_parsing
|
|
try:
|
|
with contextlib.redirect_stderr(buffer), contextlib.redirect_stdout(buffer):
|
|
module.main(argv)
|
|
except _Parsed:
|
|
return True, ""
|
|
except SystemExit as exc:
|
|
if exc.code == 2: # argparse's "bad arguments"
|
|
return False, buffer.getvalue().strip()
|
|
return True, ""
|
|
except BaseException:
|
|
# Failed before reaching parse_args — an import guard, a missing DB.
|
|
# Not this test's business either way.
|
|
return True, ""
|
|
finally:
|
|
argparse.ArgumentParser.parse_args = real
|
|
return True, ""
|
|
|
|
|
|
def test_every_option_is_known_to_its_parser(failures: list[str]) -> None:
|
|
import importlib
|
|
|
|
for verb, (module_name, positionals) in IMPLEMENTATIONS.items():
|
|
module = importlib.import_module(f"tooling.domains.atlas.planet.{module_name}")
|
|
for opt, param_type in _router_options(verb):
|
|
argv = list(positionals)
|
|
argv.append(opt)
|
|
type_name = getattr(param_type, "name", "text")
|
|
if type_name != "boolean":
|
|
argv.append(CONSTRAINED.get((verb, opt), SAMPLE.get(type_name, "x")))
|
|
|
|
ok, err = _parser_accepts(module, argv)
|
|
if not ok:
|
|
failures.append(
|
|
f"`reach atlas planet {verb} {opt}` — {module_name}.py's parser "
|
|
f"does not accept it: {err.splitlines()[-1] if err else 'rejected'}"
|
|
)
|
|
|
|
|
|
def test_every_verb_has_an_implementation(failures: list[str]) -> None:
|
|
"""A verb missing from IMPLEMENTATIONS would be silently unchecked."""
|
|
declared = set(typer.main.get_command(planet_router.app).commands) # type: ignore[attr-defined]
|
|
covered = set(IMPLEMENTATIONS)
|
|
for verb in sorted(declared - covered):
|
|
failures.append(
|
|
f"`reach atlas planet {verb}` is not in IMPLEMENTATIONS — add it, or "
|
|
"its options are never checked against a parser"
|
|
)
|
|
for verb in sorted(covered - declared):
|
|
failures.append(f"IMPLEMENTATIONS names '{verb}', which the router does not declare")
|
|
|
|
|
|
def test_flags_builder_drops_defaults(failures: list[str]) -> None:
|
|
"""None and False must not reach argv — the module owns its defaults."""
|
|
built = planet_router._flags(system=None, force=False, body="GJ380c", limit=7, dry_run=True)
|
|
if "--system" in built or "--force" in built:
|
|
failures.append(f"_flags passed an unset option through: {built}")
|
|
if built != ["--body", "GJ380c", "--limit", "7", "--dry-run"]:
|
|
failures.append(f"_flags built unexpected argv: {built}")
|
|
# sol-import's --body is action="append"; a list must repeat the flag, or
|
|
# `--body GJ0d --body GJ0e` silently keeps only one of them.
|
|
repeated = planet_router._flags(body=["GJ0d", "GJ0e"])
|
|
if repeated != ["--body", "GJ0d", "--body", "GJ0e"]:
|
|
failures.append(f"_flags did not repeat a list option: {repeated}")
|
|
|
|
|
|
def main() -> int:
|
|
failures: list[str] = []
|
|
test_every_verb_has_an_implementation(failures)
|
|
test_flags_builder_drops_defaults(failures)
|
|
test_every_option_is_known_to_its_parser(failures)
|
|
|
|
if failures:
|
|
print("test_planet_router: FAIL", file=sys.stderr)
|
|
for failure in failures:
|
|
print(f" - {failure}", file=sys.stderr)
|
|
return 1
|
|
total = sum(len(_router_options(v)) for v in IMPLEMENTATIONS)
|
|
print(
|
|
f"test_planet_router: OK — {len(IMPLEMENTATIONS)} verbs, {total} options, "
|
|
"every one accepted by the module's own parser"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|