SPB Git

spb/firc0001_uqo

Public
4.7 KB · 107 lines python
1"""Reproducibility checks for the exhibit layer (protocol Part IX).23Run:  python3 -I -m pytest -q tests41. every CSV export reconciles with stats.json (sums of the marginal tables equal the headline counts);52. code/make_tables.py regenerated into a scratch directory reproduces the committed paper/tables/*.tex byte for byte6   and the committed results/inline_numbers.json;73. make_tables.py never rewrites data/stats.json and contains no hand-typed data literal.8"""9import csv, hashlib, json, subprocess, sys, tokenize, io10from pathlib import Path11import pytest1213ROOT = Path(__file__).resolve().parent.parent14D = ROOT / "data"15S = json.load(open(D / "stats.json"))161718def rows(name):19    with open(D / f"{name}.csv", newline="") as f:20        return list(csv.DictReader(f))212223def col_sum(name, col, where=None):24    return sum(int(float(r[col])) for r in rows(name) if where is None or where(r))252627@pytest.mark.parametrize("name,col,key", [28    ("ledger_by_form", "sections", "ledger_total"),29    ("mentions_by_form", "n", "mentions"),30    ("mentions_by_year", "mentions", "mentions"),31    ("confidence_dist", "n", "mentions"),32    ("projects_by_type", "n", "projects"),33    ("projects_by_status", "n", "projects"),34    ("duration", "n", "projects"),35    ("mentions_per_project", "n", "projects"),36    ("projects_by_first_seen_year", "n", "projects"),37    ("projects_by_sector", "n", "projects"),38    ("projects_by_index", "projects", "projects"),39    ("filings_by_form", "n", "filings_total"),40    ("filings_by_year_form", "n", "filings_total"),41    ("issuers_by_year", "filings", "filings_total"),42    ("parse_status", "n", "filings_total"),43    ("sections_by_form", "sections", "sections_total"),44])45def test_csv_sums_reconcile_with_stats(name, col, key):46    assert col_sum(name, col) == S[key], f"{name}.{col} does not sum to stats.json[{key}]"474849def test_universe_reconciles():50    U = S["universe"]51    assert U["both"] + U["sp_only"] + U["ru_only"] == U["n"]52    assert U["n_russell"] == U["russell_distinct_ciks"]53    assert S["issuers_with_filings"] + S["issuers_without_filings"] == U["n"]54    assert col_sum("companies_by_sector", "n") == U["n"]55    assert len(rows("issuers_without_filings")) == S["issuers_without_filings"]565758def test_items_reconcile_with_ledger_by_form():59    lb = {r["form_type"]: r for r in rows("ledger_by_form")}60    for form, name in (("10-K", "ledger_10k_items"), ("10-Q", "ledger_10q_items")):61        assert col_sum(name, "n") == int(lb[form]["sections"])62        assert col_sum(name, "mentions") == int(lb[form]["mentions"])636465def test_amounts_and_fields():66    assert int(rows("amount_dist")[0]["n"]) == S["projects_fields"][0]67    assert col_sum("projects_by_type", "with_amount") == S["projects_fields"][0]68    assert S["mentions_fields"][-1] == S["mentions"] and S["projects_fields"][-1] == S["projects"]697071def test_ledger_mentions_exceed_stored_mentions_by_collisions():72    # the ledger counts mentions before id de-duplication; stored mentions can only be fewer73    assert col_sum("ledger_by_form", "mentions") >= S["mentions"]74    assert col_sum("ledger_by_form", "with_mentions") <= S["ledger_total"]757677def _sha(p):78    return hashlib.sha256(p.read_bytes()).hexdigest()798081def test_regeneration_reproduces_committed_tables(tmp_path):82    before = _sha(D / "stats.json")83    r = subprocess.run([sys.executable, "-I", str(ROOT / "code" / "make_tables.py"), "--out", str(tmp_path)],84                       capture_output=True, text=True, cwd=ROOT)85    assert r.returncode == 0, r.stderr86    assert _sha(D / "stats.json") == before, "make_tables.py must never rewrite stats.json"87    mism = []88    for p in sorted((tmp_path / "tables").glob("*.tex")):89        q = ROOT / "paper" / "tables" / p.name90        if not q.exists() or q.read_text() != p.read_text():91            mism.append(p.name)92    assert not mism, f"regenerated tables differ from the committed ones: {mism} (run python3 -I code/make_tables.py and commit)"93    gen = json.load(open(tmp_path / "results" / "inline_numbers.json"))94    com = json.load(open(ROOT / "results" / "inline_numbers.json"))95    assert gen == com, "results/inline_numbers.json is stale"969798def test_make_tables_has_no_data_literals():99    """Integer literals with three or more digits are not allowed in the generator (the scales 100, 1e3, 1e6, 1e9 are)."""100    src = (ROOT / "code" / "make_tables.py").read_text()101    bad = []102    for tok in tokenize.generate_tokens(io.StringIO(src).readline):103        if tok.type == tokenize.NUMBER and "e" not in tok.string.lower() and "." not in tok.string and len(tok.string) >= 3 and tok.string != "100":104            bad.append((tok.start[0], tok.string))105    assert not bad, f"numeric literals in make_tables.py: {bad}"106    assert 'json.dump(S' not in src, "make_tables.py must not write stats.json"107
Files · 0d1f76183dfb024d676f555cf5ad099a684a4656
Menu

Theme