signal-bot/tests/test_changelog.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

108 lines
3.3 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""DOCS-changelog.md is the only copy, so the parser has to earn that."""
from __future__ import annotations
import re
from pathlib import Path
from signal_bot.changelog import Release, load, parse, since
from signal_bot.versioning import (
__version__,
latin_name,
ordinal_for,
pope_for,
pope_line,
)
HEADING = re.compile(r"^##\s+(\d+\.\d+\.\d+)\s*—")
DOCUMENT = """\
# Changelog
Prose between releases is ignored.
- a bullet before any heading belongs to no release
## 0.0.1 — Linus
- druga zmiana
- trzecia zmiana
Some more prose.
## 0.0.0 — Święty Piotr Apostoł
- pierwsza zmiana
"""
def test_parse_reads_headings_and_their_bullets() -> None:
releases = parse(DOCUMENT)
assert [r.version for r in releases] == ["0.0.0", "0.0.1"]
assert releases[0].pope == "Święty Piotr Apostoł"
assert releases[1].lines == ("druga zmiana", "trzecia zmiana")
def test_a_bullet_above_every_heading_belongs_to_nobody() -> None:
"""Otherwise the file's own format documentation becomes a changelog entry."""
assert all("belongs to no release" not in line for r in parse(DOCUMENT) for line in r.lines)
def test_parse_of_nothing_is_nothing() -> None:
assert parse("# Changelog\n\njust prose\n") == ()
def test_releases_come_back_oldest_first_whatever_the_file_order() -> None:
assert [r.ordinal for r in parse(DOCUMENT)] == [1, 2]
def test_since_returns_only_what_the_group_has_not_seen() -> None:
releases = parse(DOCUMENT)
assert [r.version for r in since(releases, "0.0.0")] == ["0.0.1"]
assert since(releases, "0.0.1") == ()
def test_since_an_unknown_version_announces_nothing() -> None:
"""A hand-edited or downgraded record must not dump the whole backlog."""
assert since(parse(DOCUMENT), "9.9.9") == ()
def test_a_missing_changelog_costs_an_announcement_not_the_process() -> None:
assert load(Path("/nonexistent/DOCS-changelog.md")) == ()
def test_the_real_changelog_covers_the_running_version() -> None:
"""A release with no entry would announce a version and then say nothing."""
versions = [release.version for release in load()]
assert __version__ in versions
def test_release_knows_its_ordinal() -> None:
assert Release("2.13.7", "Jan Paweł II", ()).ordinal == 264
def test_every_heading_name_matches_the_derived_latin_one() -> None:
"""The doc is checked, not trusted: a typo here fails here, not in a group."""
for release in load():
assert release.pope == latin_name(release.pope_entry), release.version
def test_every_section_carries_its_derived_papacy_line() -> None:
"""The ordinal and the reign are data, so the document may not disagree.
Read off the raw file rather than the parsed releases: the parser ignores
anything that is not a heading or a bullet, which is what makes this line
free to add -- and what would let it rot unnoticed without this test.
"""
lines = Path("DOCS-changelog.md").read_text(encoding="utf-8").splitlines()
checked = 0
for index, line in enumerate(lines):
heading = HEADING.match(line)
if heading is None:
continue
pope = pope_for(ordinal_for(heading.group(1)))
assert lines[index + 2] == pope_line(pope), heading.group(1)
checked += 1
assert checked == len(load())