Files
teng-lin--notebooklm-py/tests/_guardrails/test_v080_release_gate.py
T
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

171 lines
7.6 KiB
Python

"""Release gate: at the v0.8.0 flip, every deferred break must be flipped — no orphans.
``scripts/check_deprecation_targets.py`` forces only the *warning-message*
removals (a warn whose stated removal target equals the shipping version is
incoherent — issue #1214). This gate adds the **behavioral** half: when
``pyproject.toml`` reaches 0.8.0, the ``NOTEBOOKLM_FUTURE_ERRORS`` preview
machinery and the deprecation shims must be **gone** (every preview-gated branch
flipped to its default), and the :data:`V080_BREAKING_CHANGES` break table must
be **empty**. Otherwise a behavioral break (#1290 / #1342 / #1362 / #1344) —
which carries no warning message and so is invisible to the deprecation-targets
gate — could ship un-flipped and silently un-enforced.
The gate is **bidirectional** around the 0.8.0 bump (its pivot is the
``pyproject.toml`` version, so it is dormant during 0.7.x and *activates*
automatically at the bump):
* **Below 0.8.0** — the runway is live: the preview/deprecation machinery and a
non-empty break table MUST be present. You cannot remove the runway (or empty
the table) before the flip ships.
* **At/after 0.8.0** — the flip shipped: the machinery MUST be gone and the table
MUST be empty. Every break flipped, no orphans.
This turns "remember to flip everything in lockstep" into a CI invariant
(umbrella #1346; ADR-0019). It is intentionally distinct from — and complementary
to — the deprecation-coverage gate (which verifies *prep-time* runways exist) and
``check_deprecation_targets.py`` (which catches self-referential warn versions).
"""
from __future__ import annotations
import re
from pathlib import Path
# Sibling-relative import keeps this guard independent of pytest import-mode
# details and avoids depending on the absolute ``tests._guardrails`` package
# name.
from .test_v080_deprecation_coverage import (
PROJECT_ROOT,
SRC_ROOT,
V080_BREAKING_CHANGES,
)
# The release that flips the breaking half of ADR-0019. The gate's behavior
# pivots here: < this version is "runway live", >= is "flip shipped".
_FLIP_VERSION = (0, 8, 0)
# The v0.8.0 preview/deprecation machinery, as code-level markers (a call site or
# a ``def``/``class`` definition). Each exists while the runway is live and must
# be deleted at the flip (#1365). Mapped to a human reason for legible failures.
# NOTE: ``future_errors_enabled()`` (with parens) matches both the resolver
# ``def`` and every gate call site; the ``def``/``class`` prefixes anchor the
# others to their definitions so a passing mention in a docstring/name doesn't
# count. Comments are stripped before scanning (see :func:`_src_code`).
_MACHINERY: dict[str, str] = {
"future_errors_enabled()": (
"the NOTEBOOKLM_FUTURE_ERRORS preview-gate (resolver + every "
"#1247/#1251/#1254/#1290/#1342/#1362 gate site)"
),
"class MappingCompatMixin": "the dict-style compat bridge (#1251)",
"def warn_get_returns_none": "the get()-returns-None warn runway (#1247)",
"def deprecated_kwarg": "the kwarg-alias warn runway (#1254)",
}
def _version_tuple(pyproject: Path) -> tuple[int, int, int]:
"""Parse ``(major, minor, patch)`` from ``pyproject.toml``'s ``version``."""
text = pyproject.read_text(encoding="utf-8")
match = re.search(r'(?m)^version\s*=\s*"(\d+)\.(\d+)\.(\d+)', text)
if match is None: # pragma: no cover - guard; pyproject always has a version
raise AssertionError('could not parse version = "X.Y.Z" from pyproject.toml')
return (int(match[1]), int(match[2]), int(match[3]))
def _strip_comments(text: str) -> str:
"""Drop ``#`` line-comments so a marker named only in a comment doesn't count."""
return re.sub(r"#.*$", "", text, flags=re.MULTILINE)
def _src_code() -> str:
"""Comment-stripped concatenation of every ``src/notebooklm`` module."""
return "\n".join(
_strip_comments(path.read_text(encoding="utf-8")) for path in sorted(SRC_ROOT.rglob("*.py"))
)
# --- Pure detector (no I/O) — the real gate and the self-checks share it -------
def _orphans(
version: tuple[int, int, int],
src_code: str,
breaks: tuple[object, ...],
) -> list[str]:
"""Return no-orphan violations for ``(version, src, table)``; empty == healthy.
The version pivots the direction: below :data:`_FLIP_VERSION` the machinery
and a non-empty table must be PRESENT (runway live); at/after it they must be
ABSENT / empty (flip complete).
"""
present = sorted(marker for marker in _MACHINERY if marker in src_code)
violations: list[str] = []
if version < _FLIP_VERSION:
missing = sorted(set(_MACHINERY) - set(present))
if missing:
violations.append(
"v0.8.0 runway machinery removed before the flip (pyproject is "
f"still pre-0.8.0): {missing}. Re-add it, or bump pyproject to 0.8.0 "
"if the flip is shipping."
)
if not breaks:
violations.append(
"V080_BREAKING_CHANGES is empty before the 0.8.0 flip — the break "
"table must stay populated until each break actually ships."
)
else:
if present:
detail = {marker: _MACHINERY[marker] for marker in present}
violations.append(
f"pyproject is at v{'.'.join(map(str, version))} (>= 0.8.0) but the "
f"preview/deprecation machinery is still in src/: {detail}. Flip every "
"gate to its v0.8.0 default and delete the shims (#1365)."
)
if breaks:
issues = [getattr(change, "issue", change) for change in breaks]
violations.append(
f"pyproject is at v{'.'.join(map(str, version))} (>= 0.8.0) but "
f"V080_BREAKING_CHANGES still lists {issues} — these breaks were not "
"flipped (orphans). Remove each from the table as its flip ships."
)
return violations
def test_no_orphaned_v080_breaks_at_release() -> None:
"""Every v0.8.0 break is handled in lockstep with the version bump (no orphans)."""
version = _version_tuple(PROJECT_ROOT / "pyproject.toml")
violations = _orphans(version, _src_code(), V080_BREAKING_CHANGES)
assert not violations, "v0.8.0 release-gate violation(s):\n - " + "\n - ".join(violations)
# --- Self-checks: the detector must be non-vacuous in BOTH directions ----------
# A crafted "runway live" source (all four machinery markers present) and a
# static non-empty stand-in table. The dummy is deliberately *decoupled* from the
# real ``V080_BREAKING_CHANGES`` (it is NOT ``[:1]`` of it): at the 0.8.0 flip the
# real table is emptied, which would otherwise make these self-checks vacuous /
# spuriously fail. ``_orphans`` reads only truthiness + ``.issue`` (via getattr
# fallback), so a bare sentinel string is a valid stand-in entry.
_LIVE_SRC = "\n".join(_MACHINERY)
_NONEMPTY_TABLE: tuple[object, ...] = ("dummy-break-sentinel",)
def test_selfcheck_pre_flip_healthy_when_runway_intact() -> None:
assert _orphans((0, 7, 0), _LIVE_SRC, _NONEMPTY_TABLE) == []
def test_selfcheck_pre_flip_flags_early_machinery_removal() -> None:
assert _orphans((0, 7, 0), "", _NONEMPTY_TABLE)
def test_selfcheck_pre_flip_flags_early_table_emptying() -> None:
assert _orphans((0, 7, 0), _LIVE_SRC, ())
def test_selfcheck_post_flip_healthy_when_fully_flipped() -> None:
assert _orphans((0, 8, 0), "", ()) == []
def test_selfcheck_post_flip_flags_leftover_machinery() -> None:
assert _orphans((0, 8, 0), _LIVE_SRC, ())
def test_selfcheck_post_flip_flags_unflipped_breaks() -> None:
assert _orphans((0, 8, 0), "", _NONEMPTY_TABLE)