"""Tests for build/release version guards, report writing and preflight steps.""" from __future__ import annotations import io import json import subprocess import sys import urllib.error from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[1] BUILD_ROOT = REPO_ROOT / "build" if str(BUILD_ROOT) not in sys.path: sys.path.insert(0, str(BUILD_ROOT)) from release import report as report_mod # noqa: E402 from release import version as version_mod # noqa: E402 from release.preflight import Preflight, PreflightOptions # noqa: E402 PYPROJECT = """ [project] name = "orisys" description = "synthetic" [tool.setuptools.dynamic] version = {attr = "orisys.__version__"} """.lstrip() def _make_repo(tmp_path: Path, *, version: str = "1.2.3", with_git: bool = False) -> Path: (tmp_path / "pyproject.toml").write_text(PYPROJECT, encoding="utf-8") package = tmp_path / "src" / "orisys" package.mkdir(parents=True) (package / "__init__.py").write_text(f'__version__ = "{version}"\n', encoding="utf-8") if with_git: (tmp_path / ".git").mkdir() return tmp_path def _stage_wheel(repo: Path, version: str, name: str | None = None) -> Path: directory = repo / "SDK" / version directory.mkdir(parents=True, exist_ok=True) wheel = directory / (name or f"orisys-{version}-cp310-cp310-win_amd64.whl") wheel.write_bytes(b"not really a wheel") return wheel class _FakeResponse: def __init__(self, payload: dict) -> None: self._buffer = io.BytesIO(json.dumps(payload).encode("utf-8")) def read(self, *args): return self._buffer.read(*args) def __enter__(self): return self def __exit__(self, *exc) -> bool: return False def test_wheel_version_parsing(): assert version_mod.wheel_version(Path("orisys-1.2.3-cp310-cp310-win_amd64.whl")) == "1.2.3" assert version_mod.wheel_version(Path("orisys-1.0.0-1-cp312-abi3-manylinux_2_28_x86_64.whl")) == "1.0.0" assert version_mod.wheel_version(Path("something-else.whl")) is None def test_sdk_versions_are_numerically_sorted(tmp_path): for name in ("0.9.0", "0.4.10", "1.2.3"): (tmp_path / "SDK" / name).mkdir(parents=True) (tmp_path / "SDK" / "not-a-version").mkdir() assert version_mod.sdk_versions(tmp_path)[:3] == ["0.4.10", "0.9.0", "1.2.3"] def test_sdk_versions_empty_without_sdk_dir(tmp_path): assert version_mod.sdk_versions(tmp_path) == [] def test_sdk_wheels_lists_only_that_version(tmp_path): repo = _make_repo(tmp_path) _stage_wheel(repo, "1.2.3") _stage_wheel(repo, "0.4.3") wheels = version_mod.sdk_wheels(repo, "1.2.3") assert [w.name for w in wheels] == ["orisys-1.2.3-cp310-cp310-win_amd64.whl"] def test_dynamic_version_attr_read_from_pyproject(tmp_path): repo = _make_repo(tmp_path) assert version_mod.dynamic_version_attr(repo) == "orisys.__version__" def test_dynamic_version_attr_none_when_absent(tmp_path): (tmp_path / "pyproject.toml").write_text("[project]\nname = 'x'\n", encoding="utf-8") assert version_mod.dynamic_version_attr(tmp_path) is None def test_pypi_released_versions_parses_response(tmp_path, monkeypatch): fake = _FakeResponse({"releases": {"0.4.2": [{"filename": "a"}], "1.2.3": [{"filename": "b"}], "0.0.0": []}}) monkeypatch.setattr(version_mod.urllib.request, "urlopen", lambda *a, **k: fake) versions, error = version_mod.pypi_released_versions() assert error is None # An empty file list means nothing was ever uploaded for that version. assert versions == ["0.4.2", "1.2.3"] def test_pypi_released_versions_reports_http_404_as_empty(monkeypatch): def _raise(*args, **kwargs): raise urllib.error.HTTPError(version_mod.PYPI_JSON_URL, 404, "not found", {}, None) monkeypatch.setattr(version_mod.urllib.request, "urlopen", _raise) assert version_mod.pypi_released_versions() == ([], None) def test_pypi_released_versions_returns_error_on_failure(monkeypatch): def _raise(*args, **kwargs): raise urllib.error.URLError("no network") monkeypatch.setattr(version_mod.urllib.request, "urlopen", _raise) versions, error = version_mod.pypi_released_versions() assert versions is None assert error and "URLError" in error def test_python_for_tag_uses_envs_root(tmp_path): executable = tmp_path / "py310" / "python.exe" executable.parent.mkdir(parents=True) executable.write_bytes(b"") assert version_mod.python_for_tag("cp310", envs_root=tmp_path) == executable assert version_mod.python_for_tag("cp311", envs_root=tmp_path) is None # --- report ----------------------------------------------------------------- def test_write_report_round_trips(tmp_path): payload = { "version": "1.2.3", "ok": False, "git": {"commit": "abc", "branch": "dev", "dirty": True}, "interpreter": {"version": "3.10.19", "executable": "python"}, "duration_s": 1.5, "options": {"skip_tests": True}, "steps": [{"name": "tests", "ok": True, "detail": "skipped", "duration_s": 0.0, "data": {}}], "wheels": [{"name": "w.whl", "size": 12, "sha256": "deadbeef"}], } json_path, text_path = report_mod.write_report(payload, tmp_path / "reports") loaded = json.loads(json_path.read_text(encoding="utf-8")) assert loaded["schema"] == report_mod.SCHEMA_VERSION assert loaded["wheels"][0]["sha256"] == "deadbeef" text = text_path.read_text(encoding="utf-8") assert "FAIL" in text assert "deadbeef" in text assert "waived guards: skip_tests" in text # --- preflight steps -------------------------------------------------------- def test_preflight_resolves_version_from_pyproject(tmp_path): repo = _make_repo(tmp_path, version="0.6.0") preflight = Preflight(PreflightOptions(), repo_root=repo) assert preflight.version == "0.6.0" assert preflight.version_source == "pyproject.toml dynamic attr" def test_preflight_explicit_version_wins(tmp_path): repo = _make_repo(tmp_path, version="0.6.0") preflight = Preflight(PreflightOptions(version="0.7.0"), repo_root=repo) assert preflight.version == "0.7.0" assert preflight.version_source == "explicit --version" def test_repo_state_fails_outside_git(tmp_path, capsys): repo = _make_repo(tmp_path) step = Preflight(PreflightOptions(), repo_root=repo)._repo_state() assert not step.ok assert "not a git repository" in step.detail def test_version_consistency_detects_mismatch(tmp_path): repo = _make_repo(tmp_path, version="1.2.3") step = Preflight(PreflightOptions(version="9.9.9"), repo_root=repo)._version_consistency() assert not step.ok assert "1.2.3" in step.detail and "9.9.9" in step.detail def test_version_consistency_passes_on_match(tmp_path): repo = _make_repo(tmp_path, version="1.2.3") step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._version_consistency() assert step.ok, step.detail def test_duplicate_guard_allows_staged_sdk_dir(tmp_path, monkeypatch): """A populated SDK// is the normal preflight input, not a hazard.""" repo = _make_repo(tmp_path, version="1.2.3") _stage_wheel(repo, "1.2.3") monkeypatch.setattr(version_mod, "git_tags", lambda root: []) step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._duplicate_guard() assert step.ok, step.detail assert "staged wheel(s) to verify" in step.detail def test_duplicate_guard_blocks_released_tag(tmp_path, monkeypatch): repo = _make_repo(tmp_path, version="1.2.3") _stage_wheel(repo, "1.2.3") monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v1.2.3", "v0.5.1"]) step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._duplicate_guard() assert not step.ok assert "already mark 1.2.3 as released" in step.detail assert "--force" in step.detail def test_duplicate_guard_force_accepts_released_tag(tmp_path, monkeypatch): repo = _make_repo(tmp_path, version="1.2.3") monkeypatch.setattr(version_mod, "git_tags", lambda root: ["1.2.3"]) step = Preflight( PreflightOptions(version="1.2.3", force=True), repo_root=repo )._duplicate_guard() assert step.ok, step.detail assert "accepted via --force" in step.detail def test_duplicate_guard_reports_unreleased_version(tmp_path, monkeypatch): repo = _make_repo(tmp_path, version="0.5.3") monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v1.2.3"]) step = Preflight(PreflightOptions(version="0.5.3"), repo_root=repo)._duplicate_guard() assert step.ok, step.detail assert "no local git tag marks 0.5.3" in step.detail def test_duplicate_guard_flags_published_version(tmp_path, monkeypatch): repo = _make_repo(tmp_path, version="1.2.3") monkeypatch.setattr(version_mod, "git_tags", lambda root: []) monkeypatch.setattr(version_mod, "pypi_released_versions", lambda *a, **k: (["1.2.3"], None)) step = Preflight( PreflightOptions(version="1.2.3", check_pypi=True), repo_root=repo )._duplicate_guard() assert not step.ok assert "already published on PyPI" in step.detail def test_released_tags_for_matches_version_spellings(): tags = ["v0.5.1", "0.5.1", "v0.4.3", "0.50"] assert version_mod.released_tags_for("0.5.1", tags) == ["0.5.1", "v0.5.1"] assert version_mod.released_tags_for("1.2.3", tags) == [] def test_git_tags_reads_repo_tags(tmp_path): repo = tmp_path / "repo" repo.mkdir() for command in ( ["git", "init", "-q"], ["git", "config", "user.email", "test@example.invalid"], ["git", "config", "user.name", "test"], ): subprocess.run(command, cwd=repo, check=True, capture_output=True) (repo / "f.txt").write_text("x", encoding="utf-8") subprocess.run(["git", "add", "f.txt"], cwd=repo, check=True, capture_output=True) subprocess.run(["git", "commit", "-qm", "init"], cwd=repo, check=True, capture_output=True) subprocess.run(["git", "tag", "v1.2.3"], cwd=repo, check=True, capture_output=True) assert "v1.2.3" in version_mod.git_tags(repo) def test_version_string_scan_flags_stale_literal(tmp_path): repo = tmp_path / "repo" (repo / "docs").mkdir(parents=True) (repo / "docs" / "a.md").write_text("版本:0.4.0\n", encoding="utf-8") (repo / "README.md").write_text("release 1.2.3\n", encoding="utf-8") hits = version_mod.find_version_literals(repo, "1.2.3") assert [(path, line) for path, line, _ in hits] == [("docs/a.md", 1)] def test_version_string_scan_ignores_dependency_constraints(tmp_path): repo = tmp_path / "repo" repo.mkdir() (repo / "docs.md").write_text( "pip install numpy>=1.24.4 scipy==1.13 opencv-python>=4.10.0.84\n", encoding="utf-8", ) assert version_mod.find_version_literals(repo, "1.2.3") == [] def test_version_string_scan_honours_allowlist(tmp_path, monkeypatch): monkeypatch.setitem( version_mod.VERSION_STRING_ALLOWLIST, "docs.md", frozenset({"0.4.0"}) ) repo = tmp_path / "repo" repo.mkdir() (repo / "docs.md").write_text("历史版本:0.4.0\n", encoding="utf-8") assert version_mod.find_version_literals(repo, "1.2.3") == [] def test_version_string_scan_skips_tests_directory(tmp_path): repo = tmp_path / "repo" (repo / "tests").mkdir(parents=True) (repo / "tests" / "test_x.py").write_text('v = "0.1.2"\n', encoding="utf-8") assert version_mod.find_version_literals(repo, "1.2.3") == [] def test_version_string_scan_accepts_current_version_and_v_prefix(tmp_path): repo = tmp_path / "repo" repo.mkdir() (repo / "README.md").write_text("current 1.2.3 and tag v1.2.3\n", encoding="utf-8") assert version_mod.find_version_literals(repo, "1.2.3") == [] def test_preflight_version_strings_step_fails_on_leftover(tmp_path): repo = _make_repo(tmp_path, version="1.2.3") (repo / "docs").mkdir() (repo / "docs" / "guide.md").write_text("版本:0.4.0\n", encoding="utf-8") step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._version_strings() assert not step.ok assert "0.4.0" in step.detail assert "VERSION_STRING_ALLOWLIST" in step.detail def test_preflight_version_strings_step_passes_on_clean_tree(tmp_path): repo = _make_repo(tmp_path, version="1.2.3") (repo / "README.md").write_text("no version literal here\n", encoding="utf-8") step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._version_strings() assert step.ok, step.detail def test_repo_has_no_stale_version_literals(): """The real repository must satisfy its own version-string gate.""" from common.pyproject_utils import get_version version = get_version(REPO_ROOT) assert version_mod.find_version_literals(REPO_ROOT, version) == [] def test_bundle_precheck_fails_when_pattern_resolves_nothing(tmp_path): repo = _make_repo(tmp_path, version="1.2.3") (repo / "pyproject.toml").write_text( PYPROJECT + """ [[tool.orisys.bundle.items]] name = "configs" source = "config" target = "configs" patterns = ["*.json"] """, encoding="utf-8", ) step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._bundle_precheck() assert not step.ok assert "resolved 0 files" in step.detail def test_wheel_inspection_fails_without_wheels(tmp_path): repo = _make_repo(tmp_path, version="1.2.3") step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._wheel_inspection() assert not step.ok assert "does not exist" in step.detail def test_wheel_runtime_fails_without_video(tmp_path, monkeypatch): repo = _make_repo(tmp_path, version="1.2.3") _stage_wheel(repo, "1.2.3") monkeypatch.setattr("release.preflight.default_verification_video", lambda root: None) preflight = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo) preflight._discover_wheels() step = preflight._wheel_runtime() assert not step.ok assert "never silently skipped" in step.detail def test_wheel_runtime_skip_is_recorded(tmp_path): repo = _make_repo(tmp_path, version="1.2.3") step = Preflight( PreflightOptions(version="1.2.3", no_runtime=True), repo_root=repo )._wheel_runtime() assert step.ok assert "SKIPPED" in step.detail def test_tests_skip_is_loud_and_recorded(tmp_path): repo = _make_repo(tmp_path, version="1.2.3") step = Preflight( PreflightOptions(version="1.2.3", skip_tests=True), repo_root=repo )._tests() assert step.ok assert "SKIPPED" in step.detail assert step.data["status"] == "skipped" def test_execute_marks_failure_and_aborts(tmp_path, capsys): repo = _make_repo(tmp_path, version="1.2.3") preflight = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo) preflight._execute(preflight._repo_state) assert preflight.aborted assert preflight.steps[-1].name == "repo-state" assert not preflight.steps[-1].ok def test_execute_keep_going_does_not_abort(tmp_path): repo = _make_repo(tmp_path, version="1.2.3") preflight = Preflight(PreflightOptions(version="1.2.3", keep_going=True), repo_root=repo) preflight._execute(preflight._repo_state) assert not preflight.aborted