Private
Public Access
feat(scripts): add heuristics to audit_exception_handling for review pass patterns (10 new heuristics + tests)
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
"""Tests for the new heuristics added in scripts/audit_exception_handling.py.
|
||||
|
||||
Each test creates a fixture file with a specific pattern and runs the audit
|
||||
script against it, verifying the pattern is classified correctly.
|
||||
|
||||
The new heuristics (added by result_migration_review_pass_20260617):
|
||||
1. list.index with ValueError fallback to default index (INTERNAL_COMPLIANT)
|
||||
2. dict lookup (KeyError) with default value (INTERNAL_COMPLIANT)
|
||||
3. datetime.fromisoformat(s) with ValueError: None (INTERNAL_COMPLIANT)
|
||||
4. Path.resolve(strict=True) with OSError/ValueError fallback (INTERNAL_COMPLIANT)
|
||||
5. Path.relative_to with ValueError: pass (INTERNAL_COMPLIANT)
|
||||
6. asyncio.get_running_loop() with RuntimeError: asyncio.run() (INTERNAL_COMPLIANT)
|
||||
7. Narrow except (ImportError, AttributeError) + fallback stub (INTERNAL_COMPLIANT)
|
||||
8. raise NotImplementedError() as entire function body (INTERNAL_PROGRAMMER_RAISE)
|
||||
9. if None: raise ImportError() validation pattern (INTERNAL_PROGRAMMER_RAISE)
|
||||
10. try/except (json.JSONDecodeError, KeyError) around JSON parse + print + return (INTERNAL_COMPLIANT)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "audit_exception_handling.py"
|
||||
|
||||
|
||||
def _run_audit_on_fixture(source: str) -> dict:
|
||||
"""Run the audit script on a fixture and return the parsed JSON output."""
|
||||
# Use a temp dir outside tests/artifacts/ to avoid the audit's
|
||||
# default artifacts-exclusion filter.
|
||||
import tempfile
|
||||
tmpdir = Path(tempfile.mkdtemp(prefix="audit_fixture_"))
|
||||
fixture = tmpdir / "audit_heuristic_fixture.py"
|
||||
fixture.write_text(textwrap.dedent(source), encoding="utf-8")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--json", "--src", str(tmpdir), "--verbose"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
cwd=str(ROOT),
|
||||
)
|
||||
finally:
|
||||
if fixture.exists():
|
||||
fixture.unlink()
|
||||
if tmpdir.exists():
|
||||
tmpdir.rmdir()
|
||||
if result.returncode not in (0, 1):
|
||||
raise RuntimeError(f"audit failed: {result.stderr}")
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def _classifications_for_file(data: dict, filename_suffix: str) -> list[dict]:
|
||||
"""Return all findings for files whose path ends with `filename_suffix`."""
|
||||
return [
|
||||
f
|
||||
for file_info in data.get("files", [])
|
||||
for f in file_info.get("findings", [])
|
||||
if file_info["filename"].endswith(filename_suffix)
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 1: list.index with ValueError fallback to default index
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_list_index_valueerror_fallback_is_compliant():
|
||||
"""try: list.index(x); except ValueError: idx = N is compliant."""
|
||||
src = '''
|
||||
def func(items, target):
|
||||
try:
|
||||
idx = items.index(target)
|
||||
except ValueError:
|
||||
idx = 0
|
||||
return idx
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
|
||||
assert len(excepts) == 1, f"expected 1 except, got {len(excepts)}: {excepts}"
|
||||
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
|
||||
f"list.index+ValueError fallback should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 2: KeyError lookup with default value
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_keyerror_lookup_with_default_is_compliant():
|
||||
"""try: dict[x]; except KeyError: val = default is compliant."""
|
||||
src = '''
|
||||
def func(d, key):
|
||||
try:
|
||||
val = d[key]
|
||||
except KeyError:
|
||||
val = "default"
|
||||
return val
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
|
||||
assert len(excepts) == 1
|
||||
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
|
||||
f"KeyError+default should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 3: datetime.fromisoformat(s) with ValueError: None
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_fromisoformat_valueerror_none_is_compliant():
|
||||
"""try: datetime.fromisoformat(s); except ValueError: x = None is compliant."""
|
||||
src = '''
|
||||
import datetime
|
||||
def func(s):
|
||||
try:
|
||||
d = datetime.datetime.fromisoformat(s)
|
||||
except ValueError:
|
||||
d = None
|
||||
return d
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
|
||||
assert len(excepts) == 1
|
||||
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
|
||||
f"fromisoformat+ValueError:None should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 4: Path.resolve(strict=True) with OSError/ValueError fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_path_resolve_strict_fallback_is_compliant():
|
||||
"""try: Path(p).resolve(strict=True); except (OSError, ValueError): Path(p).resolve() is compliant."""
|
||||
src = '''
|
||||
from pathlib import Path
|
||||
def func(p):
|
||||
try:
|
||||
rp = Path(p).resolve(strict=True)
|
||||
except (OSError, ValueError):
|
||||
rp = Path(p).resolve()
|
||||
return rp
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
|
||||
assert len(excepts) == 1
|
||||
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
|
||||
f"Path.resolve(strict=True)+fallback should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 5: Path.relative_to with ValueError: pass
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_path_relative_to_valueerror_is_compliant():
|
||||
"""try: rp.relative_to(base); except ValueError: pass is compliant (canonical subpath check)."""
|
||||
src = '''
|
||||
from pathlib import Path
|
||||
def is_subpath(rp, base):
|
||||
try:
|
||||
rp.relative_to(base)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
|
||||
assert len(excepts) == 1
|
||||
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
|
||||
f"Path.relative_to+ValueError should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 6: asyncio.get_running_loop() with RuntimeError: asyncio.run()
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_asyncio_get_running_loop_fallback_is_compliant():
|
||||
"""try: get_running_loop(); except RuntimeError: asyncio.run() is compliant."""
|
||||
src = '''
|
||||
import asyncio
|
||||
def bridge(coro):
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
return loop
|
||||
except RuntimeError:
|
||||
return asyncio.run(coro)
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
|
||||
assert len(excepts) == 1
|
||||
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
|
||||
f"get_running_loop+RuntimeError fallback should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 7: narrow except (ImportError, AttributeError) + fallback stub
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_import_attr_fallback_stub_is_compliant():
|
||||
"""narrow except (ImportError, AttributeError) with fallback attribute assignment is compliant."""
|
||||
src = '''
|
||||
class Stub:
|
||||
available = False
|
||||
def get_thing():
|
||||
try:
|
||||
import real_module
|
||||
return real_module
|
||||
except (ImportError, ModuleNotFoundError, AttributeError):
|
||||
return Stub()
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
|
||||
assert len(excepts) == 1
|
||||
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
|
||||
f"narrow except + fallback stub should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 8: raise NotImplementedError() as entire function body
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_raise_notimplemented_entire_body_is_programmer_error():
|
||||
"""raise NotImplementedError() as the entire function body is INTERNAL_PROGRAMMER_RAISE."""
|
||||
src = '''
|
||||
class Base:
|
||||
def abstract_method(self):
|
||||
raise NotImplementedError()
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
raises = [f for f in findings if f["kind"] == "RAISE"]
|
||||
assert len(raises) == 1
|
||||
assert raises[0]["category"] == "INTERNAL_PROGRAMMER_RAISE", (
|
||||
f"raise NotImplementedError() in entire body should be INTERNAL_PROGRAMMER_RAISE, got {raises[0]['category']}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 9: if None: raise ImportError() validation
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_validation_raise_is_programmer_error():
|
||||
"""if <var> is None: raise ImportError() is INTERNAL_PROGRAMMER_RAISE."""
|
||||
src = '''
|
||||
def func(dep):
|
||||
if dep is None:
|
||||
raise ImportError("dependency missing")
|
||||
return dep
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
raises = [f for f in findings if f["kind"] == "RAISE"]
|
||||
assert len(raises) == 1
|
||||
assert raises[0]["category"] == "INTERNAL_PROGRAMMER_RAISE", (
|
||||
f"validation raise should be INTERNAL_PROGRAMMER_RAISE, got {raises[0]['category']}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heuristic 10: try/except (json.JSONDecodeError, KeyError) + print + return
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_json_parse_with_print_is_compliant():
|
||||
"""try/except (json.JSONDecodeError, KeyError) around JSON parse with print is compliant."""
|
||||
src = '''
|
||||
import json
|
||||
def parse(s):
|
||||
try:
|
||||
data = json.loads(s)
|
||||
if not isinstance(data, list):
|
||||
print("not a list")
|
||||
return
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"json error: {e}")
|
||||
except KeyError as e:
|
||||
print(f"missing key: {e}")
|
||||
'''
|
||||
data = _run_audit_on_fixture(src)
|
||||
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
|
||||
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
|
||||
assert len(excepts) == 2
|
||||
for e in excepts:
|
||||
assert e["category"] == "INTERNAL_COMPLIANT", (
|
||||
f"json parse with print should be INTERNAL_COMPLIANT, got {e['category']}"
|
||||
)
|
||||
Reference in New Issue
Block a user