Coverage for session_buddy / tools / quality_metrics.py: 100.00%

77 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-04 00:43 -0800

1"""Quality metrics extraction from crackerjack output.""" 

2 

3import re 

4import typing as t 

5from dataclasses import dataclass 

6from typing import Any 

7 

8 

9@dataclass 

10class QualityMetrics: 

11 """Structured quality metrics from crackerjack execution.""" 

12 

13 coverage_percent: float | None = None 

14 max_complexity: int | None = None 

15 complexity_violations: int = 0 

16 security_issues: int = 0 

17 tests_passed: int = 0 

18 tests_failed: int = 0 

19 type_errors: int = 0 

20 formatting_issues: int = 0 

21 

22 def to_dict(self) -> dict[str, Any]: 

23 """Convert to dictionary, excluding None and zero values.""" 

24 return { 

25 k: v 

26 for k, v in self.__dict__.items() 

27 if v is not None and (not isinstance(v, int) or v > 0) 

28 } 

29 

30 def _format_coverage(self) -> str: 

31 """Format coverage metric.""" 

32 if self.coverage_percent is None: 

33 return "" 

34 emoji = "✅" if self.coverage_percent >= 42 else "⚠️" 

35 result = f"- {emoji} Coverage: {self.coverage_percent:.1f}%" 

36 if self.coverage_percent < 42: 

37 result += " (below 42% baseline)" 

38 return result + "\n" 

39 

40 def _format_complexity(self) -> str: 

41 """Format complexity metric.""" 

42 if not self.max_complexity: 

43 return "" 

44 emoji = "✅" if self.max_complexity <= 15 else "❌" 

45 result = f"- {emoji} Max Complexity: {self.max_complexity}" 

46 if self.max_complexity > 15: 

47 result += " (exceeds limit of 15)" 

48 return result + "\n" 

49 

50 def _format_violations(self) -> str: 

51 """Format violation metrics.""" 

52 lines = [] 

53 if self.complexity_violations: 

54 plural = "s" if self.complexity_violations != 1 else "" 

55 lines.append( 

56 f"- ⚠️ Complexity Violations: {self.complexity_violations} function{plural}", 

57 ) 

58 if self.security_issues: 

59 plural = "s" if self.security_issues != 1 else "" 

60 lines.append( 

61 f"- 🔒 Security Issues: {self.security_issues} (Bandit finding{plural})", 

62 ) 

63 return "\n".join(lines) + ("\n" if lines else "") 

64 

65 def _format_tests(self) -> str: 

66 """Format test results.""" 

67 if self.tests_failed: 

68 return f"- ❌ Tests Failed: {self.tests_failed}\n" 

69 if self.tests_passed: 

70 return f"- ✅ Tests Passed: {self.tests_passed}\n" 

71 return "" 

72 

73 def _format_errors(self) -> str: 

74 """Format error metrics.""" 

75 lines = [] 

76 if self.type_errors: 

77 lines.append(f"- 📝 Type Errors: {self.type_errors}") 

78 if self.formatting_issues: 

79 lines.append(f"- ✨ Formatting Issues: {self.formatting_issues}") 

80 return "\n".join(lines) + ("\n" if lines else "") 

81 

82 def format_for_display(self) -> str: 

83 """Format metrics for user-friendly display.""" 

84 if not self.to_dict(): 

85 return "" 

86 

87 return ( 

88 "\n📈 **Quality Metrics**:\n" 

89 + self._format_coverage() 

90 + self._format_complexity() 

91 + self._format_violations() 

92 + self._format_tests() 

93 + self._format_errors() 

94 ) 

95 

96 

97class QualityMetricsExtractor: 

98 """Extract structured quality metrics from crackerjack output.""" 

99 

100 # Regex patterns for metric extraction 

101 PATTERNS: t.Final[dict[str, str]] = { 

102 "coverage": r"coverage:?\s*(\d+(?:\.\d+)?)%", 

103 "complexity": r"Complexity of (\d+) is too high", 

104 "security": r"B\d{3}:", # Bandit security codes 

105 "tests": r"(\d+) passed(?:.*?(\d+) failed)?", 

106 "type_errors": r"error:|Found (\d+) error", 

107 "formatting": r"would reformat|line too long", 

108 } 

109 

110 @classmethod 

111 def extract(cls, stdout: str, stderr: str) -> QualityMetrics: 

112 """Extract metrics from crackerjack output. 

113 

114 Args: 

115 stdout: Standard output from crackerjack execution 

116 stderr: Standard error from crackerjack execution 

117 

118 Returns: 

119 QualityMetrics object with extracted values 

120 

121 """ 

122 metrics = QualityMetrics() 

123 combined = stdout + stderr 

124 

125 # Coverage 

126 if match := re.search( # REGEX OK: coverage pattern from PATTERNS dict 

127 cls.PATTERNS["coverage"], 

128 combined, 

129 ): 

130 metrics.coverage_percent = float(match.group(1)) 

131 

132 # Complexity 

133 complexity_matches = ( 

134 re.findall( # REGEX OK: complexity pattern from PATTERNS dict 

135 cls.PATTERNS["complexity"], 

136 stderr, 

137 ) 

138 ) 

139 if complexity_matches: 

140 complexities = [int(c) for c in complexity_matches] 

141 metrics.max_complexity = max(complexities) 

142 metrics.complexity_violations = len(complexities) 

143 

144 # Security (Bandit codes like B108, B603, etc.) 

145 metrics.security_issues = len( 

146 re.findall( # REGEX OK: security pattern from PATTERNS dict 

147 cls.PATTERNS["security"], 

148 stderr, 

149 ), 

150 ) 

151 

152 # Tests 

153 if match := re.search( # REGEX OK: test results pattern from PATTERNS dict 

154 cls.PATTERNS["tests"], 

155 stdout, 

156 ): 

157 metrics.tests_passed = int(match.group(1)) 

158 if match.group(2): 

159 metrics.tests_failed = int(match.group(2)) 

160 

161 # Type errors 

162 type_error_match = re.search( # REGEX OK: type error count extraction 

163 r"Found (\d+) error", 

164 stderr, 

165 ) 

166 if type_error_match: 

167 metrics.type_errors = int(type_error_match.group(1)) 

168 else: 

169 # Count error lines 

170 metrics.type_errors = len( 

171 [line for line in stderr.split("\n") if "error:" in line.lower()], 

172 ) 

173 

174 # Formatting 

175 metrics.formatting_issues = len( 

176 re.findall( # REGEX OK: formatting pattern from PATTERNS dict 

177 cls.PATTERNS["formatting"], 

178 combined, 

179 ), 

180 ) 

181 

182 return metrics