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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
1"""Quality metrics extraction from crackerjack output."""
3import re
4import typing as t
5from dataclasses import dataclass
6from typing import Any
9@dataclass
10class QualityMetrics:
11 """Structured quality metrics from crackerjack execution."""
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
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 }
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"
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"
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 "")
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 ""
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 "")
82 def format_for_display(self) -> str:
83 """Format metrics for user-friendly display."""
84 if not self.to_dict():
85 return ""
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 )
97class QualityMetricsExtractor:
98 """Extract structured quality metrics from crackerjack output."""
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 }
110 @classmethod
111 def extract(cls, stdout: str, stderr: str) -> QualityMetrics:
112 """Extract metrics from crackerjack output.
114 Args:
115 stdout: Standard output from crackerjack execution
116 stderr: Standard error from crackerjack execution
118 Returns:
119 QualityMetrics object with extracted values
121 """
122 metrics = QualityMetrics()
123 combined = stdout + stderr
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))
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)
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 )
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))
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 )
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 )
182 return metrics