#!/usr/bin/env python3 """Cells for `wizard-work/ledger/verify_wizards.py` (queue Q113). Every cell builds its own fixture tree under a temporary directory -- a copy of WIZARDS-TABLE.md plus `wizard-work/ledger/{wizards.csv,wizards.manifest.json}` -- mutates exactly one thing, and runs the real verifier against it with `--root`. Nothing in the repository is touched, so a red cell cannot leave the shared tree dirty. The cell the queue filed is `test_corrupted_lane_cell_is_a_fault`: the old `verify_wizards.py` never opened the source, so 30 of 50 cells replaced by `GARBAGE LANE` read `OK ... rc 0`. Here it names the row and the column. Run: python3 -m unittest discover -p 'test_verify_wizards.py' -v """ import csv import io import json import os import shutil import subprocess import sys import tempfile import unittest HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.dirname(os.path.dirname(HERE)) VERIFIER = os.path.join(REPO, "wizard-work", "ledger", "verify_wizards.py") SOURCE = "WIZARDS-TABLE.md" CSV_REL = os.path.join("wizard-work", "ledger", "wizards.csv") MAN_REL = os.path.join("wizard-work", "ledger", "wizards.manifest.json") def read_rows(path): with open(path, newline="", encoding="utf-8") as fh: return list(csv.reader(fh)) def write_rows(path, rows): with open(path, "w", newline="", encoding="utf-8") as fh: csv.writer(fh).writerows(rows) class Fixture(unittest.TestCase): def setUp(self): self.tmp = tempfile.mkdtemp(prefix="q113-") os.makedirs(os.path.join(self.tmp, "wizard-work", "ledger")) shutil.copy(os.path.join(REPO, SOURCE), os.path.join(self.tmp, SOURCE)) for rel in (CSV_REL, MAN_REL): shutil.copy(os.path.join(REPO, rel), os.path.join(self.tmp, rel)) def tearDown(self): shutil.rmtree(self.tmp, ignore_errors=True) # -- helpers --------------------------------------------------------- def csv_path(self): return os.path.join(self.tmp, CSV_REL) def man_path(self): return os.path.join(self.tmp, MAN_REL) def load_manifest(self): with open(self.man_path(), encoding="utf-8") as fh: return json.load(fh) def save_manifest(self, manifest): with open(self.man_path(), "w", encoding="utf-8") as fh: json.dump(manifest, fh, indent=2, sort_keys=True) fh.write("\n") def repin_csv(self, rows=None): """Rewrite the CSV and make the manifest agree with its bytes/rows. This is how a cell isolates the leg it means to grade: the shape leg stays green, so a red reading can only come from the leg under test. """ import hashlib path = self.csv_path() if rows is not None: write_rows(path, rows) with open(path, "rb") as fh: data = fh.read() manifest = self.load_manifest() manifest["csv_sha256"] = hashlib.sha256(data).hexdigest() manifest["row_count"] = len(read_rows(path)) - 1 self.save_manifest(manifest) def run_verifier(self, cwd=None, extra=()): return subprocess.run( [sys.executable, VERIFIER, "--root", self.tmp] + list(extra), cwd=cwd or self.tmp, capture_output=True, text=True, timeout=60) # -- cells ----------------------------------------------------------- def test_clean_fixture_is_green(self): proc = self.run_verifier() self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("VERDICT TRUE", proc.stdout) self.assertIn("18 row(s), 7 column(s), 126 cell(s) compared, 0 fault(s)", proc.stdout) def test_renamed_wizard_is_a_fault(self): rows = read_rows(self.csv_path()) idx = [r[1] for r in rows].index("Merlin") rows[idx][1] = "Merlin the Red" self.repin_csv(rows) proc = self.run_verifier() self.assertEqual(proc.returncode, 1, proc.stdout) self.assertIn("column 'Wizard'", proc.stdout) self.assertIn("Merlin the Red", proc.stdout) def test_corrupted_lane_cell_is_a_fault(self): # The filed defect: every Lane cell replaced by GARBAGE LANE was green. rows = read_rows(self.csv_path()) for row in rows[1:]: row[3] = "GARBAGE LANE" self.repin_csv(rows) proc = self.run_verifier() self.assertEqual(proc.returncode, 1, proc.stdout) self.assertIn("column 'Lane'", proc.stdout) self.assertIn("VERDICT FALSE", proc.stdout) def test_corrupted_current_unit_is_a_fault(self): rows = read_rows(self.csv_path()) idx = [r[1] for r in rows].index("Volt") rows[idx][5] = "queue replies" self.repin_csv(rows) proc = self.run_verifier() self.assertEqual(proc.returncode, 1, proc.stdout) self.assertIn("column 'Current unit'", proc.stdout) def test_dropped_correction_marker_is_a_fault(self): rows = read_rows(self.csv_path()) idx = [r[1] for r in rows].index("Zephyrus Sparkborn") self.assertTrue(rows[idx][-1], "fixture row carries no marker to drop") rows[idx][-1] = "" self.repin_csv(rows) proc = self.run_verifier() self.assertEqual(proc.returncode, 1, proc.stdout) self.assertIn("does not carry the withdrawn marker", proc.stdout) def test_marker_on_a_row_with_nothing_withdrawn_is_a_fault(self): rows = read_rows(self.csv_path()) idx = [r[1] for r in rows].index("Timmy") rows[idx][-1] = "WITHDRAWN - nothing was withdrawn here" self.repin_csv(rows) proc = self.run_verifier() self.assertEqual(proc.returncode, 1, proc.stdout) self.assertIn("marker with no withdrawal recorded", proc.stdout) def test_source_mutation_without_repin_is_a_pin_fault(self): path = os.path.join(self.tmp, SOURCE) with open(path, encoding="utf-8") as fh: text = fh.read() mutated = text.replace("| Merlin | StepFun Step 5 |", "| Merlin | StepFun Step 6 |") self.assertNotEqual(mutated, text, "source mutation did not apply") with open(path, "w", encoding="utf-8") as fh: fh.write(mutated) proc = self.run_verifier() self.assertEqual(proc.returncode, 1, proc.stdout) self.assertIn("source moved without a re-extract", proc.stdout) def test_missing_row_is_a_fault(self): rows = read_rows(self.csv_path()) dropped = rows.pop(3) self.repin_csv(rows) proc = self.run_verifier() self.assertEqual(proc.returncode, 1, proc.stdout) self.assertIn("row %r missing from the CSV" % dropped[1], proc.stdout) def test_short_row_is_a_fault(self): rows = read_rows(self.csv_path()) rows[1] = rows[1][:-1] # drop the Correction cell from row 1 self.repin_csv(rows) proc = self.run_verifier() self.assertEqual(proc.returncode, 1, proc.stdout) self.assertIn("has 6 column(s), the header has 7", proc.stdout) def test_tampered_row_count_is_a_shape_fault(self): manifest = self.load_manifest() manifest["row_count"] = 17 self.save_manifest(manifest) proc = self.run_verifier() self.assertEqual(proc.returncode, 1, proc.stdout) self.assertIn("data row(s) != manifest row_count", proc.stdout) def test_missing_manifest_cannot_grade(self): os.remove(self.man_path()) proc = self.run_verifier() self.assertEqual(proc.returncode, 2, proc.stdout) self.assertIn("CANNOT GRADE", proc.stdout) def test_missing_csv_cannot_grade(self): os.remove(self.csv_path()) proc = self.run_verifier() self.assertEqual(proc.returncode, 2, proc.stdout) self.assertIn("CANNOT GRADE", proc.stdout) def test_same_verdict_from_a_foreign_cwd(self): proc = self.run_verifier(cwd="/") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("VERDICT TRUE", proc.stdout) if __name__ == "__main__": unittest.main(verbosity=2)