From 456837f600c705fc369234b043aff6ef50c3b744 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 2 Nov 2026 03:24:28 +0800 Subject: [PATCH] test: align coverage scope with platform source layout --- .github/workflows/ci.yml | 17 +++ pyproject.toml | 5 +- tests/test_coverage_source_layout.py | 164 +++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 tests/test_coverage_source_layout.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d29b521..7302cbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,5 +148,22 @@ jobs: - name: Run tests run: uv run --no-sync python -m pytest -q + - name: Measure coverage against the production source layout + run: | + set -euo pipefail + uv run --no-sync python -m coverage run -m pytest -q + uv run --no-sync python -m coverage report --show-missing + uv run --no-sync python - <<'PY' + from coverage import CoverageData + + data = CoverageData(basename=".coverage") + data.read() + files = sorted(data.measured_files()) + if not files: + raise SystemExit("coverage measured no files") + for path in files: + print(path) + PY + - name: Build package run: uv run --no-sync python -m build diff --git a/pyproject.toml b/pyproject.toml index 4bf1ac0..60c8a3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,11 +74,12 @@ ignore = ["E701", "E702", "E741", "F841", "F821", "B008", "F401", "F601", "E402" [tool.coverage.run] branch = true -source = ["src"] -omit = ["tests/*", "*/external/*"] +source = ["."] +omit = ["tests/*", "*/external/*", ".venv/*", "build/*", "dist/*"] [tool.coverage.report] show_missing = true +include_namespace_packages = true [tool.uv] override-dependencies = [ diff --git a/tests/test_coverage_source_layout.py b/tests/test_coverage_source_layout.py new file mode 100644 index 0000000..15f1c7d --- /dev/null +++ b/tests/test_coverage_source_layout.py @@ -0,0 +1,164 @@ +"""Verify the coverage source layout measures shipped production code. + +The denominator is the repository tree minus explicitly excluded categories. +A hermetic synthetic tree proves that unexecuted in-scope lines and files +remain in the denominator while excluded test/external/build code cannot +inflate the measurement. +""" + +from __future__ import annotations + +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import tomllib +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + +SYNTHETIC_MODULE = ( + "def executed():\n" + " return 1\n" + "\n" + "\n" + "def never_executed():\n" + " value = 1\n" + " return value\n" +) + +CANONICAL_OMIT_CATEGORIES = ("tests/", "external/", ".venv/", "build/", "dist/") + + +def pyproject(): + return tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + + +def coverage_run_config(): + return pyproject()["tool"]["coverage"]["run"] + + +def coverage_report_config(): + return pyproject()["tool"]["coverage"]["report"] + + +def declared_production_sources(): + data = pyproject() + modules = data["tool"]["setuptools"]["py-modules"] + include = data["tool"]["setuptools"]["packages"]["find"]["include"] + packages = sorted({p.name for pattern in include for p in ROOT.glob(pattern) if p.is_dir()}) + return modules, packages + + +def matches_any_omit(path, omit_patterns): + for pattern in omit_patterns: + regex = "".join(".*" if char == "*" else re.escape(char) for char in pattern) + if re.fullmatch(regex, path): + return True + return False + + +class CoverageSourceLayoutTests(unittest.TestCase): + def test_source_is_repository_root_with_explicit_category_exclusions(self): + run = coverage_run_config() + report = coverage_report_config() + self.assertEqual(run["source"], ["."]) + self.assertTrue(report.get("include_namespace_packages")) + self.assertFalse((ROOT / "src").exists()) + self.assertNotIn("fail_under", str(pyproject()["tool"]["coverage"])) + omit = " ".join(run["omit"]) + for category in CANONICAL_OMIT_CATEGORIES: + self.assertIn(category, omit) + + def test_declared_production_paths_are_measured(self): + modules, packages = declared_production_sources() + omit = coverage_run_config()["omit"] + for module in modules: + relative = f"{module}.py" + self.assertTrue((ROOT / relative).exists(), relative) + self.assertFalse(matches_any_omit(relative, omit), relative) + for package in packages: + self.assertFalse(matches_any_omit(package, omit), package) + + def test_unexecuted_in_scope_files_remain_in_denominator(self): + modules, packages = declared_production_sources() + run = coverage_run_config() + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + for module in modules: + (root / f"{module}.py").write_text(SYNTHETIC_MODULE, encoding="utf-8") + for package in packages: + (root / package).mkdir() + (root / packages[0] / "synthetic_service.py").write_text( + SYNTHETIC_MODULE, encoding="utf-8" + ) + excluded = "def never_executed():\n value = 1\n return value\n" + for directory in ("tests", "external", ".venv", "build", "dist"): + (root / directory).mkdir(parents=True) + (root / directory / "excluded_module.py").write_text(excluded, encoding="utf-8") + imported = modules[:-1] + imports = [ + f"importlib.import_module({module!r}).executed()" for module in imported + ] + (root / "runner.py").write_text( + "import importlib\n" + "\n".join(imports) + "\nprint('done')\n", + encoding="utf-8", + ) + (root / ".coveragerc").write_text( + "[run]\n" + "branch = true\n" + "source =\n" + + "".join(f" {entry}\n" for entry in run["source"]) + + "omit =\n" + + "".join(f" {pattern}\n" for pattern in run["omit"]) + + "[report]\n" + f"include_namespace_packages = {coverage_report_config().get('include_namespace_packages', False)}\n", + encoding="utf-8", + ) + collected = subprocess.run( + [sys.executable, "-m", "coverage", "run", "runner.py"], + cwd=root, + capture_output=True, + text=True, + timeout=60, + check=False, + ) + self.assertEqual(collected.returncode, 0, collected.stderr) + report = subprocess.run( + [sys.executable, "-m", "coverage", "report", "--show-missing"], + cwd=root, + capture_output=True, + text=True, + timeout=60, + check=False, + ) + self.assertEqual(report.returncode, 0, report.stderr) + output = report.stdout + self.assertNotIn("No data to report", output) + lines = { + candidate.split()[0]: candidate + for candidate in output.splitlines() + if candidate and not candidate.startswith(("Name", "-", "TOTAL")) + } + for module in imported: + self.assertIn(module + ".py", lines) + self.assertIn("6-7", lines[module + ".py"]) + never_imported = modules[-1] + self.assertIn(never_imported + ".py", lines) + self.assertIn("0%", lines[never_imported + ".py"]) + service_path = f"{packages[0]}/synthetic_service.py" + self.assertIn(service_path, lines) + self.assertIn("0%", lines[service_path]) + for directory in ("tests", "external", ".venv", "build", "dist"): + self.assertNotIn(f"{directory}/excluded_module.py", output, directory) + + def test_ci_measurement_step_runs_the_suite_instrumented(self): + workflow = (ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8") + self.assertIn("coverage run -m pytest", workflow) + self.assertIn("coverage report --show-missing", workflow) + + +if __name__ == "__main__": + unittest.main()