diff --git a/scripts/generate_test_inventory_html.py b/scripts/generate_test_inventory_html.py
index d8c40afb..dfc04eea 100755
--- a/scripts/generate_test_inventory_html.py
+++ b/scripts/generate_test_inventory_html.py
@@ -1,11 +1,13 @@
+#!/usr/bin/env python3
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: BSD-3-Clause
-#!/usr/bin/env python3
-import os
+
import html
-from pathlib import Path
-from datetime import datetime, timezone
+import os
+import sys
from collections import defaultdict
+from datetime import datetime, timezone
+from pathlib import Path
import requests
@@ -19,10 +21,23 @@
if TOKEN:
HEADERS["Authorization"] = f"Bearer {TOKEN}"
+MAX_SEGMENTS_PER_AREA = 99
+MIN_SLICE_PCT = 8.0
+LABEL_SLICE_PCT = 6.0
+PALETTE = [
+ (214, "#93c5fd"),
+ (160, "#86efac"),
+ (36, "#fcd34d"),
+ (280, "#d8b4fe"),
+ (345, "#f9a8d4"),
+ (14, "#fdba74"),
+ (190, "#67e8f9"),
+ (95, "#bef264"),
+]
+
def find_repo_root():
candidates = []
-
cwd = Path.cwd().resolve()
script_dir = Path(__file__).resolve().parent
@@ -49,13 +64,23 @@ def find_repo_root():
BASE = REPO_ROOT / "Runner" / "suites"
+
def gh_get(url, params=None):
response = requests.get(url, headers=HEADERS, params=params, timeout=30)
response.raise_for_status()
return response.json()
+def get_open_prs():
+ return gh_get(f"{API}/pulls", params={"state": "open", "per_page": 100})
+
+
+def get_pr_files(pr_number):
+ return gh_get(f"{API}/pulls/{pr_number}/files", params={"per_page": 100})
+
+
def h(text):
+
return html.escape(str(text))
@@ -67,9 +92,7 @@ def html_link(text, url):
def parse_suite_path(runsh: Path):
- rel_runsh = runsh.relative_to(REPO_ROOT).as_posix()
rel_dir = runsh.parent.relative_to(REPO_ROOT).as_posix()
-
parts = runsh.relative_to(BASE).parts
area = parts[0] if len(parts) >= 1 else ""
functional_area = parts[1] if len(parts) >= 2 else ""
@@ -104,25 +127,19 @@ def discover_tests():
return tests
-def get_open_prs():
- return gh_get(f"{API}/pulls", params={"state": "open", "per_page": 100})
-
-
-def get_pr_files(pr_number):
- return gh_get(f"{API}/pulls/{pr_number}/files", params={"per_page": 100})
-
-
def mark_open_pr_tests(tests):
by_path = {t["Suite Path"]: t for t in tests}
open_pr_numbers = set()
- for pr in get_open_prs():
+ prs = get_open_prs()
+ if not prs:
+ return open_pr_numbers
+
+ for pr in prs:
pr_number = pr["number"]
pr_link = html_link(f"PR #{pr_number}", pr["html_url"])
-
- try:
- files = get_pr_files(pr_number)
- except Exception:
+ files = get_pr_files(pr_number)
+ if not files:
continue
touched_dirs = set()
@@ -136,7 +153,6 @@ def mark_open_pr_tests(tests):
open_pr_numbers.add(pr_number)
- # Suites not yet on main: surface in Open PR column.
for suite_dir in touched_dirs:
if suite_dir not in by_path:
fake_runsh = REPO_ROOT / suite_dir / "run.sh"
@@ -146,7 +162,6 @@ def mark_open_pr_tests(tests):
tests.append(meta)
by_path[suite_dir] = meta
- # Existing suites on main: show Update PR only for suite-specific PRs.
if len(touched_dirs) == 1:
suite_dir = next(iter(touched_dirs))
if suite_dir in by_path and by_path[suite_dir]["Present on main"] == "yes":
@@ -168,26 +183,145 @@ def group_tests(tests):
def build_area_summary(tests):
by_area = defaultdict(int)
- by_functional_area = defaultdict(int)
-
for test in tests:
by_area[test["Area"]] += 1
- by_functional_area[(test["Area"], test["Functional Area"])] += 1
- area_rows = []
+ rows = []
for area, count in sorted(by_area.items()):
- area_rows.append(f"
| {h(area)} | {count} |
")
+ rows.append(f"| {h(area)} | {count} |
")
+ return "\n".join(rows)
+
+
+def aggregate_area_segments(tests):
+ by_area = defaultdict(lambda: defaultdict(lambda: {"merged": 0, "open_pr": 0}))
+ for test in tests:
+ area = test["Area"] or "Other"
+ functional_area = test["Functional Area"] or "Other"
+ bucket = by_area[area][functional_area]
+ if test["Present on main"] == "yes":
+ bucket["merged"] += 1
+ elif test["Open PR"]:
+ bucket["open_pr"] += 1
+
+ result = []
+ for area, fa_map in sorted(by_area.items()):
+ items = []
+ for functional_area, counts in fa_map.items():
+ total = counts["merged"] + counts["open_pr"]
+ if total <= 0:
+ continue
+ items.append(
+ {
+ "functional_area": functional_area,
+ "merged": counts["merged"],
+ "open_pr": counts["open_pr"],
+ "total": total,
+ }
+ )
+
+ items.sort(key=lambda item: (-item["total"], item["functional_area"].lower()))
+
+ area_total = sum(item["total"] for item in items)
+ result.append({"area": area, "total": area_total, "segments": items})
+
+ result.sort(key=lambda item: (-item["total"], item["area"].lower()))
+ return result
+
+
+
+def build_functional_area_chart(tests):
+ areas = aggregate_area_segments(tests)
+ if not areas:
+ return 'No functional area data available.
'
+
+ columns = []
+ for area_index, area_info in enumerate(areas):
+ area = area_info["area"]
+ total = area_info["total"]
+ segments = area_info["segments"]
+
+ normalized_segments = []
+ for seg_index, segment in enumerate(segments):
+ hue, chip = PALETTE[(area_index + seg_index) % len(PALETTE)]
+ seg_total = segment["total"]
+ actual_pct = (seg_total * 100.0) / total if total > 0 else 0.0
+ display_pct = max(actual_pct, MIN_SLICE_PCT if seg_total > 0 else 0.0)
- functional_area_rows = []
- for (area, functional_area), count in sorted(by_functional_area.items()):
- functional_area_rows.append(
- f"| {h(area)} | {h(functional_area)} | {count} |
"
+ normalized_segments.append(
+ {
+ "segment": segment,
+ "hue": hue,
+ "chip": chip,
+ "actual_pct": actual_pct,
+ "display_pct": display_pct,
+ }
+ )
+
+ display_sum = sum(item["display_pct"] for item in normalized_segments) or 1.0
+ for item in normalized_segments:
+ item["display_pct"] = (item["display_pct"] * 100.0) / display_sum
+
+ seg_html = []
+ list_html = []
+ for item in normalized_segments:
+ segment = item["segment"]
+ seg_total = segment["total"]
+ merged_width_pct = (segment["merged"] * 100.0) / seg_total if seg_total > 0 else 0.0
+ open_width_pct = (segment["open_pr"] * 100.0) / seg_total if seg_total > 0 else 0.0
+ label = segment["functional_area"]
+
+ label_html = ""
+ if item["display_pct"] >= LABEL_SLICE_PCT:
+ label_class = "area-stack-seg-label"
+ if item["display_pct"] < 8.5:
+ label_class += " small"
+ label_html = f'{seg_total}'
+
+ seg_html.append(
+ f'''
+
+'''
+ )
+ list_html.append(
+ f'''
+
+
+ {h(label)}
+ {seg_total}
+ M {segment['merged']} | PR {segment['open_pr']}
+
+'''
+ )
+
+ columns.append(
+ f'''
+
+
{total}
+
+
+ {''.join(seg_html)}
+
+
+
{h(area)}
+
{len(segments)} segment(s)
+
+ {''.join(list_html)}
+
+
+'''
)
- return "\n".join(area_rows), "\n".join(functional_area_rows)
+ return "\n".join(columns)
def build_test_table_rows(items):
+
rows = []
for test in items:
rows.append(
@@ -227,7 +361,7 @@ def build_sections(tests):
{h(functional_area)}
{functional_area_count} test(s)
-
+
@@ -277,8 +411,7 @@ def build_html(
open_pr_count,
new_test_suites_in_open_prs,
existing_suites_updated_in_open_prs,
- area_summary_rows,
- functional_area_summary_rows,
+ functional_area_chart_html,
sections_html,
):
template = """
@@ -404,14 +537,9 @@ def build_html(
}
.summary-grid {
display: grid;
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: 1fr;
gap: 20px;
}
- @media (max-width: 1000px) {
- .summary-grid {
- grid-template-columns: 1fr;
- }
- }
.toolbar {
display: flex;
flex-wrap: wrap;
@@ -436,12 +564,17 @@ def build_html(
border: 1px solid var(--line);
border-radius: 12px;
}
+ .inventory-table-wrap {
+ width: 100%;
+ }
table {
width: 100%;
border-collapse: collapse;
- min-width: 950px;
background: #0d142b;
}
+ .inventory-table {
+ min-width: 950px;
+ }
th, td {
padding: 10px 12px;
border-bottom: 1px solid var(--line);
@@ -473,6 +606,206 @@ def build_html(
font-size: 13px;
margin-top: 8px;
}
+ .chart-wrap {
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ background: #0d142b;
+ padding: 14px 10px 10px 10px;
+ overflow-x: auto;
+ overflow-y: hidden;
+ }
+ .chart-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 14px;
+ }
+ .legend-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ border-radius: 999px;
+ padding: 6px 10px;
+ border: 1px solid var(--line);
+ background: #0a1228;
+ font-size: 12px;
+ color: var(--text);
+ white-space: nowrap;
+ }
+ .legend-swatch {
+ width: 12px;
+ height: 12px;
+ border-radius: 3px;
+ display: inline-block;
+ }
+ .legend-swatch.merged {
+ background: linear-gradient(180deg, hsl(214 84% 75%), hsl(214 84% 68%));
+ }
+ .legend-swatch.open {
+ background: repeating-linear-gradient(
+ 135deg,
+ hsl(214 84% 75%) 0px,
+ hsl(214 84% 75%) 4px,
+ rgba(255,255,255,0.15) 4px,
+ rgba(255,255,255,0.15) 8px
+ );
+ }
+ .fa-chart {
+ display: inline-flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+ gap: 18px;
+ min-width: 100%;
+ width: max-content;
+ padding: 10px 0 4px 0;
+ }
+
+.area-chart-col {
+ min-width: 176px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 7px;
+}
+.area-chart-total {
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--text);
+ line-height: 1;
+ min-height: 18px;
+}
+.area-chart-bars {
+ height: 320px;
+ width: 100%;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ padding: 0 18px;
+ border-bottom: 1px solid var(--line);
+ position: relative;
+ background:
+ linear-gradient(to top, transparent 24.5%, rgba(255,255,255,0.04) 25%, transparent 25.5%),
+ linear-gradient(to top, transparent 49.5%, rgba(255,255,255,0.04) 50%, transparent 50.5%),
+ linear-gradient(to top, transparent 74.5%, rgba(255,255,255,0.04) 75%, transparent 75.5%);
+}
+.area-chart-stack {
+ width: 78px;
+ height: 100%;
+ min-height: 100%;
+ display: flex;
+ flex-direction: column-reverse;
+ border: 1px solid var(--line);
+ border-radius: 10px 10px 0 0;
+ background: #0a1228;
+ overflow: hidden;
+ box-shadow: 0 0 0 1px rgba(255,255,255,0.02) inset;
+}
+.area-stack-seg {
+ width: 100%;
+ min-height: 10px;
+ border-top: 1px solid rgba(255,255,255,0.08);
+ display: block;
+ position: relative;
+ overflow: hidden;
+}
+.area-stack-seg-split {
+ width: 100%;
+ height: 100%;
+ display: flex;
+}
+.area-stack-merged {
+ height: 100%;
+ background: linear-gradient(
+ 180deg,
+ hsl(var(--seg-hue) 90% 76%),
+ hsl(var(--seg-hue) 78% 54%)
+ );
+}
+.area-stack-open {
+ height: 100%;
+ background:
+ repeating-linear-gradient(
+ 135deg,
+ rgba(255,255,255,0.70) 0px,
+ rgba(255,255,255,0.70) 3px,
+ rgba(255,255,255,0.10) 3px,
+ rgba(255,255,255,0.10) 6px
+ ),
+ linear-gradient(
+ 180deg,
+ hsl(var(--seg-hue) 58% 44%),
+ hsl(var(--seg-hue) 52% 30%)
+ );
+ box-shadow: inset 1px 0 0 rgba(11, 16, 32, 0.40);
+}
+.area-stack-seg-label {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ font-weight: 700;
+ color: #08101f;
+ text-shadow: 0 1px 0 rgba(255,255,255,0.20);
+ pointer-events: none;
+ line-height: 1;
+}
+.area-stack-seg-label.small {
+ font-size: 9px;
+}
+.area-chart-name {
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text);
+ text-align: center;
+ line-height: 1.2;
+ min-height: 16px;
+}
+.area-chart-meta {
+ font-size: 11px;
+ color: var(--muted);
+ text-align: center;
+ line-height: 1.15;
+ min-height: 12px;
+}
+.area-breakdown-list {
+ width: 100%;
+ display: grid;
+ gap: 5px;
+ margin-top: 4px;
+ padding-top: 4px;
+}
+.area-breakdown-item {
+ display: grid;
+ grid-template-columns: 10px 1fr auto auto;
+ gap: 6px;
+ align-items: center;
+ font-size: 11px;
+ color: var(--muted);
+}
+.area-breakdown-swatch {
+ width: 10px;
+ height: 10px;
+ border-radius: 2px;
+ background: var(--seg-chip);
+ display: inline-block;
+}
+.area-breakdown-name {
+ color: var(--text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+ .area-breakdown-count {
+ color: var(--text);
+ font-weight: 700;
+ padding-left: 4px;
+ }
+ .area-breakdown-meta {
+ white-space: nowrap;
+ }
details {
border: 1px solid var(--line);
border-radius: 12px;
@@ -572,31 +905,18 @@ def build_html(
Summary
-
-
By Area
-
-
-
- | Area | Test Count |
-
-
- __AREA_SUMMARY_ROWS__
-
-
-
-
-
By Functional Area
-
-
-
- | Area | Functional Area | Test Count |
-
-
- __FUNCTIONAL_AREA_SUMMARY_ROWS__
-
-
+
+
+ Merged
+ Open PR
+ Bars grouped by Area
+ Stacks show Functional Areas
+
+
+ __FUNCTIONAL_AREA_CHART_HTML__
+
@@ -716,8 +1036,7 @@ def build_html(
.replace("__REPO_HOME__", h(repo_home))
.replace("__REPO_ACTIONS__", h(repo_actions))
.replace("__REPO_SUITES__", h(repo_suites))
- .replace("__AREA_SUMMARY_ROWS__", area_summary_rows)
- .replace("__FUNCTIONAL_AREA_SUMMARY_ROWS__", functional_area_summary_rows)
+ .replace("__FUNCTIONAL_AREA_CHART_HTML__", functional_area_chart_html)
.replace("__SECTIONS_HTML__", sections_html)
)
@@ -751,7 +1070,7 @@ def main():
repo_actions = f"{REPO_WEB}/actions"
repo_suites = f"{MAIN_TREE}/Runner/suites"
- area_summary_rows, functional_area_summary_rows = build_area_summary(tests)
+ functional_area_chart_html = build_functional_area_chart(tests)
sections_html = build_sections(tests)
print(
@@ -765,8 +1084,7 @@ def main():
open_pr_count=open_pr_count,
new_test_suites_in_open_prs=new_test_suites_in_open_prs,
existing_suites_updated_in_open_prs=existing_suites_updated_in_open_prs,
- area_summary_rows=area_summary_rows,
- functional_area_summary_rows=functional_area_summary_rows,
+ functional_area_chart_html=functional_area_chart_html,
sections_html=sections_html,
)
)