"""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")