8Run pytest under coverage.py and map executed statements to function spans.
12- Execute subproject suites in process for witnesses or in a subprocess for probes.
13- Preserve suite return codes and both output streams.
14- Mark functions in unexecuted files as zero-coverage ``never-imported`` records.
15- Intersect coverage.py statement data with the engine's pinned source spans.
22 L3 - coverage mapping details
27- Third-party imports remain lazy so :mod:`oct.complexity` stays zero-dependency.
28- Missing coverage.py produces an explicit error rather than an estimate.
29- Failed suites retain their output for UNMEASURED probe reporting.
33- coverage (not imported; optional statement measurement and data reading, loaded by name through importlib and run as python -m coverage)
34- pytest (not imported; test runner, loaded by name for in-process witnesses and run under coverage in probes)
35- oct.complexity.engine (function spans)
41from __future__
import annotations
49from dataclasses
import asdict, dataclass
50from pathlib
import Path
51from typing
import Any, Sequence
57 """Raised when a coverage-backed operation lacks coverage.py."""
60@dataclass(frozen=
True, slots=
True)
62 """Captured outcome of one pytest suite executed under coverage.py."""
66 command: tuple[str, ...]
72 """Return stdout and stderr without discarding either stream."""
73 parts = [part.rstrip()
for part
in (self.stdout, self.stderr)
if part]
74 return "\n".join(parts)
77@dataclass(frozen=True, slots=True)
79 """A P1 complexity record enriched with pinned P2 coverage fields."""
88 covered_statements: int
91 def to_dict(self) -> dict[str, str | int | float]:
92 """Return every P1/P2 field in a JSON-ready dictionary."""
97 """Import coverage.py lazily and normalize the missing-extra error."""
99 return importlib.import_module(
"coverage")
100 except ImportError
as exc:
102 "coverage.py is not installed; install oct[metrics] to map coverage"
107 """Import pytest lazily for the in-process witness runner."""
109 return importlib.import_module(
"pytest")
110 except ImportError
as exc:
112 "pytest is not installed; install oct[test] to run a suite"
117 project_root: str | Path,
118 pytest_args: Sequence[str] = (
"tests",),
119 data_file: str | Path =
".coverage",
120 source: Sequence[str | Path] = (),
122 in_process: bool =
False,
123 python_executable: str | Path |
None =
None,
124 env: dict[str, str] |
None =
None,
126 """Execute a subproject's pytest suite and retain its coverage data.
128 Production probes use the subprocess path so each subproject has clean
129 import state. The in-process path exists for worktree tests, where the
130 repository's editable install points at MAIN and subprocess assertions
131 would otherwise verify the wrong source tree.
133 root = Path(project_root).resolve()
134 resolved_data = Path(data_file)
135 if not resolved_data.is_absolute():
136 resolved_data = root / resolved_data
137 resolved_data = resolved_data.resolve()
138 resolved_data.parent.mkdir(parents=
True, exist_ok=
True)
139 source_paths = tuple(str(Path(item).resolve())
for item
in source)
152 python_executable=python_executable,
159 pytest_args: tuple[str, ...],
161 source: tuple[str, ...],
163 """Run pytest under a Coverage instance in the current interpreter."""
166 coverage = coverage_module.Coverage(
167 data_file=str(data_file),
168 source=list(source)
or None,
170 stdout = io.StringIO()
171 stderr = io.StringIO()
173 sys.path.insert(0, str(project_root))
174 command = (
"pytest", *pytest_args)
177 os.chdir(project_root)
179 with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
180 returncode = int(pytest_module.main(list(pytest_args)))
187 returncode=returncode,
188 data_file=str(data_file),
190 stdout=stdout.getvalue(),
191 stderr=stderr.getvalue(),
196 """Remove the first matching value without disturbing prior path state."""
205 pytest_args: tuple[str, ...],
207 source: tuple[str, ...],
209 python_executable: str | Path |
None,
210 env: dict[str, str] |
None,
212 """Run pytest under the coverage CLI in an isolated child process."""
215 str(python_executable
or sys.executable),
219 f
"--data-file={data_file}",
222 command.append(f
"--source={','.join(source)}")
223 command.extend([
"-m",
"pytest", *pytest_args])
224 completed = subprocess.run(
233 returncode=completed.returncode,
234 data_file=str(data_file),
235 command=tuple(command),
236 stdout=completed.stdout,
237 stderr=completed.stderr,
242 records: Sequence[FunctionComplexity],
243 data_file: str | Path,
244) -> list[CoveredFunction]:
245 """Intersect coverage.py statements with each function's source span."""
247 resolved_data = Path(data_file).resolve()
248 if not resolved_data.is_file():
249 raise FileNotFoundError(f
"coverage data file not found: {resolved_data}")
250 coverage = coverage_module.Coverage(data_file=str(resolved_data))
254 for filename
in coverage.get_data().measured_files()
257 _map_one(coverage, measured_files, record)
258 for record
in records
264 measured_files: dict[str, str],
265 record: FunctionComplexity,
267 """Map one function record, preserving unseen files as zero coverage."""
270 set(coverage.get_data().lines(measured_name)
or ())
271 if measured_name
is not None
274 status =
"measured" if executed
else "never-imported"
278 for line
in statements
279 if record.lineno <= line <= record.end_lineno
281 if status ==
"never-imported":
285 covered_count = len(span_statements & executed)
287 covered_count / len(span_statements)
293 qualname=record.qualname,
294 lineno=record.lineno,
295 end_lineno=record.end_lineno,
296 complexity=record.complexity,
298 coverage_status=status,
299 covered_statements=covered_count,
300 statement_count=len(span_statements),
305 """Return executable statement lines using coverage.py's own parser."""
306 _filename, statements, _excluded, _missing, _branches = coverage.analysis2(
309 return set(statements)
313 """Normalize measured filenames for case-insensitive Windows matching."""
314 return os.path.normcase(str(Path(filename).resolve()))
dict[str, str|int|float] to_dict(self)
CoveredFunction _map_one(Any coverage, dict[str, str] measured_files, FunctionComplexity record)
CoverageRun _run_pytest_subprocess(Path project_root, tuple[str,...] pytest_args, Path data_file, tuple[str,...] source, *, str|Path|None python_executable, dict[str, str]|None env)
str _normalized_path(str|Path filename)
None _remove_first(list[str] items, str value)
set[int] _statement_lines(Any coverage, str filename)
list[CoveredFunction] map_coverage(Sequence[FunctionComplexity] records, str|Path data_file)
CoverageRun _run_pytest_in_process(Path project_root, tuple[str,...] pytest_args, Path data_file, tuple[str,...] source)
CoverageRun run_pytest_with_coverage(str|Path project_root, Sequence[str] pytest_args=("tests",), str|Path data_file=".coverage", Sequence[str|Path] source=(), *, bool in_process=False, str|Path|None python_executable=None, dict[str, str]|None env=None)