Files
troupe-standup/wizard-work/ledger/test_verify_wizards.py
T

208 lines
8.0 KiB
Python

#!/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)