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
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
135 lines
4.7 KiB
Python
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
|