Files
github-action/tests/test_changelog.py
T
Tirsvad 930e67ad7e
TirSystem/github-action: Publish wiki / publish-wiki (push) Skipped
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 16s
Add the changelog and the check of its convention
CHANGELOG.md holds an [Unreleased] section with what the first release
v1.0.0 will contain, in the groups Breaking, Added, Changed and Fixed.
tools/changelog.py checks the convention of ADR-0006 (Unreleased first,
descending versions, dates, a Breaking entry needs a new major and a new major
needs one) and prints the notes of a release for the release page. The
release process guide uses both.

Task: MIL-004#2
2026-10-10 22:51:08 +08:00

135 lines
4.7 KiB
Python

"""Offline tests for tools/changelog.py and the CHANGELOG.md of this repository."""
import importlib.util
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
_SPEC = importlib.util.spec_from_file_location("changelog", REPO_ROOT / "tools" / "changelog.py")
assert _SPEC is not None and _SPEC.loader is not None
changelog = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(changelog)
GOOD = """# Changelog
## [Unreleased]
### Added
- A new optional input.
## [2.0.0] - 2026-12-01
### Breaking
- The input `languages` was renamed.
### Fixed
- A message.
## [1.1.0] - 2026-11-01
### Added
- A workflow.
## [1.0.0] - 2026-10-20
### Added
- The first release.
"""
class TestParse:
def test_reads_releases_groups_and_items(self):
releases = changelog.parse(GOOD)
assert [r.name for r in releases] == ["Unreleased", "2.0.0", "1.1.0", "1.0.0"]
assert releases[1].version == (2, 0, 0)
assert releases[1].date == "2026-12-01"
assert releases[1].groups["Breaking"] == ["The input `languages` was renamed."]
assert releases[0].is_unreleased and releases[0].version is None
class TestValidate:
def test_a_correct_changelog_has_no_problem(self):
assert changelog.validate(GOOD) == []
def test_requires_unreleased_first(self):
text = GOOD.replace("## [Unreleased]\n\n### Added\n\n- A new optional input.\n\n", "")
assert any("first section must be `## [Unreleased]`" in p for p in changelog.validate(text))
def test_rejects_an_unknown_group(self):
text = GOOD.replace("### Fixed", "### Misc")
assert any("group 'Misc'" in p for p in changelog.validate(text))
def test_rejects_an_empty_group(self):
text = GOOD.replace("- A message.\n", "")
assert any("has no item" in p for p in changelog.validate(text))
def test_rejects_a_release_without_a_date(self):
text = GOOD.replace("## [1.1.0] - 2026-11-01", "## [1.1.0]")
assert any("needs a date" in p for p in changelog.validate(text))
def test_rejects_a_name_that_is_not_a_version(self):
text = GOOD.replace("## [1.1.0] - 2026-11-01", "## [v1.1] - 2026-11-01")
assert any("not a version" in p for p in changelog.validate(text))
def test_versions_must_descend(self):
text = GOOD.replace("## [1.1.0]", "## [2.0.0]")
assert any("must be lower than the version above" in p for p in changelog.validate(text))
def test_breaking_needs_a_new_major(self):
text = GOOD.replace("## [1.1.0] - 2026-11-01\n\n### Added\n\n- A workflow.",
"## [1.1.0] - 2026-11-01\n\n### Breaking\n\n- A rename.")
assert any("lists Breaking changes but the major version does not increase" in p
for p in changelog.validate(text))
def test_a_new_major_needs_a_breaking_change(self):
text = GOOD.replace("### Breaking\n\n- The input `languages` was renamed.\n\n### Fixed",
"### Fixed")
assert any("raises the major version but lists no Breaking change" in p
for p in changelog.validate(text))
def test_an_empty_file_is_a_problem(self):
assert changelog.validate("# Changelog\n")
class TestNotes:
def test_prints_the_groups_in_the_fixed_order(self):
text = changelog.notes(GOOD, "2.0.0")
assert text.startswith("## 2.0.0 - 2026-12-01")
assert text.index("### Breaking") < text.index("### Fixed")
assert "The input `languages` was renamed." in text
def test_an_unknown_version_is_an_error(self):
with pytest.raises(LookupError):
changelog.notes(GOOD, "3.0.0")
class TestCommandLine:
def test_check_passes_for_a_good_file(self, tmp_path, capsys):
path = tmp_path / "CHANGELOG.md"
path.write_text(GOOD, encoding="utf-8")
assert changelog.main(["check", "--file", str(path)]) == 0
def test_check_version_fails_without_an_entry(self, tmp_path, capsys):
path = tmp_path / "CHANGELOG.md"
path.write_text(GOOD, encoding="utf-8")
assert changelog.main(["check", "--file", str(path), "--version", "3.0.0"]) == 1
assert "no changelog entry" in capsys.readouterr().err
def test_notes_prints_the_entry(self, tmp_path, capsys):
path = tmp_path / "CHANGELOG.md"
path.write_text(GOOD, encoding="utf-8")
assert changelog.main(["notes", "1.1.0", "--file", str(path)]) == 0
assert "A workflow." in capsys.readouterr().out
def test_the_changelog_of_this_repository_follows_the_convention():
text = (REPO_ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert changelog.validate(text) == []
assert changelog.parse(text)[0].is_unreleased