diff --git a/check-qualification.py b/check-qualification.py index 0bbd568..94d1501 100644 --- a/check-qualification.py +++ b/check-qualification.py @@ -22,6 +22,7 @@ from pythonbuild import waiver from pythonbuild.qualification import ( + QUALIFICATION_ROOT, QualificationError, previous_qualified_tag, shipped_api_levels, @@ -88,15 +89,36 @@ def changed_since(tag: str) -> list[str]: return [line for line in result.stdout.splitlines() if line] +def _git_tag_exists(tag: str) -> bool: + """Whether ``tag`` names a real Git tag in the release checkout.""" + result = run(["git", "show-ref", "--verify", "--quiet", f"refs/tags/{tag}"]) + return result.returncode == 0 + + +def previous_released_qualified_tag( + tag: str, root: Path = QUALIFICATION_ROOT +) -> str | None: + """Newest earlier qualified candidate that was actually released as a Git tag. + + Qualification receipts may intentionally exist for candidates that were never + released. Those receipts remain useful evidence, but there is no commit range + to diff from unless the candidate also has a Git tag. + """ + previous = previous_qualified_tag(tag, root=root) + while previous is not None and not _git_tag_exists(previous): + previous = previous_qualified_tag(previous, root=root) + return previous + + def consider_waiver( build: Build, tag: str, refusal: QualificationError, report: Path | None ) -> int: """Permit an unattended release only when the change is upstream's alone.""" - previous = previous_qualified_tag(tag) + previous = previous_released_qualified_tag(tag) if previous is None: print( f"qualification gate: REFUSED\n\n{refusal}\n\n" - f"No earlier qualified tag to compare against, so there is nothing a " + f"No earlier released qualified tag to compare against, so there is nothing a " f"waiver could rest on.", file=sys.stderr, ) diff --git a/tests/test_check_qualification.py b/tests/test_check_qualification.py new file mode 100644 index 0000000..79aeb39 --- /dev/null +++ b/tests/test_check_qualification.py @@ -0,0 +1,73 @@ +"""Regression tests for the release qualification command.""" + +from __future__ import annotations + +import json +import runpy +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace +from typing import Any, cast + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = runpy.run_path(str(ROOT / "check-qualification.py")) + + +class ReleasedQualificationHistoryTest(unittest.TestCase): + def write_receipt(self, root: Path, tag: str) -> None: + directory = root / tag + directory.mkdir(parents=True, exist_ok=True) + document: dict[str, Any] = { + "receipt_kind": "android-device-qualification", + "verdict": {"pass": True, "failures": []}, + "executed_artifact": { + "filename": ( + f"cpython-3.14.6+{tag}-aarch64-linux-android-" + "install_only_stripped.tar.gz" + ) + }, + "checks": {"identity": {"android_api_level": 34}}, + } + (directory / "receipt.json").write_text(json.dumps(document), encoding="utf-8") + + def with_git_tags(self, tags: set[str]) -> Any: + def fake_run(argv: list[str]) -> SimpleNamespace: + ref = argv[-1] + return SimpleNamespace( + returncode=0 if ref.removeprefix("refs/tags/") in tags else 1, + stdout="", + stderr="", + ) + + return fake_run + + def call_with_git_tags(self, tag: str, root: Path, tags: set[str]) -> str | None: + function = SCRIPT["previous_released_qualified_tag"] + globals_ = function.__globals__ + original = globals_["run"] + globals_["run"] = self.with_git_tags(tags) + try: + return cast(str | None, function(tag, root=root)) + finally: + globals_["run"] = original + + def test_unreleased_qualified_candidate_is_skipped(self) -> None: + with TemporaryDirectory() as tmp: + root = Path(tmp) + self.write_receipt(root, "20260729") + self.write_receipt(root, "20260730") + self.assertEqual( + self.call_with_git_tags("20260814", root, {"20260729"}), + "20260729", + ) + + def test_no_released_qualified_candidate_returns_none(self) -> None: + with TemporaryDirectory() as tmp: + root = Path(tmp) + self.write_receipt(root, "20260730") + self.assertIsNone(self.call_with_git_tags("20260814", root, set())) + + +if __name__ == "__main__": + unittest.main()