test(release): keep release tooling coverage with its owner
This commit is contained in:
191
tests/test_release_doctor.py
Normal file
191
tests/test_release_doctor.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
|
||||||
|
SCRIPT = RELEASE_TOOLS_ROOT / "release-doctor.py"
|
||||||
|
|
||||||
|
if str(RELEASE_TOOLS_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(RELEASE_TOOLS_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
def load_doctor_module():
|
||||||
|
spec = importlib.util.spec_from_file_location("release_doctor", SCRIPT)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError(f"Could not load {SCRIPT}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseDoctorTests(unittest.TestCase):
|
||||||
|
def test_release_repo_names_include_catalog_and_migration_baseline_owners(self) -> None:
|
||||||
|
doctor = load_doctor_module()
|
||||||
|
|
||||||
|
names = set(doctor.release_repo_names(META_ROOT.parent))
|
||||||
|
|
||||||
|
self.assertIn("govoplan-core", names)
|
||||||
|
self.assertIn("govoplan-files", names)
|
||||||
|
self.assertIn("govoplan-idm", names)
|
||||||
|
|
||||||
|
def test_repo_findings_detect_dirty_repositories(self) -> None:
|
||||||
|
doctor = load_doctor_module()
|
||||||
|
repo = doctor.RepoState(
|
||||||
|
name="govoplan-files",
|
||||||
|
path="/tmp/govoplan-files",
|
||||||
|
exists=True,
|
||||||
|
dirty_entries=(" M pyproject.toml",),
|
||||||
|
pyproject_version="0.1.7",
|
||||||
|
local_tag_exists=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
findings = doctor.repo_findings((repo,), target_version="0.1.7", target_tag="v0.1.7")
|
||||||
|
|
||||||
|
self.assertEqual("blocker", findings[0].severity)
|
||||||
|
self.assertEqual("repo-dirty", findings[0].check_id)
|
||||||
|
self.assertIn("git status --short", [item.command for item in findings[0].commands])
|
||||||
|
|
||||||
|
def test_dubious_ownership_errors_suggest_safe_directory_command(self) -> None:
|
||||||
|
doctor = load_doctor_module()
|
||||||
|
repo = doctor.RepoState(
|
||||||
|
name="govoplan-files",
|
||||||
|
path="/tmp/govoplan-files",
|
||||||
|
exists=True,
|
||||||
|
safe_directory_required=True,
|
||||||
|
safe_directory_command="git config --global --add safe.directory /tmp/govoplan-files",
|
||||||
|
errors=("fatal: detected dubious ownership in repository",),
|
||||||
|
)
|
||||||
|
|
||||||
|
findings = doctor.repo_findings((repo,), target_version="0.1.7", target_tag="v0.1.7")
|
||||||
|
|
||||||
|
self.assertEqual("repo-safe-directory", findings[0].check_id)
|
||||||
|
self.assertEqual("blocker", findings[0].severity)
|
||||||
|
self.assertTrue(findings[0].commands[0].mutating)
|
||||||
|
self.assertIn("safe.directory", findings[0].commands[0].command)
|
||||||
|
|
||||||
|
def test_dubious_ownership_detection_matches_git_error(self) -> None:
|
||||||
|
doctor = load_doctor_module()
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
doctor.is_dubious_ownership_error(
|
||||||
|
"fatal: detected dubious ownership in repository at '/workspace/repo'\n"
|
||||||
|
"git config --global --add safe.directory /workspace/repo"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_release_migration_strict_errors_suggest_recording_baseline(self) -> None:
|
||||||
|
doctor = load_doctor_module()
|
||||||
|
|
||||||
|
findings = doctor.migration_findings(
|
||||||
|
{
|
||||||
|
"release": {
|
||||||
|
"ok": True,
|
||||||
|
"graph_errors": [],
|
||||||
|
"strict_errors": ["current migration heads are not recorded"],
|
||||||
|
},
|
||||||
|
"dev": {"ok": True, "graph_errors": [], "strict_errors": []},
|
||||||
|
},
|
||||||
|
target_version="0.2.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("migration-release-baseline", findings[0].check_id)
|
||||||
|
self.assertEqual("blocker", findings[0].severity)
|
||||||
|
commands = [item.command for item in findings[0].commands]
|
||||||
|
self.assertTrue(any("--record-release 0.2.0" in item for item in commands))
|
||||||
|
|
||||||
|
def test_catalog_findings_detect_missing_signatures(self) -> None:
|
||||||
|
doctor = load_doctor_module()
|
||||||
|
|
||||||
|
findings = doctor.catalog_findings(
|
||||||
|
{
|
||||||
|
"catalog_exists": True,
|
||||||
|
"catalog_path": "/tmp/stable.json",
|
||||||
|
"core_release_version": "0.1.7",
|
||||||
|
"signature_count": 0,
|
||||||
|
"keyring_exists": True,
|
||||||
|
"key_count": 1,
|
||||||
|
},
|
||||||
|
target_version="0.1.7",
|
||||||
|
online=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("catalog-signatures", findings[0].check_id)
|
||||||
|
self.assertEqual("blocker", findings[0].severity)
|
||||||
|
|
||||||
|
def test_suggested_commands_are_deduplicated(self) -> None:
|
||||||
|
doctor = load_doctor_module()
|
||||||
|
suggested = doctor.SuggestedCommand(title="Status", command="git status --short", cwd="/tmp/repo")
|
||||||
|
report = doctor.DoctorReport(
|
||||||
|
generated_at="2026-07-11T00:00:00Z",
|
||||||
|
workspace_root="/tmp",
|
||||||
|
target_version="0.1.7",
|
||||||
|
target_tag="v0.1.7",
|
||||||
|
online=False,
|
||||||
|
overall_status="blocked",
|
||||||
|
repos=(),
|
||||||
|
findings=(
|
||||||
|
doctor.Finding("a", "blocker", "A", "", (suggested,)),
|
||||||
|
doctor.Finding("b", "warning", "B", "", (suggested,)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual((suggested,), doctor.collect_suggested_commands(report))
|
||||||
|
|
||||||
|
def test_default_report_is_concise_and_keeps_details_out(self) -> None:
|
||||||
|
doctor = load_doctor_module()
|
||||||
|
repo = doctor.RepoState(
|
||||||
|
name="govoplan-files",
|
||||||
|
path="/tmp/govoplan-files",
|
||||||
|
exists=True,
|
||||||
|
dirty_entries=(" M pyproject.toml",),
|
||||||
|
)
|
||||||
|
report = doctor.DoctorReport(
|
||||||
|
generated_at="2026-07-11T00:00:00Z",
|
||||||
|
workspace_root="/tmp",
|
||||||
|
target_version="0.1.7",
|
||||||
|
target_tag="v0.1.7",
|
||||||
|
online=False,
|
||||||
|
overall_status="blocked",
|
||||||
|
repos=(repo,),
|
||||||
|
findings=(doctor.Finding("repo-dirty", "blocker", "govoplan-files has uncommitted changes", "M pyproject.toml"),),
|
||||||
|
)
|
||||||
|
|
||||||
|
concise = doctor.render_text_report(report, detailed=False, include_commands=False)
|
||||||
|
detailed = doctor.render_text_report(report, detailed=True, include_commands=False)
|
||||||
|
|
||||||
|
self.assertIn("dirty=1", concise)
|
||||||
|
self.assertNotIn("M pyproject.toml", concise)
|
||||||
|
self.assertIn("M pyproject.toml", detailed)
|
||||||
|
|
||||||
|
def test_interactive_actions_offer_dirty_bulk_commit_push(self) -> None:
|
||||||
|
doctor = load_doctor_module()
|
||||||
|
repo = doctor.RepoState(
|
||||||
|
name="govoplan-files",
|
||||||
|
path="/tmp/govoplan-files",
|
||||||
|
exists=True,
|
||||||
|
dirty_entries=(" M pyproject.toml",),
|
||||||
|
)
|
||||||
|
report = doctor.DoctorReport(
|
||||||
|
generated_at="2026-07-11T00:00:00Z",
|
||||||
|
workspace_root="/tmp",
|
||||||
|
target_version="0.1.7",
|
||||||
|
target_tag="v0.1.7",
|
||||||
|
online=False,
|
||||||
|
overall_status="blocked",
|
||||||
|
repos=(repo,),
|
||||||
|
findings=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
actions = dict(doctor.interactive_actions(report))
|
||||||
|
|
||||||
|
self.assertIn("commit-push-dirty", actions)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
190
tests/test_release_migration_audit.py
Normal file
190
tests/test_release_migration_audit.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPT = META_ROOT / "tools" / "release" / "release-migration-audit.py"
|
||||||
|
|
||||||
|
|
||||||
|
def load_audit_module():
|
||||||
|
spec = importlib.util.spec_from_file_location("release_migration_audit", SCRIPT)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError(f"Could not load {SCRIPT}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseMigrationAuditTests(unittest.TestCase):
|
||||||
|
def test_typed_alembic_variables_are_parsed(self) -> None:
|
||||||
|
audit = load_audit_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="migration-audit-test-") as directory:
|
||||||
|
path = Path(directory) / "1234_example.py"
|
||||||
|
path.write_text(
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
revision: str = "1234"
|
||||||
|
down_revision: Union[str, None] = "base"
|
||||||
|
depends_on: Union[str, None] = "core"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
migration = audit.parse_migration_file("govoplan-core", path)
|
||||||
|
|
||||||
|
self.assertIsNotNone(migration)
|
||||||
|
self.assertEqual(migration.revision, "1234")
|
||||||
|
self.assertEqual(migration.down_revisions, ("base",))
|
||||||
|
self.assertEqual(migration.depends_on, ("core",))
|
||||||
|
self.assertEqual(migration.branch_labels, ())
|
||||||
|
|
||||||
|
def test_release_baseline_matches_current_heads_in_strict_report(self) -> None:
|
||||||
|
audit = load_audit_module()
|
||||||
|
migrations = [
|
||||||
|
audit.Migration("govoplan-core", Path("base.py"), "base", (), (), ()),
|
||||||
|
audit.Migration("govoplan-core", Path("head.py"), "head", ("base",), (), ()),
|
||||||
|
]
|
||||||
|
report = audit.build_report(
|
||||||
|
migrations,
|
||||||
|
baseline={
|
||||||
|
"version": 1,
|
||||||
|
"releases": [
|
||||||
|
{
|
||||||
|
"release": "0.1.0",
|
||||||
|
"heads": [{"owner": "govoplan-core", "revision": "head"}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||||
|
workspace_root=Path("/workspace"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(report["current_heads"], ["head"])
|
||||||
|
self.assertEqual(report["graph_errors"], [])
|
||||||
|
self.assertEqual(report["strict_errors"], [])
|
||||||
|
|
||||||
|
def test_owner_heads_are_accepted_for_subset_installs(self) -> None:
|
||||||
|
audit = load_audit_module()
|
||||||
|
migrations = [
|
||||||
|
audit.Migration("govoplan-core", Path("base.py"), "base", (), (), ()),
|
||||||
|
audit.Migration("govoplan-core", Path("core_head.py"), "core-head", ("base",), (), ()),
|
||||||
|
]
|
||||||
|
report = audit.build_report(
|
||||||
|
migrations,
|
||||||
|
baseline={
|
||||||
|
"version": 1,
|
||||||
|
"releases": [
|
||||||
|
{
|
||||||
|
"release": "0.1.0",
|
||||||
|
"heads": [{"owner": "govoplan-files", "revision": "files-head"}],
|
||||||
|
"owner_heads": [{"owner": "govoplan-core", "revisions": ["core-head"]}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||||
|
workspace_root=Path("/workspace"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(report["current_heads"], ["core-head"])
|
||||||
|
self.assertEqual(report["strict_errors"], [])
|
||||||
|
|
||||||
|
def test_records_current_release_baseline(self) -> None:
|
||||||
|
audit = load_audit_module()
|
||||||
|
migrations = [
|
||||||
|
audit.Migration("govoplan-core", Path("base.py"), "base", (), (), ()),
|
||||||
|
audit.Migration("govoplan-core", Path("core_head.py"), "core-head", ("base",), (), ()),
|
||||||
|
audit.Migration("govoplan-files", Path("files_head.py"), "files-head", ("core-head",), (), ()),
|
||||||
|
]
|
||||||
|
report = audit.build_report(
|
||||||
|
migrations,
|
||||||
|
baseline={"version": 1, "releases": []},
|
||||||
|
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||||
|
workspace_root=Path("/workspace"),
|
||||||
|
)
|
||||||
|
|
||||||
|
baseline = audit.record_release_baseline(
|
||||||
|
{"version": 1, "releases": []},
|
||||||
|
report,
|
||||||
|
release="0.2.0",
|
||||||
|
replace=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
release = baseline["releases"][0]
|
||||||
|
self.assertEqual(release["release"], "0.2.0")
|
||||||
|
self.assertEqual(release["squash_policy"], "reviewed-manual")
|
||||||
|
self.assertEqual(release["heads"], [{"owner": "govoplan-files", "revision": "files-head"}])
|
||||||
|
self.assertIn({"owner": "govoplan-core", "revisions": ["core-head"]}, release["owner_heads"])
|
||||||
|
|
||||||
|
def test_missing_down_revision_is_a_graph_error(self) -> None:
|
||||||
|
audit = load_audit_module()
|
||||||
|
migrations = [
|
||||||
|
audit.Migration("govoplan-core", Path("head.py"), "head", ("missing",), (), ()),
|
||||||
|
]
|
||||||
|
report = audit.build_report(
|
||||||
|
migrations,
|
||||||
|
baseline={"version": 1, "releases": []},
|
||||||
|
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||||
|
workspace_root=Path("/workspace"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("references missing down_revision", report["graph_errors"][0])
|
||||||
|
|
||||||
|
def test_depends_on_participates_in_graph_validation_and_heads(self) -> None:
|
||||||
|
audit = load_audit_module()
|
||||||
|
migrations = [
|
||||||
|
audit.Migration("govoplan-core", Path("core.py"), "core", (), (), ()),
|
||||||
|
audit.Migration("govoplan-files", Path("files.py"), "files", (), ("core",), ()),
|
||||||
|
]
|
||||||
|
report = audit.build_report(
|
||||||
|
migrations,
|
||||||
|
baseline={"version": 1, "releases": []},
|
||||||
|
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||||
|
workspace_root=Path("/workspace"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(report["graph_errors"], [])
|
||||||
|
self.assertEqual(report["current_heads"], ["files"])
|
||||||
|
|
||||||
|
def test_missing_depends_on_is_a_graph_error(self) -> None:
|
||||||
|
audit = load_audit_module()
|
||||||
|
migrations = [
|
||||||
|
audit.Migration("govoplan-files", Path("files.py"), "files", (), ("missing",), ()),
|
||||||
|
]
|
||||||
|
report = audit.build_report(
|
||||||
|
migrations,
|
||||||
|
baseline={"version": 1, "releases": []},
|
||||||
|
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||||
|
workspace_root=Path("/workspace"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("references missing depends_on", report["graph_errors"][0])
|
||||||
|
|
||||||
|
def test_dev_track_does_not_emit_release_baseline_warnings(self) -> None:
|
||||||
|
audit = load_audit_module()
|
||||||
|
migrations = [
|
||||||
|
audit.Migration("govoplan-core", Path("base.py"), "base", (), (), ()),
|
||||||
|
audit.Migration("govoplan-core", Path("head.py"), "head", ("base",), (), ()),
|
||||||
|
]
|
||||||
|
report = audit.build_report(
|
||||||
|
migrations,
|
||||||
|
baseline={"version": 1, "releases": []},
|
||||||
|
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||||
|
workspace_root=Path("/workspace"),
|
||||||
|
track="dev",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(report["track"], "dev")
|
||||||
|
self.assertEqual(report["strict_errors"], [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user