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

1"""Coverage report processing for pytest. 

2 

3This module provides functions for parsing and extracting coverage reports. 

4""" 

5 

6from __future__ import annotations 

7 

8import re 

9from typing import Any 

10 

11 

12def parse_coverage_summary(raw_output: str) -> dict[str, Any] | None: 

13 """Parse coverage summary statistics from raw pytest output. 

14 

15 Extracts the TOTAL line from coverage output to get summary stats. 

16 

17 Args: 

18 raw_output: Raw output from pytest containing coverage report. 

19 

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 

31 

32 lines = raw_output.split("\n") 

33 

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 

39 

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 

46 

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 

55 

56 if not total_line: 

57 return None 

58 

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 

64 

65 total_stmts = int(match.group(1)) 

66 missing_stmts = int(match.group(2)) 

67 coverage_pct = float(match.group(3)) 

68 

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 } 

76 

77 

78def extract_coverage_report(raw_output: str) -> str | None: 

79 """Extract coverage report section from raw pytest output. 

80 

81 Args: 

82 raw_output: Raw output from pytest. 

83 

84 Returns: 

85 str | None: Coverage report section if found, None otherwise. 

86 """ 

87 if not raw_output: 

88 return None 

89 

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 ] 

99 

100 lines = raw_output.split("\n") 

101 coverage_start = None 

102 coverage_end = None 

103 

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 

134 

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 

141 

142 return None