Skip to content

Commit 23b4cbe

Browse files
authored
Merge pull request #10 from JustAGhosT/tembo/refactor-scripts-tools-structure
Improve Scripts and Tools Structure
2 parents 6c3b9f4 + bc06bd8 commit 23b4cbe

19 files changed

Lines changed: 1137 additions & 865 deletions

scripts/check-coverage.ps1

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,12 @@
11
# Check test coverage and enforce quality gates
2+
# This script delegates to the unified Python coverage utility
23

34
param(
45
[int]$CoverageThreshold = 70
56
)
67

78
$ErrorActionPreference = 'Stop'
89

9-
$CoverageFile = "coverage.xml"
10-
11-
Write-Host "========================================" -ForegroundColor Cyan
12-
Write-Host "CodeFlow Engine - Coverage Check" -ForegroundColor Cyan
13-
Write-Host "========================================" -ForegroundColor Cyan
14-
Write-Host ""
15-
16-
# Check if coverage file exists
17-
if (-not (Test-Path $CoverageFile)) {
18-
Write-Host "⚠️ Coverage file not found. Running tests with coverage..." -ForegroundColor Yellow
19-
poetry run pytest --cov=codeflow_engine --cov-report=xml --cov-report=term
20-
}
21-
22-
# Get current coverage percentage
23-
$CoverageOutput = poetry run coverage report | Select-String "TOTAL"
24-
$Coverage = [regex]::Match($CoverageOutput, '(\d+(?:\.\d+)?)%').Groups[1].Value
25-
$CoverageValue = [double]$Coverage
26-
27-
Write-Host "Current coverage: ${Coverage}%"
28-
Write-Host "Target coverage: ${CoverageThreshold}%"
29-
Write-Host ""
30-
31-
# Check if coverage meets threshold
32-
if ($CoverageValue -lt $CoverageThreshold) {
33-
Write-Host "❌ Coverage ${Coverage}% is below threshold of ${CoverageThreshold}%" -ForegroundColor Red
34-
Write-Host ""
35-
Write-Host "Coverage by module:" -ForegroundColor Yellow
36-
poetry run coverage report --show-missing | Select-String "codeflow_engine" | Select-Object -First 20
37-
exit 1
38-
}
39-
else {
40-
Write-Host "✅ Coverage ${Coverage}% meets threshold of ${CoverageThreshold}%" -ForegroundColor Green
41-
exit 0
42-
}
10+
# Use the unified Python coverage utility
11+
python -m scripts.coverage.runner check --threshold $CoverageThreshold
4312

scripts/check-coverage.sh

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,11 @@
11
#!/bin/bash
22
# Check test coverage and enforce quality gates
3+
# This script delegates to the unified Python coverage utility
34

45
set -e
56

67
COVERAGE_THRESHOLD=${1:-70}
7-
COVERAGE_FILE="coverage.xml"
88

9-
echo "=========================================="
10-
echo "CodeFlow Engine - Coverage Check"
11-
echo "=========================================="
12-
echo ""
13-
14-
# Check if coverage file exists
15-
if [ ! -f "$COVERAGE_FILE" ]; then
16-
echo "⚠️ Coverage file not found. Running tests with coverage..."
17-
poetry run pytest --cov=codeflow_engine --cov-report=xml --cov-report=term
18-
fi
19-
20-
# Get current coverage percentage
21-
COVERAGE=$(poetry run coverage report | grep TOTAL | awk '{print $NF}' | sed 's/%//')
22-
23-
echo "Current coverage: ${COVERAGE}%"
24-
echo "Target coverage: ${COVERAGE_THRESHOLD}%"
25-
echo ""
26-
27-
# Check if coverage meets threshold
28-
if (( $(echo "$COVERAGE < $COVERAGE_THRESHOLD" | bc -l) )); then
29-
echo "❌ Coverage ${COVERAGE}% is below threshold of ${COVERAGE_THRESHOLD}%"
30-
echo ""
31-
echo "Coverage by module:"
32-
poetry run coverage report --show-missing | grep -E "codeflow_engine" | head -20
33-
exit 1
34-
else
35-
echo "✅ Coverage ${COVERAGE}% meets threshold of ${COVERAGE_THRESHOLD}%"
36-
exit 0
37-
fi
9+
# Use the unified Python coverage utility
10+
python -m scripts.coverage.runner check --threshold "$COVERAGE_THRESHOLD"
3811

