Files
manual_slop/tests/test_audit_heuristics.py
T
ed a5a06f8516 test(audit_heuristics): add 5 regression tests for dunder raise (Phase 11)
Five regression-guard tests verify the new dunder-method bare-raise
heuristic in scripts/audit_exception_handling.py:_classify_raise:
- test_bare_raise_attribute_error_in_getattr_is_programmer_raise
- test_bare_raise_name_error_in_getattr_is_programmer_raise
- test_bare_raise_in_setattr_is_programmer_raise
- test_bare_raise_in_delattr_is_programmer_raise
- test_bare_raise_in_getattribute_is_programmer_raise

Each test feeds a minimal source sample through the visitor's
_classify_raise and asserts INTERNAL_PROGRAMMER_RAISE. The tests
cover all 4 dunder methods (__getattr__, __getattribute__,
__setattr__, __delattr__) and both programmer-error exception types
(AttributeError, NameError).

Phase 11 result_migration_gui_2_20260619.
2026-06-20 01:57:33 -04:00

224 lines
9.0 KiB
Python

# Phase 7 Task 7.8 - Regression-guard tests for audit heuristic.
# Per Phase 7 spec 22.5.5 (FR5):
# - BOUNDARY_FASTAPI classification requires ast.Raise(exc=HTTPException)
# OR a return of Result(...) in the except body.
# - Otherwise re-classify as INTERNAL_SILENT_SWALLOW (logging body) or
# INTERNAL_COMPLIANT (try/finally cleanup).
import ast
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from audit_exception_handling import ( # noqa: E402
ExceptionVisitor,
audit_file,
)
def _make_visitor(source: str, func_name: str):
"""Create an ExceptionVisitor positioned inside the named function."""
tree = ast.parse(source)
visitor = ExceptionVisitor(str(ROOT / "src" / "_test_dummy.py"))
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name == func_name:
visitor._func_stack = [node]
return visitor
raise ValueError(f"Function {func_name} not found in source")
def _find_handler(visitor):
"""Find the first Try node in the function body."""
for node in visitor._func_stack[0].body:
if isinstance(node, ast.Try):
return node
raise AssertionError("expected a try/except in function")
def test_is_api_handler_requires_http_exception_in_body():
# OLD STYLE: only stderr.write (should NOT be BOUNDARY_FASTAPI after Phase 7)
src = (
"def _api_generate(controller):\n"
" HTTPException = None\n"
" try:\n"
" do_something()\n"
" except Exception as e:\n"
" sys.stderr.write('err: ' + str(e))\n"
" sys.stderr.flush()\n"
)
visitor = _make_visitor(src, "_api_generate")
handler = _find_handler(visitor).handlers[0]
category, _ = visitor._classify_except(handler, _find_handler(visitor))
assert category != "BOUNDARY_FASTAPI", (
f"Phase 7 FR5 tightening failed: stale body (only stderr.write) "
f"should NOT be BOUNDARY_FASTAPI; got {category}."
)
def test_api_handler_with_http_exception_raise_is_boundary_fastapi():
# NEW STYLE: raises HTTPException (the canonical FastAPI pattern)
src = (
"def _api_generate(controller):\n"
" HTTPException = None\n"
" try:\n"
" do_something()\n"
" except Exception as e:\n"
" raise HTTPException(status_code=500, detail=str(e))\n"
)
visitor = _make_visitor(src, "_api_generate")
try_node = _find_handler(visitor)
handler = try_node.handlers[0]
category, _ = visitor._classify_except(handler, try_node)
assert category == "BOUNDARY_FASTAPI", (
f"Phase 7 FR5 regression: handler with HTTPException raise should be "
f"BOUNDARY_FASTAPI; got {category}."
)
def test_non_api_handler_with_logging_is_still_internal_compliant():
# Non-_api_* function with logging-only except body
src = (
"def regular_handler():\n"
" try:\n"
" do_something()\n"
" except Exception as e:\n"
" logging.getLogger('x').debug('err: %s', e)\n"
" print('err: ' + str(e))\n"
)
visitor = _make_visitor(src, "regular_handler")
try_node = _find_handler(visitor)
handler = try_node.handlers[0]
category, _ = visitor._classify_except(handler, try_node)
assert category in ("INTERNAL_COMPLIANT", "INTERNAL_SILENT_SWALLOW", "INTERNAL_BROAD_CATCH"), (
f"Non-api handler should NOT be BOUNDARY_FASTAPI; got {category}."
)
def test_15_existing_fastapi_sites_remain_classified():
# The 13 BOUNDARY_FASTAPI sites in src/app_controller.py must remain
# classified after the heuristic tightening (Phase 7 FR5).
# Note: src/api_hooks.py functions do NOT have _api_ prefix, so they
# were never classified BOUNDARY_FASTAPI; the 13 sites are all in
# _api_* handlers in app_controller.py.
app_controller_path = ROOT / "src" / "app_controller.py"
if not app_controller_path.exists():
pytest.skip(f"{app_controller_path} not found")
report = audit_file(app_controller_path)
fastapi_sites = [f for f in report.findings if f.category == "BOUNDARY_FASTAPI"]
assert len(fastapi_sites) >= 10, (
f"Phase 7 regression: expected at least 10 BOUNDARY_FASTAPI sites in "
f"src/app_controller.py, got {len(fastapi_sites)}. The known sites "
f"must remain classified after heuristic tightening."
)
src = app_controller_path.read_text(encoding="utf-8")
for site in fastapi_sites[:3]:
lines = src.split("\n")
line_num = site.line
window = "\n".join(lines[max(0, line_num - 5):line_num + 5])
assert "HTTPException" in window or "Result[" in window, (
f"Phase 7 regression: site at app_controller.py:{line_num} "
f"classified BOUNDARY_FASTAPI but window doesn't contain "
f"HTTPException or Result["
)
def test_phase7_migrated_sites_no_longer_silent_swallow():
# L242/L256/L5064/L5093 must not be INTERNAL_SILENT_SWALLOW after Phase 7.
app_controller_path = ROOT / "src" / "app_controller.py"
if not app_controller_path.exists():
pytest.skip(f"{app_controller_path} not found")
report = audit_file(app_controller_path)
for f in report.findings:
if f.line in (242, 256, 5064, 5093):
assert f.category != "INTERNAL_SILENT_SWALLOW", (
f"Phase 7 regression: L{f.line} should not be "
f"INTERNAL_SILENT_SWALLOW after migration; got {f.category}"
)
# Phase 11 Task 11.4 - Regression-guard tests for dunder-method bare-raise heuristic.
# Per Phase 11 spec (INTERNAL_RETHROW classification for dunder methods):
# - Bare `raise AttributeError(name)` / `raise NameError(name)` in
# `__getattr__`, `__getattribute__`, `__setattr__`, `__delattr__` is the
# canonical dunder-method programmer-error pattern (per styleguide
# "Re-Raise Patterns": bare raises are reserved for programmer errors).
# - The audit previously classified these as INTERNAL_RETHROW (suspicious);
# the heuristic must reclassify them as INTERNAL_PROGRAMMER_RAISE.
DUNDER_RAISE_TESTS = {
"__getattr__": "def __getattr__(self, name):\n if name == 'controller':\n raise AttributeError(name)\n raise AttributeError(name)",
"__getattribute__": "def __getattribute__(self, name):\n if name == 'controller':\n raise AttributeError(name)\n raise AttributeError(name)",
"__setattr__": "def __setattr__(self, name, value):\n raise AttributeError(name)",
"__delattr__": "def __delattr__(self, name):\n raise AttributeError(name)",
}
def _classify_first_raise(source, func_name):
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name == func_name:
visitor = ExceptionVisitor(str(ROOT / "src" / "_test_dummy.py"))
visitor._func_stack = [node]
for sub in ast.walk(node):
if isinstance(sub, ast.Raise) and sub.exc is not None:
return visitor._classify_raise(sub)
raise AssertionError(f"No raise found in {func_name}")
def test_bare_raise_attribute_error_in_getattr_is_programmer_raise():
src = DUNDER_RAISE_TESTS["__getattr__"]
category, hint = _classify_first_raise(src, "__getattr__")
assert category == "INTERNAL_PROGRAMMER_RAISE", (
f"Phase 11 regression: bare `raise AttributeError(name)` in __getattr__ "
f"should be INTERNAL_PROGRAMMER_RAISE (canonical dunder-method pattern); "
f"got {category}. Hint: {hint}"
)
def test_bare_raise_name_error_in_getattr_is_programmer_raise():
src = (
"def __getattr__(self, name):\n"
" if not hasattr(self, 'x'):\n"
" raise NameError(name)\n"
" return self.x"
)
category, hint = _classify_first_raise(src, "__getattr__")
assert category == "INTERNAL_PROGRAMMER_RAISE", (
f"Phase 11 regression: bare `raise NameError(name)` in __getattr__ "
f"should be INTERNAL_PROGRAMMER_RAISE (canonical dunder-method pattern); "
f"got {category}. Hint: {hint}"
)
def test_bare_raise_in_setattr_is_programmer_raise():
src = DUNDER_RAISE_TESTS["__setattr__"]
category, hint = _classify_first_raise(src, "__setattr__")
assert category == "INTERNAL_PROGRAMMER_RAISE", (
f"Phase 11 regression: bare `raise AttributeError` in __setattr__ "
f"should be INTERNAL_PROGRAMMER_RAISE (canonical dunder-method pattern); "
f"got {category}. Hint: {hint}"
)
def test_bare_raise_in_delattr_is_programmer_raise():
src = DUNDER_RAISE_TESTS["__delattr__"]
category, hint = _classify_first_raise(src, "__delattr__")
assert category == "INTERNAL_PROGRAMMER_RAISE", (
f"Phase 11 regression: bare `raise AttributeError` in __delattr__ "
f"should be INTERNAL_PROGRAMMER_RAISE (canonical dunder-method pattern); "
f"got {category}. Hint: {hint}"
)
def test_bare_raise_in_getattribute_is_programmer_raise():
src = DUNDER_RAISE_TESTS["__getattribute__"]
category, hint = _classify_first_raise(src, "__getattribute__")
assert category == "INTERNAL_PROGRAMMER_RAISE", (
f"Phase 11 regression: bare `raise AttributeError(name)` in __getattribute__ "
f"should be INTERNAL_PROGRAMMER_RAISE (canonical dunder-method pattern); "
f"got {category}. Hint: {hint}"
)