aboutsummaryrefslogtreecommitdiff
path: root/languages/python/tests
diff options
context:
space:
mode:
Diffstat (limited to 'languages/python/tests')
-rw-r--r--languages/python/tests/test_coverage_summary.py156
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