Q80 (spark lane by authorship, unclaimed and taken). Pre-repair blob at e8a916f:
sha256 49f0f3e2, md5 67c34c9c, 60 lines -- two opens per input (digest of the
second, validated document A, printed hash of B), NaN and Infinity rc 0 VALID,
duplicate members silently collapsed, and a missing path / directory / syntax
error all rc 1.
Repaired: 185 lines, sha256 26cb270c. One read serves both halves
(Path.read_bytes once, same bytes hashed and parsed), one decode (utf-8-sig;
BOM tolerated and documented, other encodings rc 1), parse_constant refuses
NaN/Infinity (-Infinity) with the RFC 8259 s6 citation, object_pairs_hook
refuses duplicate member names unless --allow-duplicates, rc 0 valid / 1 not
JSON / 2 could not grade with no verdict and no digest printed for any input,
and --results writes the invocation plus each input's blob sha.
Cells: wizard-work/spark/test_json_sha256_validator.py (new), 15 cells,
sha256 0d2038ea -- "Ran 15 tests ... OK", rc 0. Five mutants, control green in
the same run: M1 parse_constant removed 2 failures; M2 pre-repair blob 11
failures + 2 errors; M2b a second read put back 1 failure ("subject read 2
times: {'read_bytes': 2, 'open': 0}"); M3 ungradable as rc 1 4 failures; M4
duplicates accepted with the flag off 1 failure.
Wiring: wizard-work/spark/CHECK leg 13. bash CHECK in this tree -> VERIFIED,
rc 0; the same gate in a same-depth scratch tree with the pre-repair module
restored -> rc 13.
Gate: python3 tools/verify.py --repo . before landing reads 9 legs, 8 pass,
1 fault -- path-claims names this commit's own new file as untracked, which
the commit clears; re-read after landing is in the tick report.
CHECK: python3 wizard-work/spark/test_json_sha256_validator.py
199 lines
7.4 KiB
Python
199 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Cells for wizard-work/spark/json_sha256_validator.py (Q80).
|
|
|
|
Black box: every CLI reading runs the shipped module as a subprocess, so the
|
|
cells cannot agree with the code by sharing an implementation. The one
|
|
in-process cell imports the module by path to count how many times the subject
|
|
is read -- the property Q80 asks for, which a subprocess cannot observe.
|
|
|
|
Run: python3 wizard-work/spark/test_json_sha256_validator.py
|
|
Exit 0 iff every cell passes.
|
|
"""
|
|
import hashlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
TOOL = Path(__file__).resolve().with_name("json_sha256_validator.py")
|
|
|
|
|
|
def run(*args, cwd=None):
|
|
proc = subprocess.run([sys.executable, str(TOOL), *args],
|
|
capture_output=True, text=True, cwd=cwd)
|
|
return proc.returncode, proc.stdout, proc.stderr
|
|
|
|
|
|
class Base(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.dir = Path(self._tmp.name)
|
|
self.ok = self.write("ok.json", b'{"a": [1, 2, 3], "b": "x"}\n')
|
|
self.nan = self.write("nan.json", b'{"a": NaN}\n')
|
|
self.inf = self.write("inf.json", b'{"a": Infinity}\n')
|
|
self.neginf = self.write("neginf.json", b'{"a": -Infinity}\n')
|
|
self.dup = self.write("dup.json", b'{"a": 1, "a": 2}\n')
|
|
self.broken = self.write("broken.json", b'{"a": 1,\n')
|
|
self.bom = self.write("bom.json", b'\xef\xbb\xbf{"a": 1}\n')
|
|
|
|
def tearDown(self):
|
|
self._tmp.cleanup()
|
|
|
|
def write(self, name, data):
|
|
path = self.dir / name
|
|
path.write_bytes(data)
|
|
return path
|
|
|
|
def digest(self, path):
|
|
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
|
|
|
|
class Contract(Base):
|
|
def test_usage_error_is_rc_2(self):
|
|
rc, out, err = run()
|
|
self.assertEqual(rc, 2, out + err)
|
|
|
|
def test_valid_document_rc_0_and_digest_covers_the_file(self):
|
|
rc, out, err = run(str(self.ok))
|
|
self.assertEqual(rc, 0, out + err)
|
|
self.assertIn("VALID", out)
|
|
self.assertIn(f"SHA256={self.digest(self.ok)}", out)
|
|
|
|
def test_nan_is_invalid_rc_1(self):
|
|
rc, out, err = run(str(self.nan))
|
|
self.assertEqual(rc, 1, out + err)
|
|
self.assertIn("INVALID", out)
|
|
self.assertIn("NaN", out)
|
|
self.assertNotIn("SHA256", out)
|
|
|
|
def test_infinity_and_negative_infinity_are_invalid(self):
|
|
for path, name in ((self.inf, "Infinity"), (self.neginf, "Infinity")):
|
|
rc, out, err = run(str(path))
|
|
self.assertEqual(rc, 1, f"{path.name}: {out}{err}")
|
|
self.assertIn("INVALID", out)
|
|
self.assertIn(name, out)
|
|
|
|
def test_duplicate_member_names_are_refused_by_default(self):
|
|
rc, out, err = run(str(self.dup))
|
|
self.assertEqual(rc, 1, out + err)
|
|
self.assertIn("duplicate object member name", out)
|
|
self.assertNotIn("SHA256", out)
|
|
|
|
def test_allow_duplicates_accepts_them(self):
|
|
rc, out, err = run("--allow-duplicates", str(self.dup))
|
|
self.assertEqual(rc, 0, out + err)
|
|
self.assertIn(f"SHA256={self.digest(self.dup)}", out)
|
|
|
|
def test_syntax_error_is_rc_1(self):
|
|
rc, out, err = run(str(self.broken))
|
|
self.assertEqual(rc, 1, out + err)
|
|
self.assertIn("INVALID", out)
|
|
|
|
def test_bom_is_tolerated_and_the_choice_is_documented(self):
|
|
rc, out, err = run(str(self.bom))
|
|
self.assertEqual(rc, 0, out + err)
|
|
self.assertIn(f"SHA256={self.digest(self.bom)}", out)
|
|
self.assertIn("byte order mark", TOOL.read_text(encoding="utf-8").lower())
|
|
|
|
def test_undecodable_bytes_are_invalid_rc_1(self):
|
|
bad = self.write("bad.bin", b'\xff\xfe\x00\x01')
|
|
rc, out, err = run(str(bad))
|
|
self.assertEqual(rc, 1, out + err)
|
|
self.assertIn("not decodable JSON text", out)
|
|
|
|
|
|
class Ungradable(Base):
|
|
def test_missing_input_is_rc_2(self):
|
|
rc, out, err = run(str(self.dir / "nope.json"))
|
|
self.assertEqual(rc, 2, out + err)
|
|
self.assertIn("UNGRADABLE", out)
|
|
self.assertNotIn("VALID", out)
|
|
self.assertNotIn("SHA256", out)
|
|
|
|
def test_directory_input_is_rc_2(self):
|
|
rc, out, err = run(str(self.dir))
|
|
self.assertEqual(rc, 2, out + err)
|
|
self.assertIn("UNGRADABLE", out)
|
|
|
|
def test_non_directory_in_path_is_rc_2(self):
|
|
rc, out, err = run(str(self.ok / "child.json"))
|
|
self.assertEqual(rc, 2, out + err)
|
|
self.assertIn("UNGRADABLE", out)
|
|
|
|
def test_ungradable_suppresses_every_verdict_and_digest(self):
|
|
rc, out, err = run(str(self.ok), str(self.dir / "nope.json"))
|
|
self.assertEqual(rc, 2, out + err)
|
|
self.assertNotIn("VALID", out)
|
|
self.assertNotIn("INVALID", out)
|
|
self.assertNotIn("SHA256", out)
|
|
self.assertIn("no verdict and no digest printed", out)
|
|
|
|
|
|
class SingleRead(Base):
|
|
def test_one_read_serves_both_the_parse_and_the_digest(self):
|
|
"""Serve payload A first and B after; the digest must be A's, read once."""
|
|
target = self.dir / "race.json"
|
|
target.write_bytes(b'{"a": 1}\n')
|
|
a = b'{"a": 1}\n'
|
|
b = b'{"a": 999}\n'
|
|
counts = {"read_bytes": 0, "open": 0}
|
|
|
|
spec = importlib.util.spec_from_file_location("json_sha256_validator", TOOL)
|
|
if spec is None or spec.loader is None:
|
|
self.fail(f"cannot load the module under test at {TOOL}")
|
|
loader = spec.loader
|
|
mod = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(mod)
|
|
|
|
real_read_bytes = Path.read_bytes
|
|
import builtins
|
|
real_builtin_open = builtins.open
|
|
|
|
def fake_read_bytes(self):
|
|
counts["read_bytes"] += 1
|
|
return a if counts["read_bytes"] == 1 else b
|
|
|
|
def fake_open(path, mode="r", *args, **kwargs):
|
|
if str(path) == str(target):
|
|
counts["open"] += 1
|
|
if "b" in mode:
|
|
return io.BytesIO(b)
|
|
return io.StringIO(b.decode())
|
|
return real_builtin_open(path, mode, *args, **kwargs)
|
|
|
|
Path.read_bytes = fake_read_bytes
|
|
builtins.open = fake_open
|
|
try:
|
|
record = mod.grade(str(target))
|
|
finally:
|
|
Path.read_bytes = real_read_bytes
|
|
builtins.open = real_builtin_open
|
|
|
|
reads = counts["read_bytes"] + counts["open"]
|
|
self.assertEqual(record["verdict"], "valid", record)
|
|
self.assertEqual(reads, 1, f"subject read {reads} times: {counts}")
|
|
self.assertEqual(record["sha256"], hashlib.sha256(a).hexdigest())
|
|
|
|
|
|
class Results(Base):
|
|
def test_results_file_carries_the_invocation_and_each_blob_sha(self):
|
|
out_path = self.dir / "run.json"
|
|
rc, out, err = run("--results", str(out_path), str(self.ok), str(self.dup))
|
|
self.assertIn(rc, (0, 1), out + err)
|
|
payload = json.loads(out_path.read_text(encoding="utf-8"))
|
|
self.assertEqual(payload["tool"], TOOL.name)
|
|
self.assertIn(str(TOOL.resolve()), payload["invocation"])
|
|
self.assertIn(str(self.ok), payload["argv"])
|
|
by_path = {item["path"]: item for item in payload["inputs"]}
|
|
self.assertEqual(by_path[str(self.ok)]["sha256"], self.digest(self.ok))
|
|
self.assertEqual(by_path[str(self.ok)]["verdict"], "valid")
|
|
self.assertEqual(by_path[str(self.dup)]["verdict"], "invalid")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|