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

Class diagram orchestrata_ultimate.refactor.codemod.analysis.test_coverage

Dependency diagram

Dependency diagram orchestrata_ultimate.refactor.codemod.analysis.test_coverage

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_&lt;name&gt; 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

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/refactor/codemod/analysis/test_coverage.py

"""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()}