Files
hermes-agent/tests/scripts/test_code_health_rules.py
Teknium 592a02fd13 fix(code-health): track HX006 process handles per lexical scope
Adolanium (comment 5983756525 §4), corroborated by andrexibiza (F13 receiver
class) and helix4u: _process_handles() keyed handles by variable spelling for
the whole module, so an async `proc` in one function made a correctly bounded
sync `proc.communicate(timeout=1)` in another function fail HX006 (and a
`def stop(proc)` parameter inherited whatever `proc` meant elsewhere).

A plain-name handle now belongs to the scope that binds it: the innermost
scope binding the name decides (class bodies are invisible to methods,
closures see their enclosing function), and within it the latest binding
before the call, so reassignment (Popen -> asyncio process -> None)
re-classifies the receiver. Parameters and other bindings clear it. Attribute
handles (`self._proc`) stay shared by the methods of their class, keyed by
that class rather than by spelling across the module.
2026-10-04 21:29:39 -07:00

139 lines
6.3 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
# --- 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]
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]