Skip to content

Commit d98222b

Browse files
committed
Add nightly pytest combined report artifact
1 parent 1440fe1 commit d98222b

File tree

2 files changed

+187
-0
lines changed

2 files changed

+187
-0
lines changed

.github/workflows/nightly-tests.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,33 @@ jobs:
106106
name: nightly-gtest-report-${{ matrix.nic }}
107107
path: |
108108
gtest.log
109+
aggregate-python-reports:
110+
needs: run-nightly-tests
111+
if: ${{ always() }}
112+
runs-on: ubuntu-24.04
113+
steps:
114+
- name: 'preparation: Checkout MTL'
115+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
116+
- name: Download python report artifacts
117+
uses: actions/download-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
118+
with:
119+
pattern: nightly-test-report-*
120+
path: python-reports
121+
merge-multiple: false
122+
if-no-files-found: error
123+
- name: Install report dependencies
124+
run: |
125+
python3 -m pip install --upgrade pip
126+
python3 -m pip install pandas beautifulsoup4 openpyxl
127+
- name: Combine python reports
128+
run: |
129+
python3 tests/validation/tools/combine_reports.py \
130+
--report E810=python-reports/nightly-test-report-e810/report.html \
131+
--report "E810-Dell=python-reports/nightly-test-report-e810-dell/report.html" \
132+
--report E830=python-reports/nightly-test-report-e830/report.html \
133+
--output python-reports/combined_report.xlsx
134+
- name: Upload combined python report
135+
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
136+
with:
137+
name: nightly-python-combined-report
138+
path: python-reports/combined_report.xlsx
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env python3
2+
"""Combine individual pytest HTML reports into a single Excel summary."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
from pathlib import Path
8+
from typing import Dict, Iterable, Tuple
9+
10+
import pandas as pd
11+
from bs4 import BeautifulSoup
12+
13+
STATUS_CLASSES = {
14+
"passed",
15+
"failed",
16+
"skipped",
17+
"xfailed",
18+
"xpassed",
19+
"error",
20+
"warning",
21+
"notrun",
22+
"rerun",
23+
}
24+
25+
STATUS_LABELS = {
26+
"passed": "PASSED",
27+
"failed": "FAILED",
28+
"skipped": "SKIPPED",
29+
"xfailed": "XFAILED",
30+
"xpassed": "XPASSED",
31+
"error": "ERROR",
32+
"warning": "WARNING",
33+
"notrun": "NOT RUN",
34+
"rerun": "RERUN",
35+
"unknown": "UNKNOWN",
36+
}
37+
38+
DEFAULT_STATUS = "NOT FOUND"
39+
40+
41+
def parse_args() -> argparse.Namespace:
42+
parser = argparse.ArgumentParser(
43+
description="Combine pytest HTML reports generated for different NICs into an Excel file."
44+
)
45+
parser.add_argument(
46+
"--report",
47+
action="append",
48+
required=True,
49+
metavar="NIC=PATH",
50+
help="Mapping between a NIC label and the HTML report path (e.g. '--report E810=reports/e810/report.html').",
51+
)
52+
parser.add_argument(
53+
"--output",
54+
type=Path,
55+
default=Path("combined_report.xlsx"),
56+
help="Destination Excel file path (default: combined_report.xlsx).",
57+
)
58+
return parser.parse_args()
59+
60+
61+
def parse_report_options(values: Iterable[str]) -> Dict[str, Path]:
62+
mapping: Dict[str, Path] = {}
63+
for value in values:
64+
if "=" not in value:
65+
raise ValueError(f"Invalid --report option '{value}'. Expected format NIC=PATH.")
66+
nic, raw_path = value.split("=", 1)
67+
nic = nic.strip()
68+
if not nic:
69+
raise ValueError(f"Invalid NIC label in option '{value}'.")
70+
path = Path(raw_path).expanduser().resolve()
71+
mapping[nic] = path
72+
return mapping
73+
74+
75+
def read_html(path: Path) -> BeautifulSoup:
76+
html_text = path.read_text(encoding="utf-8", errors="ignore")
77+
return BeautifulSoup(html_text, "html.parser")
78+
79+
80+
def parse_report(path: Path) -> Dict[Tuple[str, str], str]:
81+
soup = read_html(path)
82+
results: Dict[Tuple[str, str], str] = {}
83+
84+
for file_details in soup.select("details.file"):
85+
file_path_element = file_details.select_one(".fspath")
86+
if not file_path_element:
87+
continue
88+
file_path = file_path_element.get_text(strip=True)
89+
90+
for test_details in file_details.select("details.test"):
91+
status_token = next(
92+
(cls for cls in test_details.get("class", []) if cls in STATUS_CLASSES),
93+
"unknown",
94+
)
95+
test_name_element = test_details.select_one(".test-name")
96+
if test_name_element:
97+
test_name = test_name_element.get_text(separator=" ", strip=True)
98+
else:
99+
title_element = test_details.select_one(".title")
100+
test_name = (
101+
title_element.get_text(separator=" ", strip=True)
102+
if title_element
103+
else "UNKNOWN"
104+
)
105+
106+
results[(file_path, test_name)] = STATUS_LABELS.get(
107+
status_token, STATUS_LABELS["unknown"]
108+
)
109+
110+
return results
111+
112+
113+
def build_dataframe(
114+
keys: Iterable[Tuple[str, str]], data: Dict[str, Dict[Tuple[str, str], str]]
115+
) -> pd.DataFrame:
116+
ordered_tests = sorted(set(keys), key=lambda item: (item[0], item[1]))
117+
rows = []
118+
119+
for test_file, test_case in ordered_tests:
120+
row = {
121+
"Test File": test_file,
122+
"Test Case": test_case,
123+
}
124+
for nic, mapping in data.items():
125+
row[nic] = mapping.get((test_file, test_case), DEFAULT_STATUS)
126+
rows.append(row)
127+
128+
return pd.DataFrame(rows)
129+
130+
131+
def main() -> None:
132+
args = parse_args()
133+
reports = parse_report_options(args.report)
134+
135+
parsed_data: Dict[str, Dict[Tuple[str, str], str]] = {}
136+
all_keys = set()
137+
138+
for nic_name, report_path in reports.items():
139+
if not report_path.exists():
140+
raise FileNotFoundError(f"Report not found for {nic_name}: {report_path}")
141+
parsed = parse_report(report_path)
142+
parsed_data[nic_name] = parsed
143+
all_keys.update(parsed.keys())
144+
print(f"Parsed {len(parsed)} tests for {nic_name} from {report_path}")
145+
146+
if not all_keys:
147+
raise RuntimeError("No tests discovered across provided reports.")
148+
149+
df = build_dataframe(all_keys, parsed_data)
150+
output_path = args.output
151+
output_path.parent.mkdir(parents=True, exist_ok=True)
152+
df.to_excel(output_path, index=False)
153+
print(f"Combined report written to {output_path}")
154+
155+
156+
if __name__ == "__main__":
157+
main()

0 commit comments

Comments
 (0)