Skip to content

Commit 1f47e7a

Browse files
Check ison-py against the parity corpus it generates
Every other implementation had a parity test. The reference did not, because it produces the expected files -- which is precisely why it needs one. If ison-py's behaviour drifts, the goldens silently become stale and the only signal is some *other* language failing, pointing at the wrong implementation. test_parity_corpus.py covers the flat corpus across all four renderings, the permuted cases for order independence, and canonical idempotence. 84 tests; ison-py goes 234 -> 318. benchmark/parity/built/ stays out: those cases assert that unrepresentable field and block names are rejected, which is not implemented yet, so wiring them into CI would only make it permanently red. run_extended_parity.py reports their status locally and is the tracker for that work. With this, CI exercises the shared corpus in all seven implementations -- verified that each one's existing CI command actually collects its parity test rather than assuming it does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1e36012 commit 1f47e7a

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""ison-py against the shared cross-language parity corpus.
2+
3+
Every other implementation has a parity test; the reference did not, because it
4+
generates the expected files. That is exactly why it needs one: if ison-py's
5+
behaviour drifts, the goldens silently become stale and the only signal is some
6+
*other* language failing, which points at the wrong implementation.
7+
8+
Covers benchmark/parity/*.ison (one input, four renderings) and
9+
benchmark/parity/permuted/ (many inputs, one shared expected -- the order
10+
independence ISONCS promises).
11+
12+
benchmark/parity/built/ is deliberately excluded: those cases assert that
13+
unrepresentable field and block names are rejected, which is not implemented
14+
yet. Run benchmark/parity/run_extended_parity.py to see their status.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import pathlib
20+
21+
import pytest
22+
23+
from ison_parser import (
24+
ISONLSerializer,
25+
dumps,
26+
dumps_canonical,
27+
dumps_isonl,
28+
loads,
29+
)
30+
31+
CORPUS = pathlib.Path(__file__).resolve().parents[2] / "benchmark" / "parity"
32+
33+
RENDERINGS = {
34+
"canonical": dumps_canonical,
35+
"dumps": dumps,
36+
"isonl": dumps_isonl,
37+
"canonical_isonl": ISONLSerializer.dumps_canonical,
38+
}
39+
40+
41+
def _read(path: pathlib.Path) -> str:
42+
return path.read_text(encoding="utf-8").replace("\r\n", "\n")
43+
44+
45+
def _flat_cases() -> list[str]:
46+
if not CORPUS.exists():
47+
return []
48+
return sorted(p.stem for p in CORPUS.glob("*.ison"))
49+
50+
51+
def _permuted_cases() -> list[str]:
52+
permuted = CORPUS / "permuted"
53+
if not permuted.exists():
54+
return []
55+
return sorted(d.name for d in permuted.iterdir() if d.is_dir())
56+
57+
58+
@pytest.mark.skipif(not _flat_cases(), reason="parity corpus not available")
59+
@pytest.mark.parametrize("case", _flat_cases())
60+
@pytest.mark.parametrize("mode", sorted(RENDERINGS))
61+
def test_flat_case_matches_expected(case: str, mode: str) -> None:
62+
expected_path = CORPUS / f"{case}.{mode}.expected"
63+
if not expected_path.exists():
64+
pytest.skip(f"{case} has no {mode} expectation")
65+
66+
doc = loads(_read(CORPUS / f"{case}.ison"))
67+
assert RENDERINGS[mode](doc) == _read(expected_path)
68+
69+
70+
@pytest.mark.skipif(not _permuted_cases(), reason="permuted corpus not available")
71+
@pytest.mark.parametrize("case", _permuted_cases())
72+
def test_every_permutation_yields_the_same_bytes(case: str) -> None:
73+
"""Order independence: a one-input case cannot express this.
74+
75+
Its output is deterministic whether or not the row sort is total, so ties
76+
resolved by input order stay invisible until two permutations are compared.
77+
"""
78+
case_dir = CORPUS / "permuted" / case
79+
variants = sorted(case_dir.glob("*.ison"))
80+
assert len(variants) > 1, f"{case}: a permuted case needs at least two variants"
81+
82+
for mode in ("canonical", "canonical_isonl"):
83+
expected_path = case_dir / f"{mode}.expected"
84+
if not expected_path.exists():
85+
continue
86+
expected = _read(expected_path)
87+
88+
for variant in variants:
89+
doc = loads(_read(variant))
90+
assert RENDERINGS[mode](doc) == expected, f"{case}/{variant.name} {mode}"
91+
92+
93+
@pytest.mark.skipif(not _flat_cases(), reason="parity corpus not available")
94+
@pytest.mark.parametrize("case", _flat_cases())
95+
def test_canonical_is_idempotent(case: str) -> None:
96+
"""Re-canonicalizing must change nothing, or content addressing is unusable."""
97+
once = dumps_canonical(loads(_read(CORPUS / f"{case}.ison")))
98+
assert dumps_canonical(loads(once)) == once

0 commit comments

Comments
 (0)