157 lines
6.7 KiB
Python
157 lines
6.7 KiB
Python
"""
|
|
Unit tests for ``scripts/update_versions.py`` (the lockstep version bumper).
|
|
|
|
The ``repo_copy`` fixture copies the repo's *real* ``pyproject.toml``
|
|
files into a temp tree, so the regex anchors are exercised against the
|
|
actual file formatting — a drift in either the script or the files
|
|
fails here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
# ``scripts`` is a namespace package (no ``__init__.py``), so a bare
|
|
# ``from scripts import update_versions`` is shadowed by the regular
|
|
# ``tests/scripts`` package when both resolve as a top-level ``scripts`` during
|
|
# a full-suite collection (pytest's default "prepend" import mode), failing with
|
|
# ``ImportError: cannot import name 'update_versions' from 'scripts'``. Load the
|
|
# module by its repo-root file path instead, which is immune to the collision.
|
|
_UPDATE_VERSIONS_SPEC = importlib.util.spec_from_file_location(
|
|
"_update_versions_under_test", _REPO_ROOT / "scripts" / "update_versions.py"
|
|
)
|
|
assert _UPDATE_VERSIONS_SPEC is not None and _UPDATE_VERSIONS_SPEC.loader is not None
|
|
update_versions = importlib.util.module_from_spec(_UPDATE_VERSIONS_SPEC)
|
|
# Register before exec so dataclasses defined in the module can resolve their
|
|
# defining module via ``sys.modules`` during class creation.
|
|
sys.modules[_UPDATE_VERSIONS_SPEC.name] = update_versions
|
|
_UPDATE_VERSIONS_SPEC.loader.exec_module(update_versions)
|
|
_PYPROJECTS = [
|
|
"pyproject.toml",
|
|
"sdks/python-client/pyproject.toml",
|
|
"sdks/ui/pyproject.toml",
|
|
]
|
|
# The runtime version constant is stamped/verified alongside the pyprojects.
|
|
_VERSION_PY = "omnigent/version.py"
|
|
|
|
|
|
@pytest.fixture
|
|
def repo_copy(tmp_path: Path) -> Path:
|
|
"""Copy the real pyproject.toml files + version.py into a temp repo root."""
|
|
root = tmp_path / "repo"
|
|
for rel in (*_PYPROJECTS, _VERSION_PY):
|
|
dst = root / rel
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
dst.write_text((_REPO_ROOT / rel).read_text())
|
|
return root
|
|
|
|
|
|
def test_set_version_rewrites_every_location(repo_copy: Path) -> None:
|
|
changed = update_versions.set_version(repo_copy, "9.9.9")
|
|
# Three pyprojects plus omnigent/version.py.
|
|
assert len(changed) == 4
|
|
# root: version line + two sibling pins; SDKs: version line + one pin.
|
|
assert (repo_copy / "pyproject.toml").read_text().count("9.9.9") == 3
|
|
assert (repo_copy / "sdks/python-client/pyproject.toml").read_text().count("9.9.9") == 2
|
|
assert (repo_copy / "sdks/ui/pyproject.toml").read_text().count("9.9.9") == 2
|
|
# The runtime constant is stamped too.
|
|
assert 'VERSION = "9.9.9"' in (repo_copy / _VERSION_PY).read_text()
|
|
# check() round-trips: all agree and pins are exact.
|
|
assert update_versions.check(repo_copy, expect="9.9.9") == "9.9.9"
|
|
|
|
|
|
def test_set_version_updates_runtime_constant(repo_copy: Path) -> None:
|
|
"""The bump path keeps omnigent/version.py's VERSION in lockstep.
|
|
|
|
This is the gap that would otherwise make the automated bot bump commit a
|
|
stale constant and trip the ``test_version_matches_pyproject`` backstop.
|
|
"""
|
|
version_py = repo_copy / _VERSION_PY
|
|
assert 'VERSION = "9.9.9"' not in version_py.read_text()
|
|
changed = update_versions.set_version(repo_copy, "9.9.9")
|
|
assert version_py in changed
|
|
assert 'VERSION = "9.9.9"' in version_py.read_text()
|
|
|
|
|
|
def test_check_detects_version_py_drift(repo_copy: Path) -> None:
|
|
"""A stale VERSION constant (pyprojects consistent) fails check()."""
|
|
update_versions.set_version(repo_copy, "9.9.9")
|
|
version_py = repo_copy / _VERSION_PY
|
|
version_py.write_text(version_py.read_text().replace('VERSION = "9.9.9"', 'VERSION = "9.9.8"'))
|
|
with pytest.raises(ValueError, match=r"omnigent/version\.py VERSION"):
|
|
update_versions.check(repo_copy)
|
|
|
|
|
|
def test_set_version_fails_loud_when_constant_absent(repo_copy: Path) -> None:
|
|
"""A version.py missing the VERSION line must raise, not silently no-op."""
|
|
version_py = repo_copy / _VERSION_PY
|
|
version_py.write_text('"""No constant here."""\n')
|
|
with pytest.raises(ValueError, match="expected exactly 1 match"):
|
|
update_versions.set_version(repo_copy, "9.9.9")
|
|
|
|
|
|
def test_set_version_preserves_unrelated_version_literals(repo_copy: Path) -> None:
|
|
root_pyproject = repo_copy / "pyproject.toml"
|
|
before = root_pyproject.read_text()
|
|
# Real third-party floor that shares the old version digits — must
|
|
# survive a bump untouched (anchored-on-name replacement, not blind).
|
|
assert '"databricks-mcp>=0.1.0",' in before
|
|
update_versions.set_version(repo_copy, "9.9.9")
|
|
assert '"databricks-mcp>=0.1.0",' in root_pyproject.read_text()
|
|
|
|
|
|
def test_check_detects_version_drift(repo_copy: Path) -> None:
|
|
update_versions.set_version(repo_copy, "9.9.9")
|
|
# Knock one package out of lockstep but keep it internally consistent
|
|
# (version + its own sibling pin both move) so the cross-package
|
|
# disagreement is what surfaces, not a missing pin.
|
|
ui = repo_copy / "sdks/ui/pyproject.toml"
|
|
ui.write_text(ui.read_text().replace("9.9.9", "9.9.8"))
|
|
with pytest.raises(ValueError, match="disagree"):
|
|
update_versions.check(repo_copy)
|
|
|
|
|
|
def test_check_detects_missing_pin(repo_copy: Path) -> None:
|
|
update_versions.set_version(repo_copy, "9.9.9")
|
|
# Break the sibling pin while leaving the version intact.
|
|
client = repo_copy / "sdks/python-client/pyproject.toml"
|
|
client.write_text(client.read_text().replace('"omnigent==9.9.9"', '"omnigent==9.9.8"'))
|
|
with pytest.raises(ValueError, match="missing exact pin"):
|
|
update_versions.check(repo_copy)
|
|
|
|
|
|
def test_set_version_fails_loud_when_line_absent(tmp_path: Path) -> None:
|
|
# A pyproject missing the version line must raise, not silently no-op.
|
|
root = tmp_path / "repo"
|
|
(root / "sdks/python-client").mkdir(parents=True)
|
|
(root / "sdks/ui").mkdir(parents=True)
|
|
(root / "pyproject.toml").write_text('[project]\nname = "omnigent"\n')
|
|
(root / "sdks/python-client/pyproject.toml").write_text("[project]\n")
|
|
(root / "sdks/ui/pyproject.toml").write_text("[project]\n")
|
|
with pytest.raises(ValueError, match="expected exactly 1 match"):
|
|
update_versions.set_version(root, "9.9.9")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("released", "expected"),
|
|
[
|
|
("0.1.2", "0.1.3.dev0"),
|
|
("1.0.0", "1.0.1.dev0"),
|
|
("0.1.2rc1", "0.1.3.dev0"),
|
|
("2.5.9", "2.5.10.dev0"),
|
|
],
|
|
)
|
|
def test_next_dev_version(released: str, expected: str) -> None:
|
|
assert update_versions.next_dev_version(released) == expected
|
|
|
|
|
|
def test_validate_pep440_rejects_junk() -> None:
|
|
with pytest.raises(SystemExit, match="invalid version"):
|
|
update_versions._validate_pep440("not-a-version")
|