Files
hermes-webui/tests/test_ruff_forward_lint.py
T

134 lines
5.5 KiB
Python

"""Forward-looking ruff lint gate — in-suite enforcement.
The Python twin of ``tests/test_static_js_runtime_lint.py`` (the ESLint runtime
guard). Issue #3273.
What this asserts:
1. The whole tree is free of E9 (real syntax / IO / runtime-error) findings. This
is the one ruleset we hold green tree-wide — there is no backlog of E9 errors,
and a new one is always a genuine bug. (The F/B families have a known cosmetic
backlog in the existing tree, deliberately NOT reformatted — see #3273 — so we
do NOT assert those are tree-clean; they are enforced on NEW code only by
``scripts/ruff_lint.py --diff``, which runs in CI and the pre-release gate.)
2. The curated ruff config is present and shaped as intended (E9 + F + B selected,
no global ignore of the F/B rules that would blind the new-code gate).
3. The line-scoped gate runner imports and its diff machinery works.
Skips cleanly when ruff is not installed, so a contributor without the dev tool is
never blocked — CI installs ruff so enforcement holds there. Same contract as the
ESLint guard.
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import pytest
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _ruff_argv():
"""Resolve a runnable ruff invocation, or None."""
if shutil.which("ruff"):
return ["ruff"]
probe = subprocess.run(
[sys.executable, "-m", "ruff", "--version"], capture_output=True, text=True
)
if probe.returncode == 0:
return [sys.executable, "-m", "ruff"]
if shutil.which("uvx"):
probe = subprocess.run(["uvx", "ruff", "--version"], capture_output=True, text=True)
if probe.returncode == 0:
return ["uvx", "ruff"]
return None
RUFF = _ruff_argv()
ruff_required = pytest.mark.skipif(RUFF is None, reason="ruff not installed (dev-only tool; CI installs it)")
def test_pyproject_ruff_config_present_and_shaped():
"""The curated [tool.ruff] config exists and selects the intended families."""
path = os.path.join(REPO_ROOT, "pyproject.toml")
assert os.path.exists(path), "pyproject.toml with [tool.ruff] config is required (#3273)"
text = open(path, encoding="utf-8").read()
assert "[tool.ruff]" in text, "[tool.ruff] section missing"
assert "[tool.ruff.lint]" in text, "[tool.ruff.lint] section missing"
# The curated correctness-leaning set: pyflakes + syntax + bugbear.
assert '"E9"' in text and '"F"' in text and '"B"' in text, (
"ruff select must include E9, F, B (curated correctness ruleset)"
)
# Guard the design intent: no GLOBAL ignore of F401/F841 etc. A blanket ignore
# would defeat the new-code gate (a stray unused import is the single most
# common new-code defect). Per-file-ignores for tests are fine.
lint_section = text.split("[tool.ruff.lint]", 1)[1]
before_subtables = lint_section.split("[tool.ruff.lint.", 1)[0]
assert "ignore = [" not in before_subtables and "ignore=[" not in before_subtables, (
"do not globally ignore F/B rules — that blinds the forward gate; "
"use per-file-ignores or scoped # noqa instead"
)
@ruff_required
def test_tree_is_E9_clean():
"""No real syntax / IO / runtime-error (E9) findings anywhere in the tree.
E9 is the one ruleset held green tree-wide: there is no backlog, and any new
E9 finding is a genuine bug (the kind that bricks a module on import).
"""
proc = subprocess.run(
RUFF + ["check", "--select", "E9", "--output-format", "json", "--no-cache", "."],
cwd=REPO_ROOT,
capture_output=True,
text=True,
)
findings = json.loads(proc.stdout) if proc.stdout.strip() else []
if findings:
lines = [
f" {os.path.relpath(f['filename'], REPO_ROOT)}:{f['location']['row']} "
f"{f.get('code')} {f.get('message', '').strip()}"
for f in findings
]
pytest.fail("ruff E9 (syntax/runtime) findings present:\n" + "\n".join(lines))
@ruff_required
def test_known_F821_false_positives_are_noqa_suppressed():
"""The two known F821 false positives carry scoped # noqa, keeping the tree F821-clean.
Regression guard: if someone strips a noqa or reintroduces the bare pattern,
this catches it. (#3273 documented exactly two F821 hits, both false positives.)
"""
proc = subprocess.run(
RUFF + ["check", "--select", "F821", "--output-format", "json", "--no-cache", "."],
cwd=REPO_ROOT,
capture_output=True,
text=True,
)
findings = json.loads(proc.stdout) if proc.stdout.strip() else []
locs = [f"{os.path.relpath(f['filename'], REPO_ROOT)}:{f['location']['row']}" for f in findings]
assert not findings, f"unexpected F821 findings (annotate genuine false positives with # noqa): {locs}"
def test_ruff_lint_runner_importable_and_has_modes():
"""scripts/ruff_lint.py imports and exposes the --diff / --all entry points."""
scripts_dir = os.path.join(REPO_ROOT, "scripts")
sys.path.insert(0, scripts_dir)
try:
import ruff_lint # noqa: PLC0415 intentional local import for test
finally:
sys.path.remove(scripts_dir)
assert hasattr(ruff_lint, "run_diff")
assert hasattr(ruff_lint, "run_all")
assert hasattr(ruff_lint, "_added_lines")
# The hunk-line parser is the gate's safety-critical core (only NEW lines gated).
# Smoke-test it against a synthetic unified diff via the module's regex.
assert ruff_lint._HUNK_RE.match("@@ -1,0 +5,3 @@ def f():").group(1) == "5"