"""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", )