- 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
91 lines
3 KiB
Python
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")
|