Files
typikon/tests/python/test_typikon.py
funkpower 3e3b585b2a a jurisdiction is not just a key any more
describe() in Python was answering a different fast than the same date on the
command line: xerophagy on a Wednesday of the Great Fast read as wine_oil. Not a
cosmetic difference in a tool whose whole job is to say what may be eaten.

The cause is that Jurisdiction grew a tradition and a discipline, and config
carries all three — main.rs reads the saved jurisdiction, then the saved
tradition, then whether the Typikon's full discipline was asked for, and builds
an overridden Jurisdiction from the lot. The bindings resolved the key and
stopped, so everything the other two decide was silently dropped.

They now take the same three steps, and tradition= and typikon= are arguments, so
the library can say what the command line can. Pascha does not take them: it
follows the paschalion alone and neither can move it.

The overrides are interned rather than leaked per call. main.rs leaks one per run
and exits; a library gets called in a loop, and a leak per call adds up. The
combinations are bounded by jurisdictions x traditions x discipline, so the map
cannot grow past that.

The parity test spread widens to seven dates across both fasts and both
calendars, and clippy now runs under the feature as well — the binding code only
exists there, so a bare clippy run had never looked at it.
2026-09-18 12:33:56 -04:00

137 lines
5.3 KiB
Python

"""Tests for the Python bindings.
The one that matters is `test_matches_cli`: the bindings and the binary have to
answer identically for the same date, because they are the same engine and any
divergence means the Python layer has quietly grown its own opinion. It caught a
real one — the bindings were resolving a built-in default jurisdiction while the
CLI read the configured one, so every field downstream of that differed.
python -m pytest tests/python -q
Needs the wheel installed (`maturin develop` or `pip install target/wheels/*.whl`)
and, for the parity test, `cargo build --release`.
"""
import datetime
import json
import re
import subprocess
from pathlib import Path
import pytest
typikon = pytest.importorskip("typikon")
BINARY = Path(__file__).resolve().parents[2] / "target" / "release" / "typikon"
# Pascha, Nativity, Theophany and an ordinary weekday: the movable cycle, the
# fixed calendar, the julian offset and the plain case.
DATES = [
datetime.date(2026, 4, 12), # Pascha
datetime.date(2026, 12, 25), # Nativity
datetime.date(2027, 1, 7), # Theophany on the old calendar
datetime.date(2026, 3, 18), # a Wednesday of the Great Fast
datetime.date(2026, 11, 20), # the Nativity Fast
datetime.date(2026, 8, 6), # Transfiguration, fish permitted
datetime.date(2026, 6, 10), # an ordinary weekday
]
def _snake(s: str) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
@pytest.mark.parametrize("date", DATES, ids=str)
def test_matches_cli(date: datetime.date) -> None:
if not BINARY.exists():
pytest.skip(f"{BINARY} not built")
out = subprocess.run(
[str(BINARY), "--json", "--date", date.isoformat()],
capture_output=True, text=True, check=True,
).stdout
cli = {_snake(k): v for k, v in json.loads(out).items()}
assert typikon.describe(date).to_dict() == cli
def test_pascha_is_its_own_zero() -> None:
for year in (2024, 2025, 2026, 2030):
p = typikon.pascha(year)
assert typikon.describe(p).days_to_pascha == 0
assert p.year == year
def test_week_length_and_order() -> None:
start = datetime.date(2026, 4, 10)
days = typikon.week(start, 5)
assert len(days) == 5
assert [d.date for d in days] == [
start + datetime.timedelta(days=i) for i in range(5)
]
def test_week_rejects_nonsense() -> None:
with pytest.raises(ValueError):
typikon.week(datetime.date(2026, 4, 10), 0)
def test_tradition_and_typikon_reach_the_answer() -> None:
# A Jurisdiction is not just a key: it carries a tradition and a discipline,
# and config supplies all three. The bindings resolved only the key once, and
# reported wine_oil where the CLI said xerophagy on a Lenten Wednesday — the
# tool's whole job, answered differently in the two front ends.
lent = datetime.date(2026, 3, 18)
assert typikon.describe(lent, tradition="greek").tradition == "greek"
assert typikon.describe(lent, tradition="slavic").tradition == "slavic"
# The Typikon's full discipline can only tighten a relaxed rule, never relax it.
strict = typikon.describe(lent, jurisdiction="ruthenian", typikon=True)
normal = typikon.describe(lent, jurisdiction="ruthenian", typikon=False)
assert (strict.fast, strict.fast_label) != (normal.fast, normal.fast_label)
def test_unknown_tradition_raises() -> None:
with pytest.raises(ValueError, match="unknown tradition"):
typikon.describe(datetime.date(2026, 4, 12), tradition="martian")
def test_unknown_jurisdiction_raises() -> None:
with pytest.raises(ValueError, match="unknown jurisdiction"):
typikon.describe(datetime.date(2026, 4, 12), jurisdiction="narnia")
def test_jurisdiction_actually_changes_the_answer() -> None:
# Old and new calendar disagree about which church date a civil date is, so
# on 25 December the new calendar keeps the Nativity and the old calendar is
# still thirteen days short of it. julian_offset is not the witness to reach
# for: it is the gap between the calendars, so it reads 13 either way.
old = typikon.describe(datetime.date(2026, 12, 25), jurisdiction="old")
new = typikon.describe(datetime.date(2026, 12, 25), jurisdiction="new")
assert old.church_date != new.church_date
assert old.title != new.title
assert "Nativity" in (new.feast or "")
def test_every_jurisdiction_resolves() -> None:
keys = [j["key"] for j in typikon.jurisdictions()]
assert keys, "no jurisdictions listed"
for key in keys:
assert typikon.describe(datetime.date(2026, 4, 12), jurisdiction=key)
def test_day_is_immutable() -> None:
day = typikon.describe(datetime.date(2026, 4, 12))
with pytest.raises(AttributeError):
day.feast = "something else" # type: ignore[misc]
def test_accepts_datetime_as_well_as_date() -> None:
# to_date reads attributes rather than the datetime C API, so anything with
# year/month/day works. Worth pinning: it is a consequence of the abi3 build,
# not a decision, and it would be easy to lose by accident.
d = datetime.date(2026, 4, 12)
assert typikon.describe(datetime.datetime(2026, 4, 12, 13, 30)).to_dict() == \
typikon.describe(d).to_dict()
def test_to_dict_is_json_serialisable() -> None:
assert json.dumps(typikon.describe(datetime.date(2026, 4, 12)).to_dict())