From e06810a62f5788e1d15593dcedc3ea3de2deb7af Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Thu, 9 Apr 2026 14:48:51 +0530 Subject: [PATCH] fix(report): show actual functional area names instead of collapsing into Other Stop collapsing excess functional areas into a synthetic "Other" bucket in the summary chart. This keeps the chart labels aligned with the real folder names under each Area, so sections like Multimedia show their actual functional areas instead of an aggregated placeholder. Signed-off-by: Srikanth Muppandam --- scripts/generate_test_inventory_html.py | 458 ++++++++++++++++++++---- 1 file changed, 388 insertions(+), 70 deletions(-) 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''' +
+
+
+
+
+ {label_html} +
+''' + ) + 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_SUMMARY_ROWS__ - -
AreaTest Count
-
-
-

By Functional Area

-
- - - - - - __FUNCTIONAL_AREA_SUMMARY_ROWS__ - -
AreaFunctional AreaTest Count
+
+
+ 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, ) )