verify.sh no longer cds to /root/troupe-standup; repo root comes from $0 /
__file__. Paths are read with ls-tree -z (raw), the census is pinned to the
commit it describes (repo_census.pin), and the verifier grades that pin, so a
later commit does not redden the gate. New committed extractor regenerates the
three TSVs; artifacts re-derived from 716c3b3 (5311 files, 217 dirs, 34 exts).
159 lines
6.6 KiB
Python
159 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify the file-tree census & extension census artifact against a fresh recompute.
|
|
|
|
Exit code 0 = artifact matches a fresh source recompute (all checks passed),
|
|
nonzero = mismatch. Not a tautology: it re-enumerates a git tree and compares
|
|
the produced tables field-for-field against the committed files.
|
|
|
|
The repository root is derived from __file__, never a hardcoded host path. The
|
|
tree graded is the one named in `repo_census.pin` -- the commit checked out when
|
|
the census was taken, immutable -- so this gate does not turn red merely because
|
|
HEAD has since moved. Paths are read with `git ls-tree -z` -- raw, not git's
|
|
quoted rendering. Run inside a `git archive` export (no .git) the export's own
|
|
files are walked instead; that export must be the pinned commit, which is the
|
|
case Q69's done-means asks for. The two paths the census run itself added (the
|
|
pin and the extractor) post-date the pinned commit and are excluded from a walk.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
import csv
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from collections import Counter
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
DEST = os.path.join(REPO, "wizard-work", "ledger")
|
|
|
|
FILES = ["repo_tree.tsv", "repo_exts.tsv", "repo_files.tsv"]
|
|
PIN_NAME = "repo_census.pin"
|
|
PIN_SELF = "wizard-work/ledger/" + PIN_NAME
|
|
# Added by the census run itself, so absent from the commit the census describes.
|
|
NEW_SELF = {PIN_SELF, "wizard-work/ledger/extract_tree_census.py"}
|
|
|
|
|
|
def read_pin():
|
|
fields = {}
|
|
with open(os.path.join(DEST, PIN_NAME), encoding="utf-8") as f:
|
|
for line in f:
|
|
if line.strip():
|
|
key, value = line.split(None, 1)
|
|
fields[key] = value.strip()
|
|
if "commit" not in fields or "files" not in fields:
|
|
raise SystemExit("FATAL %s lacks commit/files lines" % PIN_NAME)
|
|
return fields["commit"], int(fields["files"])
|
|
|
|
|
|
def enumerate_paths(commit):
|
|
"""Paths of the pinned commit, plus the source they were read from.
|
|
|
|
git present: `git ls-tree -z` on the pinned commit -- raw names.
|
|
export: every regular file in the export (content is the pinned commit plus
|
|
this run's two new files, which are dropped).
|
|
"""
|
|
if os.path.exists(os.path.join(str(REPO), ".git")):
|
|
proc = subprocess.run(["git", "ls-tree", "-r", "-z", "--name-only", commit],
|
|
cwd=REPO, capture_output=True)
|
|
if proc.returncode != 0:
|
|
return None, "git ls-tree rc=%d: %s" % (
|
|
proc.returncode,
|
|
proc.stderr.decode("utf-8", "replace").strip())
|
|
raw = proc.stdout.decode("utf-8", "surrogateescape")
|
|
return [p for p in raw.split("\0") if p], "git commit %s" % commit[:12]
|
|
walked = []
|
|
for root, dirs, files in os.walk(REPO):
|
|
dirs[:] = sorted(d for d in dirs if d != ".git")
|
|
for name in files:
|
|
rel = str(Path(os.path.join(root, name)).relative_to(REPO)
|
|
.as_posix())
|
|
if rel not in NEW_SELF:
|
|
walked.append(rel)
|
|
return sorted(walked), "export walk"
|
|
|
|
|
|
def read_tsv(name):
|
|
with open(os.path.join(DEST, name), newline="", encoding="utf-8") as f:
|
|
return list(csv.reader(f, delimiter="\t"))
|
|
|
|
|
|
def main():
|
|
failures = 0
|
|
|
|
def check(name, cond, detail=""):
|
|
nonlocal failures
|
|
print(("%s %s" % ("PASS" if cond else "FAIL", name)) + ((" " + detail) if (detail and not cond) else ""))
|
|
if not cond:
|
|
failures += 1
|
|
|
|
# 1) fresh enumeration of the pinned tree
|
|
pin, pin_files = read_pin()
|
|
paths, source = enumerate_paths(pin)
|
|
print("enumerate source: %s" % source)
|
|
if paths is None:
|
|
print("FATAL could not enumerate the tree: %s" % source)
|
|
raise SystemExit(2)
|
|
check("pin file count matches enumeration", len(paths) == pin_files,
|
|
"pin says %d, enumerated %d" % (pin_files, len(paths)))
|
|
check("enumeration lists files", len(paths) > 0, "got %d" % len(paths))
|
|
check("enumeration has no duplicate paths", len(paths) == len(set(paths)),
|
|
"%d duplicate(s)" % (len(paths) - len(set(paths))))
|
|
|
|
dir_counts = Counter()
|
|
ext_counts = Counter()
|
|
for p in paths:
|
|
d = os.path.dirname(p)
|
|
dir_counts[d if d else "(root)"] += 1
|
|
ext = os.path.splitext(p)[1].lstrip(".").lower() or "(none)"
|
|
ext_counts[ext] += 1
|
|
|
|
# 2) repo_tree.tsv
|
|
rows = read_tsv("repo_tree.tsv")
|
|
check("repo_tree header", rows[0] == ["rank", "directory", "file_count"], str(rows[0]))
|
|
data = rows[1:]
|
|
expected_tree = sorted(dir_counts.items(), key=lambda kv: (-kv[1], kv[0]))
|
|
check("repo_tree rows == directory groups",
|
|
len(data) == len(expected_tree),
|
|
"got %d, expected %d" % (len(data), len(expected_tree)))
|
|
check("repo_tree dir/counts match recompute",
|
|
[(r[1], int(r[2])) for r in data] == [(d, c) for d, c in expected_tree])
|
|
|
|
# 3) repo_exts.tsv
|
|
rows = read_tsv("repo_exts.tsv")
|
|
check("repo_exts header", rows[0] == ["rank", "extension", "file_count"], str(rows[0]))
|
|
data = rows[1:]
|
|
expected_ext = sorted(ext_counts.items(), key=lambda kv: (-kv[1], kv[0]))
|
|
check("repo_exts rows == extension groups",
|
|
len(data) == len(expected_ext),
|
|
"got %d, expected %d" % (len(data), len(expected_ext)))
|
|
check("repo_exts ext/counts match recompute",
|
|
[(r[1], int(r[2])) for r in data] == [(d, c) for d, c in expected_ext])
|
|
|
|
# 4) repo_files.tsv
|
|
rows = read_tsv("repo_files.tsv")
|
|
check("repo_files header", rows[0] == ["rank", "path"], str(rows[0]))
|
|
file_paths = [r[1] for r in rows[1:]]
|
|
check("repo_files data rows == enumeration rows",
|
|
len(file_paths) == len(paths),
|
|
"got %d, enumerated %d" % (len(file_paths), len(paths)))
|
|
check("repo_files rows equal enumeration set",
|
|
sorted(file_paths) == sorted(paths))
|
|
check("repo_files ranks are 1..N",
|
|
len(rows) > 1 and all(r[0] == str(i) for i, r in enumerate(rows[1:], 1)),
|
|
"rank sequence broken")
|
|
control = [r[1] for r in rows[1:] if any(ord(c) < 32 for c in r[1])]
|
|
check("repo_files paths carry no control characters", not control,
|
|
"%d row(s), first %r" % (len(control), control[:1]))
|
|
|
|
# 5) group sums reconcile to total
|
|
check("dir sum == total", sum(dir_counts.values()) == len(paths),
|
|
"got %d, total %d" % (sum(dir_counts.values()), len(paths)))
|
|
check("ext sum == total", sum(ext_counts.values()) == len(paths),
|
|
"got %d, total %d" % (sum(ext_counts.values()), len(paths)))
|
|
|
|
print("result:", "PASS" if failures == 0 else "FAIL")
|
|
sys.exit(1 if failures else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|