scp_format_check.py (5553B)
1 #!/usr/bin/env python3 2 """Opt-in formatter integration checks; never part of the fast host gate.""" 3 import argparse 4 from pathlib import Path 5 import re 6 import subprocess 7 import tempfile 8 9 ROOT = Path(__file__).resolve().parent.parent 10 # Independent lexical preservation oracle, including comments and raw strings. 11 TOKENS = re.compile(r'"(?:\\.|[^"\\])*"|--[^\r\n]*|=>|[A-Za-z_][A-Za-z0-9_.?\-]*|[0-9]+|[^\s]', re.S) 12 13 14 def main(): 15 parser = argparse.ArgumentParser(description=__doc__) 16 parser.add_argument("tricu", nargs="?", default="result/bin/tricu") 17 parser.add_argument("--bootstrap", type=Path, required=True) 18 args = parser.parse_args() 19 executable = str((ROOT / args.tricu).resolve()) 20 with tempfile.TemporaryDirectory(prefix="scp-format-") as scratch: 21 store = Path(scratch) / "store" 22 source_file = Path(scratch) / "input.scp" 23 def command(*flags): 24 return subprocess.run([executable, "scp", "format", str(source_file), 25 "--store", str(store), "--bootstrap", str(args.bootstrap.resolve()), *flags], cwd=ROOT, 26 capture_output=True, timeout=120) 27 def format_source(source): 28 source_file.write_bytes(source.encode("utf-8")) 29 result = command() 30 if result.returncode: 31 if result.stderr.startswith(b"SCP syntax error:"): 32 return "__FORMAT_ERROR__" 33 raise AssertionError(result.stderr.decode()) 34 return result.stdout.decode("utf-8") 35 fixtures = sorted((ROOT / "lib/consumer").rglob("*.scp")) 36 fixtures += [ROOT / "demos/consumers/fizzbuzz/fizzbuzz.scp", ROOT / "demos/consumers/registry/registry.scp", 37 ROOT / "demos/consumers/configurations/application.scp"] 38 cases = [path.read_text() for path in fixtures] 39 cases += [ 40 'consumer x\n leaf @ _ => call worker "as @ => \\" --" as a in recur a @ leaf -- @ => as\n (leaf, tail) @ state => call worker tail as b in recur b @ state\n', 41 'consumer x\n leaf @ _ => call a leaf as x in x\n (leaf, a) @ _ => call a call a a as inner in inner as outer in outer\n', 42 'consumer x\n leaf @ _ => leaf\n (leaf, x) @ _ => x\n\n leaf @ _ => leaf\nconsumer y\n leaf @ _ => leaf\n', 43 'consumer x\n leaf @ _ => match leaf of (leaf => call a leaf as x in x | stem leaf => call a leaf as y in y)\n', 44 'consumer x\r\n leaf @ _ => "literal spaces" -- comment spaces\r\n', 45 'consumer x\n leaf @ _ => leaf', 46 'consumer x\n \"first\n second\" @ _ => leaf\n', 47 'consumer x\n leaf @ _ => \"first\n!not a header\"\n', 48 ] 49 for i, source in enumerate(cases): 50 formatted = format_source(source) 51 assert formatted != "__FORMAT_ERROR__", (i, source) 52 assert TOKENS.findall(formatted) == TOKENS.findall(source), (i, "tokens changed") 53 assert format_source(formatted) == formatted, (i, "not idempotent") 54 add = format_source((ROOT / "lib/consumer/add.scp").read_text()) 55 rules = [line for line in add.splitlines() if " => " in line] 56 assert len({line.index("@") for line in rules}) == 1 57 assert len({line.index("=>") for line in rules}) == 1 58 assert len({line.index(" as ") for line in rules if "call " in line}) == 1 59 assert len({line.rindex("@") for line in rules if "recur " in line}) == 1 60 nested = format_source('consumer x\n leaf @ _ => call a leaf as x in x\n (leaf, a) @ _ => call a call a a as inner in inner as outer in outer\n') 61 nested_rules = nested.splitlines()[1:] 62 assert len({line.rindex(" as ") for line in nested_rules}) == 1 63 scoped = format_source('consumer x\n leaf @ _ => leaf\n (leaf, x) @ _ => x\n\n leaf @ _ => leaf\nconsumer y\n leaf @ _ => leaf\n') 64 scoped_lines = scoped.splitlines() 65 assert scoped_lines[1].index("@") == scoped_lines[2].index("@") 66 assert scoped_lines[4].index("@") == scoped_lines[6].index("@") == 7 67 assert scoped_lines[1].index("@") > scoped_lines[4].index("@") 68 assert format_source('consumer x\n leaf @ _ => call a leaf as\n') == "__FORMAT_ERROR__" 69 source_file.write_bytes(cases[0].encode("utf-8")) 70 expected = command().stdout 71 original = source_file.read_bytes() 72 assert command("--check").returncode == int(expected != original) 73 assert source_file.read_bytes() == original 74 source_file.chmod(0o640) 75 assert command("--write").returncode == 0 76 assert source_file.read_bytes() == expected 77 assert source_file.stat().st_mode & 0o777 == 0o640 78 assert command("--check").returncode == 0 79 assert command("--check", "--write").returncode != 0 80 source_file.write_bytes(b"consumer x\n leaf @ _ => call\n") 81 broken = source_file.read_bytes() 82 assert command("--write").returncode != 0 83 assert source_file.read_bytes() == broken 84 target = Path(scratch) / "target.scp" 85 source_file.rename(target) 86 source_file.symlink_to(target) 87 assert command("--write").returncode != 0 88 assert source_file.is_symlink() 89 source_file.unlink() 90 source_file.write_bytes(b"\xff") 91 assert command("--write").returncode != 0 92 assert source_file.read_bytes() == b"\xff" 93 print(f"SCP formatter: {len(cases)} preservation/idempotence cases, columns, CLI modes/error safety OK") 94 95 96 if __name__ == "__main__": 97 main()