crafting-table/tests/test_parsers/test_python.py
Kayos d467b2f5be v0.1 wave 2A (steps 5+6): per-language parsers + findings extraction
- parsers/ package: rust / python / go / typescript / generic
- parser registry with language+recipe -> fallback resolution
- fingerprint hash (kind+file+line+code) for cross-run dedup
- runner.py post-exec hook: parse log, persist findings, count on job row
  (extraction runs before mark_job_finished so callers polling on terminal
  status see findings_count populated atomically)
- db.insert_finding / list_findings / increment_findings_count DAOs already
  shipped in wave 1; wired here
- GET /jobs/{id}/findings now returns real data (server route already
  shipped; was returning empty list because nothing populated the table)
- tests/test_parsers/: 6 modules + 11 fixtures (rust/python/go/typescript)
- tests/test_runner_findings.py: 3 integration tests
- README: tick steps 2-6, add Findings section

Suite: 108 passing (62 wave-1 + 46 new).
Spec: memory/spec-crafting-table.md
2026-04-29 08:36:16 -07:00

91 lines
3 KiB
Python

"""PythonParser unit tests — ruff + mypy + pip-audit + pytest."""
from __future__ import annotations
from .conftest import load_fixture
from crafting_table.parsers.python import PythonParser
def test_python_lint_ruff_array():
raw = load_fixture("python", "ruff.json")
findings = PythonParser.parse(raw, exit_code=1, recipe="lint")
# 2 ruff entries → 2 findings.
assert len(findings) == 2
by_code = {f.code: f for f in findings}
assert "F401" in by_code and "E501" in by_code
f401 = by_code["F401"]
assert f401.kind == "lint"
assert f401.severity == "warn"
assert f401.file == "/work/src/app.py"
assert f401.line == 3
# ruff fix.message should map into suggested_fix
assert f401.suggested_fix is not None
def test_python_lint_mypy_jsonl():
raw = load_fixture("python", "mypy.jsonl")
findings = PythonParser.parse(raw, exit_code=1, recipe="lint")
# 2 mypy lines: 1 error (kept), 1 note (still parsed but warn).
assert len(findings) == 2
err = next(f for f in findings if f.severity == "error")
assert err.code == "return-value"
assert err.file == "src/app.py"
assert err.line == 17
def test_python_lint_handles_garbage():
findings = PythonParser.parse("oops not json", exit_code=1, recipe="lint")
assert findings == []
def test_python_audit_pip_audit():
raw = load_fixture("python", "pip_audit.json")
findings = PythonParser.parse(raw, exit_code=1, recipe="audit")
# Only requests has a vuln; fastapi has none.
assert len(findings) == 1
f = findings[0]
assert f.kind == "cve"
assert f.severity == "high"
assert f.code == "PYSEC-2018-28"
assert "requests" in f.message
assert f.suggested_fix == "bump requests to 2.20.1"
assert f.extras["package"] == "requests"
def test_python_audit_clean_log_no_findings():
raw = '{"dependencies":[]}'
findings = PythonParser.parse(raw, exit_code=0, recipe="audit")
assert findings == []
def test_python_test_parses_failed_lines():
raw = load_fixture("python", "pytest.txt")
findings = PythonParser.parse(raw, exit_code=1, recipe="test")
assert len(findings) == 2
codes = sorted(f.code for f in findings)
assert codes == sorted(["tests/test_a.py::test_two", "tests/test_b.py::test_four"])
for f in findings:
assert f.kind == "test_fail"
assert f.severity == "error"
assert f.file is not None
assert f.file.endswith(".py")
def test_python_test_zero_exit_no_findings():
findings = PythonParser.parse("all passed", exit_code=0, recipe="test")
assert findings == []
def test_python_test_nonzero_no_failed_marker_emits_synthetic():
findings = PythonParser.parse("collection error", exit_code=2, recipe="test")
assert len(findings) == 1
assert findings[0].kind == "test_fail"
assert "exit_2" in findings[0].code
def test_python_matches():
assert PythonParser.matches("python", "lint")
assert PythonParser.matches("python", "audit")
assert PythonParser.matches("python", "test")
assert not PythonParser.matches("rust", "lint")