signal-bot/tests/test_versioning.py
2026-08-30 21:28:41 +02:00

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