mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 21:37:03 +01:00
"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
195 lines
6.6 KiB
Python
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")
|