212 lines
5.8 KiB
Python
212 lines
5.8 KiB
Python
"""Release-note changelog extraction tests (DC-6, DC-7)."""
|
|
import importlib.util
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
EXTRACTOR_PATH = REPO_ROOT / "scripts" / "extract_changelog.py"
|
|
RELEASE_REQUEST_PATH = REPO_ROOT / "scripts" / "release_request.py"
|
|
CHANGELOG_PATH = REPO_ROOT / "CHANGELOG.md"
|
|
|
|
|
|
def _extractor_module():
|
|
spec = importlib.util.spec_from_file_location("extract_changelog", EXTRACTOR_PATH)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def test_extracts_the_requested_version_section_verbatim():
|
|
extractor = _extractor_module()
|
|
changelog = """# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
## [1.4.0] - 2026-09-10
|
|
|
|
### Added
|
|
|
|
- Show a release summary to consumers.
|
|
|
|
## [1.3.0] - 2026-09-01
|
|
|
|
### Fixed
|
|
|
|
- Preserve the observation history during upgrades.
|
|
"""
|
|
expected = """## [1.4.0] - 2026-09-10
|
|
|
|
### Added
|
|
|
|
- Show a release summary to consumers.
|
|
|
|
"""
|
|
|
|
assert extractor.extract_version_section(changelog, "1.4.0") == expected
|
|
|
|
|
|
def test_checked_in_changelog_keeps_unreleased_first_and_categories_limited():
|
|
lines = CHANGELOG_PATH.read_text(encoding="utf-8").splitlines()
|
|
unreleased = lines.index("## [Unreleased]")
|
|
version_headings = [
|
|
index for index, line in enumerate(lines)
|
|
if line.startswith("## [") and line != "## [Unreleased]"
|
|
]
|
|
first_version = version_headings[0] if version_headings else len(lines)
|
|
categories = [
|
|
line.removeprefix("### ")
|
|
for line in lines[unreleased + 1:first_version]
|
|
if line.startswith("### ")
|
|
]
|
|
|
|
assert unreleased < first_version
|
|
assert set(categories) <= {"Added", "Changed", "Fixed"}
|
|
|
|
|
|
def test_release_footer_verifies_the_clearsigned_checksum_asset():
|
|
footer = (REPO_ROOT / "packaging" / "release-footer.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
assert "gpg --output SHA256SUMS --decrypt SHA256SUMS.asc" in footer
|
|
assert "sha256sum -c SHA256SUMS" in footer
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("changelog", "expected_error"),
|
|
[
|
|
("# Changelog\n\n## [Unreleased]\n", "missing"),
|
|
(
|
|
"# Changelog\n\n## [Unreleased]\n\n## [1.4.0] - 2026-09-10\n",
|
|
"empty",
|
|
),
|
|
(
|
|
"# Changelog\n\n## [Unreleased]\n\n## [1.4.0] - 2026-02-30\n\n- Add a note.\n",
|
|
"malformed release date",
|
|
),
|
|
],
|
|
)
|
|
def test_fails_closed_for_missing_empty_or_malformed_sections(
|
|
changelog, expected_error
|
|
):
|
|
extractor = _extractor_module()
|
|
|
|
with pytest.raises(extractor.ChangelogError, match=expected_error):
|
|
extractor.extract_version_section(changelog, "1.4.0")
|
|
|
|
|
|
def test_command_emits_a_workflow_error_and_nonzero_status(tmp_path):
|
|
changelog = tmp_path / "CHANGELOG.md"
|
|
changelog.write_text("# Changelog\n\n## [Unreleased]\n", encoding="utf-8")
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(EXTRACTOR_PATH), str(changelog), "1.4.0"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
assert result.returncode != 0
|
|
assert result.stderr.startswith("::error::")
|
|
assert "missing" in result.stderr
|
|
|
|
|
|
def test_assembles_a_release_body_without_changing_the_section():
|
|
extractor = _extractor_module()
|
|
section = "## [1.4.0] - 2026-09-10\n\n### Added\n\n- Show a release summary.\n"
|
|
footer = "## Install\n\nUse the package channel.\n"
|
|
|
|
assert extractor.assemble_release_body(section, footer) == (
|
|
section + "\n" + footer
|
|
)
|
|
|
|
|
|
def test_command_can_write_the_complete_release_body(tmp_path):
|
|
changelog = tmp_path / "CHANGELOG.md"
|
|
changelog.write_text(
|
|
"# Changelog\n\n## [Unreleased]\n\n## [1.4.0] - 2026-09-10\n\n"
|
|
"### Added\n\n- Show a release summary.\n",
|
|
encoding="utf-8",
|
|
)
|
|
footer = tmp_path / "footer.md"
|
|
footer.write_text("## Install\n\nUse the package channel.\n", encoding="utf-8")
|
|
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(EXTRACTOR_PATH),
|
|
str(changelog),
|
|
"1.4.0",
|
|
"--footer",
|
|
str(footer),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
assert result.returncode == 0
|
|
assert result.stdout == (
|
|
"## [1.4.0] - 2026-09-10\n\n### Added\n\n- Show a release summary.\n\n"
|
|
"## Install\n\nUse the package channel.\n"
|
|
)
|
|
|
|
|
|
def test_release_request_command_reports_create_or_patch_decisions(tmp_path):
|
|
body = tmp_path / "release-body.md"
|
|
body.write_text("## [1.4.0] - 2026-09-10\n", encoding="utf-8")
|
|
|
|
create = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(RELEASE_REQUEST_PATH),
|
|
"--version",
|
|
"1.4.0",
|
|
"--body-file",
|
|
str(body),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
existing = tmp_path / "existing-release.json"
|
|
existing.write_text('{"id": 17, "assets": []}', encoding="utf-8")
|
|
patch = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(RELEASE_REQUEST_PATH),
|
|
"--version",
|
|
"1.4.0",
|
|
"--body-file",
|
|
str(body),
|
|
"--existing-release",
|
|
str(existing),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
assert create.returncode == patch.returncode == 0
|
|
assert json.loads(create.stdout) == {
|
|
"method": "POST",
|
|
"path": "/releases",
|
|
"payload": {
|
|
"tag_name": "v1.4.0",
|
|
"name": "v1.4.0",
|
|
"body": "## [1.4.0] - 2026-09-10\n",
|
|
},
|
|
}
|
|
assert json.loads(patch.stdout) == {
|
|
"method": "PATCH",
|
|
"path": "/releases/17",
|
|
"payload": {"body": "## [1.4.0] - 2026-09-10\n"},
|
|
}
|