# 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 " (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")