diff options
Diffstat (limited to 'languages/python/tests')
| -rw-r--r-- | languages/python/tests/test_coverage_summary.py | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/languages/python/tests/test_coverage_summary.py b/languages/python/tests/test_coverage_summary.py new file mode 100644 index 0000000..b65effd --- /dev/null +++ b/languages/python/tests/test_coverage_summary.py @@ -0,0 +1,156 @@ +"""Tests for the Python bundle coverage-summary kernel. + +The script under test lives at languages/python/claude/scripts/coverage-summary.py +and is loaded by path (hyphenated CLI name, not importable as a module). + +The kernel is the missing-file detection and the file-weighted project number; +coverage.py's own `coverage report` prints the per-file table, so this script +does not reimplement that. Tests build a throwaway package tree plus a faithful +coverage.py JSON report and assert the kernel's numbers against hand-computed +values, with Normal / Boundary / Error coverage per function. +""" +import importlib.util +import json +from pathlib import Path + +import pytest + +# Load the hyphenated CLI script by path. +_SCRIPT = Path(__file__).resolve().parents[1] / "claude" / "scripts" / "coverage-summary.py" +_spec = importlib.util.spec_from_file_location("coverage_summary", _SCRIPT) +cs = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(cs) + + +def write_report(root: Path, files: dict) -> Path: + """Write a coverage.py-shaped JSON report. + + files maps a project-relative path to (covered, total). Mirrors the real + schema: files[path] = {"summary": {"covered_lines", "num_statements", ...}}. + """ + payload = {"meta": {"version": "7.0"}, "files": {}, "totals": {}} + for rel, (cov, tot) in files.items(): + payload["files"][rel] = { + "executed_lines": list(range(1, cov + 1)), + "summary": { + "covered_lines": cov, + "num_statements": tot, + "percent_covered": (100.0 * cov / tot) if tot else 100.0, + "missing_lines": tot - cov, + "excluded_lines": 0, + }, + "missing_lines": list(range(cov + 1, tot + 1)), + "excluded_lines": [], + } + p = root / "coverage.json" + p.write_text(json.dumps(payload)) + return p + + +@pytest.fixture +def project(tmp_path): + """A package tree under src/ plus a place for the report. Returns (root, src).""" + src = tmp_path / "src" + (src / "sub").mkdir(parents=True) + return tmp_path, src + + +# --- parse: covered / total ------------------------------------------------ + +def test_parse_counts_covered_and_statements(project): + root, src = project + report = write_report(root, {"src/a.py": (3, 5)}) + table = cs.parse_report(report) + key = str((root / "src/a.py").resolve()) + assert table[key] == (3, 5) + + +def test_parse_missing_report_raises(project): + root, _ = project + with pytest.raises(SystemExit): + cs.parse_report(root / "nope.json") + + +def test_parse_malformed_json_raises(project): + root, _ = project + bad = root / "coverage.json" + bad.write_text("{not json") + with pytest.raises(SystemExit): + cs.parse_report(bad) + + +# --- file percentage ------------------------------------------------------- + +def test_file_pct_normal(): + assert cs.file_pct(1, 2) == 50.0 + + +def test_file_pct_no_statements_is_100(): + assert cs.file_pct(0, 0) == 100.0 + + +def test_file_pct_fully_covered(): + assert cs.file_pct(4, 4) == 100.0 + + +# --- missing-file detection (the kernel) ----------------------------------- + +def test_missing_finds_ondisk_file_absent_from_report(project): + root, src = project + (src / "tracked.py").write_text("x = 1\n") + (src / "untested.py").write_text("y = 2\n") + report = write_report(root, {"src/tracked.py": (1, 1)}) + table = cs.under_dir(cs.parse_report(report), src, root) + missing = cs.missing(list(table), src, root) + assert missing == ["src/untested.py"] + + +def test_missing_recurses_into_subpackages(project): + root, src = project + (src / "top.py").write_text("a = 1\n") + (src / "sub" / "deep.py").write_text("b = 2\n") + report = write_report(root, {"src/top.py": (1, 1)}) + table = cs.under_dir(cs.parse_report(report), src, root) + missing = cs.missing(list(table), src, root) + assert "src/sub/deep.py" in missing + + +def test_missing_empty_when_all_tracked(project): + root, src = project + (src / "a.py").write_text("x = 1\n") + report = write_report(root, {"src/a.py": (1, 1)}) + table = cs.under_dir(cs.parse_report(report), src, root) + assert cs.missing(list(table), src, root) == [] + + +# --- project number (file-weighted, missing as 0%) ------------------------- + +def test_project_pct_unit_weighted_with_missing_as_zero(project): + root, src = project + for name in ("full.py", "half.py", "untested.py"): + (src / name).write_text("x = 1\n") + report = write_report(root, {"src/full.py": (2, 2), "src/half.py": (1, 2)}) + # full=100, half=50, untested missing=0 -> (100+50+0)/3 = 50.0 + assert cs.project_pct(report, src, root) == 50.0 + + +def test_project_pct_no_missing(project): + root, src = project + for name in ("full.py", "half.py"): + (src / name).write_text("x = 1\n") + report = write_report(root, {"src/full.py": (2, 2), "src/half.py": (1, 2)}) + assert cs.project_pct(report, src, root) == 75.0 + + +# --- end-to-end text ------------------------------------------------------- + +def test_summary_text_reports_missing_and_project_number(project): + root, src = project + (src / "a.py").write_text("x = 1\n") + (src / "orphan.py").write_text("y = 2\n") + report = write_report(root, {"src/a.py": (1, 2)}) + text = cs.summary_text(report, src, root) + assert "orphan.py" in text + assert "0%" in text + # a.py = 50%, orphan missing = 0% -> 25.0% + assert "25.0%" in text |
