|
| 1 | +"""Extract every plausible test-name string from the v1 test suite: |
| 2 | +string literals passed as the first argument to HumanName(...) calls, |
| 3 | +plus string elements/keys of module-level list/dict banks. Dev tooling |
| 4 | +-- over-collection is fine, the comparator just parses more names. |
| 5 | +
|
| 6 | +PROVENANCE: the checked-in tools/differential/corpus.jsonl was built |
| 7 | +against commit 2d5d8c2 ("Trim constant-factor waste on the tokenize |
| 8 | +hot path"), the last commit before M12 reconciled/edited the v1 test |
| 9 | +banks (M12 rewrote expectations and deleted the bucket-A tests, which |
| 10 | +would have shrunk the corpus). Reading historical test content is |
| 11 | +fine; per the migration rules, no git *operations* are ever run against |
| 12 | +the original (non-worktree) checkout -- this script only shells out to |
| 13 | +`git show <ref>:<path>` inside the current worktree's own history, |
| 14 | +which contains that commit as an ancestor. |
| 15 | +
|
| 16 | +Regenerate with: |
| 17 | + uv run python tools/differential/build_corpus.py --ref 2d5d8c2 \\ |
| 18 | + > tools/differential/corpus.jsonl |
| 19 | +""" |
| 20 | +import argparse |
| 21 | +import ast |
| 22 | +import json |
| 23 | +import subprocess |
| 24 | +import sys |
| 25 | +from pathlib import Path |
| 26 | + |
| 27 | +ROOT = Path(__file__).resolve().parents[2] |
| 28 | +TESTS = ROOT / "tests" |
| 29 | + |
| 30 | +# Obvious non-names the AST heuristic sweeps up along with real names: |
| 31 | +# format-string placeholders, decorators/emails, escape sequences. |
| 32 | +_NOISE_CHARS = ("{", "@", "\\") |
| 33 | + |
| 34 | + |
| 35 | +def _is_name_like(value: str) -> bool: |
| 36 | + return not any(ch in value for ch in _NOISE_CHARS) |
| 37 | + |
| 38 | + |
| 39 | +def _strings_from(tree: ast.Module) -> set[str]: |
| 40 | + found: set[str] = set() |
| 41 | + for node in ast.walk(tree): |
| 42 | + if (isinstance(node, ast.Call) |
| 43 | + and getattr(node.func, "id", getattr( |
| 44 | + node.func, "attr", "")) == "HumanName" |
| 45 | + and node.args |
| 46 | + and isinstance(node.args[0], ast.Constant) |
| 47 | + and isinstance(node.args[0].value, str)): |
| 48 | + found.add(node.args[0].value) |
| 49 | + if isinstance(node, ast.Assign) and isinstance( |
| 50 | + node.value, (ast.List, ast.Tuple, ast.Dict)): |
| 51 | + for c in ast.walk(node.value): |
| 52 | + if isinstance(c, ast.Constant) and isinstance(c.value, str) \ |
| 53 | + and " " in c.value and "\n" not in c.value: |
| 54 | + found.add(c.value) |
| 55 | + return {n for n in found if _is_name_like(n)} |
| 56 | + |
| 57 | + |
| 58 | +def _working_tree_sources() -> dict[str, str]: |
| 59 | + return {path.name: path.read_text() |
| 60 | + for path in sorted(TESTS.glob("test_*.py"))} |
| 61 | + |
| 62 | + |
| 63 | +def _ref_sources(ref: str) -> dict[str, str]: |
| 64 | + """Read every top-level tests/test_*.py file as it existed at `ref`, |
| 65 | + via `git show`, without touching the working tree or index.""" |
| 66 | + listing = subprocess.run( |
| 67 | + ["git", "-C", str(ROOT), "ls-tree", "-r", "--name-only", ref, |
| 68 | + "--", "tests"], |
| 69 | + capture_output=True, text=True, check=True).stdout |
| 70 | + paths = sorted( |
| 71 | + p for p in listing.splitlines() |
| 72 | + if p.startswith("tests/") and Path(p).parent == Path("tests") |
| 73 | + and Path(p).name.startswith("test_") and p.endswith(".py")) |
| 74 | + sources = {} |
| 75 | + for path in paths: |
| 76 | + show = subprocess.run( |
| 77 | + ["git", "-C", str(ROOT), "show", f"{ref}:{path}"], |
| 78 | + capture_output=True, text=True, check=True) |
| 79 | + sources[Path(path).name] = show.stdout |
| 80 | + return sources |
| 81 | + |
| 82 | + |
| 83 | +def main() -> None: |
| 84 | + ap = argparse.ArgumentParser() |
| 85 | + ap.add_argument( |
| 86 | + "--ref", default=None, |
| 87 | + help="git ref to read tests/test_*.py from (via `git show`) " |
| 88 | + "instead of the working tree, e.g. 2d5d8c2 (pre-M12).") |
| 89 | + args = ap.parse_args() |
| 90 | + |
| 91 | + sources = _ref_sources(args.ref) if args.ref else _working_tree_sources() |
| 92 | + names: set[str] = set() |
| 93 | + for filename, text in sources.items(): |
| 94 | + try: |
| 95 | + tree = ast.parse(text, filename=filename) |
| 96 | + except SyntaxError as e: |
| 97 | + print(f"skipping {filename}: {e}", file=sys.stderr) |
| 98 | + continue |
| 99 | + names |= _strings_from(tree) |
| 100 | + |
| 101 | + for name in sorted(names): |
| 102 | + print(json.dumps(name, ensure_ascii=False)) |
| 103 | + print(f"{len(names)} names", file=sys.stderr) |
| 104 | + |
| 105 | + |
| 106 | +if __name__ == "__main__": |
| 107 | + main() |
0 commit comments