orchestrata_tools.test_analyzer.coverage

Reference › orchestrata_tools › test_analyzer › coverage

CoverageAnalyzer turns test-to-method matches into coverage figures and a list of gaps. It is built from a TestMatcher (matcher) and an ASTIndex, and returns the CoverageReport defined in test_models. The package CLI __main__ uses it. No test module imports it directly.

analyze counts only public methods. Method coverage is the share of public methods that the matcher found tested. Class coverage is, per class, the share of its public methods with a matched name. Module coverage counts matched public, non-property methods, excluding methods of protocol classes. Each untested method becomes a CoverageGap with a suggested test name made from the lower-cased class name and the method name, or just the method name when it has no class. The result holds the percentages, counts, gaps and per-module figures.

At a glance

Source src/orchestrata_tools/test_analyzer/coverage.py
Lines of code 102
Classes 1
Functions 0
Imports (internal) orchestrata_tools.models.ast_models, orchestrata_tools.models.test_models, orchestrata_tools.parser.indexer, orchestrata_tools.test_analyzer.matcher
Imported by orchestrata_tools, orchestrata_tools.test_analyzer
Tests that import it none

Class diagram

Class diagram orchestrata_tools.test_analyzer.coverage

Dependency diagram

Dependency diagram orchestrata_tools.test_analyzer.coverage

API

Classes

CoverageAnalyzer

Line 16

Analyze test coverage for a source codebase.

Method Signature Summary
__init__ __init__(matcher: TestMatcher, source_index: ASTIndex)
analyze analyze() -> CoverageReport Run full coverage analysis and return a report.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_tools/test_analyzer/coverage.py

"""Coverage analysis: compute metrics and identify gaps."""

from __future__ import annotations

from collections import defaultdict
from typing import TYPE_CHECKING

from orchestrata_tools.models.test_models import CoverageGap, CoverageReport

if TYPE_CHECKING:
    from orchestrata_tools.models.ast_models import MethodInfo
    from orchestrata_tools.parser.indexer import ASTIndex
    from orchestrata_tools.test_analyzer.matcher import TestMatcher


class CoverageAnalyzer:
    """Analyze test coverage for a source codebase."""

    def __init__(self, matcher: TestMatcher, source_index: ASTIndex):
        self.matcher = matcher
        self.source_index = source_index

    def analyze(self) -> CoverageReport:
        """Run full coverage analysis and return a report."""
        matches = self.matcher.find_matches(self.source_index)
        all_tested_keys = self.matcher.find_tested_methods(self.source_index)
        all_public = self.source_index.public_methods()
        untested = self.matcher.find_untested_methods(self.source_index)

        total = len(all_public)
        # Count only public tested methods for the percentage
        tested_keys = {
            f"{m.module_path}.{m.class_name}.{m.name}"
            for m in all_public
            if f"{m.module_path}.{m.class_name}.{m.name}" in all_tested_keys
        }
        tested_count = len(tested_keys)
        pct = (tested_count / total * 100) if total else 0.0

        # Class-level coverage
        class_methods: dict[str, list[MethodInfo]] = defaultdict(list)
        for m in all_public:
            class_methods[m.class_name].append(m)
        class_tested: dict[str, set[str]] = defaultdict(set)
        for match in matches:
            class_tested[match.method.class_name].add(match.method.name)
        class_coverage = {}
        for cls_name, methods in class_methods.items():
            tc = len(class_tested[cls_name])
            class_coverage[cls_name] = (tc / len(methods) * 100) if methods else 0.0

        # Module-level coverage (public methods only)
        module_methods: dict[str, list[MethodInfo]] = defaultdict(list)
        for m in all_public:
            module_methods[m.module_path].append(m)
        module_tested: dict[str, set[str]] = defaultdict(set)
        for match in matches:
            # Only count if the matched method is public
            if match.method.is_public and not match.method.is_property:
                cls = self.source_index.find_class(match.method.class_name)
                if cls is None or not cls.is_protocol:
                    module_tested[match.method.module_path].add(
                        match.method.qualified_name
                    )
        module_coverage = {}
        for mod_path, methods in module_methods.items():
            tc = len(module_tested[mod_path])
            module_coverage[mod_path] = (tc / len(methods) * 100) if methods else 0.0

        # Build gaps
        gaps = []
        for method in untested:
            suggested = f"test_{method.name}"
            if method.class_name:
                suggested = f"test_{method.class_name.lower()}_{method.name}"
            gaps.append(
                CoverageGap(
                    method=method,
                    module_path=method.module_path,
                    class_name=method.class_name,
                    suggested_test_name=suggested,
                )
            )

        # Class coverage pct (classes with any methods)
        classes_with_methods = [c for c in class_methods if class_methods[c]]
        classes_covered = sum(1 for c in classes_with_methods if class_tested[c])
        class_pct = (
            classes_covered / len(classes_with_methods) * 100
            if classes_with_methods
            else 0.0
        )

        return CoverageReport(
            total_methods=total,
            tested_methods=tested_count,
            untested_methods=total - tested_count,
            method_coverage_pct=round(pct, 1),
            class_coverage_pct=round(class_pct, 1),
            gaps=gaps,
            module_coverage=module_coverage,
        )