orchestrata_ultimate.refactor.codemod.analysis.test_coverage
Reference › orchestrata_ultimate › refactor › codemod › analysis › test_coverage
TestCoverageMapper maps production functions to the tests that cover them, for the whole graph or one module. analyze(target) returns a TestCoverageMap (see _types) and raises ValueError when the target module is not in the graph. A function counts as covered when the graph has a TESTED_BY edge for it, read in both directions (_edge_tests), or when a test is found by the name heuristic test_<name> (_heuristic_matches); the heuristic_only field lists the functions covered only by name. _is_test_path and _is_test separate test code from production code. The class sets __test__ to false so pytest does not collect it despite its name. Read-only; imported by the analysis facade.
At a glance
| Source | src/orchestrata_ultimate/refactor/codemod/analysis/test_coverage.py |
| Lines of code | 150 |
| Classes | 1 |
| Functions | 2 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.refactor.codemod._cig, orchestrata_ultimate.refactor.codemod._types, orchestrata_ultimate.refactor.codemod.analysis.protocol |
| Imported by | orchestrata_ultimate.refactor.codemod.analysis |
| Tests that import it | none |
Class diagram
Dependency diagram
API
Classes
TestCoverageMapper
Decorators: traces(spec='110', story='US-110-006', reqs=['REQ-110-031', 'REQ-110-032']) · Line 54
Map production functions to the tests that cover them.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(cig: CodeIntelligenceGraph) -> None |
|
analyze |
analyze(target: str | None = None) -> TestCoverageMap |
Coverage of every production function, or of one module's functions. |
_edge_tests |
_edge_tests(function_id: int) -> frozenset[int] |
Tests linked to function_id by a TESTED_BY edge, either direction. |
_heuristic_matches |
_heuristic_matches() -> dict[int, frozenset[int]] |
Function id -> ids of the test_<name> tests matching it by name. |
Functions
| Function | Signature | Summary |
|---|---|---|
_is_test_path |
_is_test_path(path: str) -> bool |
|
_is_test |
_is_test(node: CIGNode) -> bool |
A Test node, or a test_* callable the builder typed Function/Method. |
Source
"""TestCoverageMapper — which functions have tests, by TESTED_BY edge or name.
GAP-3: the CIG links a Test node to the key ``function:<TEST FILE path>:<name>``
so a TESTED_BY edge resolves only when the tested function lives in the same
file as its test, and ``CodeIntelligenceGraph.affected_tests`` under-reports.
This analyzer therefore adds the REQ-110-031 name heuristic itself: a test
named ``test_<x>`` covers every function or method named ``<x>``, across all
files. Functions covered only by the heuristic are listed in
``TestCoverageMap.heuristic_only``.
``affected_tests`` also reads the TESTED_BY edges in the opposite direction
to the one the builder writes (see ``_edge_tests``); the mapper reads both.
"""
from __future__ import annotations
from pathlib import PurePosixPath
from typing import TYPE_CHECKING
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.refactor.codemod._types import CIGNode, TestCoverageMap
from orchestrata_ultimate.refactor.codemod.analysis.protocol import resolve_node_id
if TYPE_CHECKING:
from orchestrata_ultimate.refactor.codemod._cig import CodeIntelligenceGraph
__all__ = ["TestCoverageMapper"]
_CALLABLE_TYPES = ("Function", "Method")
def _is_test_path(path: str) -> bool:
pure = PurePosixPath(path)
return "tests" in pure.parts or pure.name.startswith("test_")
def _is_test(node: CIGNode) -> bool:
"""A Test node, or a ``test_*`` callable the builder typed Function/Method.
The CIG builder only types a callable ``Test`` for methods under
``tests/`` (its condition binds ``and`` before ``or``), so a top-level
``tests/test_x.py::test_foo`` stays ``Function``; the path rule covers it.
"""
if node.node_type == "Test":
return True
return (
node.node_type in _CALLABLE_TYPES
and node.name.startswith("test_")
and _is_test_path(node.path)
)
@traces(spec="110", story="US-110-006", reqs=["REQ-110-031", "REQ-110-032"])
class TestCoverageMapper:
"""Map production functions to the tests that cover them."""
__test__ = False # not a pytest class despite the ``Test`` prefix
name = "test_coverage"
@traces(spec="110", story="US-110-006", reqs=["REQ-110-031", "REQ-110-032"])
def __init__(self, cig: CodeIntelligenceGraph) -> None:
self._cig = cig
@traces(spec="110", story="US-110-006", reqs=["REQ-110-031", "REQ-110-032"])
def analyze(self, target: str | None = None) -> TestCoverageMap:
"""Coverage of every production function, or of one module's functions.
Raises:
ValueError: ``target`` names no module in the graph.
"""
nodes = self._cig.nodes
path_filter: str | None = None
if target is not None:
node_id = resolve_node_id(self._cig, target)
if node_id is None:
raise ValueError(f"module '{target}' not found in CIG")
path_filter = nodes[node_id].path
candidates = sorted(
nid
for nid, node in nodes.items()
if node.node_type in _CALLABLE_TYPES
and not _is_test(node)
and (path_filter is None or node.path == path_filter)
)
# TESTED_BY edges only resolve inside the test's own file, so only
# functions sharing a path with a Test node can carry one.
test_paths = {node.path for node in nodes.values() if node.node_type == "Test"}
heuristic = self._heuristic_matches()
covered: dict[int, frozenset[int]] = {}
uncovered: set[int] = set()
heuristic_only: set[int] = set()
for fid in candidates:
path = nodes[fid].path
edge_tests = (
self._edge_tests(fid) if path in test_paths else frozenset[int]()
)
name_tests = heuristic.get(fid, frozenset())
if edge_tests or name_tests:
covered[fid] = edge_tests | name_tests
if not edge_tests:
heuristic_only.add(fid)
elif not _is_test_path(path):
# Helpers inside test files are not production targets unless
# a TESTED_BY edge says a test covers them.
uncovered.add(fid)
total = len(covered) + len(uncovered)
return TestCoverageMap(
covered=covered,
uncovered=frozenset(uncovered),
heuristic_only=frozenset(heuristic_only),
coverage_ratio=len(covered) / total if total else 0.0,
)
@traces(spec="110", story="US-110-006", reqs=["REQ-110-031"])
def _edge_tests(self, function_id: int) -> frozenset[int]:
"""Tests linked to ``function_id`` by a TESTED_BY edge, either direction.
``affected_tests`` reads TESTED_BY edges INTO the function, but the
builder emits them OUT of it (``function -> test``, from
``sink_edge_from_key``), so on a built graph it finds nothing. The
edges out of the function are reachable through the public
``dependencies``; the Test-typed ones are the TESTED_BY targets.
"""
nodes = self._cig.nodes
outgoing = {
dep
for dep in self._cig.dependencies(function_id)
if nodes[dep].node_type == "Test"
}
return self._cig.affected_tests(frozenset({function_id})) | outgoing
@traces(spec="110", story="US-110-006", reqs=["REQ-110-031"])
def _heuristic_matches(self) -> dict[int, frozenset[int]]:
"""Function id -> ids of the ``test_<name>`` tests matching it by name."""
nodes = self._cig.nodes
by_name: dict[str, list[int]] = {}
for nid, node in nodes.items():
if (
node.node_type in _CALLABLE_TYPES
and not _is_test(node)
and not _is_test_path(node.path)
):
by_name.setdefault(node.name, []).append(nid)
matches: dict[int, set[int]] = {}
for tid, node in nodes.items():
if not _is_test(node) or not node.name.startswith("test_"):
continue
for fid in by_name.get(node.name[len("test_") :], ()):
matches.setdefault(fid, set()).add(tid)
return {fid: frozenset(tids) for fid, tids in matches.items()}