Files
wildminder-ComfyUI-DyPE/tests/test_version_sync.py
T

123 lines
4.3 KiB
Python

"""Version-sync guard (plan 2026-08-25 W1.1, tracker CRIT-001).
``pyproject.toml`` is the SINGLE SOURCE OF TRUTH for the pack version.
The README changelog's top ``#### vX.Y.Z`` entry must mirror it.
These tests make drift impossible to merge unnoticed: any future version
bump that forgets the changelog (or vice versa) fails here — and therefore
fails CI (see .github/workflows/tests.yml, job ``meta``).
Also runnable standalone for pre-commit:
python tests/test_version_sync.py
"""
from __future__ import annotations
import os
import re
import sys
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_PYPROJECT = os.path.join(_PROJECT_ROOT, "pyproject.toml")
_README = os.path.join(_PROJECT_ROOT, "README.md")
_SEMVER_RE = re.compile(r"\d+\.\d+\.\d+")
# Accept both changelog heading styles: '#### vX.Y.Z' (legacy) and
# '### vX.Y.Z' (condensed README format adopted 2026-08-25).
_CHANGELOG_ENTRY_RE = re.compile(r"^#{3,4}\s+v(\d+\.\d+\.\d+)", re.MULTILINE)
def _pyproject_version() -> str:
"""Read ``[project] version`` from pyproject.toml.
Prefers stdlib ``tomllib`` (Python >= 3.11); falls back to a line regex
on Python 3.10 where tomllib is unavailable.
"""
with open(_PYPROJECT, "r", encoding="utf-8") as fh:
text = fh.read()
try:
import tomllib # Python >= 3.11
except ImportError:
m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
if m is None:
raise AssertionError(
"pyproject.toml has no parseable 'version = \"...\"' line"
)
return m.group(1)
data = tomllib.loads(text)
version = data.get("project", {}).get("version")
if not version:
raise AssertionError("pyproject.toml [project] table has no 'version'")
return version
def _changelog_versions() -> list:
"""Return every ``vX.Y.Z`` from the README changelog, top-first."""
with open(_README, "r", encoding="utf-8") as fh:
text = fh.read()
return _CHANGELOG_ENTRY_RE.findall(text)
def test_pyproject_version_matches_changelog():
"""CRIT-001 guard: pyproject version == top README changelog entry."""
py_version = _pyproject_version()
entries = _changelog_versions()
assert entries, (
"README.md contains no '### vX.Y.Z' / '#### vX.Y.Z' changelog "
"entries; add one matching the pyproject.toml version."
)
top = entries[0]
assert py_version == top, (
f"VERSION DRIFT detected:\n"
f" pyproject.toml version : {py_version!r}\n"
f" README changelog top : v{top!r}\n"
f"Fix: bump pyproject.toml to {top} OR add a matching "
f"'#### v{py_version}' changelog entry at the top of the README "
f"Changelog section."
)
def test_version_is_semver():
"""Both versions must be full X.Y.Z semver strings (catches '2.8' typos)."""
py_version = _pyproject_version()
entries = _changelog_versions()
assert _SEMVER_RE.fullmatch(py_version), (
f"pyproject.toml version {py_version!r} is not X.Y.Z semver."
)
for entry in entries:
assert _SEMVER_RE.fullmatch(entry), (
f"README changelog entry v{entry!r} is not X.Y.Z semver."
)
def test_changelog_entries_are_descending():
"""Changelog entries must be strictly descending top->bottom.
Catches inserting a new release entry in the wrong place (e.g. below an
older entry), which would silently break the parity guard's meaning.
"""
entries = _changelog_versions()
assert len(entries) >= 2, "expected at least two changelog entries"
def key(v):
return tuple(int(p) for p in v.split("."))
for prev, cur in zip(entries, entries[1:]):
assert key(prev) > key(cur), (
f"Changelog order violation: v{prev} must be NEWER than the "
f"entry directly below it (v{cur}). Newest releases go on TOP."
)
if __name__ == "__main__": # pre-commit / standalone runner
failures = []
for name, fn in sorted(globals().items()):
if name.startswith("test_") and callable(fn):
try:
fn()
print(f"PASS {name}")
except AssertionError as exc:
failures.append((name, str(exc)))
print(f"FAIL {name}: {exc}")
sys.exit(1 if failures else 0)