Coverage for lintro / tools / implementations / pytest / coverage_processor.py: 24%
68 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Coverage report processing for pytest.
3This module provides functions for parsing and extracting coverage reports.
4"""
6from __future__ import annotations
8import re
9from typing import Any
12def parse_coverage_summary(raw_output: str) -> dict[str, Any] | None:
13 """Parse coverage summary statistics from raw pytest output.
15 Extracts the TOTAL line from coverage output to get summary stats.
17 Args:
18 raw_output: Raw output from pytest containing coverage report.
20 Returns:
21 dict | None: Coverage summary with keys:
22 - total_stmts: Total number of statements
23 - missing_stmts: Number of missing statements
24 - covered_stmts: Number of covered statements
25 - coverage_pct: Coverage percentage
26 - files_count: Number of files in coverage report
27 Returns None if no coverage data found.
28 """
29 if not raw_output:
30 return None
32 lines = raw_output.split("\n")
34 # Find the TOTAL line which contains summary stats
35 # Format: "TOTAL 20731 12738 39%"
36 total_line = None
37 files_count = 0
38 in_coverage_section = False
40 for line in lines:
41 stripped = line.strip()
42 # Detect start of coverage table (Name header line)
43 if stripped.startswith("Name") and "Stmts" in stripped:
44 in_coverage_section = True
45 continue
47 # Count files in coverage report (lines with coverage data)
48 if in_coverage_section and stripped and not stripped.startswith("-"):
49 if stripped.startswith("TOTAL"):
50 total_line = stripped
51 break
52 # Count file lines (have .py extension or look like module paths)
53 if "%" in stripped and not stripped.startswith("TOTAL"):
54 files_count += 1
56 if not total_line:
57 return None
59 # Parse TOTAL line: "TOTAL 20731 12738 39%" or "TOTAL 20731 12738 39.5%"
60 # Split by whitespace and extract values (support decimal percentages)
61 match = re.search(r"TOTAL\s+(\d+)\s+(\d+)\s+(\d+(?:\.\d+)?)%", total_line)
62 if not match:
63 return None
65 total_stmts = int(match.group(1))
66 missing_stmts = int(match.group(2))
67 coverage_pct = float(match.group(3))
69 return {
70 "total_stmts": total_stmts,
71 "missing_stmts": missing_stmts,
72 "covered_stmts": total_stmts - missing_stmts,
73 "coverage_pct": coverage_pct,
74 "files_count": files_count,
75 }
78def extract_coverage_report(raw_output: str) -> str | None:
79 """Extract coverage report section from raw pytest output.
81 Args:
82 raw_output: Raw output from pytest.
84 Returns:
85 str | None: Coverage report section if found, None otherwise.
86 """
87 if not raw_output:
88 return None
90 # Look for coverage report markers
91 # pytest-cov outputs coverage in a section starting with a header line
92 coverage_markers = [
93 "---------- coverage:",
94 "----------- coverage:",
95 "coverage:",
96 "Name ", # Start of coverage table
97 "TOTAL ", # Coverage summary line
98 ]
100 lines = raw_output.split("\n")
101 coverage_start = None
102 coverage_end = None
104 for i, line in enumerate(lines):
105 # Find the start of coverage section
106 if coverage_start is None:
107 for marker in coverage_markers:
108 if marker in line:
109 # Go back to find the header line with dashes
110 start = i
111 for j in range(max(0, i - 3), i + 1):
112 if "coverage" in lines[j].lower() or lines[j].startswith("---"):
113 start = j
114 break
115 coverage_start = start
116 break
117 elif coverage_start is not None:
118 # Find the end of coverage section (empty line or new section)
119 if line.strip() == "" and i > coverage_start + 2:
120 # Check if next non-empty line is not part of coverage
121 for j in range(i + 1, min(i + 3, len(lines))):
122 if lines[j].strip():
123 if not any(
124 m in lines[j] for m in ["Name", "TOTAL", "---", "Missing"]
125 ):
126 coverage_end = i
127 break
128 break
129 if coverage_end:
130 break
131 elif line.startswith("===") and coverage_start is not None:
132 coverage_end = i
133 break
135 if coverage_start is not None:
136 if coverage_end is None:
137 coverage_end = len(lines)
138 coverage_section = "\n".join(lines[coverage_start:coverage_end]).strip()
139 if coverage_section:
140 return coverage_section
142 return None