Files
hermes-agent/tests/scripts/test_code_health_rules.py
Teknium 16fba82ce3 fix(code-health): global/nonlocal no longer hide an import alias
_Binder.visit_Global marked the name ambiguous in the module, so `global execute` in a
function hid `from subprocess import run as execute` and HX006 (missing timeout) never
fired on `execute(cmd)`; nonlocal did the same for an enclosing function's import. A
declaration selects the outer binding, it does not rebind it: declared names now resolve
in the scope they point at, and only an assignment (or import) through the declaration
rebinds that outer name.
2026-10-04 22:35:16 -07:00

382 lines
18 KiB
Python

"""Rule and structure precision of the code-health ratchet (scripts/code_health): each case is a
reviewer's repro paired with the control that must keep its verdict."""
from __future__ import annotations
import ast
import pytest
from scripts.code_health.config import RULES_BY_ID
from scripts.code_health.py_rules import CHECKERS, Ctx, canonical_tree
from scripts.code_health.py_structure import body_hash, nesting_depth
from tests.scripts.test_code_health import _commit, _repo, _verdict
def _hits(rule: str, src: str) -> list[int]:
"""Lines ``rule`` reports for ``src``, through the same canonical tree the measurer uses."""
return sorted(set(CHECKERS[rule](canonical_tree(ast.parse(src)), Ctx())))
def _judge(tmp_path, capsys, base_files: dict[str, str], head_files: dict[str, str | None]):
repo, base = _repo(tmp_path)
if base_files:
base = _commit(repo, base_files)
return _verdict(repo, base, head_files, capsys)
# --- F12: an import alias is shadowed only in the scope that rebinds it ---
_ALIAS = "from subprocess import run as execute\n\n\n{}\n\n\ndef launch(cmd):\n{} return execute(cmd)\n"
@pytest.mark.parametrize("elsewhere, local, flagged", [
("def identity(execute):\n return execute", "", True), # unrelated parameter
("def other():\n execute = 42\n return execute", "", True), # unrelated local
("def other():\n def execute():\n return 1\n return execute", "", True), # nested def
("def identity(x):\n return x", "", True),
# genuine shadowing where the call is made still stops resolution
("def identity(x):\n return x", " execute = print\n", False),
("execute = print", "", False), # the module itself rebinds it: ambiguous
])
def test_import_alias_resolves_per_scope(elsewhere, local, flagged):
src = _ALIAS.format(elsewhere, local)
assert bool(_hits("HX006", src)) is flagged, src
def test_import_alias_parameter_of_consuming_function_shadows():
src = "from subprocess import run as execute\n\n\ndef launch(cmd, execute):\n return execute(cmd)\n"
assert _hits("HX006", src) == []
def test_import_alias_with_unrelated_parameter_blocks_through_the_verdict(tmp_path, capsys):
head = {"pkg/c.py": _ALIAS.format("def identity(execute):\n return execute", "")}
code, out = _judge(tmp_path, capsys, {}, head)
assert code == 1 and "HX006" in out, out
# `global` / `nonlocal` select the outer binding; only an assignment through them rebinds it.
@pytest.mark.parametrize("src, flagged", [
(_ALIAS.format("", " global execute\n"), True),
("def outer(cmd):\n from subprocess import run as execute\n\n def launch():\n"
" nonlocal execute\n return execute(cmd)\n return launch\n", True),
(_ALIAS.format("def setup():\n global execute\n execute = print", ""), False),
])
def test_global_and_nonlocal_select_the_outer_binding(src, flagged):
assert bool(_hits("HX006", src)) is flagged, src
# --- F19: every independent eager capture is its own occurrence ---
_DICT = 'import os\n\nCACHE = {{\n{} "old": os.getenv("PATH"),\n}}\n'
_LIST = "import os\n\nPATHS = [\n{} os.getenv(\"PATH\"),\n]\n"
_DEFAULT = "import os\n\n\ndef f(\n{} a=os.getenv(\"PATH\"),\n):\n return a\n"
@pytest.mark.parametrize("shape, added, blocks", [
(_DICT, ' "new": os.getenv("HOME"),\n', True),
(_LIST, ' os.getenv("HOME"),\n', True),
(_DEFAULT, ' b=os.getenv("HOME"),\n', True),
(_DICT, ' "new": lambda: os.getenv("HOME"),\n', False), # deferred read: the fix
(_DICT, ' "new": "literal",\n', False), # unchanged debt stays grandfathered
])
def test_added_capture_inside_an_existing_expression_is_new(tmp_path, capsys, shape, added, blocks):
code, out = _judge(tmp_path, capsys, {"pkg/c.py": shape.format("")},
{"pkg/c.py": shape.format(added)})
assert code == (1 if blocks else 0), out
assert ("HX005" in out) is blocks, out
def test_added_capture_in_its_own_assignment_is_new(tmp_path, capsys):
base = _DICT.format("")
code, out = _judge(tmp_path, capsys, {"pkg/c.py": base},
{"pkg/c.py": base + 'NEW = os.getenv("HOME")\n'})
assert code == 1 and "HX005" in out, out
def test_capture_lines_reports_each_independent_capture_once():
two = 'import os\n\nCACHE = {\n "a": os.getenv("A"),\n "b": os.getenv("B"),\n}\n'
assert _hits("HX005", two) == [4, 5]
# a capture nested inside another is the same occurrence
nested = 'import os\n\nHOME = os.path.expanduser(\n os.getenv("A", "~")\n)\n'
assert _hits("HX005", nested) == [3]
# --- HX006: process handles are tracked per lexical scope ---
_SYNC_ASYNC = (
"import asyncio\nimport subprocess\n\n\n"
"def sync(cmd):\n proc = subprocess.Popen(cmd)\n return proc.communicate(timeout=1)\n\n\n"
"async def asynchronous(cmd):\n {name} = await asyncio.create_subprocess_exec(*cmd)\n"
" return await asyncio.wait_for({name}.communicate(), timeout=1)\n"
)
@pytest.mark.parametrize("name", ["proc", "child"])
def test_process_handles_in_different_functions_do_not_collide(name):
assert _hits("HX006", _SYNC_ASYNC.format(name=name)) == []
def test_adding_a_bounded_async_helper_keeps_the_sync_one_clean(tmp_path, capsys):
sync_only = _SYNC_ASYNC.format(name="proc").split("\n\n\nasync def")[0] + "\n"
code, out = _judge(tmp_path, capsys, {"pkg/p.py": sync_only},
{"pkg/p.py": _SYNC_ASYNC.format(name="proc")})
assert code == 0, out
def test_process_handle_follows_reassignment():
src = ("import asyncio\nimport subprocess\n\n\nasync def f(c):\n"
" proc = subprocess.Popen(c)\n proc.wait(timeout=1)\n"
" proc = await asyncio.create_subprocess_exec(*c)\n await proc.wait()\n"
" proc = None\n return proc\n")
assert _hits("HX006", src) == [9] # only the asyncio wait, which nothing bounds
unbounded = "import subprocess\n\n\ndef f(c):\n proc = subprocess.Popen(c)\n return proc.wait()\n"
assert _hits("HX006", unbounded) == [6]
with_block = "import subprocess\n\n\ndef f(c):\n with subprocess.Popen(c) as proc:\n return proc.wait()\n"
assert _hits("HX006", with_block) == [6]
# a parameter that merely shares the spelling is not the other function's process
param = unbounded + "\n\ndef stop(proc):\n return proc.wait()\n"
assert _hits("HX006", param) == [6]
# ...but one annotated as a process is a process
typed = param.replace("def stop(proc):", "def stop(proc: subprocess.Popen[str]):")
assert _hits("HX006", typed) == [6, 10]
typed_async = ("import asyncio\n\n\nasync def stop(proc: asyncio.subprocess.Process):\n"
" return await proc.wait()\n")
assert _hits("HX006", typed_async) == [5]
def test_process_handle_on_an_attribute_spans_the_class_methods():
src = ("import subprocess\n\n\nclass Runner:\n def start(self, c):\n"
" self._proc = subprocess.Popen(c)\n\n def stop(self):\n"
" return self._proc.wait()\n")
assert _hits("HX006", src) == [9]
closure = ("import subprocess\n\n\ndef f(c):\n proc = subprocess.Popen(c)\n\n"
" def reap():\n return proc.wait()\n return reap\n")
assert _hits("HX006", closure) == [8]
# --- HX006: a wait right after kill() is bounded, however its value is used ---
_KILL = "import asyncio\nimport subprocess\n\n\ndef reap(cmd, other):\n proc = subprocess.Popen(cmd)\n{}"
@pytest.mark.parametrize("tail, flagged", [
(" proc.kill()\n proc.wait()\n", False),
(" proc.kill()\n rc = proc.wait()\n return rc\n", False),
(" proc.kill()\n return proc.wait()\n", False),
(" proc.kill()\n rc: int = proc.wait()\n return rc\n", False),
(" proc.kill()\n return proc.wait(timeout=5)\n", False),
(" return proc.wait()\n", True), # no preceding kill
(" other.kill()\n return proc.wait()\n", True), # a different receiver was killed
(" proc.kill()\n return proc.communicate()\n", True), # reads until grandchildren exit
])
def test_wait_after_kill(tail, flagged):
assert bool(_hits("HX006", _KILL.format(tail))) is flagged
def test_async_wait_after_kill_stays_flagged():
src = ("import asyncio\n\n\nasync def f(c):\n proc = await asyncio.create_subprocess_exec(*c)\n"
" proc.kill()\n return await proc.wait()\n")
assert _hits("HX006", src) == [7]
def test_assignment_refactored_into_return_after_kill_passes(tmp_path, capsys):
base = _KILL.format(" proc.kill()\n rc = proc.wait()\n return rc\n")
head = _KILL.format(" proc.kill()\n return proc.wait()\n")
code, out = _judge(tmp_path, capsys, {"pkg/k.py": base}, {"pkg/k.py": head})
assert code == 0, out
# --- HX009: annotated results and projected loop variables ---
_GATHER = "import asyncio\n\n\nasync def run(awaitables, payloads):\n results{ann} = await asyncio.gather(\n *awaitables, return_exceptions=True\n )\n{loop}"
@pytest.mark.parametrize("ann, loop, flagged", [
(": list", " for result in results:\n if isinstance(result, Exception):\n raise result\n", True),
("", " for result in results:\n if isinstance(result, Exception):\n raise result\n", True),
("", " for payload, result in zip(payloads, results):\n if isinstance(result, Exception):\n raise result\n", True),
("", " for payload, result in zip(payloads, results):\n if isinstance(payload, Exception):\n raise payload\n", False),
("", " for payload, result in zip(payloads, results, strict=True):\n if isinstance(payload, Exception):\n raise payload\n", False),
("", " for i, r in enumerate(results):\n if isinstance(r, Exception):\n raise r\n", True),
("", " for i, r in enumerate(results):\n if isinstance(i, Exception):\n raise i\n", False),
("", " return [r for i, r in enumerate(results) if isinstance(i, Exception)]\n", False),
(": list", " return [r for i, r in enumerate(results) if isinstance(r, Exception)]\n", True),
])
def test_gather_results_projection(ann, loop, flagged):
assert bool(_hits("HX009", _GATHER.format(ann=ann, loop=loop))) is flagged
# --- HX005: annotations are evaluated at def time unless postponed ---
_FUTURE = "from __future__ import annotations\n\n"
@pytest.mark.parametrize("src, flagged", [
("import os\n\n\ndef f(x: os.getenv('A')):\n return x\n", True),
("import os\n\n\ndef f(x) -> os.getenv('A'):\n return x\n", True),
("import os\n\n\nclass C:\n def f(self, *a: os.getenv('A')):\n return a\n", True),
(_FUTURE + "import os\n\n\ndef f(x: os.getenv('A')):\n return x\n", False),
(_FUTURE + "import os\n\n\ndef f(x) -> os.getenv('A'):\n return x\n", False),
(_FUTURE + "import os\n\nx: os.getenv('PATH')\n", False),
(_FUTURE + "import os\n\n\nclass C:\n x: os.getenv('PATH')\n", False),
(_FUTURE + "import os\n\nx: str = os.getenv('PATH')\n", True), # the value still runs
("import os\n\nx: os.getenv('PATH')\n", True),
("import os\n\n\ndef f():\n def g(x: os.getenv('A')):\n return x\n return g\n", False),
])
def test_annotation_evaluation_time(src, flagged):
assert bool(_hits("HX005", src)) is flagged, src
# --- F15: only the function's own bindings stop the recursive-rename normalization ---
def _recursive(name: str, inner: str = "legacy") -> str:
return (f"def {name}(n):\n def identity({inner}):\n return {inner}\n" + " pass\n" * 300
+ f" return {name}(n-1) if n else identity(0)\n")
def _hash(src: str) -> str:
return body_hash(ast.parse(src).body[0])
@pytest.mark.parametrize("inner", ["legacy", "value"])
def test_recursive_rename_ignores_nested_bindings(inner):
assert _hash(_recursive("legacy", inner)) == _hash(_recursive("renamed", inner))
def test_recursive_rename_with_nested_parameter_keeps_its_cap(tmp_path, capsys):
code, out = _judge(tmp_path, capsys, {"pkg/r.py": _recursive("legacy")},
{"pkg/r.py": _recursive("renamed")})
assert code == 0, out
def test_recursive_rename_controls():
# a rename that leaves the old self-call behind is different code
stale = _recursive("renamed").replace("return renamed(", "return legacy(")
assert _hash(_recursive("legacy")) != _hash(stale)
# a body that rebinds the name in its own scope refers to that local, not to itself
own = "def {0}(n):\n {0} = n\n return {0}\n"
assert _hash(own.format("legacy")) != _hash(own.format("renamed"))
# a closure's reference to the enclosing function is still a self-reference
closure = "def {0}(n):\n def helper():\n return {0}(n - 1)\n return helper()\n"
assert _hash(closure.format("legacy")) == _hash(closure.format("renamed"))
# --- nesting: `else:` + indented `if` is nesting; `elif` is not ---
# `>` keeps the fixtures off HX011 (an if/elif ladder on one name).
def _else_if(levels: int, inert: bool = False) -> str:
out, pad = ["def f(x):"], " "
for k in range(levels):
if inert and k:
out.append(pad + "pass")
out += [f"{pad}if x > {k}:", f"{pad} return {k}", f"{pad}else:"]
pad += " "
out.append(pad + "return -1")
return "\n".join(out) + "\n"
def _elif(levels: int) -> str:
arms = "".join(f" {'if' if k == 0 else 'elif'} x > {k}:\n return {k}\n" for k in range(levels))
return "def f(x):\n" + arms + " else:\n return -1\n"
def _depth(src: str) -> int:
return nesting_depth(ast.parse(src).body[0].body)
def test_nesting_counts_physically_nested_else_if():
assert _depth(_else_if(7)) == _depth(_else_if(7, inert=True)) == 7
assert _depth(_elif(7)) == 1
assert _depth(_else_if(6)) == 6
def test_seven_physical_else_if_levels_block(tmp_path, capsys):
code, out = _judge(tmp_path / "nested", capsys, {}, {"pkg/n.py": _else_if(7)})
assert code == 1 and "NESTING" in out, out
code, out = _judge(tmp_path / "flat", capsys, {}, {"pkg/n.py": _elif(7)})
assert code == 0, out
# --- HX001 / HX003 / HX012 precision ---
@pytest.mark.parametrize("expr, flagged", [
('Path.home() / ".hermes"', True),
('Path.home() / ".hermes/profiles"', True),
('Path.home() / ".hermes" / "x"', True),
('Path.home() / ".hermes-profile-exports"', False),
('Path.home() / ".hermes_backup"', False),
('os.path.expanduser("~/.hermes")', True),
('os.path.expanduser("~/.hermes/config.yaml")', True),
('os.path.expanduser("~/.hermes-profile-exports")', False),
])
def test_hardcoded_home_matches_the_exact_component(expr, flagged):
src = f"import os\nfrom pathlib import Path\n\n\ndef f():\n return {expr}\n"
assert bool(_hits("HX001", src)) is flagged
@pytest.mark.parametrize("src, flagged", [
('"""Unlike `ps aux`, this reads /proc."""\n', False),
('def f():\n """Never `pgrep -f hermes`: argv substrings lie."""\n return 1\n', False),
('import subprocess\n\n\ndef f():\n return subprocess.run("ps aux", shell=True, timeout=5)\n', True),
('CMD = "pgrep -f hermes"\n', True),
('def f(n):\n return f"pgrep -f {n}"\n', True),
])
def test_argv_identity_ignores_docstrings(src, flagged):
assert bool(_hits("HX003", src)) is flagged
_THREAD = "import contextvars\nimport threading\n\n\ndef f(fn):\n{}"
@pytest.mark.parametrize("body, flagged", [
(" return threading.Thread(target=contextvars.copy_context().run, args=(fn,))\n", False),
(" ctx = contextvars.copy_context()\n return threading.Thread(target=ctx.run, args=(fn,))\n", False),
(" ctx = contextvars.copy_context()\n return threading.Thread(None, ctx.run, args=(fn,))\n", False),
(" return threading.Thread(target=fn)\n", True),
(" ctx = make()\n return threading.Thread(target=ctx.run, args=(fn,))\n", True),
(" ctx = contextvars.copy_context()\n ctx = make()\n return threading.Thread(target=ctx.run)\n", True),
(" return threading.Thread(target=ctx.run, args=(fn,))\n", True), # ctx from elsewhere
])
def test_raw_thread_accepts_a_copied_context(body, flagged):
assert bool(_hits("HX012", _THREAD.format(body))) is flagged
def test_raw_thread_accepts_from_import_copy_context():
src = ("from contextvars import copy_context\nimport threading\n\n\ndef f(fn):\n"
" return threading.Thread(target=copy_context().run, args=(fn,))\n")
assert _hits("HX012", src) == []
def test_raw_thread_context_from_another_function_is_not_accepted():
src = ("import contextvars\nimport threading\n\n\ndef g():\n ctx = contextvars.copy_context()\n"
" return ctx\n\n\ndef f(fn, ctx):\n return threading.Thread(target=ctx.run)\n")
assert _hits("HX012", src) == [11]
# --- fix texts say what the engine actually accepts ---
def test_fix_texts():
hx010 = RULES_BY_ID["HX010"].fix
assert "is not None" in hx010 and "is_truthy_value" in hx010
assert hx010.index("is not None") < hx010.index("is_truthy_value")
assert "allow" in RULES_BY_ID["HX001"].fix
ble = RULES_BY_ID["BLE001"].fix
assert "noqa" in ble and "health: allow BLE001" in ble and "exc_info=True" in ble
_HANDLER = "import logging\n\nlogger = logging.getLogger(__name__)\n\n\ndef boundary():\n try:\n pass\n{}"
@pytest.mark.parametrize("handler, blocks", [
(" except Exception:\n logger.exception('boundary failed')\n", False),
(" except Exception:\n logger.warning('boundary failed', exc_info=True)\n", False),
(" except Exception as exc:\n raise RuntimeError('boundary failed') from exc\n", False),
(" except Exception as exc:\n logger.warning('boundary failed: %s', exc)\n", True),
(" except Exception: # noqa: BLE001\n logger.warning('boundary failed')\n", True),
(" except Exception: # health: allow BLE001 -- top-level boundary\n logger.warning('x')\n", False),
])
def test_ble001_fix_text_matches_the_engine(tmp_path, capsys, handler, blocks):
code, out = _judge(tmp_path, capsys, {}, {"pkg/h.py": _HANDLER.format(handler)})
assert code == (1 if blocks else 0), out