Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,6 @@ graphify-out/

# ADR-013 §10 golden fixture: canonical bytes, must be tracked despite *.zip/*.parquet rules
!tests/fixtures/wire_contract/*

# #126 historical characterization golden (small, must be tracked despite *.parquet rule)
!tests/fixtures/historical_golden/*
Binary file not shown.
66 changes: 66 additions & 0 deletions tests/test_historical_parity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Reader-level parity oracle for the historical artifact (#126, Feathers
amendment): the characterization golden captures what the LEGACY pandas chain
produced from a real 2-month × 200-cell slice, and every future builder must
yield the same frame AS SEEN THROUGH FAOAPI'S READER — not the same bytes.

Reader semantics mirrored from views-faoapi (`dataset_service.py:255-334`,
`handlers.py:1365-1417`): read parquet; if no MultiIndex, set
``["month_id", "priogrid_id"|"priogrid_gid"]``; require the 9 metadata columns
present and non-null; historical targets = every remaining column.

Captured truth worth naming: the legacy chain would have shipped two junk
columns (``row``, ``col`` — datafactory grid coordinates) which faoapi's
auto-detect would have served as bogus "targets". The frame-native builder
drops them deliberately; the parity comparison is therefore over the REAL
targets + metadata, and a dedicated test pins the junk exclusion.
"""

from pathlib import Path

import pyarrow.parquet as pq

from views_postprocessing.unfao.gaul_schema import METADATA_COLS

GOLDEN = Path(__file__).resolve().parent / "fixtures" / "historical_golden" / "legacy_historical_slice.parquet"
REAL_TARGETS = ["lr_ged_sb", "lr_ged_ns", "lr_ged_os"]
_INDEX_CANDIDATES = ("priogrid_id", "priogrid_gid")


def reader_view(path) -> dict:
"""faoapi's reader semantics, pandas-free: returns {(month, gid): {col: value}}
for target+metadata columns, plus the detected target list."""
table = pq.read_table(path)
names = set(table.column_names)
id_col = next((c for c in _INDEX_CANDIDATES if c in names), None)
assert "month_id" in names and id_col, f"index columns missing: {sorted(names)}"
missing = [c for c in METADATA_COLS if c not in names]
assert not missing, f"metadata columns missing: {missing}"
targets = [c for c in table.column_names if c not in {"month_id", *_INDEX_CANDIDATES, *METADATA_COLS}]
rows = table.to_pylist()
view = {
(r["month_id"], r[id_col]): {c: r[c] for c in (*targets, *METADATA_COLS)}
for r in rows
}
return {"targets": targets, "rows": view}


def test_golden_passes_reader_semantics():
view = reader_view(GOLDEN)
assert len(view["rows"]) == 400 # 2 months × 200 cells
# the legacy chain's auto-detected targets include the junk row/col columns —
# the captured hazard the frame-native builder must NOT reproduce
assert set(view["targets"]) == {*REAL_TARGETS, "row", "col"}


def test_golden_metadata_is_complete_and_nonnull():
view = reader_view(GOLDEN)
for key, cols in view["rows"].items():
for meta in METADATA_COLS:
assert cols[meta] is not None and cols[meta] == cols[meta], (key, meta)


def test_golden_carries_real_values():
view = reader_view(GOLDEN)
# spot sanity: coordinates are real half-degree grid values
some = next(iter(view["rows"].values()))
assert isinstance(some["pg_xcoord"], float) and abs(some["pg_xcoord"]) <= 180.0
Loading