"""Offline tests for the compatibility tracker.""" import importlib.util import io import json import tempfile import unittest from pathlib import Path from unittest.mock import patch SCRIPT = Path(__file__).with_name("track_gitea_release.py") SPEC = importlib.util.spec_from_file_location("track_gitea_release", SCRIPT) TRACKER = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(TRACKER) RELEASE_URL = "https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest" FIXTURES = { "COMPATIBILITY.md": """# Gitea Compatibility | Template Release | Min Gitea | Max Tested Gitea | Status | |---|---|---|---| | **v28.0.0** | **28.0.0** | **28.0.0** | [PASS] Active | > **Latest verified:** Release v28.0.0 passes tests. | Gitea version | Template release | |---|---| | 28.1.0 | [PENDING] Compatibility verification; no tested template release yet | | 28.0.0 | **v28.0.0** | | Gitea | Release Date | Mail Template Changes | Breaking? | |---|---|---|---| | **28.1.0** | 2026-10-06 | [PENDING] Mail-template compatibility verification | TBD | | **28.0.0** | 2026-09-29 | FormatByteSize | Yes | | **≤ 1.24.x** | — | Old directory | [UNSUPPORTED] | """, "README.md": f"""# Gitea Mail Templates > Latest Release: [v28.0.0]({RELEASE_URL}) [![Gitea](https://img.shields.io/badge/Gitea-28.1.0%20%5BPENDING%5D%20%7C%2028.0.0%20tested-yellow)](COMPATIBILITY.md) - **Latest tested:** Gitea 28.0.0 - **Latest release:** [v28.0.0]({RELEASE_URL}) - **Upstream Gitea 28.1.0:** [PENDING] """, "AGENTS.md": """# Agents - Current template release: **v28.0.0**. - Latest upstream Gitea release: 28.1.0 [PENDING]. """, "docs/README.zh-CN.md": f"""# Gitea 邮件模板 > 最新发布版:[v28.0.0]({RELEASE_URL}) - **最新测试:** Gitea 28.0.0 - **最新发布版:** [v28.0.0]({RELEASE_URL}) - **上游 Gitea 28.1.0:** [PENDING] """, } class TrackerTests(unittest.TestCase): def setUp(self): self.directory = tempfile.TemporaryDirectory(prefix="tracker-test-", dir=TRACKER.ROOT) self.addCleanup(self.directory.cleanup) self.root = Path(self.directory.name) self.paths = ("COMPATIBILITY.md", "README.md", "AGENTS.md", "docs/README.zh-CN.md") self.originals = {} for name in self.paths: destination = self.root / name destination.parent.mkdir(parents=True, exist_ok=True) content = FIXTURES[name] destination.write_text(content, encoding="utf-8") self.originals[name] = content def read(self, name): return (self.root / name).read_text(encoding="utf-8") def assert_unchanged(self): for name, content in self.originals.items(): self.assertEqual(content, self.read(name), name) def test_new_release_adds_only_pending_rows(self): changed = TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") self.assertEqual({self.root / name for name in self.paths}, set(changed)) compatibility = self.read("COMPATIBILITY.md") self.assertIn("| 28.2.0 | [PENDING] Compatibility verification", compatibility) self.assertIn("| **28.2.0** | 2026-11-01 | [PENDING]", compatibility) self.assertIn("| **v28.0.0** | **28.0.0** | **28.0.0** | [PASS] Active |", compatibility) self.assertIn("Latest verified:** Release v28.0.0", compatibility) self.assertIn("Gitea-28.2.0%20%5BPENDING%5D%20%7C%2028.0.0%20tested-yellow", self.read("README.md")) self.assertIn("Gitea 28.2.0:** [PENDING]", self.read("README.md")) self.assertIn("Gitea 28.2.0:** [PENDING]", self.read("docs/README.zh-CN.md")) self.assertIn("Gitea release: 28.2.0 [PENDING]", self.read("AGENTS.md")) self.assertIn("Latest tested:** Gitea 28.0.0", self.read("README.md")) self.assertIn("最新测试:** Gitea 28.0.0", self.read("docs/README.zh-CN.md")) self.assertEqual([], TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")) def test_repository_markers_are_valid(self): documents, blocks = TRACKER.documents(TRACKER.ROOT) self.assertTrue({TRACKER.ROOT / name for name in self.paths}.issubset(documents)) self.assertIn(("TRACKER", "HISTORY"), {(block.subject, block.content) for block in blocks}) self.assertIn(("RELEASE", "HEADER"), {(block.subject, block.content) for block in blocks}) TRACKER.latest_tested(documents, blocks) def test_existing_version_is_idempotent(self): self.assertEqual([], TRACKER.apply_release(self.root, "28.1.0", "2026-10-06")) self.assert_unchanged() def test_partial_update_is_repaired(self): compatibility = self.root / "COMPATIBILITY.md" partial = self.read("COMPATIBILITY.md").replace("| 28.1.0 | [PENDING] Compatibility verification; no tested template release yet |\n", "") compatibility.write_text(partial, encoding="utf-8") self.assertEqual([compatibility], TRACKER.apply_release(self.root, "28.1.0", "2026-10-06")) self.assertIn("| 28.1.0 | [PENDING] Compatibility verification", self.read("COMPATIBILITY.md")) def test_older_version_does_not_rewrite_history(self): self.assertEqual([], TRACKER.apply_release(self.root, "28.0.0", "2026-09-29")) self.assert_unchanged() def test_dry_run_does_not_write(self): self.assertEqual(4, len(TRACKER.apply_release(self.root, "28.2.0", "2026-11-01", dry_run=True))) self.assert_unchanged() def test_missing_marker_fails_without_writing(self): compatibility = self.root / "COMPATIBILITY.md" compatibility.write_text(self.read("COMPATIBILITY.md").replace("\n", ""), encoding="utf-8") with self.assertRaises(ValueError): TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") self.assertEqual(self.originals["README.md"], self.read("README.md")) def test_unknown_marker_fails_without_writing(self): news = self.root / "docs/NEWS.md" news.write_text('\n', encoding="utf-8") with self.assertRaisesRegex(ValueError, "Unknown TRACKER content"): TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") self.assert_unchanged() def test_new_marked_document_is_discovered(self): news = self.root / "misc/notes/NEWS.md" news.parent.mkdir(parents=True) news.write_text('\n\nGitea 28.1.0 [PENDING]\n\n', encoding="utf-8") changed = TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") self.assertIn(news, changed) self.assertIn("Gitea 28.2.0 [PENDING]", news.read_text(encoding="utf-8")) def test_duplicate_block_uses_first_in_document_order(self): english = self.root / "README.md" duplicate = "\nGitea 28.1.0 [PENDING]\n\n" english.write_text(self.read("README.md") + duplicate, encoding="utf-8") TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") self.assertIn("**Upstream Gitea 28.2.0:** [PENDING]", self.read("README.md")) self.assertTrue(self.read("README.md").endswith(duplicate)) def test_unclosed_block_rejected_without_writing(self): english = self.root / "README.md" english.write_text(self.read("README.md").replace("", ""), encoding="utf-8") with self.assertRaisesRegex(ValueError, "Unclosed tag block"): TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") self.assertEqual(self.originals["COMPATIBILITY.md"], self.read("COMPATIBILITY.md")) def test_undeclared_block_rejected(self): english = self.root / "README.md" english.write_text(self.read("README.md") + "\nv28.0.0\n\n", encoding="utf-8") with self.assertRaisesRegex(ValueError, "Undeclared RELEASE:CURRENT"): TRACKER.apply_template_release(self.root, "v28.0.1") def test_misspelled_subject_rejected(self): english = self.root / "README.md" english.write_text(self.read("README.md").replace("", ""), encoding="utf-8") with self.assertRaisesRegex(ValueError, "Undeclared TRACER:UPSTREAM"): TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") def test_block_without_manifest_rejected(self): news = self.root / "NEWS.md" news.write_text("\nGitea 28.1.0 [PENDING]\n\n", encoding="utf-8") with self.assertRaisesRegex(ValueError, "without DOC-TAGS"): TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") def test_mismatched_close_rejected(self): english = self.root / "README.md" english.write_text(self.read("README.md").replace("", ""), encoding="utf-8") with self.assertRaisesRegex(ValueError, "Unmatched closing tag"): TRACKER.apply_template_release(self.root, "v28.0.1") def test_disagreeing_tested_versions_fail(self): chinese = self.root / "docs/README.zh-CN.md" chinese.write_text(self.read("docs/README.zh-CN.md").replace("**最新测试:** Gitea 28.0.0", "**最新测试:** Gitea 28.1.0"), encoding="utf-8") with self.assertRaisesRegex(ValueError, "disagree"): TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") def test_after_verification_new_version_becomes_pending(self): english = self.root / "README.md" chinese = self.root / "docs/README.zh-CN.md" agents = self.root / "AGENTS.md" english.write_text(self.read("README.md") .replace("**Latest tested:** Gitea 28.0.0", "**Latest tested:** Gitea 28.1.0") .replace("Gitea-28.1.0%20%5BPENDING%5D%20%7C%2028.0.0%20tested-yellow", "Gitea-28.1.0%20%7C%2028.1.0%20tested-blue") .replace("**Upstream Gitea 28.1.0:** [PENDING]", "**Upstream Gitea 28.1.0:** [PASS]"), encoding="utf-8") chinese.write_text(self.read("docs/README.zh-CN.md") .replace("**最新测试:** Gitea 28.0.0", "**最新测试:** Gitea 28.1.0") .replace("**上游 Gitea 28.1.0:** [PENDING]", "**上游 Gitea 28.1.0:** [PASS]"), encoding="utf-8") agents.write_text(self.read("AGENTS.md").replace("28.1.0 [PENDING]", "28.1.0 [PASS]"), encoding="utf-8") TRACKER.apply_release(self.root, "28.2.0", "2026-11-01") self.assertIn("Gitea-28.2.0%20%5BPENDING%5D%20%7C%2028.1.0%20tested-yellow", self.read("README.md")) self.assertIn("Gitea 28.2.0:** [PENDING]", self.read("README.md")) self.assertIn("Latest tested:** Gitea 28.1.0", self.read("README.md")) self.assertIn("最新测试:** Gitea 28.1.0", self.read("docs/README.zh-CN.md")) def test_invalid_version_rejected(self): with self.assertRaises(ValueError): TRACKER.apply_release(self.root, "28.2.0-rc1", "2026-11-01") def test_template_release_updates_all_marked_labels(self): changed = TRACKER.apply_template_release(self.root, "v28.0.1") self.assertEqual({self.root / name for name in ("README.md", "docs/README.zh-CN.md", "AGENTS.md")}, set(changed)) for name in ("README.md", "docs/README.zh-CN.md"): self.assertEqual(2, self.read(name).count("[v28.0.1](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)")) self.assertNotIn("/releases/tag/v28.0.1", self.read(name)) self.assertIn("Current template release: **v28.0.1**", self.read("AGENTS.md")) self.assertEqual(self.originals["COMPATIBILITY.md"], self.read("COMPATIBILITY.md")) self.assertEqual([], TRACKER.apply_template_release(self.root, "v28.0.1")) self.assertEqual([], TRACKER.apply_template_release(self.root, "v28.0.0")) def test_template_release_dry_run_does_not_write(self): self.assertEqual(3, len(TRACKER.apply_template_release(self.root, "v28.0.1", dry_run=True))) self.assert_unchanged() def test_template_release_rejects_mismatched_labels(self): english = self.root / "README.md" english.write_text(self.read("README.md").replace("Latest Release: [v28.0.0]", "Latest Release: [v28.0.1]"), encoding="utf-8") with self.assertRaisesRegex(ValueError, "disagree"): TRACKER.apply_template_release(self.root, "v28.0.2") def test_template_release_rejects_tagged_link(self): english = self.root / "README.md" english.write_text(self.read("README.md").replace("[v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)", "[v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/tag/v28.0.0)", 1), encoding="utf-8") with self.assertRaisesRegex(ValueError, "/releases/latest"): TRACKER.apply_template_release(self.root, "v28.0.1") def test_template_release_rejects_invalid_tag(self): with self.assertRaises(ValueError): TRACKER.apply_template_release(self.root, "v28.0.1-rc1") def test_release_api_uses_published_date(self): payload = {"tag_name": "v28.2.0", "published_at": "2026-11-01T08:15:00Z", "draft": False, "prerelease": False} with patch.object(TRACKER, "urlopen", return_value=io.BytesIO(json.dumps(payload).encode("utf-8"))) as mocked: self.assertEqual(("28.2.0", "2026-11-01"), TRACKER.fetch_release("28.2.0")) self.assertEqual(f"{TRACKER.RELEASES_API}/tags/v28.2.0", mocked.call_args.args[0].full_url) def test_prerelease_is_rejected(self): payload = {"tag_name": "v28.2.0", "published_at": "2026-11-01T08:15:00Z", "prerelease": True} with patch.object(TRACKER, "urlopen", return_value=io.BytesIO(json.dumps(payload).encode("utf-8"))): with self.assertRaises(ValueError): TRACKER.fetch_release(None) if __name__ == "__main__": unittest.main()