scripts/coverage/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Coverage utilities for test quality gates."""
2+
3+
from scripts.coverage.runner import CoverageRunner
4+
5+
__all__ = ["CoverageRunner"]

scripts/coverage/runner.py

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
"""Unified coverage runner for test quality gates.
2+
3+
This module provides a cross-platform Python implementation for running
4+
test coverage, replacing the duplicated bash and PowerShell scripts.
5+
"""
6+
7+
import argparse
8+
import re
9+
import subprocess
10+
import sys
11+
from dataclasses import dataclass
12+
from pathlib import Path
13+
14+
15+
@dataclass
16+
class CoverageResult:
17+
"""Result of a coverage run."""
18+
19+
total_coverage: float
20+
by_module: dict[str, float]
21+
low_coverage_files: list[tuple[str, float]]
22+
no_coverage_files: list[str]
23+
success: bool
24+
error_message: str = ""
25+
26+
27+
class CoverageRunner:
28+
"""Unified coverage runner for running tests and generating reports."""
29+
30+
def __init__(
31+
self,
32+
module_name: str = "codeflow_engine",
33+
threshold: float = 70.0,
34+
low_coverage_threshold: float = 50.0,
35+
):
36+
"""Initialize the coverage runner.
37+
38+
Args:
39+
module_name: The Python module to measure coverage for
40+
threshold: Minimum coverage percentage required to pass
41+
low_coverage_threshold: Threshold below which files are flagged
42+
"""
43+
self.module_name = module_name
44+
self.threshold = threshold
45+
self.low_coverage_threshold = low_coverage_threshold
46+
47+
def run_tests(self, generate_html: bool = True, generate_xml: bool = True) -> bool:
48+
"""Run pytest with coverage.
49+
50+
Args:
51+
generate_html: Whether to generate HTML report
52+
generate_xml: Whether to generate XML report
53+
54+
Returns:
55+
True if tests passed, False otherwise
56+
"""
57+
cmd = [
58+
"poetry", "run", "pytest",
59+
f"--cov={self.module_name}",
60+
"--cov-report=term",
61+
]
62+
63+
if generate_html:
64+
cmd.append("--cov-report=html")
65+
if generate_xml:
66+
cmd.append("--cov-report=xml")
67+
68+
result = subprocess.run(cmd, capture_output=False)
69+
return result.returncode == 0
70+
71+
def get_coverage_report(self) -> str:
72+
"""Get the coverage report output."""
73+
result = subprocess.run(
74+
["poetry", "run", "coverage", "report"],
75+
capture_output=True,
76+
text=True,
77+
)
78+
return result.stdout
79+
80+
def get_coverage_report_with_missing(self) -> str:
81+
"""Get the coverage report with missing lines."""
82+
result = subprocess.run(
83+
["poetry", "run", "coverage", "report", "--show-missing"],
84+
capture_output=True,
85+
text=True,
86+
)
87+
return result.stdout
88+
89+
def parse_coverage(self, report: str) -> CoverageResult:
90+
"""Parse coverage report and extract metrics.
91+
92+
Args:
93+
report: The coverage report output
94+
95+
Returns:
96+
CoverageResult with parsed metrics
97+
"""
98+
total_coverage = 0.0
99+
by_module: dict[str, float] = {}
100+
low_coverage_files: list[tuple[str, float]] = []
101+
no_coverage_files: list[str] = []
102+
103+
lines = report.strip().split("\n")
104+
105+
for line in lines:
106+
# Parse TOTAL line
107+
if line.startswith("TOTAL"):
108+
match = re.search(r"(\d+(?:\.\d+)?)%", line)
109+
if match:
110+
total_coverage = float(match.group(1))
111+
continue
112+
113+
# Parse module lines
114+
if self.module_name in line:
115+
parts = line.split()
116+
if len(parts) >= 4:
117+
file_path = parts[0]
118+
# Extract coverage percentage
119+
match = re.search(r"(\d+(?:\.\d+)?)%", line)
120+
if match:
121+
coverage = float(match.group(1))
122+
by_module[file_path] = coverage
123+
124+
if coverage == 0:
125+
no_coverage_files.append(file_path)
126+
elif coverage < self.low_coverage_threshold:
127+
low_coverage_files.append((file_path, coverage))
128+
129+
return CoverageResult(
130+
total_coverage=total_coverage,
131+
by_module=by_module,
132+
low_coverage_files=sorted(low_coverage_files, key=lambda x: x[1]),
133+
no_coverage_files=no_coverage_files,
134+
success=total_coverage >= self.threshold,
135+
)
136+
137+
def print_header(self, title: str) -> None:
138+
"""Print a formatted header."""
139+
print("=" * 42)
140+
print(f"CodeFlow Engine - {title}")
141+
print("=" * 42)
142+
print()
143+
144+
def print_section(self, title: str) -> None:
145+
"""Print a formatted section header."""
146+
print(title)
147+
print("-" * 40)
148+
149+
def check_coverage(self) -> int:
150+
"""Check if coverage meets the threshold.
151+
152+
Returns:
153+
Exit code (0 for success, 1 for failure)
154+
"""
155+
self.print_header("Coverage Check")
156+
157+
# Check if coverage file exists
158+
if not Path("coverage.xml").exists():
159+
print("Coverage file not found. Running tests with coverage...")
160+
if not self.run_tests():
161+
print("Tests failed!")
162+
return 1
163+
print()
164+
165+
report = self.get_coverage_report()
166+
result = self.parse_coverage(report)
167+
168+
print(f"Current coverage: {result.total_coverage}%")
169+
print(f"Target coverage: {self.threshold}%")
170+
print()
171+
172+
if result.success:
173+
print(f"Coverage {result.total_coverage}% meets threshold of {self.threshold}%")
174+
return 0
175+
else:
176+
print(f"Coverage {result.total_coverage}% is below threshold of {self.threshold}%")
177+
print()
178+
self.print_section("Coverage by module:")
179+
report_with_missing = self.get_coverage_report_with_missing()
180+
for line in report_with_missing.split("\n"):
181+
if self.module_name in line:
182+
print(line)
183+
return 1
184+
185+
def measure_coverage(self) -> int:
186+
"""Measure coverage and generate detailed report.
187+
188+
Returns:
189+
Exit code (always 0)
190+
"""
191+
self.print_header("Coverage Measurement")
192+
193+
print("Running tests with coverage...")
194+
tests_passed = self.run_tests(generate_html=True, generate_xml=True)
195+
if not tests_passed:
196+
print("Warning: Some tests failed. Coverage report may be incomplete.")
197+
print()
198+
199+
self.print_header("Coverage Summary")
200+
201+
report = self.get_coverage_report()
202+
result = self.parse_coverage(report)
203+
204+
print(f"Overall Coverage: {result.total_coverage}%")
205+
print()
206+
207+
self.print_section("Coverage by Module (sorted by coverage %):")
208+
report_with_missing = self.get_coverage_report_with_missing()
209+
module_lines = [
210+
line for line in report_with_missing.split("\n")
211+
if self.module_name in line
212+
]
213+
for line in sorted(module_lines, key=lambda x: self._extract_coverage(x)):
214+
print(line)
215+
print()
216+
217+
self.print_section(f"Files with Coverage < {self.low_coverage_threshold}%:")
218+
if result.low_coverage_files:
219+
for file_path, coverage in result.low_coverage_files:
220+
print(f" {file_path}: {coverage}%")
221+
else:
222+
print("None")
223+
print()
224+
225+
self.print_section("Files with No Coverage:")
226+
if result.no_coverage_files:
227+
for file_path in result.no_coverage_files:
228+
print(f" {file_path}")
229+
else:
230+
print("None")
231+
print()
232+
233+
self.print_header("Detailed Report")
234+
print("HTML report generated: htmlcov/index.html")
235+
print("XML report generated: coverage.xml")
236+
print()
237+
print("Open HTML report:")
238+
print(" - macOS/Linux: open htmlcov/index.html")
239+
print(" - Windows: start htmlcov/index.html")
240+
print()
241+
242+
return 0
243+
244+
def _extract_coverage(self, line: str) -> float:
245+
"""Extract coverage percentage from a report line."""
246+
match = re.search(r"(\d+(?:\.\d+)?)%", line)
247+
return float(match.group(1)) if match else 0.0
248+
249+
250+
def main() -> int:
251+
"""Main entry point for the coverage CLI."""
252+
parser = argparse.ArgumentParser(
253+
description="Unified coverage tool for test quality gates"
254+
)
255+
256+
parser.add_argument(
257+
"command",
258+
choices=["check", "measure"],
259+
help="Command to run: 'check' validates threshold, 'measure' generates full report",
260+
)
261+
parser.add_argument(
262+
"--threshold",
263+
type=float,
264+
default=70.0,
265+
help="Coverage threshold percentage (default: 70)",
266+
)
267+
parser.add_argument(
268+
"--module",
269+
type=str,
270+
default="codeflow_engine",
271+
help="Module name to measure coverage for (default: codeflow_engine)",
272+
)
273+
parser.add_argument(
274+
"--low-threshold",
275+
type=float,
276+
default=50.0,
277+
help="Threshold for flagging low coverage files (default: 50)",
278+
)
279+
280+
args = parser.parse_args()
281+
282+
runner = CoverageRunner(
283+
module_name=args.module,
284+
threshold=args.threshold,
285+
low_coverage_threshold=args.low_threshold,
286+
)
287+
288+
if args.command == "check":
289+
return runner.check_coverage()
290+
elif args.command == "measure":
291+
return runner.measure_coverage()
292+
293+
return 1
294+
295+
296+
if __name__ == "__main__":
297+
sys.exit(main())

0 commit comments

Comments
 (0)