from __future__ import annotations import argparse import importlib.util from pathlib import Path import subprocess import tempfile import unittest SCRIPT_PATH = Path(__file__).resolve().parents[1] / "release_pr.py" SPEC = importlib.util.spec_from_file_location("navop_release_pr", SCRIPT_PATH) assert SPEC is not None and SPEC.loader is not None release_pr = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(release_pr) MANIFEST = """\ [package] name = "main" version = "0.0.9" publish.workspace = true """ LOCK = """\ # This file is automatically @generated by Cargo. [[package]] name = "main" version = "0.0.9" dependencies = ["one-assets"] [[package]] name = "one-assets" version = "3.4.5" """ CHANGELOG = """\ # Changelog Navop user-facing release notes. """ ENTRY = """\ ## [v0.1.0] - 2026-01-01 #### 修复与优化 - 修复了某个问题。 国内下载:如果 GitHub 下载较慢,可从 [CNB 镜像](https://cnb.cool/navop-dev/navop/-/releases/tag/v0.1.0) 下载桌面端安装包 --- #### Fixes and Improvements - Fixed something. """ def git(cwd: Path, *args: str) -> None: completed = subprocess.run( ["git", *args], cwd=cwd, capture_output=True, text=True, check=False ) assert completed.returncode == 0, completed.stderr def write(path: Path, content: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") class ClassifyTests(unittest.TestCase): def test_release_only_changes_qualify_for_the_fast_path(self) -> None: release_only, reason = release_pr.classify( ["CHANGELOG.md", "main/Cargo.toml", "Cargo.lock"], '--- a/main/Cargo.toml\n+++ b/main/Cargo.toml\n-version = "0.0.9"\n' '+version = "0.1.0"\n', ) self.assertTrue(release_only, reason) def test_code_changes_require_the_full_matrix(self) -> None: release_only, reason = release_pr.classify( ["CHANGELOG.md", "crates/terminal/src/wsl_distributions.rs"], "", ) self.assertFalse(release_only) self.assertIn("crates/terminal/src/wsl_distributions.rs", reason) def test_dependency_changes_require_the_full_matrix(self) -> None: release_only, reason = release_pr.classify( ["main/Cargo.toml"], '--- a/main/Cargo.toml\n+++ b/main/Cargo.toml\n' '-version = "0.0.9"\n+version = "0.1.0"\n' '+"one-assets = { workspace = true }"\n', ) self.assertFalse(release_only) self.assertIn("beyond the version bump", reason) def test_empty_changes_are_not_a_release_pull_request(self) -> None: release_only, reason = release_pr.classify([], "") self.assertFalse(release_only) self.assertIn("no file changed", reason) def test_diff_headers_do_not_count_as_changes(self) -> None: self.assertTrue( release_pr.only_version_lines_changed( "diff --git a/Cargo.lock b/Cargo.lock\n" "--- a/Cargo.lock\n" "+++ b/Cargo.lock\n" "-version = \"0.0.9\"\n" "+version = \"0.1.0\"\n" ) ) class VersionTests(unittest.TestCase): def test_manifest_version_reads_the_package_section(self) -> None: self.assertEqual(release_pr.manifest_version(MANIFEST), "0.0.9") def test_lock_version_reads_the_main_package(self) -> None: self.assertEqual(release_pr.lock_main_version(LOCK), "0.0.9") def test_missing_manifest_version_is_reported(self) -> None: with self.assertRaises(release_pr.ReleasePrError): release_pr.manifest_version('[workspace]\nmembers = ["main"]\n') def test_missing_lock_package_is_reported(self) -> None: with self.assertRaises(release_pr.ReleasePrError): release_pr.lock_main_version('[[package]]\nname = "other"\nversion = "1.0.0"\n') class ChangelogTests(unittest.TestCase): def test_entry_for_the_bumped_version_validates(self) -> None: release_pr.validate_changelog_entry(f"{CHANGELOG}\n{ENTRY}", "0.1.0") def test_missing_entry_is_reported(self) -> None: with self.assertRaises(release_pr.ReleasePrError): release_pr.validate_changelog_entry(CHANGELOG, "0.1.0") class CheckCommandTests(unittest.TestCase): def setUp(self) -> None: self._temporary = tempfile.TemporaryDirectory() self.addCleanup(self._temporary.cleanup) self.root = Path(self._temporary.name) git(self.root, "init", "-b", "main") git(self.root, "config", "user.email", "release@example.com") git(self.root, "config", "user.name", "Release Test") write(self.root / "main/Cargo.toml", MANIFEST) write(self.root / "Cargo.lock", LOCK) write(self.root / "CHANGELOG.md", CHANGELOG) git(self.root, "add", ".") git(self.root, "commit", "-m", "base") def _release_commit(self) -> None: write( self.root / "main/Cargo.toml", MANIFEST.replace('version = "0.0.9"', 'version = "0.1.0"'), ) write( self.root / "Cargo.lock", LOCK.replace('version = "0.0.9"', 'version = "0.1.0"'), ) write(self.root / "CHANGELOG.md", f"{CHANGELOG}\n{ENTRY}") git(self.root, "commit", "-am", "release") def _check(self, base: str = "HEAD~1") -> None: release_pr.command_check( argparse.Namespace( base=base, head="HEAD", root=self.root, github_output="" ) ) def test_release_commit_is_classified_and_validated(self) -> None: self._release_commit() self._check() def test_release_commit_publishes_the_release_output(self) -> None: self._release_commit() output = self.root / "github-output.txt" release_pr.command_check( argparse.Namespace( base="HEAD~1", head="HEAD", root=self.root, github_output=str(output), ) ) self.assertEqual( output.read_text(encoding="utf-8"), "release_only=true\nversion=0.1.0\n", ) def test_unavailable_diff_falls_back_to_the_full_matrix(self) -> None: output = self.root / "github-output.txt" release_pr.command_check( argparse.Namespace( base="does-not-exist", head="HEAD", root=self.root, github_output=str(output), ) ) self.assertFalse(output.exists(), "fallback must not skip the matrix") def test_version_mismatch_fails_the_release_check(self) -> None: write( self.root / "main/Cargo.toml", MANIFEST.replace('version = "0.0.9"', 'version = "0.1.0"'), ) write(self.root / "CHANGELOG.md", f"{CHANGELOG}\n{ENTRY}") git(self.root, "commit", "-am", "release with a stale lock") with self.assertRaises(release_pr.ReleasePrError) as error: self._check() self.assertIn("Cargo.lock pins main at", str(error.exception)) def test_code_change_is_not_a_release_pull_request(self) -> None: self._release_commit() write(self.root / "crates/terminal/src/lib.rs", "pub fn placeholder() {}\n") git(self.root, "add", ".") git(self.root, "commit", "-m", "feature") self._check(base="HEAD~2") if __name__ == "__main__": unittest.main()