signal-bot/tests/test_versioning.py
Krzysztof kuhy Rudnicki 9db3687752 release: 0.1.7 «Callistus I» — a version now says which pope, and when
"Wersja 0.1.7 «Callistus I»" told nobody whether Callistus was the
fifteenth pope or the hundredth, and the changelog was the one place the
ladder is actually read. pope_line() derives both facts from popes.json --
the ordinal, and the `reign` string exactly as the table spells it, so the
one reigning pope keeps his single year instead of being padded into a
range he never had.

It appears in all four places a version is named: the group announcement
(header and every block of a backlog), each section of DOCS-changelog.md,
/healthz, and the !papiez caption, which now describes the release's pope
in the same sentence it uses for every other one.

The changelog's copy is checked rather than trusted, like the heading name
already was: a test walks the raw file and compares each line against
popes.json, so a hand-edited ordinal fails here instead of in a group.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
2026-08-30 22:12:09 +02:00

195 lines
6.6 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""The pope ladder, and the one fixed point the whole scheme exists for."""
from __future__ import annotations
import itertools
import json
import tomllib
from pathlib import Path
import pytest
from signal_bot.versioning import (
LATIN_ORDINALS,
POPE,
POPES,
RELEASE_ORDINAL,
__version__,
article_url,
latin_name,
latin_stem,
ordinal_for,
pope_for,
pope_line,
roman_numeral,
version_for,
)
REPO_ROOT = Path(__file__).resolve().parent.parent
JOHN_PAUL_II = 264
def test_john_paul_ii_is_exactly_2_13_7() -> None:
"""The requirement the two ladder constants were solved for."""
assert version_for(JOHN_PAUL_II) == "2.13.7"
assert pope_for(JOHN_PAUL_II).name == "Jan Paweł II"
@pytest.mark.parametrize(
("ordinal", "version", "name"),
[
(1, "0.0.0", "Święty Piotr Apostoł"),
(2, "0.0.1", "Linus"),
(32, "0.3.7", "Milcjades"),
(33, "1.0.0", "Sylwester I"),
(152, "1.14.7", "Leon IX"),
(153, "2.0.0", "Wiktor II"),
(265, "2.14.0", "Benedykt XVI"),
(267, "2.14.2", "Leon XIV"),
],
)
def test_the_anchors_and_the_boundaries_either_side(ordinal: int, version: str, name: str) -> None:
assert version_for(ordinal) == version
assert pope_for(ordinal).name == name
def test_versions_never_go_backwards() -> None:
"""A ladder that is not monotonic is not a version scheme."""
ladder = [
tuple(int(part) for part in version_for(n).split(".")) for n in range(1, len(POPES) + 1)
]
assert all(earlier < later for earlier, later in itertools.pairwise(ladder))
@pytest.mark.parametrize("ordinal", [0, -1, 268, 9999])
def test_an_ordinal_off_the_list_is_refused(ordinal: int) -> None:
with pytest.raises(ValueError, match="no pope with ordinal"):
version_for(ordinal)
with pytest.raises(ValueError, match="no pope with ordinal"):
pope_for(ordinal)
def test_ordinal_for_is_the_inverse_of_version_for() -> None:
for ordinal in (1, 2, 33, 153, JOHN_PAUL_II, len(POPES)):
assert ordinal_for(version_for(ordinal)) == ordinal
def test_a_version_no_release_carries_is_refused() -> None:
with pytest.raises(ValueError, match="no release carries version"):
ordinal_for("9.9.9")
def test_this_build_names_itself() -> None:
assert __version__ == version_for(RELEASE_ORDINAL)
assert pope_for(RELEASE_ORDINAL) == POPE
def test_pyproject_agrees_with_the_ladder() -> None:
"""Two version sources with nothing reconciling them would drift silently."""
pyproject = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
assert pyproject["project"]["version"] == version_for(RELEASE_ORDINAL)
def test_the_committed_pope_list_is_whole() -> None:
"""popes.json is generated; this is what stops a bad regeneration shipping."""
document = json.loads((REPO_ROOT / "signal_bot" / "popes.json").read_text(encoding="utf-8"))
entries = document["popes"]
assert len(entries) == 267
assert [entry["ordinal"] for entry in entries] == list(range(1, 268))
assert all(entry["name"] and entry["portrait"] and entry["reign"] for entry in entries)
assert all(entry["latin"] and entry["title"] for entry in entries)
def test_the_article_title_keeps_its_disambiguator() -> None:
"""title is the URL slug; name is not, and the difference is a 404.
"Hygin" alone is a different pl.wikipedia article entirely, while
"Klemens I (papież)" does not exist -- so neither appending nor omitting
the suffix works as a rule. Only the wikitext link target is right.
"""
assert pope_for(9).title == "Hygin (papież)"
assert pope_for(4).title == "Klemens I"
assert pope_for(9).latin == "Hyginus"
# The release name: Latin stem plus Roman numeral. Not popes.json's `latin`,
# which spells the ordinal out, and not the Polish name.
ROMAN_VALUES = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100}
def roman_to_int(numeral: str) -> int:
"""Test-side reader, so the Latin ordinals can be checked against it."""
total = 0
for index, letter in enumerate(numeral):
value = ROMAN_VALUES[letter]
following = numeral[index + 1 :]
subtractive = following and ROMAN_VALUES[following[0]] > value
total += -value if subtractive else value
return total
@pytest.mark.parametrize(
("ordinal", "expected"),
[
(1, "Petrus"),
(4, "Clemens I"),
(13, "Eleutherius"),
(14, "Victor I"),
(45, "Leo I"),
(68, "Adeodatus I"),
(105, "Nicolaus I"),
(141, "Ioannes XVIII"),
(264, "Ioannes Paulus II"),
(267, "Leo XIV"),
],
)
def test_the_release_name_is_latin_plus_a_roman_numeral(ordinal: int, expected: str) -> None:
assert latin_name(pope_for(ordinal)) == expected
def test_no_pope_name_holds_two_numerals() -> None:
"""roman_numeral takes the first hit, which is only safe if there is one."""
for pope in POPES:
numerals = [token for token in pope.name.split() if set(token) <= set(ROMAN_VALUES)]
assert len(numerals) <= 1, pope.name
def test_every_stem_is_one_word_except_the_two_john_pauls() -> None:
"""The invariant that catches an epithet arriving with a regeneration."""
multiword = {latin_stem(p) for p in POPES if len(latin_stem(p).split()) > 1}
assert multiword == {"Ioannes Paulus"}
def test_the_latin_ordinal_and_the_roman_numeral_agree() -> None:
"""Two independent spellings of the same number, cross-checked."""
for pope in POPES:
words = pope.latin.split()
spelled = [LATIN_ORDINALS[w] for w in words if w in LATIN_ORDINALS]
if not spelled:
continue
numeral = roman_numeral(pope)
assert numeral, pope.name
assert roman_to_int(numeral) == sum(spelled), pope.name
def test_forty_four_popes_carry_no_numeral() -> None:
"""The branch where the name is the bare stem."""
assert sum(1 for pope in POPES if not roman_numeral(pope)) == 44
def test_the_article_url_uses_the_disambiguated_title() -> None:
"""name would 404 for the 72 popes who live at "<name> (papież)"."""
assert article_url(pope_for(13)).endswith("Eleuteriusz_%28papie%C5%BC%29")
assert article_url(pope_for(264)).endswith("Jan_Pawe%C5%82_II")
def test_the_papacy_line_counts_from_the_table_not_a_constant() -> None:
assert pope_line(pope_for(15)) == "papież nr 15 z 267, pontyfikat 199-217"
def test_a_reigning_pope_keeps_his_open_ended_year() -> None:
"""The last entry has one year, not a range; padding it would invent data."""
assert pope_line(pope_for(267)).endswith("pontyfikat 2025")