US01-01 follow-up: replace donor characterization iteration with YAML ledger + full suite (#46)
This commit was merged in pull request #46.
This commit is contained in:
88
tests/characterization/test_donor_ledger.py
Normal file
88
tests/characterization/test_donor_ledger.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Ledger lint (US01-01): every donor-ledger row must carry a real source
|
||||
reference, a target location, and either existing characterization test IDs or
|
||||
a real pending backlog story."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
LEDGER = REPO / "donor_ledger.yaml"
|
||||
STORIES = REPO / "delivery_backlog" / "stories"
|
||||
TESTS_DIR = Path(__file__).resolve().parent
|
||||
|
||||
CLASSIFICATIONS = {"reuse", "extract", "refactor", "replace"}
|
||||
REQUIRED_AREAS = {"discovery", "hashing", "imaging", "nsfw", "vision", "exif",
|
||||
"database", "ui", "configuration", "logging", "cancellation",
|
||||
"error"}
|
||||
STATUSES = {"characterized", "pending"}
|
||||
|
||||
|
||||
def load_rows():
|
||||
rows = yaml.safe_load(LEDGER.read_text(encoding="utf-8"))["rows"]
|
||||
assert rows, "empty ledger"
|
||||
return rows
|
||||
|
||||
|
||||
def collect_test_ids() -> set:
|
||||
"""module::function for every test in this suite."""
|
||||
ids = set()
|
||||
for f in TESTS_DIR.glob("test_*.py"):
|
||||
for m in re.finditer(r"^def (test_\w+)", f.read_text(encoding="utf-8"),
|
||||
re.MULTILINE):
|
||||
ids.add(f"{f.stem}::{m.group(1)}")
|
||||
return ids
|
||||
|
||||
|
||||
def test_rows_have_required_fields_and_unique_ids():
|
||||
rows = load_rows()
|
||||
ids = [r["id"] for r in rows]
|
||||
assert len(ids) == len(set(ids)), "duplicate row ids"
|
||||
for r in rows:
|
||||
for field in ("id", "area", "source", "classification", "rationale",
|
||||
"target", "status"):
|
||||
assert r.get(field), f"{r.get('id', '?')}: missing {field}"
|
||||
assert r["classification"] in CLASSIFICATIONS, r["id"]
|
||||
assert r["status"] in STATUSES, r["id"]
|
||||
assert len(str(r["rationale"]).strip()) >= 20, \
|
||||
f"{r['id']}: rationale too thin to count as a documented reason"
|
||||
|
||||
|
||||
def test_source_references_resolve():
|
||||
for r in load_rows():
|
||||
src = r["source"]
|
||||
f = REPO / src["file"]
|
||||
assert f.is_file(), f"{r['id']}: source file {src['file']} missing"
|
||||
text = f.read_text(encoding="utf-8")
|
||||
for sym in src["symbols"]:
|
||||
assert sym in text, f"{r['id']}: symbol {sym!r} not found in {src['file']}"
|
||||
|
||||
|
||||
def test_rows_have_tests_or_pending_story():
|
||||
known_tests = collect_test_ids()
|
||||
for r in load_rows():
|
||||
tests = r.get("tests", [])
|
||||
pending = r.get("pending_story")
|
||||
assert tests or pending, f"{r['id']}: neither tests nor pending_story"
|
||||
for t in tests:
|
||||
assert t in known_tests, f"{r['id']}: unknown test id {t}"
|
||||
if pending:
|
||||
matches = list(STORIES.glob(f"{pending}-*.md"))
|
||||
assert matches, f"{r['id']}: pending_story {pending} has no story file"
|
||||
if r["status"] == "characterized":
|
||||
assert tests, f"{r['id']}: characterized rows need test ids"
|
||||
|
||||
|
||||
def test_all_required_areas_covered():
|
||||
covered = {r["area"] for r in load_rows()}
|
||||
assert REQUIRED_AREAS <= covered, f"uncovered areas: {REQUIRED_AREAS - covered}"
|
||||
assert covered <= REQUIRED_AREAS, f"unknown areas: {covered - REQUIRED_AREAS}"
|
||||
|
||||
|
||||
def test_no_legacy_file_moved():
|
||||
# US01-01 explicitly forbids moving/archiving donors; the ledger's source
|
||||
# files must all still exist at their original locations.
|
||||
for donor in ("photo_analyzer.py", "nsfwtag/scoring.py", "nsfwtag/exif.py",
|
||||
"nsfwtag/server.py", "webapp/query.py", "webapp/runner.py",
|
||||
"webapp/server.py"):
|
||||
assert (REPO / donor).is_file(), f"donor moved: {donor}"
|
||||
Reference in New Issue
Block a user