Coverage for / opt / hostedtoolcache / Python / 3.11.14 / x64 / lib / python3.11 / site-packages / rattlesnake / cicd / report_pytest.py: 96%
73 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-27 18:22 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-27 18:22 +0000
1"""This module extracts key coverage metrics from a coverage output file."""
3import argparse
4import xml.etree.ElementTree as ET
5from dataclasses import dataclass
6from pathlib import Path
8import sys
10# from typing import Dict, List, Tuple
12from rattlesnake.cicd.utilities import (
13 get_score_color_coverage,
14 extend_timestamp,
15 write_report,
16)
19@dataclass(frozen=True)
20class CoverageMetric:
21 """Represents coverage metrics for a codebase.
23 Attributes:
24 lines_valid (int): The total number of valid lines in the codebase.
25 lines_covered (int): The number of lines that are covered by tests.
26 coverage (float): The coverage percentage, calculated as
27 (lines_covered / lines_valid) * 100. Defaults to 0.0.
28 color (str): The color code (e.g., red, green), based on the coverage.
29 """
31 lines_valid: int = 0
32 lines_covered: int = 0
34 @property
35 def coverage(self) -> float:
36 """
37 Calculates the coverage percentage.
39 The coverage is calculated as `(lines_covered / lines_valid) * 100`.
40 Returns 0.0 if `lines_valid` is zero to prevent division by zero errors.
41 """
43 return (
44 (self.lines_covered / self.lines_valid * 100)
45 if self.lines_valid > 0
46 else 0.0
47 )
49 @property
50 def color(self) -> str:
51 """
52 Determines the badge color based on the coverage percentage.
53 """
54 return get_score_color_coverage(str(self.coverage))
57def get_coverage_metric(coverage_file: Path) -> CoverageMetric:
58 """
59 Gets the lines-valid, lines-covered, and coverage percentage as
60 a list strings.
61 """
63 cm = CoverageMetric()
65 try:
66 tree = ET.parse(coverage_file)
67 root = tree.getroot()
68 lines_valid = int(root.attrib["lines-valid"])
69 lines_covered = int(root.attrib["lines-covered"])
70 cm = CoverageMetric(
71 lines_valid=lines_valid,
72 lines_covered=lines_covered,
73 ) # overwrite default
74 except (FileNotFoundError, ET.ParseError, KeyError) as e:
75 print(f"Error processing coverage file: {e}")
77 return cm
80def get_report_html(
81 coverage_metric: CoverageMetric,
82 timestamp: str,
83 run_id: str,
84 ref_name: str,
85 github_sha: str,
86 github_repo: str,
87) -> str:
88 """
89 Generates an HTML report from the coverage metrics.
91 Args:
92 coverage_metric: CoverageMetric object containing coverage data
93 timestamp: The timestampe from bash when pylint ran, in format YYYYMMDD_HHMMSS_UTC
94 e.g., 20250815_211112_UTC
95 run_id: GitHub Actions run ID
96 ref_name: Git reference name (branch)
97 github_sha: GitHub commit SHA
98 github_repo: GitHub repository name
100 Returns:
101 Complete HTML report as a string
102 """
103 timestamp_ext = extend_timestamp(timestamp)
104 score_color: str = coverage_metric.color
106 # Programmatically construct the full report URL
107 try:
108 owner, repo_name = github_repo.split("/")
109 full_report_url = (
110 f"https://{owner}.github.io/{repo_name}/reports/coverage/htmlcov/index.html"
111 )
112 except ValueError:
113 # Fallback or default URL in case the repo format is unexpected
114 full_report_url = "#"
116 html_content = f"""<!DOCTYPE html>
117<html lang="en">
118<head>
119 <meta charset="UTF-8">
120 <meta name="viewport" content="width=device-width, initial-scale=1.0">
121 <title>Pytest Report</title>
122 <style>
123 body {{
124 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
125 margin: 0; padding: 20px; background: #f6f8fa; line-height: 1.6;
126 background: lightgray;
127 }}
128 .container {{
129 max-width: 1200px; margin: 0 auto;
130 }}
131 .header {{
132 background: white; padding: 30px; border-radius: 8px;
133 box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px;
134 }}
135 .score {{
136 font-size: 2.5em; font-weight: bold; color: {score_color};
137 }}
138 .metadata {{
139 color: #6a737d; font-size: 0.9em; margin-top: 10px;
140 }}
141 .nav {{
142 background: white; padding: 20px; border-radius: 8px;
143 box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px;
144 }}
145 .nav a {{
146 background: #0366d6; color: white; padding: 10px 20px;
147 text-decoration: none; border-radius: 6px; margin-right: 10px;
148 display: inline-block; margin-bottom: 5px;
149 }}
150 .nav a:hover {{
151 background: #0256cc;
152 }}
153 .section {{
154 background: white; padding: 20px; border-radius: 8px;
155 box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px;
156 }}
157 </style>
158</head>
159<body>
160 <div class="container">
161 <div class="header">
162 <h1>Pytest Coverage Report</h1>
163 <div class="score">Coverage: {coverage_metric.coverage:.2f}%</div>
164 <div class="metadata">
165 <div><strong>Lines Covered:</strong> {coverage_metric.lines_covered}</div>
166 <div><strong>Total Lines:</strong> {coverage_metric.lines_valid}</div>
167 <div> </div>
168 <div><strong>Generated:</strong> {timestamp_ext}</div>
169 <div><strong>Run ID:</strong> <a href="https://github.com/{github_repo}/actions/runs/{run_id}"> {run_id}</a></div>
170 <div><strong>Branch:</strong> <a href="https://github.com/{github_repo}/tree/{ref_name}"> {ref_name}</a></div>
171 <div><strong>Commit:</strong> <a href="https://github.com/{github_repo}/commit/{github_sha}"> {github_sha[:7]}</a></div>
172 <div><strong>Repository:</strong> <a href="https://github.com/{github_repo}">{github_repo}</a></div>
173 <div> </div>
174 <div><strong>Full report:</strong> <a href="{full_report_url}">HTML</a></div>
175 </div>
176 </div>
177 </div>
178 <footer style="text-align: center; margin: 40px 0; color: #6a737d;">
179 <p>Generated by GitHub Actions</p>
180 </footer>
181</body>
182</html>"""
184 return html_content
187def run_pytest_report(
188 input_file: str,
189 output_file: str,
190 timestamp: str,
191 run_id: str,
192 ref_name: str,
193 github_sha: str,
194 github_repo: str,
195) -> CoverageMetric:
196 """
197 Main function to create HTML report from pytest output.
199 Args:
200 input_file: Path to the pytest output text file
201 output_file: Path for the generated HTML report
202 timestamp: The timestampe from bash when pylint ran, in format YYYYMMDD_HHMMSS_UTC
203 e.g., 20250815_211112_UTC
204 run_id: GitHub Actions run ID
205 ref_name: Git reference name (branch)
206 github_sha: GitHub commit SHA
207 github_repo: GitHub repository name
209 Returns:
210 CoverageMetric
211 """
212 # Get the coverage metric
213 coverage_metric = get_coverage_metric(coverage_file=Path(input_file))
214 print(f"run_pytest_report: coverage_metric={coverage_metric}")
216 # Generate HTML report
217 html_content: str = get_report_html(
218 coverage_metric,
219 timestamp,
220 run_id,
221 ref_name,
222 github_sha,
223 github_repo,
224 )
226 # Write the HTML report
227 write_report(html_content, output_file)
229 return coverage_metric
232def parse_arguments() -> argparse.Namespace:
233 """
234 Parse command line arguments.
236 Returns:
237 Parsed arguments namespace
238 """
239 parser: argparse.ArgumentParser = argparse.ArgumentParser(
240 description="Generate enhanced HTML report from pytest output",
241 formatter_class=argparse.RawDescriptionHelpFormatter,
242 epilog="""
243Example:
244 python pytest_report.py \
245 --input_file pytest_output_20240101_120000_UTC.txt \
246 --output_file pytest_report.html \
247 --timestamp 20240101_120000_UTC \
248 --run_id 1234567890 \
249 --ref_name main \
250 --github_sha abc123def456 \
251 --github_repo owner/repo-name
252 """,
253 )
255 parser.add_argument("--input_file", required=True, help="Input pytest output file")
257 parser.add_argument("--output_file", required=True, help="Output HTML report file")
259 parser.add_argument(
260 "--timestamp", required=True, help="UTC timestamp, e.g., 20240101_120000_UTC"
261 )
263 parser.add_argument("--run_id", required=True, help="GitHub Actions run ID")
265 parser.add_argument(
266 "--ref_name", required=True, help="Git reference name (branch name)"
267 )
269 parser.add_argument("--github_sha", required=True, help="GitHub commit SHA")
271 parser.add_argument(
272 "--github_repo", required=True, help="GitHub repository name (owner/repo-name)"
273 )
275 return parser.parse_args()
278def main() -> int:
279 """
280 Main entry point for the script.
282 Returns:
283 Exit code (0 for success, 1 for failure)
284 """
285 args: argparse.Namespace = parse_arguments()
287 try:
288 cm: CoverageMetric = run_pytest_report(
289 args.input_file,
290 args.output_file,
291 args.timestamp,
292 args.run_id,
293 args.ref_name,
294 args.github_sha,
295 args.github_repo,
296 )
298 print(f"✅ Pytest HTML report generated: {args.output_file}")
299 print(f"📊 - valid lines of code: {cm.lines_valid}")
300 print(f"🔍 - lines covered: {cm.lines_covered}")
301 print(f"🎉 - coverage: {cm.coverage} percent")
303 except FileNotFoundError:
304 print(f"❌ Error: The input file '{args.input_file}' was not found.")
305 return 1
306 except IOError as e:
307 print(f"❌ I/O error occurred: {e}")
308 return 1
309 except Exception as e:
310 print(f"❌ An unexpected error occurred: {e}")
311 return 1
313 return 0 # Success exit code
316if __name__ == "__main__":
317 sys.exit(main())