"""Tests for ci_gate.gate_policy re-exports."""
from __future__ import annotations
import os
import time
from datetime import date
from pathlib import Path
import pytest
import yaml
from scripts.helpers._config import ConfigError
from scripts.helpers.ci_gate.gate_policy import (
GatePolicy,
PathPatterns,
SourceExemption,
TestExemption,
_load_gate_policy_cached,
find_expired_test_exemptions,
find_expired_unmapped,
format_expired_exemptions_section,
format_expired_test_exemptions_section,
gate_policy_changed_in_diff,
is_exempt,
is_gate_test_path,
is_test_exempt,
load_gate_policy,
validate_gate_policy_if_changed,
)
from tests.helpers.fake_subprocess import FakeCompleted
from tests.regression.scripts.helpers.gate_policy_writer import (
DEFAULT_CONFIG_INCLUDE,
DEFAULT_GATE_ROOTS,
DEFAULT_TEST_EXCLUDE,
DEFAULT_TEST_INCLUDE,
write_gate_policy,
write_repo_file,
)
def _write_ci_policy(
repo: Path,
*,
exemptions: list[dict[str, object]] | None = None,
test_exemptions: list[dict[str, object]] | None = None,
roots: list[str] | None = None,
tests: dict[str, list[str]] | None = None,
) -> None:
write_gate_policy(
repo,
roots=roots,
tests=tests,
source_exemptions=exemptions,
test_exemptions=test_exemptions,
)
def _sample_exemption(file: str, symbol: str) -> SourceExemption:
return SourceExemption(
file=file,
symbol=symbol,
reason="test",
applicant="test",
approver="fangkai",
deadline=date(2099, 12, 31),
)
def _empty_policy(*, source_exemptions: tuple[SourceExemption, ...] = ()) -> GatePolicy:
return GatePolicy(
sources=PathPatterns(include_patterns=DEFAULT_GATE_ROOTS, exclude_patterns=()),
tests=PathPatterns(
include_patterns=DEFAULT_TEST_INCLUDE,
exclude_patterns=DEFAULT_TEST_EXCLUDE,
),
configs=PathPatterns(include_patterns=DEFAULT_CONFIG_INCLUDE, exclude_patterns=()),
source_exemptions=source_exemptions,
test_exemptions=(),
approvers=frozenset({"fangkai"}),
)
def test_load_gate_policy_cached_until_yaml_mtime_changes(tmp_path: Path) -> None:
repo = tmp_path / "repo"
_write_ci_policy(repo)
_load_gate_policy_cached.cache_clear()
first = load_gate_policy(repo)
second = load_gate_policy(repo)
assert first is second
policy_path = repo / "tests" / ".ci" / "gate_policy.yaml"
policy_path.write_text(policy_path.read_text(encoding="utf-8") + "\n", encoding="utf-8")
time.sleep(0.01)
bumped_mtime = time.time() + 1.0
os.utime(policy_path, (bumped_mtime, bumped_mtime))
third = load_gate_policy(repo)
assert third is not first
def test_load_gate_policy_expands_symbols(tmp_path: Path) -> None:
repo = tmp_path / "repo"
write_repo_file(repo, "tensor_cast/foo.py", "def fn():\n pass\n")
write_repo_file(repo, "cli/main.py", "def run():\n pass\n")
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["tensor_cast/foo.py::fn", "cli/main.py::run"],
"reason": "refactor",
"applicant": "alice",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
policy = load_gate_policy(repo)
assert len(policy.source_exemptions) == 2
assert policy.source_exemptions[0].file == "tensor_cast/foo.py"
assert policy.source_exemptions[0].symbol == "fn"
assert policy.roots == DEFAULT_GATE_ROOTS
def test_load_gate_policy_invalid_symbol_raises_config_error(tmp_path: Path) -> None:
repo = tmp_path / "repo"
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["bad-format"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
with pytest.raises(ConfigError, match="expected 'path::symbol'"):
load_gate_policy(repo)
def test_load_gate_policy_rejects_unknown_symbol(tmp_path: Path) -> None:
repo = tmp_path / "repo"
write_repo_file(repo, "cli/main.py", "def run():\n pass\n")
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["cli/main.py::missing"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
with pytest.raises(ConfigError, match="unknown symbol"):
load_gate_policy(repo)
def test_load_gate_policy_rejects_coverage_omitted_source(tmp_path: Path) -> None:
repo = tmp_path / "repo"
write_repo_file(
repo,
"tensor_cast/transformers/builtin_model/foo.py",
"def run():\n pass\n",
)
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["tensor_cast/transformers/builtin_model/foo.py::run"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
with pytest.raises(ConfigError, match="coverage-omitted"):
load_gate_policy(repo)
def test_load_gate_policy_accepts_decorator_suffix_symbol(tmp_path: Path) -> None:
repo = tmp_path / "repo"
write_repo_file(
repo,
"cli/main.py",
"def _decorator(arg):\n"
" def wrapper(fn):\n"
" return fn\n"
" return wrapper\n\n"
"@_decorator(torch.ops.foo.bar)\n"
"def _():\n"
" pass\n",
)
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["cli/main.py::_@_decorator(torch.ops.foo.bar)"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
policy = load_gate_policy(repo)
assert policy.source_exemptions[0].symbol == "_@_decorator(torch.ops.foo.bar)"
def test_load_gate_policy_rejects_legacy_dot_symbol(tmp_path: Path) -> None:
repo = tmp_path / "repo"
write_repo_file(
repo,
"cli/main.py",
"class Widget:\n def run(self):\n pass\n",
)
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["cli/main.py::Widget.run"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
with pytest.raises(ConfigError, match="unknown symbol"):
load_gate_policy(repo)
def test_load_gate_policy_accepts_symbol_with_internal_colons(tmp_path: Path) -> None:
repo = tmp_path / "repo"
write_repo_file(repo, "cli/main.py", "def run():\n pass\n")
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["cli/main.py::run::extra"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
with pytest.raises(ConfigError, match="unknown symbol"):
load_gate_policy(repo)
def test_load_gate_policy_accepts_canonical_class_method_symbol(tmp_path: Path) -> None:
repo = tmp_path / "repo"
write_repo_file(
repo,
"cli/main.py",
"class Widget:\n def run(self):\n pass\n",
)
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["cli/main.py::Widget::run"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
policy = load_gate_policy(repo)
assert policy.source_exemptions[0].symbol == "Widget::run"
def test_load_gate_policy_accepts_underscore_class_method_symbol(
tmp_path: Path,
) -> None:
repo = tmp_path / "repo"
write_repo_file(
repo,
"cli/main.py",
"class _InternalHelper:\n def run(self):\n pass\n",
)
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["cli/main.py::_InternalHelper::run"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
policy = load_gate_policy(repo)
assert policy.source_exemptions[0].symbol == "_InternalHelper::run"
def test_load_gate_policy_pydantic_validation_error_includes_field_path(
tmp_path: Path,
) -> None:
repo = tmp_path / "repo"
ci_dir = repo / "tests" / ".ci"
ci_dir.mkdir(parents=True, exist_ok=True)
(ci_dir / "gate_policy.yaml").write_text(
yaml.dump(
{
"roots": list(DEFAULT_GATE_ROOTS),
"tests": {"include": "not-a-list", "exclude": []},
"configs": {"include": ["pyproject.toml"], "exclude": []},
"exemptions": {"sources": [], "tests": []},
}
),
encoding="utf-8",
)
(ci_dir / "approvers.yaml").write_text(
yaml.dump({"approvers": ["fangkai"]}),
encoding="utf-8",
)
with pytest.raises(ConfigError, match=r"tests/\.ci/gate_policy\.yaml.*tests"):
load_gate_policy(repo)
def test_load_gate_policy_roots_must_end_with_slash(tmp_path: Path) -> None:
repo = tmp_path / "repo"
_write_ci_policy(repo, roots=["cli"])
with pytest.raises(ConfigError, match="must end with"):
load_gate_policy(repo)
def test_load_gate_policy_unknown_approver_allowed_without_strict_validate(
tmp_path: Path,
) -> None:
repo = tmp_path / "repo"
write_repo_file(repo, "cli/main.py", "def run():\n pass\n")
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["cli/main.py::run"],
"reason": "x",
"applicant": "a",
"approver": "unknown_person",
"deadline": "2026-06-30",
}
],
)
policy = load_gate_policy(repo)
assert policy.source_exemptions[0].approver == "unknown_person"
def test_validate_gate_policy_if_changed_checks_approver(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
repo = tmp_path / "repo"
write_repo_file(repo, "cli/main.py", "def run():\n pass\n")
_write_ci_policy(
repo,
exemptions=[
{
"symbols": ["cli/main.py::run"],
"reason": "x",
"applicant": "a",
"approver": "unknown_person",
"deadline": "2026-06-30",
}
],
)
monkeypatch.setattr(
"scripts.helpers.ci_gate.policy.gate_policy_changed_in_diff",
lambda *_args, **_kwargs: True,
)
with pytest.raises(ConfigError, match="not in approver registry"):
validate_gate_policy_if_changed(repo, "abc123")
def test_is_gate_test_path_excludes_helpers_and_assets() -> None:
discovery = GatePolicy(
sources=PathPatterns(include_patterns=DEFAULT_GATE_ROOTS, exclude_patterns=()),
tests=PathPatterns(include_patterns=DEFAULT_TEST_INCLUDE, exclude_patterns=DEFAULT_TEST_EXCLUDE),
configs=PathPatterns(include_patterns=DEFAULT_CONFIG_INCLUDE, exclude_patterns=()),
source_exemptions=(),
test_exemptions=(),
approvers=frozenset(),
).discovery
assert is_gate_test_path("tests/regression/cli/test_run.py", discovery) is True
assert is_gate_test_path("tests/helpers/assert_utils.py", discovery) is False
assert is_gate_test_path("tests/assets/model_config/foo.py", discovery) is False
def test_is_exempt_matches_file_and_symbol() -> None:
exemptions = (_sample_exemption("cli/main.py", "run"),)
assert is_exempt(exemptions, "cli/main.py", "run") is True
assert is_exempt(exemptions, "cli/main.py", "other") is False
def test_is_test_exempt_node_level_match() -> None:
exemptions = (
TestExemption(
test_id="tests/regression/nightly/test_x.py::test_case",
reason="x",
applicant="a",
approver="fangkai",
deadline=date(2099, 12, 31),
),
)
assert is_test_exempt(exemptions, "tests/regression/nightly/test_x.py::test_case") is True
assert is_test_exempt(exemptions, "tests/regression/nightly/test_x.py::test_case[param]") is True
assert is_test_exempt(exemptions, "tests/regression/nightly/test_x.py::test_other") is False
assert is_test_exempt(exemptions, "tests/regression/cli/test_run.py::test_case") is False
assert is_test_exempt(exemptions, "tests/regression/nightly/test_x.py") is False
def test_load_gate_policy_rejects_class_only_test_exemption(tmp_path: Path) -> None:
repo = tmp_path / "repo"
_write_ci_policy(
repo,
test_exemptions=[
{
"symbols": ["tests/regression/nightly/test_x.py::TestCase"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
with pytest.raises(ConfigError, match="must target a test function or method"):
load_gate_policy(repo)
def test_load_gate_policy_batches_test_exemption_pytest_collection(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
repo = tmp_path / "repo"
write_repo_file(repo, "tests/regression/cli/test_a.py", "def test_one():\n pass\n")
write_repo_file(repo, "tests/regression/cli/test_b.py", "def test_two():\n pass\n")
_write_ci_policy(
repo,
test_exemptions=[
{
"symbols": [
"tests/regression/cli/test_a.py::test_one",
"tests/regression/cli/test_b.py::test_two",
],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
calls: list[tuple[str, ...]] = []
def _fake_collect(targets: tuple[str, ...] | list[str]) -> tuple[str, ...]:
calls.append(tuple(targets))
return tuple(
f"{target}::test_one" if target.endswith("test_a.py") else f"{target}::test_two" for target in targets
)
monkeypatch.setattr(
"scripts.helpers.common.pytest_runner.collect_all_test_node_ids",
_fake_collect,
)
_load_gate_policy_cached.cache_clear()
policy = load_gate_policy(repo)
assert len(calls) == 1
assert set(calls[0]) == {
"tests/regression/cli/test_a.py",
"tests/regression/cli/test_b.py",
}
assert {entry.test_id for entry in policy.test_exemptions} == {
"tests/regression/cli/test_a.py::test_one",
"tests/regression/cli/test_b.py::test_two",
}
def test_load_gate_policy_custom_test_discovery_validates_collectible_paths(
tmp_path: Path,
) -> None:
repo = tmp_path / "repo"
custom_discovery = {
"include": ["tests/**/test_*.py", "tests/**/*_test.py"],
"exclude": [
"tests/helpers/**",
"tests/assets/**",
"tests/regression/nightly/**",
],
}
_write_ci_policy(
repo,
tests=custom_discovery,
test_exemptions=[
{
"symbols": ["tests/regression/nightly/test_x.py::test_case"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
with pytest.raises(ConfigError, match="not a collectible test module"):
load_gate_policy(repo)
_write_ci_policy(
repo,
tests=custom_discovery,
test_exemptions=[
{
"symbols": ["tests/regression/cli/test_run.py::test_case"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
policy = load_gate_policy(repo)
assert policy.test_exemptions[0].test_id == "tests/regression/cli/test_run.py::test_case"
def test_load_gate_policy_rejects_file_only_test_exemption(tmp_path: Path) -> None:
repo = tmp_path / "repo"
_write_ci_policy(
repo,
test_exemptions=[
{
"symbols": ["tests/regression/nightly/test_x.py"],
"reason": "x",
"applicant": "a",
"approver": "fangkai",
"deadline": "2026-06-30",
}
],
)
with pytest.raises(ConfigError, match=r"invalid test exemption id"):
load_gate_policy(repo)
def test_find_expired_unmapped_reports_missing_coverage() -> None:
policy_exemption = _sample_exemption("cli/main.py", "run")
expired = SourceExemption(
file=policy_exemption.file,
symbol=policy_exemption.symbol,
reason=policy_exemption.reason,
applicant=policy_exemption.applicant,
approver=policy_exemption.approver,
deadline=date(2020, 1, 1),
)
policy = _empty_policy(source_exemptions=(expired,))
reports = find_expired_unmapped(policy, {}, today=date(2026, 1, 1))
assert len(reports) == 1
assert reports[0].symbol_key == "cli/main.py::run"
def test_find_expired_unmapped_skips_when_test_map_has_symbol() -> None:
expired = SourceExemption(
file="cli/main.py",
symbol="run",
reason="x",
applicant="a",
approver="fangkai",
deadline=date(2020, 1, 1),
)
policy = _empty_policy(source_exemptions=(expired,))
test_map = {"tests/smoke/test_a.py::test_x": {"cli/main.py": ["run"]}}
assert find_expired_unmapped(policy, test_map, today=date(2026, 1, 1)) == ()
def test_find_expired_test_exemptions_reports_past_deadline() -> None:
policy = GatePolicy(
sources=PathPatterns(include_patterns=DEFAULT_GATE_ROOTS, exclude_patterns=()),
tests=PathPatterns(
include_patterns=DEFAULT_TEST_INCLUDE,
exclude_patterns=DEFAULT_TEST_EXCLUDE,
),
configs=PathPatterns(include_patterns=DEFAULT_CONFIG_INCLUDE, exclude_patterns=()),
source_exemptions=(),
test_exemptions=(
TestExemption(
test_id="tests/regression/nightly/test_x.py::test_case",
reason="x",
applicant="a",
approver="fangkai",
deadline=date(2020, 1, 1),
),
),
approvers=frozenset(),
)
reports = find_expired_test_exemptions(policy, today=date(2026, 1, 1))
assert len(reports) == 1
assert reports[0].symbol_key == "tests/regression/nightly/test_x.py::test_case"
def test_format_expired_exemptions_section_empty_returns_empty_string() -> None:
assert format_expired_exemptions_section(()) == ""
def test_format_expired_test_exemptions_section_includes_test_id() -> None:
report = find_expired_test_exemptions(
GatePolicy(
sources=PathPatterns(include_patterns=DEFAULT_GATE_ROOTS, exclude_patterns=()),
tests=PathPatterns(
include_patterns=DEFAULT_TEST_INCLUDE,
exclude_patterns=DEFAULT_TEST_EXCLUDE,
),
configs=PathPatterns(include_patterns=DEFAULT_CONFIG_INCLUDE, exclude_patterns=()),
source_exemptions=(),
test_exemptions=(
TestExemption(
test_id="tests/regression/nightly/test_x.py::test_case",
reason="x",
applicant="a",
approver="fangkai",
deadline=date(2020, 1, 1),
),
),
approvers=frozenset(),
),
today=date(2026, 1, 1),
)[0]
section = format_expired_test_exemptions_section((report,))
assert "Expired test exemptions" in section
assert "tests/regression/nightly/test_x.py::test_case" in section
assert "gate_policy.yaml" in section
def test_gate_policy_changed_in_diff_true_when_file_listed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"subprocess.run",
lambda *a, **kw: FakeCompleted(0, "tests/.ci/gate_policy.yaml\n", ""),
)
assert gate_policy_changed_in_diff(tmp_path, "abc123") is True