Files
orisys-sdk/tests/test_release_version.py
T

406 lines
15 KiB
Python

"""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/<version>/ 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