208 lines
8.0 KiB
Python
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)
|