mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 20:27:17 +01:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
185 lines
6.2 KiB
Python
185 lines
6.2 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,
|
|
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")
|