mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 07:53:11 +00:00
120 lines
3.2 KiB
Python
120 lines
3.2 KiB
Python
|
|
"""Tests for the CHANGELOG parser — the single source of truth for versioning."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from shared.changelog import (
|
||
|
|
bump_kind,
|
||
|
|
entry_for,
|
||
|
|
is_valid_bump,
|
||
|
|
latest_entry,
|
||
|
|
parse_changelog,
|
||
|
|
top_version,
|
||
|
|
version_entries,
|
||
|
|
)
|
||
|
|
|
||
|
|
# A faithful slice of the real file: preamble prose, a plain version entry, a
|
||
|
|
# legacy combined entry, and date-only historical headers with no version.
|
||
|
|
SAMPLE = """# Changelog
|
||
|
|
|
||
|
|
What's changed in the **After-Hours Shift Manager**. Newest first.
|
||
|
|
|
||
|
|
Versions are `MAJOR.MINOR.PATCH`.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## v1.10.0 — June 11, 2026
|
||
|
|
|
||
|
|
**Release notes now announce themselves.** Big new feature line.
|
||
|
|
|
||
|
|
## v1.9.2 — June 1, 2026
|
||
|
|
|
||
|
|
**Setup-guide fix.** A patch.
|
||
|
|
|
||
|
|
## v1.9.0 / v1.9.1 — June 1, 2026
|
||
|
|
|
||
|
|
**Last-minute drops blocked.** Combined entry. *(v1.9.1 was a library update.)*
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## May 2026 — Phone-system automation
|
||
|
|
|
||
|
|
- Live phone routing follows the schedule.
|
||
|
|
|
||
|
|
## April 3, 2026 — Launch 🎉
|
||
|
|
|
||
|
|
The first version.
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
def test_preamble_is_not_an_entry():
|
||
|
|
# The "# Changelog" h1 and prose before the first "##" must not parse as entries.
|
||
|
|
entries = parse_changelog(SAMPLE)
|
||
|
|
assert all("Changelog" not in e.title for e in entries)
|
||
|
|
|
||
|
|
|
||
|
|
def test_top_version_skips_preamble():
|
||
|
|
assert top_version(SAMPLE) == "1.10.0"
|
||
|
|
|
||
|
|
|
||
|
|
def test_latest_entry_fields():
|
||
|
|
entry = latest_entry(SAMPLE)
|
||
|
|
assert entry.version == "1.10.0"
|
||
|
|
assert entry.date_label == "June 11, 2026"
|
||
|
|
assert "announce themselves" in entry.body
|
||
|
|
|
||
|
|
|
||
|
|
def test_combined_header_reports_higher_version():
|
||
|
|
entry = entry_for(SAMPLE, "1.9.0")
|
||
|
|
assert entry.versions == ("1.9.0", "1.9.1")
|
||
|
|
assert entry.version == "1.9.1" # the higher of the two
|
||
|
|
|
||
|
|
|
||
|
|
def test_combined_header_is_findable_by_either_version():
|
||
|
|
assert entry_for(SAMPLE, "1.9.1") == entry_for(SAMPLE, "v1.9.0")
|
||
|
|
|
||
|
|
|
||
|
|
def test_body_excludes_thematic_break_rules():
|
||
|
|
# The "---" rule separating eras must not bleed into the combined entry's notes.
|
||
|
|
assert "---" not in entry_for(SAMPLE, "1.9.0").body
|
||
|
|
|
||
|
|
|
||
|
|
def test_date_only_headers_carry_no_version():
|
||
|
|
titles = {e.title for e in parse_changelog(SAMPLE) if not e.versions}
|
||
|
|
assert "May 2026 — Phone-system automation" in titles
|
||
|
|
assert "April 3, 2026 — Launch 🎉" in titles
|
||
|
|
|
||
|
|
|
||
|
|
def test_version_entries_excludes_date_only():
|
||
|
|
versions = [e.version for e in version_entries(SAMPLE)]
|
||
|
|
assert versions == ["1.10.0", "1.9.2", "1.9.1"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_entry_for_accepts_v_prefix():
|
||
|
|
assert entry_for(SAMPLE, "v1.10.0").version == "1.10.0"
|
||
|
|
|
||
|
|
|
||
|
|
def test_entry_for_unknown_version_is_none():
|
||
|
|
assert entry_for(SAMPLE, "2.0.0") is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_empty_changelog_has_no_top_version():
|
||
|
|
assert top_version("# Changelog\n\nNothing yet.\n") is None
|
||
|
|
assert latest_entry("# Changelog\n") is None
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
"new,prev,expected",
|
||
|
|
[
|
||
|
|
("1.10.0", "1.9.2", "minor"), # minor resets patch to 0
|
||
|
|
("2.0.0", "1.9.2", "major"),
|
||
|
|
("1.9.3", "1.9.2", "patch"),
|
||
|
|
("1.11.0", "1.9.2", None), # skips a minor
|
||
|
|
("1.9.2", "1.9.2", None), # no change
|
||
|
|
("1.9.1", "1.9.2", None), # downgrade
|
||
|
|
("3.0.0", "1.9.2", None), # skips a major
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_bump_kind(new, prev, expected):
|
||
|
|
assert bump_kind(new, prev) == expected
|
||
|
|
assert is_valid_bump(new, prev) is (expected is not None)
|