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
Dependency diagram
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
"""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,
)