kscrape / tests /test_release_tooling.py
kscrape
chore: restore repository maintenance standards
69c00d1 unverified
Raw History Blame Contribute Delete
5.74 kB
"""Tests for the local release and installed-package checks."""
from __future__ import annotations
from dataclasses import replace
from pathlib import Path
import pytest
from kscrape_dev.packaging import release_notes
from kscrape_dev.packaging.install_smoke import (
InstallSnapshot,
validate_install_snapshot,
)
def sample_changelog(
*,
version: str = "1.2.3",
unreleased_entry: str = "",
release_entry: str = "- Added a tested release workflow.",
toc_entry: bool = True,
) -> str:
"""Create a compact changelog accepted by the release-note parser.
:param version: Released version heading and link value.
:param unreleased_entry: Optional entry that blocks a release.
:param release_entry: Content placed in the released section.
:param toc_entry: Whether to include the released version in the table of contents.
:return: Changelog fixture text.
"""
anchor = f"#{version.replace('.', '')}---2026-08-31"
version_toc = (
f"3. [\\[{version}\\] - 2026-08-31]({anchor})\n" if toc_entry else ""
)
return f"""# Changelog
## Table of contents
1. [Changelog](#changelog)
2. [\\[Unreleased\\]](#unreleased)
{version_toc}
# [Unreleased]
### πŸ’₯ Breaking Change Summary
### βž• Added
{unreleased_entry}
### πŸ’” Changed
### ⚠️ Deprecated
### πŸ—‘οΈ Removed
### πŸ”¨ Fixed
# [{version}] - 2026-08-31
### βž• Added
{release_entry}
# [1.2.2] - 2026-08-30
"""
def test_release_notes_extract_a_prepared_release() -> None:
"""Extract entries and promote their category heading to release-note level."""
assert release_notes.build_release_notes(sample_changelog(), "1.2.3") == (
"## βž• Added\n\n- Added a tested release workflow.\n"
)
def test_release_notes_reject_unprepared_entries() -> None:
"""Reject a tag while a release entry remains in ``[Unreleased]``."""
with pytest.raises(ValueError, match="still contains release entries"):
release_notes.build_release_notes(
sample_changelog(unreleased_entry="- Not released."), "1.2.3"
)
@pytest.mark.parametrize("version", ["v1.2.3", "1.2", "1.2.3.4", "01.2.3"])
def test_release_notes_reject_malformed_versions(version: str) -> None:
"""Require release versions to contain three non-zero-padded components."""
with pytest.raises(ValueError, match="Invalid release version"):
release_notes.build_release_notes(sample_changelog(), version)
def test_release_notes_reject_missing_or_duplicate_release() -> None:
"""Require exactly one changelog section for the selected version."""
with pytest.raises(ValueError, match="exactly one release heading"):
release_notes.build_release_notes(sample_changelog(), "1.2.4")
duplicate = sample_changelog().replace(
"# [1.2.2] - 2026-08-30", "# [1.2.3] - 2026-08-31"
)
with pytest.raises(ValueError, match="found 2"):
release_notes.build_release_notes(duplicate, "1.2.3")
def test_release_notes_require_entries_and_toc() -> None:
"""Require a release body and its matching table-of-contents entry."""
with pytest.raises(ValueError, match="has no changelog entries"):
release_notes.build_release_notes(
sample_changelog(release_entry=""), "1.2.3"
)
with pytest.raises(ValueError, match="table of contents"):
release_notes.build_release_notes(
sample_changelog(toc_entry=False), "1.2.3"
)
def test_release_notes_validate_versions_and_cli_arguments(
tmp_path: Path,
) -> None:
"""Reject stale releases, mismatched tags, and partial tag validation arguments.
:param tmp_path: Temporary test directory supplied by pytest.
:return: None.
"""
assert release_notes.latest_release_version(sample_changelog()) == "1.2.3"
release_notes.validate_next_version("1.2.3", "1.2.4")
release_notes.validate_tag_version("v1.2.3", "1.2.3")
with pytest.raises(ValueError, match="must be newer"):
release_notes.validate_next_version("1.2.3", "1.2.3")
with pytest.raises(ValueError, match="does not match project version"):
release_notes.validate_tag_version("v1.2.4", "1.2.3")
with pytest.raises(ValueError, match="must be provided together"):
release_notes.main(
[
"1.2.3",
"--changelog",
str(tmp_path / "CHANGELOG.md"),
"--output",
str(tmp_path / "notes.md"),
"--tag",
"v1.2.3",
]
)
def valid_snapshot() -> InstallSnapshot:
"""Create a snapshot that satisfies the installed-package contract.
:return: Complete installed-package snapshot.
"""
return InstallSnapshot(
version="1.2.3",
module_file="installed/kscrape/__init__.py",
console_path="bin/kscrape",
help_returncode=0,
help_output="usage: kscrape [OPTIONS] COMMAND [ARGS]...",
)
def test_install_smoke_contract_accepts_complete_installation() -> None:
"""Accept a package import and console command that both work."""
validate_install_snapshot(valid_snapshot(), expected_version="1.2.3")
@pytest.mark.parametrize(
"field,value",
[
("module_file", None),
("console_path", None),
("help_returncode", 1),
("help_output", ""),
],
)
def test_install_smoke_contract_rejects_incomplete_installation(
field: str, value: object
) -> None:
"""Reject a distribution that lacks an importable package or usable CLI."""
with pytest.raises(AssertionError):
validate_install_snapshot(
replace(valid_snapshot(), **{field: value}),
expected_version="1.2.3",
)