#!/usr/bin/env python3 """Behaviour of the `reach validate` verbs (T-1282). Parity against the scripts these replaced was established on the live tree — `content` reproduces the original's output byte for byte including its counts, `name-collisions` likewise — and recorded in the ticket. What is pinned here is the behaviour those runs could not reach: the failure paths. That gap matters more than usual for this domain. `validate-content` currently FAILS on the live tree (13 missing schemas, pre-existing), so its success path is the one nothing exercises; `name-collisions` currently PASSES, so its detection path is the one nothing exercises. A live run proves whichever half the repo happens to be in. Run: python3 tooling/test_validate.py """ import os import shutil import subprocess import sys import tempfile from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent CULTURE = '''( id: "{cid}", naming: ( given_names: [ // a comment mentioning "decoy" which must not be collected {given} ], family_names: [ {family} ], ), ) ''' def _reach(*args: str, root: Path | None = None) -> subprocess.CompletedProcess[str]: env = {**os.environ, "SR_OUTPUT_FORMAT": "text"} if root is not None: env["SR_REPO_ROOT"] = str(root) return subprocess.run( ["reach", "validate", *args], capture_output=True, text=True, cwd=REPO_ROOT, env=env, ) def _cultures(directory: Path, pools: dict[str, tuple[list[str], list[str]]]) -> None: directory.mkdir(parents=True, exist_ok=True) for cid, (given, family) in pools.items(): (directory / f"culture-{cid}.ron").write_text( CULTURE.format( cid=cid, given=", ".join(f'"{n}"' for n in given), family=", ".join(f'"{n}"' for n in family), ), encoding="utf-8", ) def test_collision_detected(failures: list[str]) -> None: """Two cultures sharing a name is a failure that names both.""" with tempfile.TemporaryDirectory() as tmp: directory = Path(tmp) / "global" _cultures( directory, { "alpha": (["Ada", "Shared"], ["Alpha"]), "beta": (["Bo", "Shared"], ["Beta"]), }, ) result = _reach("name-collisions", str(directory)) combined = result.stdout + result.stderr if result.returncode == 0: failures.append( "[collision] a shared name exited 0 — a collision that reports " "success makes generated NPCs ambiguous about their origin, " "invisibly" ) if "Shared" not in combined: failures.append("[collision] the colliding name is not in the output") for culture in ("alpha", "beta"): if culture not in combined: failures.append( f"[collision] {culture} is not named — a collision report that " "omits an owner cannot be acted on" ) if "decoy" in combined: failures.append( "[collision] a name inside a // comment was collected; comments " "must be stripped before extracting quoted strings" ) def test_no_collision(failures: list[str]) -> None: with tempfile.TemporaryDirectory() as tmp: directory = Path(tmp) / "global" _cultures(directory, {"alpha": (["Ada"], ["Alpha"]), "beta": (["Bo"], ["Beta"])}) result = _reach("name-collisions", str(directory)) if result.returncode != 0: failures.append( f"[no-collision] exited {result.returncode} with disjoint pools" ) def test_empty_directory(failures: list[str]) -> None: """No culture files is not a failure — there is nothing to contradict.""" with tempfile.TemporaryDirectory() as tmp: directory = Path(tmp) / "global" directory.mkdir(parents=True) result = _reach("name-collisions", str(directory)) if result.returncode != 0: failures.append( f"[empty] exited {result.returncode} on a directory with no cultures" ) def test_missing_directory(failures: list[str]) -> None: result = _reach("name-collisions", "/definitely/not/a/directory") if result.returncode == 0: failures.append("[missing-dir] a nonexistent directory exited 0") def test_ron_argument_errors(failures: list[str]) -> None: """Bad arguments fail before anything is executed. Both cases matter because the alternative is invoking cargo to discover them, which is slow and reports the mistake in the validator's vocabulary rather than the caller's. """ unknown = _reach("ron", "server/content/global/culture-osse.ron", "not_a_schema") if unknown.returncode != 2: failures.append( f"[ron-schema] unknown schema exited {unknown.returncode}, expected 2" ) if "zone_type" not in (unknown.stdout + unknown.stderr): failures.append( "[ron-schema] the rejection does not list the accepted schemas — the " "closed-set rule D-263 exists for" ) missing = _reach("ron", "/definitely/not/a/file.ron", "culture") if missing.returncode == 0: failures.append("[ron-file] a nonexistent file exited 0") def main() -> int: if shutil.which("reach") is None: print( "test_validate: `reach` is not on PATH.\n Fix: make install-reach", file=sys.stderr, ) return 1 failures: list[str] = [] test_collision_detected(failures) test_no_collision(failures) test_empty_directory(failures) test_missing_directory(failures) test_ron_argument_errors(failures) if failures: print("test_validate: FAIL", file=sys.stderr) for failure in failures: print(f" - {failure}", file=sys.stderr) return 1 print("test_validate: OK — collisions detected and named, argument errors caught") return 0 if __name__ == "__main__": sys.exit(main())