mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
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.
137 lines
5.3 KiB
Python
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())
|