404 lines
15 KiB
Python
404 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
|
|
|
|
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
|