diff --git a/common/acs_test_framework_manifests/generate_acs_summary.yaml b/common/acs_test_framework_manifests/generate_acs_summary.yaml index 57ec7204..14007033 100644 --- a/common/acs_test_framework_manifests/generate_acs_summary.yaml +++ b/common/acs_test_framework_manifests/generate_acs_summary.yaml @@ -386,7 +386,7 @@ suites: path: "{dir}/summary.html" - type: file_contains path: "{dir}/bsa_detailed.html" - text: "Test_suite_info:" + text: "Test suite info:" - type: file_contains path: "{dir}/bsa_detailed.html" text: "PCIe root complex present" @@ -397,3 +397,61 @@ suites: path: "{dir}/bsa_detailed.html" text: " + OS Test Summary

OS Test Summary

Result Summary

StatusTotal
Passed1
+ os_tests_detailed.html: | + +
Test Suite: Network
details body
+ merged.json: | + { + "Suite_Name: acs_info": { + "ACS Results Summary": { + "Overall Compliance Result": "Compliant" + } + }, + "Suite_Name: OS Tests - ethtool_test_linux-debian": { + "test_results": [ + { + "Test_suite": "Network", + "Test_suite_info": [ + "OS network category information", + "Linux operating-system validation" + ] + } + ] + } + } + args: + - --merged_json + - "{dir}/merged.json" + - "{dir}/bsa_missing.html" + - "{dir}/sbsa_missing.html" + - "{dir}/fwts_missing.html" + - "{dir}/sct_missing.html" + - "{dir}/bbsr_fwts_missing.html" + - "{dir}/bbsr_sct_missing.html" + - "{dir}/bbsr_tpm_missing.html" + - "{dir}/pfdi_missing.html" + - "{dir}/post_script_missing.html" + - "{dir}/standalone_missing.html" + - "{dir}/os_tests.html" + - "{dir}/capsule_missing.html" + - "{dir}/sbmr_ib_missing.html" + - "{dir}/sbmr_oob_missing.html" + - "{dir}/scmi_missing.html" + - "{dir}/summary.html" + expect_exit_code: 0 + post_checks: + - type: file_contains + path: "{dir}/os_tests_detailed.html" + text: "Test suite info:" + - type: file_contains + path: "{dir}/os_tests_detailed.html" + text: "OS network category information" + - type: file_contains + path: "{dir}/os_tests_detailed.html" + text: "Linux operating-system validation" diff --git a/common/acs_test_framework_manifests/merge-jsons.yaml b/common/acs_test_framework_manifests/merge-jsons.yaml index a5872c4a..9765e8f9 100644 --- a/common/acs_test_framework_manifests/merge-jsons.yaml +++ b/common/acs_test_framework_manifests/merge-jsons.yaml @@ -529,6 +529,33 @@ suites: path: "{dir}/merged.json" text: "\"Test_suite_info\"" + - name: dt_bbsr_tpm_uses_measured_boot_log_category + type: module_main_with_env + scenario: + kind: merge_jsons + dt_or_sr_mode: DT + yocto_flag_path: yocto.flag + input_files: + acs_info.json: + ACS Results Summary: + Overall Compliance Result: Unknown + bbsr_tpm.json: + test_results: + - Test_suite: BBSR-TPM + test_result: PASSED + mocks: + "{module}.test_cat_dict": + value: + bbsr-standalone: + measured boot log: + Description: + - BBSR measured boot category information + expect_exit_code: 0 + post_checks: + - type: file_contains + path: "{dir}/merged.json" + text: "BBSR measured boot category information" + # The exact standalone ethtool_test.json filename should classify through # the ETHTOOL_TEST path and still be collapsed into the shared Standalone section. - name: dt_ethtool_test_json_is_collapsed_under_standalone @@ -1330,4 +1357,4 @@ suites: total_skipped: 0 total_warnings: 0 expect_stdout_or_stderr_contains: - - "Suite: Mandatory : FWTS: Not Compliant: Failed 1" + - "FWTS: Not Compliant: Failed 1" diff --git a/common/acs_test_framework_manifests/report-ui.yaml b/common/acs_test_framework_manifests/report-ui.yaml new file mode 100644 index 00000000..0af52c6a --- /dev/null +++ b/common/acs_test_framework_manifests/report-ui.yaml @@ -0,0 +1,98 @@ +suites: + - name: shared_report_ui + files: + - common/log_parser/report_ui.py + + cases: + - name: file_exists + type: file_exists + + - name: python_compiles + type: py_compile + + - name: injects_self_contained_ui + type: py_function + function: enhance_html_report + args: + - 'Test

Test

' + expect_return_contains: 'data-acs-report-ui="2026.4"' + + - name: injects_suite_profile + type: py_function + function: enhance_html_report + args: + - 'SCT

SCT

' + - suite + - sct + expect_return_contains: 'data-acs-suite="sct"' + + - name: labels_sct_category_metadata_as_test_suite_info + type: py_function + function: enhance_html_report + args: + - 'SCT
Test Suite Name: GenericTest
Test_suite_info:UEFI context
' + - suite + - sct + expect_return_contains: '"Test suite info"' + + - name: discovers_status_columns_from_headers + type: py_function + function: enhance_html_report + args: + - 'Test
Test Result
CUSTOM STATUS
' + expect_return_contains: 'function statusColumnIndex(table)' + + - name: supports_future_dynamic_statuses + type: py_function + function: enhance_html_report + args: + - 'Test
Result
FUTURE STATUS
' + expect_return_contains: 'allowDynamic ? statusSlug(text)' + + - name: styles_every_status_row + type: py_function + function: enhance_html_report + args: + - 'Test
Outcome
NEW OUTCOME
' + expect_return_contains: 'data-acs-row-status' + + - name: aligns_filter_counts_with_visible_results + type: py_function + function: enhance_html_report + args: + - 'Test
Outcome
NEW OUTCOME
' + expect_return_contains: 'record.match = (!activeStatus || record.status === activeStatus)' + + - name: browser_executes_layout_navigation_collapse_filters_links_and_print_scope + type: command_success + command: python3 + args: + - -c + - | + import pathlib, runpy, sys + project_root = pathlib.Path(sys.argv[1]).resolve().parents[2] + runpy.run_path( + str(project_root / "common/acs_test_framework_runner/report_ui_browser_smoke.py"), + run_name="__main__", + ) + - "{file}" + timeout_sec: 45 + skip_unless_commands_succeed: + - command -v chromium || command -v chromium-browser + + - name: rejects_unknown_page_type + type: py_function + function: enhance_html_report + args: + - '' + - unknown + expect_exception: ValueError + + - name: rejects_unknown_suite_type + type: py_function + function: enhance_html_report + args: + - '' + - suite + - unknown + expect_exception: ValueError diff --git a/common/acs_test_framework_runner/report_ui_browser_smoke.py b/common/acs_test_framework_runner/report_ui_browser_smoke.py new file mode 100644 index 00000000..a7f746da --- /dev/null +++ b/common/acs_test_framework_runner/report_ui_browser_smoke.py @@ -0,0 +1,1289 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026, Arm Limited or its affiliates. All rights reserved. +# SPDX-License-Identifier : Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Run dependency-free Chromium smoke tests for the shared report UI.""" + +from __future__ import annotations + +import html +import importlib +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +from threading import Thread + + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +LOG_PARSER_DIR = PROJECT_ROOT / "common" / "log_parser" + + +def _load_enhancer(): + """Import the report enhancer from the repository under test.""" + sys.path.insert(0, str(LOG_PARSER_DIR)) + return importlib.import_module("report_ui").enhance_html_report + + +def _chromium_binary() -> str: + for candidate in ("chromium", "chromium-browser"): + resolved = shutil.which(candidate) + if resolved: + return resolved + raise RuntimeError("Chromium is required for the report UI browser smoke test") + + +class _QuietHandler(SimpleHTTPRequestHandler): + """Serve smoke pages without adding request noise to test output.""" + + def log_message(self, format: str, *args) -> None: + """Suppress HTTP access logging during smoke tests.""" + del format, args + + +def _run_page( + browser: str, + directory: Path, + name: str, + content: str, + window_size: str = "1600,900", +) -> None: + page = directory / f"{name}.html" + page.write_text(content, encoding="utf-8") + handler = partial(_QuietHandler, directory=str(directory)) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + server_thread = Thread(target=server.serve_forever, daemon=True) + server_thread.start() + try: + completed = subprocess.run( + [ + browser, + "--headless", + "--no-sandbox", + "--disable-gpu", + "--disable-dev-shm-usage", + "--disable-background-networking", + "--disable-component-update", + "--no-first-run", + f"--window-size={window_size}", + "--virtual-time-budget=2500", + "--dump-dom", + f"http://127.0.0.1:{server.server_port}/{page.name}", + ], + capture_output=True, + text=True, + check=False, + timeout=20, + ) + finally: + server.shutdown() + server.server_close() + server_thread.join(timeout=2) + marker = 'data-browser-smoke="PASS"' + if completed.returncode == 0 and marker in completed.stdout: + return + + result = re.search( + r'
(.*?)
', + completed.stdout, + flags=re.DOTALL, + ) + detail = html.unescape(result.group(1)) if result else "browser result marker missing" + raise RuntimeError( + f"{name} browser smoke failed: {detail}; exit={completed.returncode}; " + f"stdout={completed.stdout[-1600:]}; stderr={completed.stderr[-1200:]}" + ) + + +DETAIL_PAGE = r""" +UI browser smoke +

UI browser smoke

+
+

Result Summary

+

For details on Rule Results Status, refer to + the guide.

+ + + + + + +
Total Tests10
Passed7
Failed2
Failed with Waiver0
CUSTOM REVIEW1
+
+
chart
+
+
+
Smoke group
+ + + + + +
DescriptionTest ResultReason
Passing outcomePASSEDShort reason
Future outcomeCUSTOM REVIEWThis deliberately long diagnostic reason verifies that SCT and FWTS reports hide + verbose evidence initially.
It retains every original character for explicit + on-demand expansion by the report reader.
+
+
+ +""" + + +BSA_PAGE = r""" +BSA compact summary smoke + +

BSA compact summary smoke

+

Result Summary

+ + + + + + + + + + + +
Total Tests116
Passed22
Failed47
Failed with Waiver0
Aborted0
Skipped12
Warnings12
Passed (Partial)3
Not implemented18
PAL not supported2
+
+
chart
+
+
Test Suite: GIC
+
Test suite info:
    +
  • GIC requirement context.
  • Failures can prevent an OS from booting.
  • +
+
Description: Generic Interrupt Controller checks
+ + + + + + + +
Test CaseDescriptionResult
B_GIC_01GIC versionPASSED(*PARTIAL)
B_GIC_02GIC failure oneFAILED
B_GIC_03GIC failure twoFAILED
+ + + +
SubtestDescriptionResult
B_GIC_04.1Nested GIC failureFAILED
B_GIC_05Waived GIC failureFAILED WITH WAIVER
+
Test Suite: MEM_MAP
+
Description: Memory map checks
+ +
Test CaseDescriptionResult
B_MEM_01Memory rangePASSED
+
+ +""" + + +POST_SCRIPT_PAGE = r""" +Post-script compact summary smoke +

Post-script compact summary smoke

+

Result Summary

+ + + +
Total Tests1
Passed1
+
+
legacy chart
+

Detailed Subtests

+

post scripts checks: Post script checks from post-script.log

+ +
Result
PASSED
+ +""" + + +COMPRESSED_COUNTS_PAGE = r""" +Compressed FWTS counts +

FWTS Test Summary

+

Result Summary

+ + + + +
Total Tests2
Passed0
Failed2
+
+
+
+ Test Suite: dmicheck +
+
Description: DMI checks
+ + +
DescriptionResult
Two diagnostics represented by this rowFAILED
+
+ +""" + + +CASE_NAV_PAGE = r""" +Test case navigation smoke +

Standalone Test Summary

+

Result Summary

+ + +
Total Tests2
Passed2
+
+
+
Test Suite: Network
+
Test suite info:
  • Network context.
+
Description: Network validation
+
Test Case: ping_test
+
Description: Ping the gateway
+ +
DescriptionResult
PingPASSED
+
Test Suite: Boot sources
+
Description: Boot source validation
+
Test Case: block_devices
+
Description: Check block devices
+ +
DescriptionResult
Block devicePASSED
+
+ +""" + + +METADATA_CONTEXT_PAGE = r""" +SCT context smoke +

SCT Test Summary

+

Result Summary

+ + + +
Total Tests4
Passed2
Failed2
+
+
+
Test Suite Name: GenericTest
+
Sub Test Suite: EFICompliantTest
+
Test Case: PlatformSpecificElements
+
Test Case Description: Check platform elements
+
Test Result: PASSED
+
Test suite info:
  • UEFI context.
+ +
DescriptionResult
Console protocolsPASSED
+
Test Suite Name: BootServicesTest
+
Sub Test Suite: EventServicesTest
+
Test Case: CheckEvent
+
Test Case Description: Check event services
+
Test Result: PASSED
+ +
DescriptionResult
Event servicesPASSED
+
Test Suite Name: GenericTest
+
Sub Test Suite: ProtocolTest
+
Test Case: CheckProtocol
+
Test Case Description: Check protocol services
+
Test Result: FAILED
+ +
DescriptionResult
Protocol servicesFAILED WITH WAIVER
+
Test Suite Name: GenericTest
+
Sub Test Suite: ProtocolTest
+
Test Case: NoSubtests
+
Test Case Description: Case without subtest rows
+
Test Result: FAILED
+
DescriptionResult
+
+ +""" + + +CARD_SUMMARY_PAGE = r""" +Card summary smoke +

SBMR-IB Test Summary

+

Result Summary

+ + + +
Total Tests2
Passed1
Failed1
+
+ +""" + + +SUMMARY_PAGE = r""" +Summary browser smoke +
ACS Summary
+ +
+

System Information

+ + + + + + + + + + + + + +
VendorExample Vendor
SystemExample System
SoC FamilyExample SoC
Firmware Version2026.08 (SCP: 2026.08)
ACS versionACS 3.1
SRS versionSRS 3.1
BSA versionBSA 1.2
BandSystemReady band
FW source codehttps://example.test/source
Flashing instructionsUnknown
product websiteProduct page
UEFI VersionUEFI 2.10
Partner ticketSR-123
+

ACS Results Summary

+ + + + + + +
BandSystemReady band
Date2026-08-30 12:34:56
Build identifierbuild-abcdefghijklmnopqrstuvwxyz-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ
SRS requirements compliance resultsNot Compliant
Mandatory: failed: BSA
Recommended: not run: SBMR-OOB
+

Extensions

+ + + + + +
BBSR compliance resultsCompliant with Waivers
Mandatory: waived: BBSR-TPM
SCMI compliance resultsNot Run
Future compliance resultUnknown
Optional compliance resultCompliant
+

Optional Extension

+

Future Extension

+

Future non-table result content must remain visible.

+
+

Test Summaries

+

BSA Test Summary

+

Result Summary

+ + + +
Total Tests2
Passed1
Passed (Partial)1
+
+ +
+

SBBR-FWTS Test Summary

+ +
+

SBBR-SCT Test Summary

+ +
+

SBMR-IB Test Summary

+
+ +
Total Tests1
Passed1
+ +
+

Standalone Test Summary

+ +
+
+
+ +""" + + +BAND_MISMATCH_PAGE = r""" +Band mismatch browser smoke +
ACS Summary
+
+

System Information

+ + +
VendorExample Vendor
BandUnknown
+

ACS Results Summary

+ + + +
BandSystemReady Devicetree band
Date2026-08-31 00:00:00
SRS requirements compliance resultsCompliant
+
+ +""" + + +def main() -> int: + """Generate synthetic reports and execute their interactions in Chromium.""" + browser = _chromium_binary() + enhance_html_report = _load_enhancer() + image_chart = ( + 'Chart stripping' + '
' + '
' + ) + stripped_detail = enhance_html_report(image_chart, suite_type="bsa") + if '
', ""), + suite_type="bsa", + ) + if '
0: + result = subtest.get("sub_test_result", {}) + if result.get("FAILED_WITH_WAIVER", 0) > 0: show_waiver = True break return { "show_waiver": show_waiver } -# Function to generate bar chart for FWTS results def generate_bar_chart_fwts(suite_summary): + """Return a base64-encoded result-distribution chart.""" labels = ['Passed', 'Failed', 'Failed with Waiver', 'Aborted', 'Skipped', 'Warnings'] sizes = [ suite_summary['total_passed'], @@ -57,11 +70,11 @@ def generate_bar_chart_fwts(suite_summary): total_tests = sum(sizes) max_size = max(sizes) if sizes else 0 - for bar, size in zip(bars, sizes): - yval = bar.get_height() + for chart_bar, size in zip(bars, sizes): + yval = chart_bar.get_height() percentage = (size / total_tests) * 100 if total_tests > 0 else 0 plt.text( - bar.get_x() + bar.get_width()/2, + chart_bar.get_x() + chart_bar.get_width()/2, yval + (0.01 * max_size if max_size > 0 else 0.05), f'{percentage:.2f}%', ha='center', @@ -74,6 +87,7 @@ def generate_bar_chart_fwts(suite_summary): plt.xticks(fontsize=12) plt.yticks(fontsize=12) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) buffer = BytesIO() plt.savefig(buffer, format='png') @@ -81,8 +95,13 @@ def generate_bar_chart_fwts(suite_summary): buffer.seek(0) return base64.b64encode(buffer.getvalue()).decode('utf-8') -# Function to generate HTML content for both summary and detailed pages -def generate_html_fwts(suite_summary, test_results, chart_data, output_html_path, is_summary_page=True): +def generate_html_fwts( + suite_summary, + test_results, + chart_data, + output_html_path, + is_summary_page=True): + """Generate either the detailed or summary FWTS HTML report.""" # Jinja2 template with ONE "Reason" column + a fixed "Waiver Reason" column template = Template(r""" @@ -237,7 +256,13 @@ def generate_html_fwts(suite_summary, test_results, chart_data, output_html_path {% if not is_summary_page %}
{% for test in test_results %} -
Test Suite: {{ test.Test_suite }}
+ {% set ui = test.report_ui_summary %} +
+ Test Suite: {{ test.Test_suite }} +
Description: {{ test.Test_suite_description }}
@@ -309,6 +334,7 @@ def generate_html_fwts(suite_summary, test_results, chart_data, output_html_path total_tests = ( suite_summary["total_passed"] + suite_summary["total_failed"] + + suite_summary["total_failed_with_waiver"] + suite_summary["total_aborted"] + suite_summary["total_skipped"] + suite_summary["total_warnings"] @@ -327,11 +353,14 @@ def generate_html_fwts(suite_summary, test_results, chart_data, output_html_path is_summary_page=is_summary_page ) - with open(output_html_path, "w") as file: + html_content = enhance_html_report(html_content, suite_type="fwts") + with open(output_html_path, "w", encoding="utf-8") as file: file.write(html_content) -def main(input_json_file, detailed_html_file, summary_html_file): - with open(input_json_file, 'r') as json_file: + +def main(input_path, detailed_path, summary_path): + """Load FWTS JSON and write detailed and summary HTML reports.""" + with open(input_path, "r", encoding="utf-8") as json_file: data = json.load(json_file) suite_summary = { @@ -348,12 +377,18 @@ def main(input_json_file, detailed_html_file, summary_html_file): # Aggregate totals & detect columns for each suite for test_suite in test_results: ts_summary = test_suite.get('test_suite_summary', {}) - suite_summary['total_passed'] += get_case_insensitive(ts_summary, 'total_passed', ts_summary.get('total_passed', 0)) - suite_summary['total_failed'] += get_case_insensitive(ts_summary, 'total_failed', ts_summary.get('total_failed', 0)) - suite_summary['total_failed_with_waiver'] += get_case_insensitive(ts_summary, 'total_failed_with_waiver', ts_summary.get('total_failed_with_waiver', 0)) - suite_summary['total_aborted'] += get_case_insensitive(ts_summary, 'total_aborted', ts_summary.get('total_aborted', 0)) - suite_summary['total_skipped'] += get_case_insensitive(ts_summary, 'total_skipped', ts_summary.get('total_skipped', 0)) - suite_summary['total_warnings'] += get_case_insensitive(ts_summary, 'total_warnings', ts_summary.get('total_warnings', 0)) + report_ui_summary = {} + for summary_key in suite_summary: + summary_value = get_case_insensitive( + ts_summary, + summary_key, + ts_summary.get(summary_key, 0), + ) + suite_summary[summary_key] += summary_value + report_ui_summary[summary_key] = summary_value + + report_ui_summary["total_outcomes"] = sum(report_ui_summary.values()) + test_suite["report_ui_summary"] = report_ui_summary # Retain column detection for minimal code changes (but we always show Waiver anyway) subtests = test_suite.get("subtests", []) @@ -363,19 +398,29 @@ def main(input_json_file, detailed_html_file, summary_html_file): chart_data = generate_bar_chart_fwts(suite_summary) # Generate the detailed summary page - generate_html_fwts(suite_summary, test_results, chart_data, detailed_html_file, is_summary_page=False) + generate_html_fwts( + suite_summary, + test_results, + chart_data, + detailed_path, + is_summary_page=False, + ) # Generate the summary page - generate_html_fwts(suite_summary, test_results, chart_data, summary_html_file, is_summary_page=True) + generate_html_fwts( + suite_summary, + test_results, + chart_data, + summary_path, + is_summary_page=True, + ) if __name__ == "__main__": - import sys if len(sys.argv) != 4: - print("Usage: python fwts_generate_html.py ") + print( + "Usage: python fwts_generate_html.py " + " " + ) sys.exit(1) - input_json_file = sys.argv[1] - detailed_html_file = sys.argv[2] - summary_html_file = sys.argv[3] - - main(input_json_file, detailed_html_file, summary_html_file) + main(sys.argv[1], sys.argv[2], sys.argv[3]) diff --git a/common/log_parser/bbr/sct/json_to_html.py b/common/log_parser/bbr/sct/json_to_html.py index 7328ab30..a7e5a016 100644 --- a/common/log_parser/bbr/sct/json_to_html.py +++ b/common/log_parser/bbr/sct/json_to_html.py @@ -14,40 +14,55 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import matplotlib.pyplot as plt +"""Render BBSR SCT JSON results as detailed and summary HTML reports.""" + import base64 +import importlib +import json +import os +import sys from io import BytesIO -from jinja2 import Environment, FileSystemLoader, Template -# Helper function to retrieve dictionary values in a case-insensitive manner -def get_case_insensitive(d, key, default=0): - for k, v in d.items(): - if k.lower() == key.lower(): - return v +from jinja2 import Environment + +plt = importlib.import_module("matplotlib.pyplot") +sys.path.insert( + 0, + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), +) +enhance_html_report = importlib.import_module("report_ui").enhance_html_report + +def get_case_insensitive(data, key, default=0): + """Return a dictionary value without requiring exact key casing.""" + for candidate_key, value in data.items(): + if candidate_key.lower() == key.lower(): + return value return default -# Helper function to determine CSS class based on subtest result + def determine_css_class(subtest_result): + """Return the CSS class associated with a rendered SCT result.""" subtest_result_upper = subtest_result.upper() - if 'FAILED WITH WAIVER' in subtest_result_upper or 'FAILURE (WITH WAIVER)' in subtest_result_upper: + if ( + 'FAILED WITH WAIVER' in subtest_result_upper + or 'FAILURE (WITH WAIVER)' in subtest_result_upper): return 'fail-waiver' - elif 'FAILED' in subtest_result_upper: + if 'FAILED' in subtest_result_upper: return 'fail' - elif 'PASSED' in subtest_result_upper: + if 'PASSED' in subtest_result_upper: return 'pass' - elif 'WARNING' in subtest_result_upper: + if 'WARNING' in subtest_result_upper: return 'warning' - elif 'ABORTED' in subtest_result_upper: + if 'ABORTED' in subtest_result_upper: return 'aborted' - elif 'SKIPPED' in subtest_result_upper: + if 'SKIPPED' in subtest_result_upper: return 'skipped' - else: - # Anything else (IGNORED, KNOWN U-BOOT LIMITATION, etc.) is 'unknown' in the detailed table - return 'unknown' + # Anything else, including IGNORED, remains unknown in the detailed table. + return 'unknown' + -# Function to generate bar chart for SCT results with 'Failed with Waiver' and 'Ignored' def generate_bar_chart_improved(suite_summary): + """Return a base64-encoded SCT result-distribution chart.""" # Updated labels to include 'Failed with Waiver' AND 'Ignored' labels = [ 'Passed', @@ -83,11 +98,11 @@ def generate_bar_chart_improved(suite_summary): # Add percentage labels on top of each bar total_tests = sum(sizes) - for bar, size in zip(bars, sizes): - yval = bar.get_height() + for chart_bar, size in zip(bars, sizes): + yval = chart_bar.get_height() percentage = (size / total_tests) * 100 if total_tests > 0 else 0 plt.text( - bar.get_x() + bar.get_width()/2, + chart_bar.get_x() + chart_bar.get_width()/2, yval + max(sizes)*0.01, f'{percentage:.2f}%', ha='center', @@ -100,6 +115,7 @@ def generate_bar_chart_improved(suite_summary): plt.xticks(fontsize=12) plt.yticks(fontsize=12) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) # Save the figure to a buffer buffer = BytesIO() @@ -108,8 +124,13 @@ def generate_bar_chart_improved(suite_summary): buffer.seek(0) return base64.b64encode(buffer.getvalue()).decode('utf-8') -# Function to generate HTML content for both summary and detailed pages -def generate_html_improved(suite_summary, test_results, chart_data, output_html_path, is_summary_page=True): +def generate_html_improved( + suite_summary, + test_results, + chart_data, + output_html_path, + is_summary_page=True): + """Generate either the detailed or summary SCT HTML report.""" # Initialize Jinja2 environment env = Environment(autoescape=True) env.filters['determine_css_class'] = determine_css_class @@ -358,12 +379,15 @@ def generate_html_improved(suite_summary, test_results, chart_data, output_html_ is_summary_page=is_summary_page ) - with open(output_html_path, "w") as file: + html_content = enhance_html_report(html_content, suite_type="sct") + with open(output_html_path, "w", encoding="utf-8") as file: file.write(html_content) -def main(input_json_file, detailed_html_file, summary_html_file): + +def main(input_path, detailed_path, summary_path): + """Load SCT JSON and write detailed and summary HTML reports.""" # Load JSON data - with open(input_json_file, 'r') as json_file: + with open(input_path, "r", encoding="utf-8") as json_file: data = json.load(json_file) # We DIRECTLY take the final suite_summary from the JSON @@ -375,19 +399,29 @@ def main(input_json_file, detailed_html_file, summary_html_file): chart_data = generate_bar_chart_improved(suite_summary) # Generate the detailed summary page - generate_html_improved(suite_summary, test_results, chart_data, detailed_html_file, is_summary_page=False) + generate_html_improved( + suite_summary, + test_results, + chart_data, + detailed_path, + is_summary_page=False, + ) # Generate the summary page with the bar chart - generate_html_improved(suite_summary, test_results, chart_data, summary_html_file, is_summary_page=True) + generate_html_improved( + suite_summary, + test_results, + chart_data, + summary_path, + is_summary_page=True, + ) if __name__ == "__main__": - import sys if len(sys.argv) != 4: - print("Usage: python json_to_html.py ") + print( + "Usage: python json_to_html.py " + " " + ) sys.exit(1) - input_json_file = sys.argv[1] - detailed_html_file = sys.argv[2] - summary_html_file = sys.argv[3] - - main(input_json_file, detailed_html_file, summary_html_file) + main(sys.argv[1], sys.argv[2], sys.argv[3]) diff --git a/common/log_parser/bbr/tpm/json_to_html.py b/common/log_parser/bbr/tpm/json_to_html.py index 3e9a7e84..fe619132 100644 --- a/common/log_parser/bbr/tpm/json_to_html.py +++ b/common/log_parser/bbr/tpm/json_to_html.py @@ -14,12 +14,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import matplotlib.pyplot as plt +"""Render BBSR TPM JSON results as detailed and summary HTML reports.""" + import base64 +import importlib +import json +import os +import sys from io import BytesIO + from jinja2 import Environment +plt = importlib.import_module("matplotlib.pyplot") +sys.path.insert( + 0, + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), +) +enhance_html_report = importlib.import_module("report_ui").enhance_html_report + def determine_css_class(subtest_result): subtest_result_upper = subtest_result.upper() if 'FAILED WITH WAIVER' in subtest_result_upper or 'FAILURE (WITH WAIVER)' in subtest_result_upper: @@ -97,6 +109,7 @@ def generate_bar_chart_improved(suite_summary): plt.xticks(fontsize=12) plt.yticks(fontsize=12) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) buffer = BytesIO() plt.savefig(buffer, format='png') @@ -354,6 +367,7 @@ def generate_html_improved(suite_summary, test_results, chart_data, output_html_ is_summary_page=is_summary_page ) + html_content = enhance_html_report(html_content, suite_type="tpm") with open(output_html_path, "w", encoding="utf-8") as file: file.write(html_content) @@ -386,8 +400,6 @@ def main(input_json_file, detailed_html_file, summary_html_file): ) if __name__ == "__main__": - import sys - if len(sys.argv) != 4: print("Usage: python tpm_json_to_html.py ") sys.exit(1) diff --git a/common/log_parser/bsa/json_to_html.py b/common/log_parser/bsa/json_to_html.py index 9154c36f..f38a4a81 100755 --- a/common/log_parser/bsa/json_to_html.py +++ b/common/log_parser/bsa/json_to_html.py @@ -17,13 +17,17 @@ """Generate BSA/SBSA HTML reports from parsed JSON results.""" import base64 +import importlib import json import os import sys from io import BytesIO from jinja2 import Template -import matplotlib.pyplot as plt # pylint: disable=import-error + +plt = importlib.import_module("matplotlib.pyplot") +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +enhance_html_report = importlib.import_module("report_ui").enhance_html_report # Helper function to retrieve dictionary values in a case-insensitive manner def get_case_insensitive(data, key, default=0): @@ -108,6 +112,7 @@ def generate_bar_chart(suite_summary): plt.xticks(fontsize=11, rotation=30, ha='right') plt.yticks(fontsize=12) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) # Save the figure to a buffer buffer = BytesIO() @@ -117,7 +122,7 @@ def generate_bar_chart(suite_summary): return base64.b64encode(buffer.getvalue()).decode('utf-8') # Function to generate HTML content for both summary and detailed pages -def generate_html( # pylint: disable=too-many-arguments,too-many-positional-arguments +def generate_html( suite_summary, test_results, chart_data, @@ -825,6 +830,8 @@ def generate_html( # pylint: disable=too-many-arguments,too-many-positional-arg test_suite_name=test_suite_name.upper() # Ensure uppercase for consistency ) + html_content = enhance_html_report(html_content, suite_type="bsa") + # Save to HTML file with open(output_html_path, "w", encoding="utf-8") as file: file.write(html_content) diff --git a/common/log_parser/generate_acs_summary.py b/common/log_parser/generate_acs_summary.py index b0ded84d..03edc846 100644 --- a/common/log_parser/generate_acs_summary.py +++ b/common/log_parser/generate_acs_summary.py @@ -27,7 +27,101 @@ import html from jinja2 import Template +from report_ui import enhance_html_report + + +YOCTO_FLAG_PATH = "/mnt/yocto_image.flag" +LEGACY_SUITE_KEYS = {"Suite_Name: FWTS", "Suite_Name: SCT"} +OBSOLETE_DT_SUITE_KEYS = {"Suite_Name: BBR-FWTS", "Suite_Name: BBR-SCT"} + + +def _prefix_from_band(band): + """Map an ACS band label to its specification prefix, if recognized.""" + normalized_band = str(band).strip().lower() + if "devicetree" in normalized_band or "device tree" in normalized_band: + return "EBBR" + if "systemready" in normalized_band: + return "SBBR" + return "" + + +def get_report_suite_prefix(merged_json_path="", config_band=""): + """Return the externally visible specification prefix for this report.""" + if merged_json_path and os.path.isfile(merged_json_path): + try: + with open(merged_json_path, "r", encoding="utf-8") as merged_file: + merged_data = json.load(merged_file) + except (OSError, ValueError, AttributeError): + merged_data = {} + + if isinstance(merged_data, dict): + legacy_keys = sorted(LEGACY_SUITE_KEYS.intersection(merged_data)) + if legacy_keys: + raise ValueError( + "Legacy merged suite keys are not supported: " + + ", ".join(legacy_keys) + ) + + obsolete_dt_keys = sorted( + OBSOLETE_DT_SUITE_KEYS.intersection(merged_data) + ) + if obsolete_dt_keys: + raise ValueError( + "Obsolete DT merged suite keys are not supported; " + "use EBBR: " + ", ".join(obsolete_dt_keys) + ) + + suite_keys = merged_data.keys() + wrapper_prefixes = { + prefix + for prefix in ("SBBR", "EBBR") + if any( + f"Suite_Name: {prefix}-{suite}" in suite_keys + for suite in ("FWTS", "SCT") + ) + } + if len(wrapper_prefixes) > 1: + raise ValueError( + "Mixed EBBR/SBBR merged suite keys are not supported" + ) + + acs_info = merged_data.get("Suite_Name: acs_info", {}) + band_prefix = "" + for section_name in ("ACS Results Summary", "System Info"): + prefix = _prefix_from_band( + acs_info.get(section_name, {}).get("Band", "") + ) + if prefix: + if band_prefix and band_prefix != prefix: + raise ValueError( + "Conflicting Band values in merged acs_info" + ) + band_prefix = prefix + + wrapper_prefix = next(iter(wrapper_prefixes), "") + if ( + wrapper_prefix + and band_prefix + and wrapper_prefix != band_prefix + ): + raise ValueError( + f"Merged {wrapper_prefix} suite keys do not match " + f"the {band_prefix} Band" + ) + if wrapper_prefix: + return wrapper_prefix + if band_prefix: + return band_prefix + + prefix = _prefix_from_band(config_band) + if prefix: + return prefix + + return "EBBR" if os.path.isfile(YOCTO_FLAG_PATH) else "SBBR" + + def get_system_info(): + """Collect fallback firmware and platform information from the host.""" system_info = {} # Get Firmware Version @@ -92,10 +186,11 @@ def get_system_info(): return system_info def parse_config(config_path): + """Read colon-separated values from an ACS configuration file.""" config_info = {} try: if config_path and os.path.exists(config_path): - with open(config_path, 'r') as config_file: + with open(config_path, "r", encoding="utf-8") as config_file: for line in config_file: # Stop parsing at the user-defined configs section if line.strip().startswith('# User-defined configs'): @@ -105,11 +200,12 @@ def parse_config(config_path): config_info[key.strip()] = value.strip() else: print(f"Config file {config_path} not provided or does not exist.") - except Exception as e: - print(f"Error reading {config_path}: {e}") + except Exception as error: + print(f"Error reading {config_path}: {error}") return config_info def get_uefi_version(uefi_version_log): + """Read the UEFI version from its UTF-16 log when available.""" uefi_version = 'Unknown' try: if uefi_version_log and os.path.exists(uefi_version_log): @@ -120,8 +216,8 @@ def get_uefi_version(uefi_version_log): break else: uefi_version = 'Not provided' - except Exception as e: - print(f"Error reading UEFI version log: {e}") + except Exception as error: + print(f"Error reading UEFI version log: {error}") return uefi_version def read_acs_info_system_info(acs_info_json_path): @@ -129,8 +225,8 @@ def read_acs_info_system_info(acs_info_json_path): if not acs_info_json_path or not os.path.exists(acs_info_json_path): return {} try: - with open(acs_info_json_path, "r") as jf: - data = json.load(jf) + with open(acs_info_json_path, "r", encoding="utf-8") as json_file: + data = json.load(json_file) return data.get("System Info", {}) if isinstance(data, dict) else {} except Exception: return {} @@ -164,15 +260,33 @@ def build_system_info(acs_config_path, system_config_path, uefi_version_log, return system_info, summary_generated_date, acs_config_info.get('Band', 'Unknown') def remove_result_summary_headings(content): + """Remove nested result-summary headings before suite HTML is embedded.""" # Use regular expressions to remove any heading containing 'Result Summary' pattern = r']*>\s*Result Summary\s*' content = re.sub(pattern, '', content, flags=re.IGNORECASE) return content def read_html_content(file_path): + """Return sanitized body content from a generated suite summary.""" if file_path and os.path.exists(file_path): - with open(file_path, 'r') as file: + with open(file_path, 'r', encoding='utf-8') as file: content = file.read() + # Suite summaries are complete standalone documents. Embed only + # their body markup in the consolidated report so nested , + # CSS, and interaction scripts cannot leak into the parent page. + body_match = re.search( + r']*>(.*?)', + content, + flags=re.IGNORECASE | re.DOTALL, + ) + if body_match: + content = body_match.group(1) + content = re.sub( + r']*>.*?', + '', + content, + flags=re.IGNORECASE | re.DOTALL, + ) # Remove 'Result Summary' headings content = remove_result_summary_headings(content) return content @@ -180,28 +294,32 @@ def read_html_content(file_path): return None def inject_test_suite_info(merged_json_path, output_dir): + """Inject suite descriptions from merged JSON into detailed reports.""" # Add Test_suite_info into detailed HTMLs after they are generated. if not merged_json_path or not os.path.isfile(merged_json_path): return try: - with open(merged_json_path, 'r') as jf: - data = json.load(jf) + with open(merged_json_path, "r", encoding="utf-8") as json_file: + data = json.load(json_file) except Exception: return - def entries(v): + def entries(value): # Always return a list of suite entries. - if isinstance(v, list): - return v - if isinstance(v, dict) and isinstance(v.get("test_results"), list): - return v["test_results"] - if isinstance(v, dict): - return [v] + if isinstance(value, list): + return value + if isinstance(value, dict) and isinstance(value.get("test_results"), list): + return value["test_results"] + if isinstance(value, dict): + return [value] return [] suite_map = {} if isinstance(data, dict): for suite_key, suite_data in data.items(): + map_key = suite_key + if suite_key.lower().startswith("suite_name: os tests -"): + map_key = "Suite_Name: OS Tests" for entry in entries(suite_data): if not isinstance(entry, dict): continue @@ -209,7 +327,7 @@ def entries(v): info = entry.get("Test_suite_info") if name and info is not None: # Store by lowercase name so PCIe/PCIE still matches. - suite_map.setdefault(suite_key, {})[name.lower()] = info + suite_map.setdefault(map_key, {})[name.lower()] = info def fmt_info(info): # Format list info as bullets; otherwise keep as plain text. @@ -229,8 +347,14 @@ def fmt_info(info): # All detailed HTML files that should get Test_suite_info. ("bsa_detailed.html", "Suite_Name: BSA"), ("sbsa_detailed.html", "Suite_Name: SBSA"), - ("fwts_detailed.html", "Suite_Name: FWTS"), - ("sct_detailed.html", "Suite_Name: SCT"), + ("fwts_detailed.html", ( + "Suite_Name: SBBR-FWTS", + "Suite_Name: EBBR-FWTS", + )), + ("sct_detailed.html", ( + "Suite_Name: SBBR-SCT", + "Suite_Name: EBBR-SCT", + )), ("bbsr_fwts_detailed.html", "Suite_Name: BBSR-FWTS"), ("bbsr_sct_detailed.html", "Suite_Name: BBSR-SCT"), ("bbsr_tpm_detailed.html", "Suite_Name: BBSR-TPM"), @@ -243,16 +367,26 @@ def fmt_info(info): ("os_tests_detailed.html", "Suite_Name: OS Tests"), ] - for filename, suite_key in files: - info_map = suite_map.get(suite_key) + for filename, suite_keys in files: + if isinstance(suite_keys, str): + suite_keys = (suite_keys,) + info_map = next( + (suite_map[key] for key in suite_keys if suite_map.get(key)), + None, + ) if not info_map: continue file_path = os.path.join(output_dir, filename) if not os.path.exists(file_path): continue - with open(file_path, 'r') as file: + with open(file_path, "r", encoding="utf-8") as file: content = file.read() - updated = content + updated = re.sub( + r"()Test_suite_info:()", + r"\1Test suite info:\2", + content, + flags=re.IGNORECASE, + ) for pattern, name_group in patterns: out = [] last = 0 @@ -263,11 +397,11 @@ def fmt_info(info): if info is not None: # Avoid duplicating Test_suite_info in the same section. lookahead = updated[match.end():match.end() + 300] - if "Test_suite_info" not in lookahead: + if not re.search(r"Test[_ ]suite[_ ]info", lookahead, re.IGNORECASE): block = ( "
" - "Test_suite_info:" + "Test suite info:" f"{fmt_info(info)}
" ) out.append(block) @@ -275,24 +409,76 @@ def fmt_info(info): out.append(updated[last:]) updated = "".join(out) if updated != content: - with open(file_path, 'w') as file: + with open(file_path, "w", encoding="utf-8") as file: file.write(updated) def adjust_bbsr_headings(content, suite_name): + """Replace a generic BBSR heading with the selected suite name.""" if content: pattern = r'(]*>)(.*? Test Summary)()' replacement = r'\1' + suite_name + r' Test Summary\3' content = re.sub(pattern, replacement, content, count=1, flags=re.IGNORECASE) return content -def adjust_detailed_summary_heading(file_path, suite_name): + +def adjust_suite_headings(content, suite_name): + """Update the first test-summary heading and document title.""" + content = adjust_bbsr_headings(content, suite_name) + if content: + title_pattern = r'()(.*? Test Summary)()' + content = re.sub( + title_pattern, + r'\1' + suite_name + r' Test Summary\3', + content, + count=1, + flags=re.IGNORECASE, + ) + return content + + +def adjust_detailed_summary_heading(file_path, suite_name, adjust_title=False): + """Update the first test-summary heading in an existing report.""" if file_path and os.path.exists(file_path): - with open(file_path, 'r') as file: + with open(file_path, "r", encoding="utf-8") as file: content = file.read() - content = adjust_bbsr_headings(content, suite_name) - with open(file_path, 'w') as file: + if adjust_title: + content = adjust_suite_headings(content, suite_name) + else: + content = adjust_bbsr_headings(content, suite_name) + with open(file_path, "w", encoding="utf-8") as file: file.write(content) + +def link_detailed_reports_to_main(output_html_path): + """Mark detailed reports that have a generated main summary page.""" + output_dir = os.path.dirname(output_html_path) or "." + main_page = html.escape(os.path.basename(output_html_path), quote=True) + for filename in sorted(os.listdir(output_dir)): + if not filename.endswith("_detailed.html"): + continue + file_path = os.path.join(output_dir, filename) + with open(file_path, "r", encoding="utf-8") as html_file: + content = html_file.read() + main_page_attribute = re.compile( + r"\bdata-acs-main-page\s*=\s*([\"']).*?\1", + re.IGNORECASE, + ) + if main_page_attribute.search(content): + content, replacements = main_page_attribute.subn( + f'data-acs-main-page="{main_page}"', content, count=1 + ) + else: + content, replacements = re.subn( + r"SBSA Summary {% endif %} {% if fwts_summary_content %} - FWTS Summary + {{ fwts_suite_name }} Summary {% endif %} {% if sct_summary_content %} - SCT Summary + {{ sct_suite_name }} Summary {% endif %} {% if scmi_summary_content %} SCMI Summary @@ -792,7 +993,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -800,7 +1001,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -808,7 +1009,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -816,7 +1017,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -824,7 +1025,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -832,7 +1033,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -840,7 +1041,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -848,7 +1049,7 @@ def generate_html(system_info, acs_results_summary,
{{ post_script_summary_content | safe }}
{% endif %} @@ -856,7 +1057,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -864,7 +1065,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -872,7 +1073,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -880,7 +1081,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -888,7 +1089,7 @@ def generate_html(system_info, acs_results_summary,
{{ pfdi_summary_content | safe }} @@ -898,7 +1099,7 @@ def generate_html(system_info, acs_results_summary, {% endif %} @@ -925,13 +1126,38 @@ def generate_html(system_info, acs_results_summary, pfdi_summary_content=pfdi_summary_content, post_script_summary_content=post_script_summary_content, standalone_summary_content=standalone_summary_content, - OS_tests_summary_content=OS_tests_summary_content + OS_tests_summary_content=os_tests_summary_content, + fwts_suite_name=fwts_suite_name, + sct_suite_name=sct_suite_name ) - with open(output_html_path, 'w') as html_file: + html_output = enhance_html_report(html_output, page_type="acs-summary") + with open(output_html_path, 'w', encoding='utf-8') as html_file: html_file.write(html_output) - # Adjust headings in the *detailed* summary pages + # Keep the individual summary and detailed pages consistent with the + # headings embedded in acs_summary.html. + prefixed_summaries = [ + (fwts_summary_path, fwts_suite_name), + (sct_summary_path, sct_suite_name), + ( + os.path.join( + os.path.dirname(output_html_path), "fwts_detailed.html" + ), + fwts_suite_name, + ), + ( + os.path.join( + os.path.dirname(output_html_path), "sct_detailed.html" + ), + sct_suite_name, + ), + ] + for file_path, suite_name in prefixed_summaries: + adjust_detailed_summary_heading( + file_path, suite_name, adjust_title=True + ) + detailed_summaries = [ (os.path.join(os.path.dirname(output_html_path), 'bbsr_fwts_detailed.html'), 'BBSR-FWTS'), (os.path.join(os.path.dirname(output_html_path), 'bbsr_sct_detailed.html'), 'BBSR-SCT'), @@ -945,6 +1171,7 @@ def generate_html(system_info, acs_results_summary, ] for file_path, suite_name in detailed_summaries: adjust_detailed_summary_heading(file_path, suite_name) + link_detailed_reports_to_main(output_html_path) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Generate ACS Summary HTML page") @@ -985,6 +1212,10 @@ def generate_html(system_info, acs_results_summary, args.acs_info_json, args.use_acs_info_system_info, ) + suite_prefix = get_report_suite_prefix( + args.merged_json, + summary_band, + ) # 5) Read in the stand-alone & capsule summary, then combine them standalone_summary_content = read_html_content(args.standalone_summary_path) @@ -1000,8 +1231,8 @@ def generate_html(system_info, acs_results_summary, suite_content_map = { "BSA": read_html_content(args.bsa_summary_path), "SBSA": read_html_content(args.sbsa_summary_path), - "FWTS": read_html_content(args.fwts_summary_path), - "SCT": read_html_content(args.sct_summary_path), + f"{suite_prefix}-FWTS": read_html_content(args.fwts_summary_path), + f"{suite_prefix}-SCT": read_html_content(args.sct_summary_path), "SCMI": read_html_content(args.scmi_summary_path), "SBMR-IB": read_html_content(args.sbmr_ib_summary_path), "SBMR-OOB": read_html_content(args.sbmr_oob_summary_path), @@ -1058,7 +1289,8 @@ def generate_html(system_info, acs_results_summary, args.post_script_summary_path, args.standalone_summary_path, args.OS_tests_summary_path, - args.output_html_path + args.output_html_path, + suite_prefix ) # Inject Test_suite_info into detailed HTMLs (no change to suite parsers) diff --git a/common/log_parser/merge_jsons.py b/common/log_parser/merge_jsons.py index f8c867fb..498dff44 100755 --- a/common/log_parser/merge_jsons.py +++ b/common/log_parser/merge_jsons.py @@ -59,6 +59,23 @@ else: DT_OR_SR_MODE = "SR" + +def get_report_suite_prefix(): + """Return the externally visible specification prefix for the active mode.""" + return "EBBR" if DT_OR_SR_MODE == "DT" else "SBBR" + + +_BAND_COMPLIANCE_SUITES = ("FWTS", "SCT") +_BAND_COMPLIANCE_PREFIXES = ("", "BBR-", "EBBR-", "SBBR-") + + +def get_report_suite_name(suite_name: str) -> str: + """Return the band-aware display name while retaining canonical internals.""" + if suite_name in _BAND_COMPLIANCE_SUITES: + return f"{get_report_suite_prefix()}-{suite_name}" + return suite_name + + ################################################################################ # 2. Define Mandatory Suites based on your table # (Recommended suites are simply "not in this list"; they won't affect compliance.) @@ -87,8 +104,21 @@ def compliance_label(suite_name: str) -> str: tag = "Extension" else: tag = "Recommended" + label_suite_name = get_report_suite_name(suite_name) # Match the console ordering: “Suite: : …” - return f"Suite_Name: {tag} : {suite_name}_compliance" + return f"Suite_Name: {tag} : {label_suite_name}_compliance" + + +def clear_band_compliance_labels(acs_results_summary): + """Remove stale FWTS/SCT labels before rebuilding the active-band summary.""" + suffixes = tuple( + f" : {prefix}{suite_name}_compliance" + for prefix in _BAND_COMPLIANCE_PREFIXES + for suite_name in _BAND_COMPLIANCE_SUITES + ) + for key in list(acs_results_summary): + if key.startswith("Suite_Name: ") and key.endswith(suffixes): + del acs_results_summary[key] def normalize_suite_name(suite_name: str) -> str: return registry_normalize_suite_name(suite_name) or suite_name @@ -359,7 +389,7 @@ def merge_json_files(json_files, output_file): section_name = "Suite_Name: BBSR-FWTS" suite_key = "BBSR-FWTS" elif "FWTS" in fn: - section_name = "Suite_Name: FWTS" + section_name = f"Suite_Name: {get_report_suite_prefix()}-FWTS" suite_key = "FWTS" elif "BBSR" in fn and "SCT" in fn: section_name = "Suite_Name: BBSR-SCT" @@ -368,7 +398,7 @@ def merge_json_files(json_files, output_file): section_name = "Suite_Name: BBSR-TPM" suite_key = "BBSR-TPM" elif "SCT" in fn: - section_name = "Suite_Name: SCT" + section_name = f"Suite_Name: {get_report_suite_prefix()}-SCT" suite_key = "SCT" elif "SBMR_IB" in fn or "SBMR-IB" in fn or "sbmr_ib" in base_lower: section_name = "Suite_Name: SBMR-IB" @@ -454,6 +484,10 @@ def merge_json_files(json_files, output_file): } if lookup_suite_key in standalone_aliases or lookup_suite_key.startswith("os_"): lookup_suite_key = "standalone" + # The TPM event-log report is represented by the Measured boot log row + # under BBSR-Standalone in test_categoryDT.json. + if lookup_suite_key == "bbsr-tpm": + lookup_suite_key = "bbsr-standalone" if lookup_suite_key in ("sbmr-ib", "sbmr-oob"): lookup_suite_key = "sbmr" if lookup_suite_key in test_cat_dict: @@ -463,7 +497,13 @@ def merge_json_files(json_files, output_file): if not isinstance(ts_dict, dict): continue - ts_name_merged = (ts_dict.get("Test_suite") or ts_dict.get("Test_suite_name") or "").strip().lower() + ts_name_merged = ( + ts_dict.get("Test_suite") + or ts_dict.get("Test_suite_name") + or "" + ).strip().lower() + if suite_key.lower() == "bbsr-tpm" and ts_name_merged == "bbsr-tpm": + ts_name_merged = "measured boot log" if ts_name_merged in test_cat_dict[lookup_suite_key]: row_vals = test_cat_dict[lookup_suite_key][ts_name_merged] if "Waivable" in row_vals: @@ -553,32 +593,35 @@ def merge_json_files(json_files, output_file): else: acs_results_summary = merged_results["Suite_Name: acs_info"].get("ACS Results Summary", {}) + clear_band_compliance_labels(acs_results_summary) + for suite_name, requirement in mandatory_suites: + report_suite_name = get_report_suite_name(suite_name) if suite_name not in suite_fail_data: label = compliance_label(suite_name) if requirement == "M": acs_results_summary[label] = "Not Compliant: not run" - print(f"{RED}Suite: Mandatory : {suite_name}: {acs_results_summary[label]}{RESET}") + print(f"{RED}Suite: Mandatory : {report_suite_name}: {acs_results_summary[label]}{RESET}") overall_comp = "Not Compliant" - mandatory_missing_list.append(suite_name) + mandatory_missing_list.append(report_suite_name) elif requirement == "CM": acs_results_summary[label] = "Not Run" - print(f"Suite: Conditional-Mandatory : {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Conditional-Mandatory : {report_suite_name}: {acs_results_summary[label]}") #overall_comp = "Not Compliant" #mandatory_missing_list.append(suite_name) elif requirement == "EM": acs_results_summary[label] = "Not Run" - print(f"Suite: Extension : {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Extension : {report_suite_name}: {acs_results_summary[label]}") else: if DT_OR_SR_MODE == "DT": acs_results_summary[label] = "Not Compliant: not run" - print(f"{RED}Suite: Recommended: {suite_name}: {acs_results_summary[label]}{RESET}") + print(f"{RED}Suite: Recommended: {report_suite_name}: {acs_results_summary[label]}{RESET}") overall_comp = "Not Compliant" - recommended_missing_list.append(suite_name) + recommended_missing_list.append(report_suite_name) else: acs_results_summary[label] = "Not Run" - print(f"Suite: Recommended: {suite_name}: {acs_results_summary[label]}") - recommended_missing_list.append(suite_name) + print(f"Suite: Recommended: {report_suite_name}: {acs_results_summary[label]}") + recommended_missing_list.append(report_suite_name) else: fail_info = suite_fail_data.get(suite_name) f = fail_info.get("Failed", 0) @@ -588,42 +631,42 @@ def merge_json_files(json_files, output_file): acs_results_summary[label] = "Compliant" if requirement in ("M", "CM"): if requirement == "M": - print(f"Suite: Mandatory : {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Mandatory : {report_suite_name}: {acs_results_summary[label]}") else: - print(f"Suite: Conditional-Mandatory : {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Conditional-Mandatory : {report_suite_name}: {acs_results_summary[label]}") elif requirement == "EM": - print(f"Suite: Extension : {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Extension : {report_suite_name}: {acs_results_summary[label]}") else: - print(f"Suite: Recommended: {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Recommended: {report_suite_name}: {acs_results_summary[label]}") elif f == 0 and fw > 0: acs_results_summary[label] = f"Compliant with waivers: Waivers {fw}" if requirement in ("M", "CM"): if requirement == "M": - print(f"Suite: Mandatory : {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Mandatory : {report_suite_name}: {acs_results_summary[label]}") else: - print(f"Suite: Conditional-Mandatory : {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Conditional-Mandatory : {report_suite_name}: {acs_results_summary[label]}") elif requirement == "EM": - print(f"Suite: Extension : {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Extension : {report_suite_name}: {acs_results_summary[label]}") else: - print(f"Suite: Recommended: {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Recommended: {report_suite_name}: {acs_results_summary[label]}") if requirement in ("M", "CM") and overall_comp != "Not Compliant": overall_comp="Compliant with waivers" else: acs_results_summary[label] = f"Not Compliant: Failed {f}" if requirement in ("M", "CM"): if requirement == "M": - print(f"{RED}Suite: Mandatory : {suite_name}: {acs_results_summary[label]}{RESET}") + print(f"{RED}Suite: Mandatory : {report_suite_name}: {acs_results_summary[label]}{RESET}") else: - print(f"{RED}Suite: Conditional-Mandatory : {suite_name}: {acs_results_summary[label]}{RESET}") + print(f"{RED}Suite: Conditional-Mandatory : {report_suite_name}: {acs_results_summary[label]}{RESET}") overall_comp="Not Compliant" - mandatory_non_waived_list.append(suite_name) + mandatory_non_waived_list.append(report_suite_name) elif requirement == "EM": - print(f"Suite: Extension : {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Extension : {report_suite_name}: {acs_results_summary[label]}") else: - print(f"Suite: Recommended: {suite_name}: {acs_results_summary[label]}") + print(f"Suite: Recommended: {report_suite_name}: {acs_results_summary[label]}") if _SELECTED_SUITE_FILTER is not None: overall_comp = "Not Compliant" - recommended_non_waived_list.append(suite_name) + recommended_non_waived_list.append(report_suite_name) #Ensure suite-wise compliance lines for *all* discovered suites (including recommended) for skey, info in suite_fail_data.items(): diff --git a/common/log_parser/os_tests/json_to_html.py b/common/log_parser/os_tests/json_to_html.py index 43da1321..ea33131e 100644 --- a/common/log_parser/os_tests/json_to_html.py +++ b/common/log_parser/os_tests/json_to_html.py @@ -14,14 +14,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import matplotlib.pyplot as plt +import argparse import base64 +import importlib +import json +import os +import sys from io import BytesIO + from jinja2 import Template -import sys -import argparse -import os + +plt = importlib.import_module("matplotlib.pyplot") +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +enhance_html_report = importlib.import_module("report_ui").enhance_html_report def detect_columns_used(subtests): """ @@ -103,6 +108,7 @@ def generate_bar_chart(suite_summary, show_extended=False): plt.title('OS Test Results', fontsize=16, fontweight='bold', pad=20) plt.ylabel('Number of Tests', fontsize=14) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) # Save the figure to a buffer buffer = BytesIO() @@ -440,6 +446,8 @@ def generate_html(suite_summary, test_results_list, output_html_path, is_summary get_subtest_status=get_subtest_status # Pass the function to the template ) + html_content = enhance_html_report(html_content, suite_type="os") + # Save to HTML file with open(output_html_path, "w") as file: file.write(html_content) diff --git a/common/log_parser/pfdi/json_to_html.py b/common/log_parser/pfdi/json_to_html.py index c9b44122..b489f45c 100644 --- a/common/log_parser/pfdi/json_to_html.py +++ b/common/log_parser/pfdi/json_to_html.py @@ -14,12 +14,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json, base64, os, sys +"""Render PFDI JSON results as detailed and summary HTML reports.""" + +import base64 +import importlib +import json +import sys from io import BytesIO from pathlib import Path -import matplotlib.pyplot as plt + from jinja2 import Template +plt = importlib.import_module("matplotlib.pyplot") +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +enhance_html_report = importlib.import_module("report_ui").enhance_html_report + def generate_bar_chart(summary_dict): labels = ["Passed", "Failed", "Failed with Waiver", @@ -50,6 +59,7 @@ def generate_bar_chart(summary_dict): plt.xticks(fontsize=12) plt.yticks(fontsize=12) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) buf = BytesIO() plt.savefig(buf, format="png") @@ -160,7 +170,9 @@ def build_html(overall_summary, test_results, chart_b64, **overall_summary ) - Path(dest_html).write_text(html, encoding="utf-8") + Path(dest_html).write_text( + enhance_html_report(html, suite_type="pfdi"), encoding="utf-8" + ) # ----------------------------- main script ----------------------------- # diff --git a/common/log_parser/post_script/json_to_html.py b/common/log_parser/post_script/json_to_html.py index dd5ac612..56608f71 100644 --- a/common/log_parser/post_script/json_to_html.py +++ b/common/log_parser/post_script/json_to_html.py @@ -14,12 +14,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import matplotlib.pyplot as plt +"""Render post-script JSON results as detailed and summary HTML reports.""" + import base64 +import importlib +import json +import os +import sys from io import BytesIO + from jinja2 import Template -import sys + +plt = importlib.import_module("matplotlib.pyplot") +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +enhance_html_report = importlib.import_module("report_ui").enhance_html_report # Helper function for case-insensitive dictionary get def get_case_insensitive(d, key, default=0): @@ -67,6 +75,7 @@ def generate_bar_chart(suite_summary): plt.xticks(fontsize=12) plt.yticks(fontsize=12) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) # Convert plot to base64 buffer = BytesIO() @@ -329,6 +338,7 @@ def generate_html(suite_summary, test_results, chart_data, output_html_path, is_ chart_data=chart_data ) + html_content = enhance_html_report(html_content, suite_type="post-script") with open(output_html_path, "w", encoding="utf-8") as f: f.write(html_content) diff --git a/common/log_parser/report_ui.py b/common/log_parser/report_ui.py new file mode 100644 index 00000000..a3adb319 --- /dev/null +++ b/common/log_parser/report_ui.py @@ -0,0 +1,3660 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026, Arm Limited or its affiliates. All rights reserved. +# SPDX-License-Identifier : Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Apply the shared, self-contained UI to generated ACS HTML reports. + +The parser's suite renderers intentionally retain ownership of their result +calculations and table markup. This module only adds presentation CSS and +progressive-enhancement JavaScript to the rendered HTML string. Reports stay +readable when JavaScript is disabled and remain single-file artifacts. +""" + +import re + + +UI_MARKER = 'data-acs-report-ui="2026.4"' +VALID_PAGE_TYPES = {"suite", "acs-summary"} +VALID_SUITE_TYPES = { + "bsa", + "fwts", + "os", + "pfdi", + "post-script", + "sbmr", + "scmi", + "sct", + "standalone", + "tpm", +} + + +def center_matplotlib_plot(axis): + """Center a Matplotlib plotting rectangle within its figure canvas.""" + position = axis.get_position() + side_margin = max(position.x0, 1.0 - position.x1) + axis.set_position([ + side_margin, + position.y0, + 1.0 - (2.0 * side_margin), + position.height, + ]) + + +REPORT_CSS = r""" +/* Shared SystemReady ACS report UI. Keep this self-contained for offline use. */ +:root { + --acs-bg: #f3f6fb; + --acs-panel: #ffffff; + --acs-panel-muted: #f8fafc; + --acs-text: #172033; + --acs-heading: #102a4c; + --acs-muted: #667085; + --acs-line: #d8e1ed; + --acs-brand: #163b68; + --acs-brand-strong: #0f2c50; + --acs-brand-soft: #edf4ff; + --acs-focus: #1769e0; + --acs-shadow: 0 8px 26px rgba(16, 42, 76, .07); + --acs-pass-bg: #dcfce7; + --acs-pass-text: #166534; + --acs-fail-bg: #fee2e2; + --acs-fail-text: #991b1b; + --acs-warn-bg: #ffedd5; + --acs-warn-text: #9a3412; + --acs-partial-bg: #e0f2fe; + --acs-partial-text: #075985; + --acs-neutral-bg: #eef2f7; + --acs-neutral-text: #475467; + --acs-info-bg: #e8efff; + --acs-info-text: #344e7a; + --acs-suite-accent: #175cd3; + --acs-suite-soft: #edf4ff; + --acs-suite-border: #bfd3f2; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body.acs-report-ui { + margin: 0 !important; + padding: 24px !important; + background: var(--acs-bg) !important; + color: var(--acs-text) !important; + font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif !important; + font-size: 14px !important; + line-height: 1.45 !important; +} +body.acs-report-ui > * { max-width: 1540px; margin-left: auto; margin-right: auto; } +body.acs-report-ui > .header { + max-width: 1540px; + padding: 22px 26px !important; + margin-bottom: 16px !important; + border-radius: 16px; + background: linear-gradient(118deg, var(--acs-brand-strong), var(--acs-brand)) !important; + box-shadow: var(--acs-shadow); + color: #fff !important; + font-size: 26px !important; + font-weight: 750 !important; + letter-spacing: -.02em; + text-align: left !important; +} +body.acs-report-ui > .container { + width: auto !important; + max-width: 1540px !important; + margin-top: 0 !important; + padding: 0 !important; + background: transparent !important; + border-radius: 0 !important; + box-shadow: none !important; +} +body.acs-report-ui h1, +body.acs-report-ui h2, +body.acs-report-ui h3 { + color: var(--acs-heading) !important; + text-align: left !important; +} +body.acs-report-ui h1 { + margin: 0 0 4px !important; + padding: 0 !important; + font-size: clamp(24px, 3vw, 31px) !important; + line-height: 1.2 !important; + letter-spacing: -.025em; +} +body.acs-report-ui > h1 { + margin-left: auto !important; + margin-right: auto !important; +} +body.acs-report-ui h2 { margin: 0 0 14px !important; font-size: 19px !important; } +body.acs-report-ui h3 { font-size: 16px !important; } +body.acs-report-ui a { color: #175cd3; } +body.acs-report-ui a:hover { color: #0b4db3; } +body.acs-report-ui button, +body.acs-report-ui input, +body.acs-report-ui select { font: inherit; } +body.acs-report-ui button:focus-visible, +body.acs-report-ui input:focus-visible, +body.acs-report-ui select:focus-visible, +body.acs-report-ui a:focus-visible { + outline: 3px solid color-mix(in srgb, var(--acs-focus) 35%, transparent); + outline-offset: 2px; +} + +.acs-report-kicker { + margin: 0 0 6px; + color: var(--acs-suite-accent); + font-size: 11px; + font-weight: 800; + letter-spacing: .09em; + text-transform: uppercase; +} +.acs-report-subtitle { + margin: 0 0 16px; + color: var(--acs-muted); + font-size: 13px; +} +.acs-report-ui[data-acs-view="detail"] .acs-report-kicker { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; +} +.acs-back-to-main { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 36px; + padding: 7px 11px; + color: var(--acs-brand) !important; + background: var(--acs-panel); + border: 1px solid #bfd0e5; + border-radius: 9px; + box-shadow: 0 3px 10px rgba(16, 42, 76, .05); + font-size: 12px; + font-weight: 750; + letter-spacing: normal; + margin-left: auto; + text-decoration: none !important; + text-transform: none; + white-space: nowrap; +} +.acs-back-to-main:hover { + color: var(--acs-brand) !important; + background: var(--acs-brand-soft); +} +.acs-report-ui[data-acs-report-kind="acs-summary"] .acs-report-kicker, +.acs-report-ui[data-acs-report-kind="acs-summary"] .acs-report-subtitle { display: none; } + +.chart-container { + display: grid !important; + place-items: center; + margin: 0 !important; + padding: 8px !important; + overflow: auto; + background: var(--acs-panel) !important; +} +.chart-container img { display: block; max-width: 100%; max-height: 300px; height: auto; object-fit: contain; } +body.acs-report-ui[data-acs-view="detail"] .chart-container, +body.acs-report-ui:has(.detailed-summary, .detailed-container) .chart-container { + display: none !important; +} +.result-summary .acs-reference-note { + margin: 8px 0 !important; + padding: 8px 10px; + text-align: left !important; + color: var(--acs-muted); + background: var(--acs-panel-muted); + border-left: 3px solid var(--acs-suite-accent); + border-radius: 7px; + font-size: 10.5px; + font-weight: 600 !important; +} +.result-summary .acs-reference-note br { display: none; } +.result-summary .acs-reference-note a { margin-left: 4px; font-weight: 750; } + +.result-summary, +.summary-container, +.system-info, +.acs-results-summary, +.summary-section, +.summary { + margin: 14px 0 !important; + padding: 16px !important; + background: var(--acs-panel) !important; + border: 1px solid var(--acs-line) !important; + border-radius: 15px !important; + box-shadow: var(--acs-shadow) !important; +} +.result-summary h2, +.summary-container h2, +.system-info h2, +.acs-results-summary h2 { + padding: 0 !important; + border: 0 !important; +} +body.acs-report-ui[data-acs-report-kind="acs-summary"] .acs-overview-source { + display: none !important; +} +.acs-summary-overview { margin: 14px 0 18px; } +.acs-summary-overview > .acs-information-overview, +.acs-overview-results > .acs-overview-result-card { + margin: 0 !important; + padding: 16px !important; +} +.acs-overview-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin: 0 0 12px; +} +.acs-report-ui .acs-overview-heading h2 { + margin: 0 !important; + padding: 0 !important; + color: var(--acs-heading); + border: 0 !important; + font-size: 20px !important; +} +.acs-overview-field-count { + color: var(--acs-muted); + font-size: 11px; + font-weight: 650; + white-space: nowrap; +} +.acs-information-band { + display: grid; + grid-template-columns: 120px minmax(0, 1fr); + gap: 14px; + padding: 7px 0; + border-top: 1px solid #e6edf5; +} +.acs-information-band:first-child { border-top: 0; } +.acs-report-ui .acs-information-band h3 { + display: flex; + align-items: center; + margin: 0 !important; + padding: 9px 10px !important; + color: #fff !important; + background: var(--acs-brand); + border: 0 !important; + border-radius: 8px; + font-size: 10.5px !important; + font-weight: 850; + letter-spacing: .07em; + text-transform: uppercase; +} +.acs-information-values { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px 14px; + margin: 0; +} +.acs-information-field { min-width: 0; } +.acs-information-field dt { + min-width: 0; + margin: 0 0 2px; + color: var(--acs-muted); + font-size: 10.5px; + font-weight: 850; + letter-spacing: .045em; + text-transform: uppercase; + overflow-wrap: anywhere; +} +.acs-information-field dd { + margin: 0; + color: var(--acs-text); + font-size: 12.5px; + font-weight: 650; + line-height: 1.4; + overflow-wrap: anywhere; +} +.acs-information-field[data-acs-unknown="true"] dd { + color: var(--acs-muted); + font-weight: 600; +} +.acs-overview-results { + display: grid; + grid-template-columns: minmax(0, 1.3fr) minmax(280px, .7fr); + align-items: start; + gap: 14px; + margin-top: 14px; +} +.acs-overview-results[data-acs-card-count="2"] { + grid-template-columns: minmax(0, 3fr) minmax(320px, 2fr); +} +.acs-overview-results[data-acs-card-count="1"] { + grid-template-columns: minmax(0, 1fr); +} +.acs-overview-result-card { + min-width: 0; + border-left: 4px solid var(--acs-suite-accent) !important; +} +.acs-overview-result-card[data-acs-card-tone="fail"] { border-left-color: #dc2626 !important; } +.acs-overview-result-card[data-acs-card-tone="pass"] { border-left-color: #16a34a !important; } +.acs-overview-result-card[data-acs-card-tone="warn"] { border-left-color: #f97316 !important; } +.acs-overview-result-meta { + display: flex; + flex-wrap: wrap; + gap: 7px; + margin: -2px 0 10px; +} +.acs-overview-meta-item { + display: inline-flex; + align-items: center; + gap: 5px; + min-width: 0; + max-width: 100%; + padding: 4px 8px; + color: var(--acs-muted); + background: var(--acs-panel-muted); + border: 1px solid var(--acs-line); + border-radius: 999px; + font-size: 10.5px; + font-weight: 650; + overflow-wrap: anywhere; +} +.acs-overview-meta-item dt, +.acs-overview-meta-item dd { margin: 0; } +.acs-overview-meta-item dt { font-weight: 800; } +.acs-overview-meta-item dd { min-width: 0; overflow-wrap: anywhere; } +.acs-overview-heading-meta { + display: flex; + flex: 0 1 auto; + margin: 0; +} +.acs-overview-heading-meta .acs-overview-meta-item { + color: var(--acs-text); + background: #fff; +} +.acs-overview-result-entry + .acs-overview-result-entry { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #e6edf5; +} +.acs-overview-status-list { margin: 0; } +.acs-overview-result-label { + color: var(--acs-text); + font-size: 11px; + font-weight: 900; + letter-spacing: .045em; + text-transform: uppercase; + overflow-wrap: anywhere; +} +.acs-overview-result-value { + margin: 3px 0 0; + color: var(--acs-text); + font-size: 12.5px; + font-weight: 650; + line-height: 1.4; + overflow-wrap: anywhere; +} +.acs-overview-result-entry[data-acs-tone="fail"] .acs-overview-result-value { + color: var(--acs-fail-text); +} +.acs-overview-result-entry[data-acs-tone="pass"] .acs-overview-result-value[data-acs-primary="true"] { + color: var(--acs-pass-text); +} +.acs-overview-result-entry[data-acs-tone="warn"] .acs-overview-result-value[data-acs-primary="true"] { + color: var(--acs-warn-text); +} +.acs-overview-result-entry[data-acs-tone="neutral"] .acs-overview-result-value[data-acs-primary="true"] { + color: var(--acs-neutral-text); +} +.acs-overview-result-value[data-acs-primary="true"] { + font-size: 15px; + font-weight: 850; +} +.acs-overview-result-value[data-acs-primary="false"] { + margin-top: 6px; + padding: 7px 9px; + background: var(--acs-panel-muted); + border: 1px solid var(--acs-line); + border-radius: 8px; +} +.acs-overview-result-entry[data-acs-tone="fail"] .acs-overview-result-value[data-acs-primary="false"] { + background: #fff7f7; + border-color: #fecaca; +} +.acs-overview-result-entry[data-acs-tone="warn"] .acs-overview-result-value[data-acs-primary="false"] { + background: #fff7ed; + border-color: #fed7aa; +} +.acs-overview-result-value[data-acs-primary="false"] strong { + color: var(--acs-text); + font-weight: 900; +} +.detailed-summary, +.detailed-container { + margin-top: 14px !important; + padding: 0 !important; + background: transparent !important; + border: 0 !important; + box-shadow: none !important; +} +.test-suite-header, +.suite-header { + display: flex; + align-items: center; + gap: 9px; + min-height: 48px; + margin: 16px 0 0 !important; + padding: 11px 13px !important; + color: var(--acs-heading) !important; + background: var(--acs-panel) !important; + border: 1px solid var(--acs-line); + border-radius: 14px 14px 0 0; + box-shadow: 0 3px 12px rgba(16, 42, 76, .045); + font-size: 16px !important; + font-weight: 800 !important; + letter-spacing: .005em; + scroll-margin-top: 105px; +} +.test-suite-description, +.test-suite-info, +.suite-reason { color: var(--acs-muted) !important; } + +.heading { + margin: 0 0 6px; + color: var(--acs-heading); + font-size: 13px; + font-weight: 750; +} +.heading span { color: var(--acs-text); font-weight: 500; } + +.acs-result-group { + position: relative; + margin: 16px 0; + padding: 14px; + overflow: hidden; + background: var(--acs-panel); + border: 1px solid var(--acs-line); + border-left: 4px solid var(--acs-suite-accent); + border-radius: 15px; + box-shadow: var(--acs-shadow); + scroll-margin-top: 160px; +} +.acs-result-group > :first-child { margin-top: 0 !important; } +.acs-result-group > :last-child { margin-bottom: 0 !important; } +.acs-result-group.acs-collapsible > .acs-case-overview .acs-case-topline, +.acs-result-group.acs-collapsible > .test-suite-header, +.acs-result-group.acs-collapsible > .suite-header { + padding-right: 110px !important; +} +.acs-result-group.acs-collapsed > :not(.acs-case-overview):not(.test-suite-header):not(.suite-header):not(.acs-group-toggle) { + display: none !important; +} +.acs-result-group.acs-collapsed > .acs-case-overview .acs-case-description, +.acs-result-group.acs-collapsed > .acs-case-overview .acs-metadata-grid { display: none !important; } +.acs-group-toggle { + position: absolute; + top: 10px; + right: 12px; + z-index: 2; + min-height: 30px; + padding: 5px 9px; + cursor: pointer; + color: var(--acs-suite-accent); + background: var(--acs-panel); + border: 1px solid var(--acs-suite-border); + border-radius: 8px; + font-size: 10.5px; + font-weight: 800; +} +.acs-result-group > .test-suite-header, +.acs-result-group > .suite-header { + min-height: auto; + margin: -14px -14px 12px !important; + padding: 12px 14px !important; + background: linear-gradient(90deg, var(--acs-suite-soft), #fff) !important; + border: 0; + border-bottom: 1px solid var(--acs-suite-border); + border-radius: 0; + box-shadow: none; +} +.acs-result-group > .test-suite-description, +.acs-result-group > .test-suite-info, +.acs-result-group > .suite-reason { + margin: 8px 0 12px !important; + padding: 10px 12px; + background: var(--acs-panel-muted); + border: 1px solid var(--acs-line); + border-radius: 10px; +} +.acs-result-group > .test-suite-info { border-left: 3px solid var(--acs-suite-accent); } +.acs-result-group > table { margin-bottom: 0 !important; } +.acs-result-group.acs-group-fail { border-left-color: #dc2626; } +.acs-result-group.acs-group-warning, +.acs-result-group.acs-group-fail-waiver { border-left-color: #f97316; } +.acs-result-group.acs-group-pass { border-left-color: #16a34a; } + +.acs-case-card, +.acs-suite-card { padding-top: 0; } +.acs-case-overview { + margin: 0 -14px 14px; + padding: 15px 16px 14px; + background: linear-gradient(120deg, var(--acs-suite-soft), #fff 72%); + border-bottom: 1px solid var(--acs-suite-border); +} +.acs-case-topline { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; +} +.acs-case-identity { min-width: 0; } +.acs-case-eyebrow { + margin: 0 0 4px; + color: var(--acs-suite-accent); + font-size: 10.5px; + font-weight: 850; + letter-spacing: .07em; + text-transform: uppercase; +} +body.acs-report-ui .acs-case-title { + margin: 0 !important; + padding: 0 !important; + color: var(--acs-heading) !important; + font-size: 18px !important; + font-weight: 820; + line-height: 1.25; +} +.acs-case-description { + max-width: 1050px; + margin: 7px 0 0; + color: var(--acs-muted); + font-size: 12.5px; +} +.acs-case-status { flex: 0 0 auto; } +.acs-metadata-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin-top: 13px; +} +.acs-meta-item { + min-width: 0; + padding: 9px 10px; + background: rgba(255, 255, 255, .78); + border: 1px solid color-mix(in srgb, var(--acs-suite-border) 72%, var(--acs-line)); + border-radius: 9px; +} +.acs-meta-label { + display: block; + margin-bottom: 3px; + color: var(--acs-muted); + font-size: 9.5px; + font-weight: 800; + letter-spacing: .045em; + text-transform: uppercase; +} +.acs-meta-value { + display: block; + color: var(--acs-text); + font-size: 12px; + font-weight: 650; + overflow-wrap: anywhere; +} +.acs-meta-item.acs-meta-wide { grid-column: span 2; } +.acs-meta-item.acs-meta-full { grid-column: 1 / -1; } +.acs-meta-list { + margin: 3px 0 0; + padding-left: 18px; +} +.acs-meta-list li + li { margin-top: 3px; } +.acs-meta-item.acs-meta-code .acs-meta-value { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 11px; +} +.acs-metadata-source { display: none !important; } + +.case-header, +.test-case-header { + margin: 14px 0 7px !important; + color: var(--acs-heading) !important; + font-size: 14px !important; + font-weight: 800 !important; +} +.test-case-description { margin: 0 0 10px !important; color: var(--acs-muted) !important; } +.acs-flat-card > h2, +.acs-flat-card > h3 { margin: 0 0 8px !important; } + +body.acs-report-ui table { + width: 100% !important; + margin: 8px 0 14px !important; + border: 1px solid var(--acs-line) !important; + border-collapse: separate !important; + border-spacing: 0 !important; + border-radius: 12px; + overflow: hidden; + background: var(--acs-panel); + table-layout: auto; +} +body.acs-report-ui th, +body.acs-report-ui td { + padding: 9px 11px !important; + border: 0 !important; + border-bottom: 1px solid var(--acs-line) !important; + color: var(--acs-text); + font-size: 12.5px !important; + line-height: 1.35; + text-align: left !important; + vertical-align: top; + overflow-wrap: anywhere; +} +body.acs-report-ui th { + background: var(--acs-brand) !important; + color: #fff !important; + font-size: 11px !important; + font-weight: 800 !important; + letter-spacing: .035em; + text-transform: uppercase; +} +body.acs-report-ui tr:last-child > td { border-bottom: 0 !important; } +body.acs-report-ui tbody tr:hover > td { background-color: #f8fbff; } +body.acs-report-ui .subtest-table { + width: calc(100% - 20px) !important; + margin: 8px 10px 12px !important; + border-radius: 10px; +} +body.acs-report-ui .subtest-table th { background: #eaf0f7 !important; color: var(--acs-heading) !important; } + +.acs-summary-source { display: none !important; } +.acs-summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(125px, 1fr)); + gap: 9px; + margin-top: 8px; +} +.acs-summary-card { + min-height: 76px; + padding: 12px; + display: flex; + flex-direction: column; + justify-content: center; + background: var(--acs-panel-muted); + border: 1px solid var(--acs-line); + border-radius: 12px; + box-shadow: 0 2px 9px rgba(16, 42, 76, .035); +} +.acs-summary-card[data-acs-zero="true"] { opacity: .72; } +.acs-summary-value { color: var(--acs-heading); font-size: 23px; font-weight: 850; line-height: 1; } +.acs-summary-label { margin-top: 6px; color: var(--acs-muted); font-size: 11.5px; font-weight: 650; } +.acs-summary-card.pass { background: var(--acs-pass-bg); border-color: #a7edc2; } +.acs-summary-card.pass .acs-summary-value, +.acs-summary-card.pass .acs-summary-label { color: var(--acs-pass-text); } +.acs-summary-card.fail { background: var(--acs-fail-bg); border-color: #fecaca; } +.acs-summary-card.fail .acs-summary-value, +.acs-summary-card.fail .acs-summary-label { color: var(--acs-fail-text); } +.acs-summary-card.fail-waiver, +.acs-summary-card.warning { background: var(--acs-warn-bg); border-color: #fed7aa; } +.acs-summary-card.fail-waiver .acs-summary-value, +.acs-summary-card.fail-waiver .acs-summary-label, +.acs-summary-card.warning .acs-summary-value, +.acs-summary-card.warning .acs-summary-label { color: var(--acs-warn-text); } +.acs-summary-card.passed-partial { background: var(--acs-partial-bg); border-color: #bae6fd; } +.acs-summary-card.passed-partial .acs-summary-value, +.acs-summary-card.passed-partial .acs-summary-label { color: var(--acs-partial-text); } +.acs-summary-card.pal-not-supported { background: var(--acs-info-bg); } +.acs-summary-card.ignored, +.acs-summary-card.unknown { background: var(--acs-neutral-bg); border-color: #cbd5e1; } +.acs-summary-card.ignored .acs-summary-value, +.acs-summary-card.ignored .acs-summary-label, +.acs-summary-card.unknown .acs-summary-value, +.acs-summary-card.unknown .acs-summary-label { color: var(--acs-neutral-text); } + +.result-summary.acs-compact-summary, +.summary-container.acs-compact-summary, +.card.acs-compact-summary { + width: min(100%, 880px); + margin: 14px auto 16px !important; + padding: 18px 20px !important; +} +.acs-compact-summary-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} +.acs-compact-summary-header h2 { + margin: 0 !important; + font-size: 16px !important; +} +.acs-compact-summary-total { + flex: 0 0 auto; + color: var(--acs-muted); + font-size: 11px; + font-weight: 650; + white-space: nowrap; +} +.acs-progress-list { + display: grid; + gap: 7px; + margin: 0; +} +.acs-progress-row { + display: grid; + grid-template-columns: minmax(125px, 185px) minmax(160px, 1fr) minmax(28px, auto); + align-items: center; + gap: 10px; + min-width: 0; +} +.acs-progress-label { + min-width: 0; + color: var(--acs-text); + font-size: 11.5px; + font-weight: 600; + line-height: 1.25; +} +.acs-progress-track { + position: relative; + height: 8px; + min-width: 0; + overflow: hidden; + background: #e9eef5; + border-radius: 999px; +} +.acs-progress-fill { + display: block; + height: 100%; + border-radius: inherit; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} +.acs-progress-count { + min-width: 2ch; + color: var(--acs-heading); + font-size: 11.5px; + font-variant-numeric: tabular-nums; + font-weight: 800; + line-height: 1; + text-align: right; +} +body.acs-report-ui td.acs-status-cell { + background: var(--acs-panel) !important; + text-align: left !important; + vertical-align: middle !important; + font-weight: 750 !important; +} +.acs-status-pill, +.acs-status-filter { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + white-space: nowrap; + font-size: 11px; + font-weight: 800; + line-height: 1.2; +} +.acs-status-pill { + padding: 4px 9px; + background: var(--acs-neutral-bg); + color: var(--acs-neutral-text); +} +.acs-status-pill.pass, +.acs-status-filter.pass[aria-pressed="true"] { background: var(--acs-pass-bg); color: var(--acs-pass-text); } +.acs-status-pill.fail, +.acs-status-filter.fail[aria-pressed="true"] { background: var(--acs-fail-bg); color: var(--acs-fail-text); } +.acs-status-pill.fail-waiver, +.acs-status-pill.warning, +.acs-status-filter.fail-waiver[aria-pressed="true"], +.acs-status-filter.warning[aria-pressed="true"] { background: var(--acs-warn-bg); color: var(--acs-warn-text); } +.acs-status-pill.passed-partial, +.acs-status-filter.passed-partial[aria-pressed="true"] { background: var(--acs-partial-bg); color: var(--acs-partial-text); } +.acs-status-pill.pal-not-supported, +.acs-status-filter.pal-not-supported[aria-pressed="true"] { background: var(--acs-info-bg); color: var(--acs-info-text); } +.acs-status-pill.skipped, +.acs-status-pill.aborted, +.acs-status-pill.ignored, +.acs-status-pill.unknown, +.acs-status-pill.not-tested, +.acs-status-pill.not-implemented, +.acs-status-pill.info, +.acs-status-filter.skipped[aria-pressed="true"], +.acs-status-filter.aborted[aria-pressed="true"], +.acs-status-filter.ignored[aria-pressed="true"], +.acs-status-filter.unknown[aria-pressed="true"], +.acs-status-filter.not-tested[aria-pressed="true"], +.acs-status-filter.not-implemented[aria-pressed="true"], +.acs-status-filter.info[aria-pressed="true"] { background: var(--acs-neutral-bg); color: var(--acs-neutral-text); } +.acs-status-filter[aria-pressed="true"] { background: var(--acs-neutral-bg); color: var(--acs-neutral-text); } +tr[data-acs-row-status] > td:first-child { + border-left: 4px solid var(--acs-row-accent, #94a3b8) !important; +} +.acs-mobile-status-preview, +.acs-mobile-table-note { display: none; } + +body[data-acs-suite="sct"] .acs-result-group > table, +body[data-acs-suite="tpm"] .acs-result-group > table { table-layout: fixed; } +body[data-acs-suite="sct"] .acs-col-guid { width: 15%; } +body[data-acs-suite="sct"] .acs-col-description { width: 23%; } +body[data-acs-suite="sct"] .acs-col-status { width: 10%; } +body[data-acs-suite="sct"] .acs-col-path { width: 22%; } +body[data-acs-suite="sct"] .acs-col-reason { width: 17%; } +body[data-acs-suite="sct"] .acs-col-waiver { width: 13%; } +body[data-acs-suite="post-script"] .acs-flat-card { + padding: 0 14px 14px !important; + background: var(--acs-panel) !important; + border: 1px solid var(--acs-line) !important; + border-left: 4px solid var(--acs-suite-accent) !important; + border-radius: 15px !important; + box-shadow: var(--acs-shadow) !important; +} +body[data-acs-suite="sbmr"] .case-header { padding-top: 5px; border-top: 1px solid var(--acs-line); } + +.acs-toolbar { + position: sticky; + top: 8px; + z-index: 30; + display: flex; + flex-wrap: wrap; + align-items: end; + gap: 7px; + margin: 10px 0; + padding: 8px; + background: var(--acs-bg); + border: 1px solid var(--acs-line); + border-radius: 14px; + box-shadow: var(--acs-shadow); +} +.acs-detail-overview { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: stretch; + gap: 14px; + margin: 14px 0 16px; +} +.acs-detail-overview > .acs-compact-summary, +.acs-detail-overview > .acs-failure-summary { + width: 100%; + max-width: none; + margin: 0 !important; +} +.acs-failure-summary { + min-width: 0; + padding: 18px 20px; + background: var(--acs-panel); + border: 1px solid var(--acs-line); + border-radius: 15px; + box-shadow: var(--acs-shadow); +} +.acs-failure-summary .acs-progress-row { + grid-template-columns: minmax(135px, 215px) minmax(130px, 1fr) minmax(34px, auto); +} +.acs-failure-summary .acs-progress-row[data-acs-zero="true"] { opacity: .7; } +.acs-failure-summary .acs-progress-label { overflow-wrap: anywhere; } +.acs-failure-summary .acs-progress-track { display: flex; } +.acs-failure-summary .acs-progress-fill { flex: 0 0 auto; } +.acs-failure-fill-waiver { background: #f97316; } +.acs-failure-summary .acs-progress-count { + min-width: 4.5ch; + white-space: nowrap; +} +.acs-failure-waiver-count { + color: var(--acs-warn-text); + font-size: 9px; + font-weight: 750; +} +.acs-control { display: flex; flex-direction: column; gap: 4px; min-width: 160px; } +.acs-control-grow { flex: 1 1 250px; } +.acs-control-label { color: var(--acs-muted); font-size: 11px; font-weight: 700; } +.acs-control input, +.acs-control select { + width: 100%; + min-height: 35px; + padding: 7px 10px; + background: #fff; + color: var(--acs-text); + border: 1px solid var(--acs-line); + border-radius: 9px; +} +.acs-status-filters { display: flex; flex: 1 1 100%; flex-wrap: wrap; align-items: center; gap: 6px; } +.acs-filter-scope { + flex: 1 1 100%; + color: var(--acs-heading); + font-size: 11px; + font-weight: 800; +} +.acs-filter-help { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +.acs-status-filter { + gap: 5px; + padding: 6px 9px; + cursor: pointer; + background: #fff; + color: var(--acs-muted); + border: 1px solid var(--acs-line); +} +.acs-status-filter:hover { background: var(--acs-suite-soft); border-color: var(--acs-suite-border); } +.acs-status-filter[aria-pressed="true"] { + border-color: currentColor; + box-shadow: inset 0 0 0 1px currentColor, 0 2px 7px rgba(16, 42, 76, .08); +} +.acs-status-filter.info[aria-pressed="true"] { + background: var(--acs-brand); + border-color: var(--acs-brand); + color: #fff; +} +.acs-filter-count { + min-width: 138px; + padding: 8px 2px; + color: var(--acs-muted); + font-size: 11.5px; + text-align: right; +} +.acs-print-scope-note { + flex: 1 1 100%; + margin: 0; + color: var(--acs-muted); + font-size: 10.5px; +} +.acs-print-banner { display: none; } +.acs-actions { display: flex; flex-wrap: wrap; gap: 6px; } +.acs-button { + min-height: 35px; + padding: 7px 10px; + cursor: pointer; + background: #fff; + color: var(--acs-text); + border: 1px solid var(--acs-line); + border-radius: 9px; + font-size: 12px; + font-weight: 750; +} +.acs-button:hover { background: var(--acs-brand-soft); border-color: #b8c9df; } +.acs-button-primary { background: var(--acs-brand); border-color: var(--acs-brand); color: #fff; } +.acs-button-primary:hover { background: var(--acs-brand-strong); color: #fff; } +.acs-filter-hidden { display: none !important; } +.acs-context-row { opacity: .76; } +.acs-empty-state { + display: none; + margin: 12px 0; + padding: 20px; + text-align: center; + color: var(--acs-muted); + background: var(--acs-panel); + border: 1px dashed #b9c6d7; + border-radius: 13px; +} +.acs-empty-state.visible { display: block; } + +.acs-col-guid, +.acs-col-path { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 10.5px !important; } +.acs-col-guid { min-width: 180px; } +.acs-col-description { min-width: 220px; } +.acs-col-status { min-width: 112px; } +.acs-col-reason, +.acs-col-waiver { min-width: 180px; } +.acs-long-value { min-width: 170px; } +.acs-long-value > summary { + width: max-content; + max-width: 100%; + padding: 4px 7px; + cursor: pointer; + list-style-position: inside; + color: var(--acs-suite-accent); + background: var(--acs-suite-soft); + border: 1px solid var(--acs-suite-border); + border-radius: 7px; + font-family: inherit; + font-size: 10.5px; + font-weight: 750; +} +.acs-long-value > code { + display: block; + min-width: 280px; + max-width: 560px; + margin-top: 7px; + padding: 8px; + white-space: normal; + overflow-wrap: anywhere; + color: var(--acs-text); + background: var(--acs-panel-muted); + border-radius: 7px; + font-size: 10px; + line-height: 1.45; +} +.acs-long-reason > .acs-long-reason-text { + display: block; + min-width: 280px; + max-width: 640px; + margin-top: 7px; + padding: 8px; + white-space: normal; + overflow-wrap: anywhere; + color: var(--acs-text); + background: var(--acs-panel-muted); + border-radius: 7px; + font-size: 10.5px; + line-height: 1.45; +} +.acs-long-reason-text > br { + display: block; + margin-top: 5px; + content: ""; +} + +.acs-suite-nav { + position: sticky; + top: 8px; + z-index: 32; + max-width: 1540px !important; + margin: 0 auto 16px !important; + padding: 9px; + display: flex; + gap: 6px; + overflow-x: auto; + background: var(--acs-bg); + border: 1px solid var(--acs-line); + border-radius: 13px; + box-shadow: var(--acs-shadow); +} +.acs-suite-nav a { + flex: 0 0 auto; + padding: 6px 10px; + text-decoration: none; + background: var(--acs-panel); + border: 1px solid var(--acs-line); + border-radius: 999px; + color: var(--acs-brand) !important; + font-size: 11.5px; + font-weight: 750; +} +.acs-suite-nav a:hover { background: var(--acs-brand-soft); } +.acs-back-to-top { + position: fixed; + right: 20px; + bottom: 20px; + z-index: 40; + width: 36px; + height: 36px; + cursor: pointer; + color: #fff; + background: var(--acs-brand); + border: 0; + border-radius: 50%; + box-shadow: 0 8px 24px rgba(15, 44, 80, .24); + font-size: 15px; + font-weight: 800; + opacity: 0; + pointer-events: none; + transform: translateY(8px); + transition: opacity .18s ease, transform .18s ease; +} +.acs-back-to-top.visible { opacity: .92; pointer-events: auto; transform: translateY(0); } + +.acs-legacy-navigation { display: none !important; } + +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: stretch; + gap: 14px; +} +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section > h2 { grid-column: 1 / -1; } +.acs-summary-heading-row { + grid-column: 1 / -1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; +} +.acs-summary-heading-row > h2 { + min-width: 0; + margin: 0 !important; +} +.acs-summary-print-button { + flex: 0 0 auto; + margin-left: auto; + white-space: nowrap; +} +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section > .acs-suite-nav { + grid-column: 1 / -1; + width: 100%; + margin: -2px 0 0 !important; +} +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section > .summary { + display: flex; + flex-direction: column; + min-width: 0; + margin: 0 !important; + padding: 14px !important; +} +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section > .summary.acs-summary-orphan { + grid-column: 1 / -1; + width: calc(50% - 7px); + justify-self: start; +} +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary > .details-link { + margin-top: auto; +} +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary h1 { font-size: 20px !important; } +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary .acs-summary-card { min-height: 62px; padding: 9px; } +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary .acs-summary-value { font-size: 20px; } +.acs-report-ui[data-acs-report-kind="acs-summary"] .summary .details-link a { padding: 6px 9px !important; } +.acs-report-ui[data-acs-report-kind="acs-summary"] .acs-compact-summary { + width: 100%; + max-width: none; + margin: 12px 0 !important; +} + +.details-link { text-align: right !important; } +.details-link a { + display: inline-flex !important; + padding: 7px 11px !important; + text-decoration: none !important; + border: 1px solid #bfd0e5 !important; + border-radius: 9px !important; + font-size: 12px; + font-weight: 750 !important; +} +.details-link a:hover { background: var(--acs-brand-soft) !important; color: var(--acs-brand) !important; } +.dropdown { text-align: left !important; margin: 10px 0 16px !important; } +.dropdown button { background: var(--acs-brand) !important; border-radius: 9px !important; } +.dropdown-content { left: 0 !important; transform: none !important; border-radius: 10px; overflow: hidden; } +.compliance-status { border-radius: 999px !important; padding: 4px 9px !important; } + +@media (max-width: 1100px) { + .acs-detail-overview { grid-template-columns: minmax(0, 1fr); } + .acs-information-values { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} + +@media (min-width: 1101px) { + .acs-information-band[data-acs-information-group="standards"] .acs-information-field:nth-child(4) { + grid-column: 1; + } +} + +@media (max-width: 900px) { + body.acs-report-ui { padding: 12px !important; } + body.acs-report-ui > .header { padding: 18px !important; border-radius: 13px; } + .acs-summary-grid { grid-template-columns: repeat(2, minmax(115px, 1fr)); } + .acs-toolbar { position: static; align-items: stretch; } + .acs-control { min-width: 100%; } + .acs-filter-count { text-align: left; } + .acs-metadata-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .acs-meta-item.acs-meta-wide { grid-column: span 2; } + .acs-meta-item.acs-meta-full { grid-column: 1 / -1; } + body.acs-report-ui table { + display: block; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: thin; + scrollbar-color: var(--acs-suite-accent) var(--acs-panel-muted); + } + body.acs-report-ui th, + body.acs-report-ui td { padding: 8px 9px !important; font-size: 12px !important; } + .acs-mobile-status-preview { + display: flex; + align-items: center; + gap: 6px; + width: max-content; + margin-top: 5px; + } + .acs-mobile-status-preview::before { + content: "Result"; + color: var(--acs-muted); + font-size: 9px; + font-weight: 750; + text-transform: uppercase; + } + .acs-mobile-table-note { + display: block; + margin: 8px 0 12px; + padding: 8px 10px; + color: var(--acs-muted); + background: var(--acs-panel-muted); + border: 1px solid var(--acs-line); + border-radius: 8px; + font-size: 10.5px; + } + .acs-suite-nav { + position: static; + flex-wrap: wrap; + overflow: visible; + } + .acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section { grid-template-columns: 1fr; } + .acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section > .summary.acs-summary-orphan { + grid-column: 1; + width: 100%; + } + .acs-overview-results { grid-template-columns: minmax(0, 1fr); } + .acs-overview-results[data-acs-card-count="2"] { + grid-template-columns: minmax(0, 1fr); + } + .acs-information-band { + grid-template-columns: minmax(0, 1fr); + gap: 7px; + } + .acs-information-values { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .system-info table, + .acs-results-summary table { + display: table !important; + width: 100% !important; + table-layout: fixed !important; + white-space: normal !important; + } + .system-info th, + .acs-results-summary th { + width: 42% !important; + min-width: 0 !important; + white-space: normal !important; + word-break: normal !important; + overflow-wrap: anywhere !important; + } + .system-info td, + .acs-results-summary td { white-space: normal !important; } + .acs-back-to-top { right: 8px; bottom: 8px; } +} + +@media (max-width: 560px) { + .result-summary.acs-compact-summary, + .summary-container.acs-compact-summary, + .card.acs-compact-summary { padding: 15px !important; } + .acs-compact-summary-header { + align-items: flex-start; + flex-wrap: wrap; + gap: 4px 12px; + } + .acs-progress-row { + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px 10px; + } + .acs-progress-track { grid-column: 1 / -1; grid-row: 2; } + .acs-progress-count { grid-column: 2; grid-row: 1; } + .acs-case-topline { display: block; } + .acs-case-status { margin-top: 10px; } + .acs-metadata-grid { grid-template-columns: 1fr; } + .acs-meta-item.acs-meta-wide { grid-column: span 1; } + .acs-meta-item.acs-meta-full { grid-column: 1; } + .acs-overview-heading { + align-items: flex-start; + flex-direction: column; + gap: 3px; + } + .acs-overview-field-count { white-space: normal; } + .acs-information-values { grid-template-columns: minmax(0, 1fr); } + .acs-result-group { padding: 11px; border-radius: 12px; } + .acs-case-overview { margin: 0 -11px 11px; padding: 13px; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } +} + +@media print { + @page { size: A4 landscape; margin: 9mm; } + body.acs-report-ui { padding: 0 !important; background: #fff !important; font-size: 10pt !important; } + body.acs-report-ui > * { max-width: none; } + .acs-toolbar, + .acs-suite-nav, + .acs-back-to-main, + .acs-back-to-top, + .acs-summary-print-button, + .acs-group-toggle, + .acs-mobile-table-note, + .details-link { display: none !important; } + .result-summary, + .summary-container, + .system-info, + .acs-results-summary, + .summary { box-shadow: none !important; break-inside: avoid; } + .acs-summary-overview > .acs-information-overview, + .acs-overview-results > .acs-overview-result-card { + padding: 12px !important; + box-shadow: none !important; + break-inside: avoid !important; + } + .acs-information-band { + grid-template-columns: 120px minmax(0, 1fr); + gap: 14px; + padding: 4px 0; + } + .acs-information-values { + grid-template-columns: repeat(4, minmax(0, 1fr)); + row-gap: 5px; + } + .acs-information-band[data-acs-information-group="standards"] .acs-information-field:nth-child(4) { + grid-column: 1; + } + .acs-overview-results { + grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr); + gap: 14px; + break-inside: avoid; + } + .acs-overview-results[data-acs-card-count="2"] { + grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); + } + .acs-information-band, + .acs-overview-result-entry { break-inside: avoid; } + .acs-report-ui .acs-information-band h3 { + color: var(--acs-heading) !important; + background: var(--acs-brand-soft) !important; + border: 1px solid var(--acs-line) !important; + } + .acs-detail-overview { display: block !important; } + .acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section { + display: block !important; + margin: 0 !important; + padding: 0 !important; + background: #fff !important; + border: 0 !important; + box-shadow: none !important; + break-before: page; + page-break-before: always; + } + .acs-summary-heading-row { + margin: 0 0 4mm; + break-after: avoid-page; + page-break-after: avoid; + } + .acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section > .summary { + display: inline-block !important; + width: 100% !important; + margin: 0 0 5mm !important; + padding: 4mm !important; + vertical-align: top; + break-inside: avoid !important; + page-break-inside: avoid !important; + } + .acs-report-ui[data-acs-report-kind="acs-summary"] .summary-section > .summary:last-child { + margin-bottom: 0 !important; + } + .acs-report-ui[data-acs-report-kind="acs-summary"] .summary .acs-compact-summary, + .acs-report-ui[data-acs-report-kind="acs-summary"] .summary h1, + .acs-report-ui[data-acs-report-kind="acs-summary"] .summary h2 { + break-inside: avoid !important; + page-break-inside: avoid !important; + } + .acs-filter-hidden, + .acs-result-group.acs-collapsed > :not(.acs-case-overview):not(.test-suite-header):not(.suite-header):not(.acs-group-toggle):not(.acs-metadata-source) { + display: revert !important; + } + .acs-result-group.acs-collapsed > .acs-case-overview .acs-case-description { + display: block !important; + } + .acs-result-group.acs-collapsed > .acs-case-overview .acs-metadata-grid { + display: grid !important; + } + tr.subtest-row-hidden { display: table-row !important; } + .acs-empty-state { display: none !important; } + .acs-print-banner { + display: block !important; + margin: 0 0 8mm; + padding: 3mm 4mm; + color: #475467; + background: #f8fafc; + border: 1px solid #d8e1ed; + border-radius: 2mm; + font-size: 9pt; + } + body.acs-report-ui table { break-inside: auto; } + body.acs-report-ui tr { break-inside: avoid; } + body.acs-report-ui a { color: inherit; text-decoration: none; } +} +""" + + +REPORT_JS = r""" +(function () { + "use strict"; + + var STATUS_ORDER = [ + "pass", "fail", "fail-waiver", "warning", "passed-partial", + "skipped", "aborted", "ignored", "not-implemented", "not-tested", + "pal-not-supported", "unknown", "info" + ]; + var STATUS_LABELS = { + "pass": "Passed", + "fail": "Failed", + "fail-waiver": "Failed with waiver", + "warning": "Warning", + "passed-partial": "Passed (Partial)", + "skipped": "Skipped", + "aborted": "Aborted", + "ignored": "Ignored", + "not-implemented": "Not implemented", + "not-tested": "Not tested", + "pal-not-supported": "PAL not supported", + "unknown": "Unknown", + "info": "Info" + }; + var STATUS_DISPLAY = { + "pass": "PASSED", + "fail": "FAILED", + "fail-waiver": "FAILED WITH WAIVER", + "warning": "WARNING", + "passed-partial": "PASSED (PARTIAL)", + "skipped": "SKIPPED", + "aborted": "ABORTED", + "ignored": "IGNORED", + "not-implemented": "NOT IMPLEMENTED", + "not-tested": "NOT TESTED", + "pal-not-supported": "PAL NOT SUPPORTED", + "unknown": "UNKNOWN", + "info": "INFO" + }; + var STATUS_ACCENTS = { + "pass": "#16a34a", + "fail": "#dc2626", + "fail-waiver": "#f97316", + "warning": "#f97316", + "passed-partial": "#0284c7", + "skipped": "#94a3b8", + "aborted": "#64748b", + "ignored": "#94a3b8", + "not-implemented": "#64748b", + "not-tested": "#64748b", + "pal-not-supported": "#4f6b9b", + "unknown": "#94a3b8", + "info": "#4f6b9b" + }; + var SUITE_PROFILES = { + "bsa": { + label: "BSA / SBSA", + subtitle: "Hierarchical rule and subtest results with focused status navigation.", + layout: "hierarchy" + }, + "sct": { + label: "UEFI SCT", + subtitle: "Case context, entry-point GUIDs, source paths, and subtest evidence in one view.", + layout: "metadata" + }, + "tpm": { + label: "BBSR TPM", + subtitle: "TPM case context and verification results grouped for quick review.", + layout: "metadata" + }, + "fwts": { + label: "FWTS", + subtitle: "Firmware test suites grouped with requirements, descriptions, and evidence.", + layout: "suite" + }, + "pfdi": { + label: "PFDI", + subtitle: "Platform fault detection checks with concise results and reasons.", + layout: "suite" + }, + "post-script": { + label: "Post-script", + subtitle: "Post-processing validation checks in a compact operational view.", + layout: "flat" + }, + "sbmr": { + label: "SBMR", + subtitle: "Management interface suites and cases grouped by execution area.", + layout: "suite" + }, + "scmi": { + label: "SCMI", + subtitle: "Protocol suites, access reasons, and testcase outcomes in focused sections.", + layout: "suite" + }, + "os": { + label: "OS tests", + subtitle: "Operating-system validations grouped by suite and testcase context.", + layout: "case" + }, + "standalone": { + label: "Standalone tests", + subtitle: "Independent validation tools grouped by suite and testcase context.", + layout: "case" + } + }; + + function normalizeText(value) { + return (value || "").replace(/\s+/g, " ").trim(); + } + + function statusSlug(value) { + var slug = normalizeText(value).toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 48); + return slug ? "status-" + slug : "unknown"; + } + + function classifyStatus(value, className, allowDynamic) { + var classes = " " + (className || "").toLowerCase().replace(/_/g, "-") + " "; + var text = normalizeText(value).toUpperCase().replace(/_/g, " "); + if (!text || text === "N/A" || text === "NA" || text === "-") { + return ""; + } + if (classes.indexOf(" fail-waiver ") >= 0 || classes.indexOf(" waiver ") >= 0 || + /FAIL(?:ED|URE)?(?:\s+|\s*\()(?:WITH\s+)?WAIVER\)?/.test(text)) { + return "fail-waiver"; + } + if (classes.indexOf(" passed-partial ") >= 0 || text.indexOf("PARTIAL") >= 0) { + return "passed-partial"; + } + if (classes.indexOf(" pal-not-supported ") >= 0 || text.indexOf("PAL NOT SUPPORTED") >= 0) { + return "pal-not-supported"; + } + if (classes.indexOf(" not-implemented ") >= 0 || text.indexOf("NOT IMPLEMENTED") >= 0) { + return "not-implemented"; + } + if (classes.indexOf(" not-tested ") >= 0 || text.indexOf("NOT TESTED") >= 0) { + return "not-tested"; + } + if (text === "IGNORED" || /KNOWN\s+.+\s+LIMITATION/.test(text) || text === "NOT SUPPORTED") { + return "ignored"; + } + if (text.indexOf("PASSED WITH WARNING") === 0) { + return "warning"; + } + if (classes.indexOf(" fail ") >= 0 || /^(FAIL|FAILED|FAILURE)(?:\b|\s)/.test(text)) { + return "fail"; + } + if (classes.indexOf(" warning ") >= 0 || classes.indexOf(" warnings ") >= 0 || + text === "WARN" || text.indexOf("WARNING") === 0) { + return "warning"; + } + if (classes.indexOf(" aborted ") >= 0 || text.indexOf("ABORT") === 0) { + return "aborted"; + } + if (classes.indexOf(" skipped ") >= 0 || text.indexOf("SKIP") === 0) { + return "skipped"; + } + if (classes.indexOf(" pass ") >= 0 || text.indexOf("PASS") === 0 || text === "SUCCESS") { + return "pass"; + } + if (classes.indexOf(" info ") >= 0 || text === "INFO") { + return "info"; + } + if (classes.indexOf(" ignored ") >= 0) { + return "ignored"; + } + if (classes.indexOf(" unknown ") >= 0 || text === "UNKNOWN") { + return "unknown"; + } + return allowDynamic ? statusSlug(text) : ""; + } + + function classifyStatusCell(cell, allowDynamic) { + var text = normalizeText(cell.textContent).toUpperCase().replace(/_/g, " "); + if (!text || text === "N/A" || text === "NA" || text === "-" || text.length > 100) { + return ""; + } + var known = classifyStatus(text, cell.className, false); + if (known) { + return known; + } + return allowDynamic ? classifyStatus(text, cell.className, true) : ""; + } + + function statusLabel(status, originalText) { + if (STATUS_LABELS[status]) { + return STATUS_LABELS[status]; + } + return normalizeText(originalText).replace(/_/g, " "); + } + + function statusDisplay(status, originalText) { + return STATUS_DISPLAY[status] || normalizeText(originalText).replace(/_/g, " "); + } + + function statusAccent(status) { + return STATUS_ACCENTS[status] || "#64748b"; + } + + function addReportHeading() { + var heading = document.querySelector("body > h1, body > .container h1"); + if (!heading || heading.previousElementSibling && + heading.previousElementSibling.classList.contains("acs-report-kicker")) { + return; + } + var profile = SUITE_PROFILES[document.body.getAttribute("data-acs-suite")] || {}; + var isDetailed = Boolean(document.querySelector(".detailed-summary, .detailed-container")); + document.body.setAttribute("data-acs-view", isDetailed ? "detail" : "summary"); + if (isDetailed && /\bTest Summary$/i.test(normalizeText(heading.textContent))) { + heading.textContent = normalizeText(heading.textContent) + .replace(/\bTest Summary$/i, "Detailed Results"); + document.title = heading.textContent; + } + var kicker = document.createElement("div"); + kicker.className = "acs-report-kicker"; + kicker.textContent = "SystemReady ACS \u00b7 " + (profile.label || "Validation report"); + heading.parentNode.insertBefore(kicker, heading); + + var subtitle = document.createElement("p"); + subtitle.className = "acs-report-subtitle"; + subtitle.textContent = profile.subtitle || + "Searchable results, clear status counts, and print-ready detail."; + var mainPage = document.body.getAttribute("data-acs-main-page"); + if (isDetailed && mainPage) { + var backLink = document.createElement("a"); + backLink.className = "acs-back-to-main"; + backLink.href = mainPage; + backLink.textContent = "\u2190 Back to Main Page"; + kicker.appendChild(backLink); + } + heading.insertAdjacentElement("afterend", subtitle); + } + + function summaryTableRows(table) { + var entries = []; + Array.prototype.forEach.call(table.querySelectorAll("tr"), function (row) { + var cells = row.cells; + if (!cells || cells.length < 2 || row.querySelector("th")) { + return; + } + var label = normalizeText(cells[0].textContent); + var value = normalizeText(cells[cells.length - 1].textContent); + if (!label) { + return; + } + var numericValue = Number(value.replace(/,/g, "")); + entries.push({ + label: label, + value: value || "0", + count: isFinite(numericValue) ? Math.max(0, numericValue) : 0, + valueClass: cells[cells.length - 1].className + }); + }); + return entries; + } + + function upgradeCompactSummary(table, entries) { + var container = table.closest(".result-summary, .summary-container, .card"); + if (!container) { + return false; + } + var totalEntry = entries.find(function (entry) { + return /^Total\b/i.test(entry.label); + }); + var statusEntries = entries.filter(function (entry) { + return entry !== totalEntry; + }); + if (!statusEntries.length) { + return false; + } + var derivedTotal = statusEntries.reduce(function (sum, entry) { + return sum + entry.count; + }, 0); + var totalCount = totalEntry ? totalEntry.count : derivedTotal; + var totalDisplay = totalEntry ? totalEntry.value : String(derivedTotal); + + container.classList.add("acs-compact-summary"); + var heading = container.querySelector("h2"); + if (heading && heading.parentNode !== container) { + heading = null; + } + var header = document.createElement("div"); + header.className = "acs-compact-summary-header"; + container.insertBefore(header, heading || container.firstChild); + if (heading) { + heading.textContent = "Test-result summary"; + header.appendChild(heading); + } else { + heading = document.createElement("h2"); + heading.textContent = "Test-result summary"; + header.appendChild(heading); + } + var total = document.createElement("span"); + total.className = "acs-compact-summary-total"; + total.textContent = totalDisplay + " suite-reported " + + (totalCount === 1 ? "test" : "tests"); + header.appendChild(total); + + var list = document.createElement("div"); + list.className = "acs-progress-list"; + list.setAttribute("role", "list"); + list.setAttribute("aria-label", "Suite-reported test results"); + statusEntries.forEach(function (entry) { + var status = classifyStatus(entry.label, entry.valueClass, true); + var progressMaximum = Math.max(totalCount, entry.count, 1); + var percentage = totalCount > 0 ? + Math.min(100, (entry.count / totalCount) * 100) : 0; + var item = document.createElement("div"); + item.className = "acs-progress-row acs-progress-" + (status || "unknown"); + item.setAttribute("role", "listitem"); + item.setAttribute("data-acs-summary-status", status || "unknown"); + item.setAttribute("data-acs-zero", String(entry.count === 0)); + + var label = document.createElement("span"); + label.className = "acs-progress-label"; + label.textContent = status === "passed-partial" ? + statusLabel(status, entry.label) : entry.label; + + var track = document.createElement("span"); + track.className = "acs-progress-track"; + track.setAttribute("role", "progressbar"); + track.setAttribute("aria-label", entry.label); + track.setAttribute("aria-valuemin", "0"); + track.setAttribute("aria-valuemax", String(progressMaximum)); + track.setAttribute("aria-valuenow", String(entry.count)); + var fill = document.createElement("span"); + fill.className = "acs-progress-fill"; + fill.setAttribute("aria-hidden", "true"); + fill.style.backgroundColor = statusAccent(status); + fill.style.width = percentage.toFixed(4).replace(/\.?0+$/, "") + "%"; + fill.style.minWidth = entry.count > 0 ? "2px" : "0"; + track.appendChild(fill); + + var count = document.createElement("span"); + count.className = "acs-progress-count"; + count.textContent = entry.value; + + item.appendChild(label); + item.appendChild(track); + item.appendChild(count); + list.appendChild(item); + }); + + table.classList.add("acs-summary-source"); + table.setAttribute("aria-hidden", "true"); + table.insertAdjacentElement("afterend", list); + return true; + } + + function upgradeSummaryTables() { + Array.prototype.forEach.call(document.querySelectorAll("table.summary-table"), function (table) { + if (table.classList.contains("acs-summary-source")) { + return; + } + upgradeCompactSummary(table, summaryTableRows(table)); + }); + } + + function textWithoutPrefix(element, prefix) { + return normalizeText(element ? element.textContent : "").replace(prefix, "").trim(); + } + + function headingFields(group) { + var fields = {}; + Array.prototype.forEach.call(group.querySelectorAll(".heading"), function (heading) { + var valueNode = heading.querySelector("span"); + var value = normalizeText(valueNode ? valueNode.textContent : ""); + var label = normalizeText(heading.textContent); + if (value) { + label = label.slice(0, Math.max(0, label.length - value.length)); + } + label = label.replace(/:\s*$/, "").toLowerCase(); + fields[label] = value || "N/A"; + heading.classList.add("acs-metadata-source"); + heading.setAttribute("aria-hidden", "true"); + }); + return fields; + } + + function metaItem(label, value, className) { + var item = document.createElement("div"); + item.className = "acs-meta-item" + (className ? " " + className : ""); + var name = document.createElement("span"); + name.className = "acs-meta-label"; + name.textContent = label; + var content = document.createElement("span"); + content.className = "acs-meta-value"; + content.textContent = value || "N/A"; + item.appendChild(name); + item.appendChild(content); + return item; + } + + function sourceMetaItem(label, source, prefix, className) { + if (!source) { + return null; + } + var item = metaItem(label, "", className); + var content = item.querySelector(".acs-meta-value"); + var listItems = source.querySelectorAll("li"); + if (listItems.length) { + var list = document.createElement("ul"); + list.className = "acs-meta-list"; + Array.prototype.forEach.call(listItems, function (entry) { + var listItem = document.createElement("li"); + listItem.textContent = normalizeText(entry.textContent); + list.appendChild(listItem); + }); + content.textContent = ""; + content.appendChild(list); + } else { + content.textContent = textWithoutPrefix(source, prefix) || "N/A"; + } + return item; + } + + function caseOverview(title, description, eyebrow, statusText) { + var overview = document.createElement("section"); + overview.className = "acs-case-overview"; + var top = document.createElement("div"); + top.className = "acs-case-topline"; + var identity = document.createElement("div"); + identity.className = "acs-case-identity"; + var context = document.createElement("div"); + context.className = "acs-case-eyebrow"; + context.textContent = eyebrow || "Test case"; + var heading = document.createElement("h3"); + heading.className = "acs-case-title"; + heading.textContent = title || "Unnamed test case"; + identity.appendChild(context); + identity.appendChild(heading); + if (description && description !== "N/A") { + var detail = document.createElement("p"); + detail.className = "acs-case-description"; + detail.textContent = description; + identity.appendChild(detail); + } + top.appendChild(identity); + if (statusText && statusText !== "N/A") { + var status = classifyStatus(statusText, "", true) || "unknown"; + var statusWrap = document.createElement("div"); + statusWrap.className = "acs-case-status"; + var pill = document.createElement("span"); + pill.className = "acs-status-pill " + status; + pill.setAttribute("data-acs-status", status); + pill.setAttribute("data-acs-status-label", statusLabel(status, statusText)); + pill.textContent = statusDisplay(status, statusText); + statusWrap.appendChild(pill); + top.appendChild(statusWrap); + } + overview.appendChild(top); + return overview; + } + + function buildMetadataOverview(group) { + var fields = headingFields(group); + var suiteName = fields["test suite name"] || "N/A"; + var subSuite = fields["sub test suite"] || "N/A"; + var overview = caseOverview( + fields["test case"], + fields["test case description"], + "Test case", + fields["test result"] + ); + overview.setAttribute( + "data-acs-jump-context", + suiteName + (subSuite !== "N/A" ? " \u00b7 " + subSuite : "") + ); + overview.setAttribute("data-acs-test-suite", suiteName); + overview.setAttribute("data-acs-sub-test-suite", subSuite); + overview.setAttribute("data-acs-test-case", fields["test case"] || "N/A"); + var grid = document.createElement("div"); + grid.className = "acs-metadata-grid"; + grid.appendChild(metaItem("Test suite", suiteName)); + grid.appendChild(metaItem("Sub test suite", subSuite)); + if (fields["test entry point guid"]) { + grid.appendChild(metaItem( + "Test entry point GUID", + fields["test entry point guid"], + "acs-meta-code" + )); + } + if (fields.reason) { + grid.appendChild(metaItem("Reason", fields.reason, "acs-meta-wide")); + } + if (fields["device path"]) { + grid.appendChild(metaItem("Device path", fields["device path"], "acs-meta-code")); + } + var suiteInfo = group.querySelector(".test-suite-info"); + if (suiteInfo) { + grid.appendChild(sourceMetaItem( + "Test suite info", + suiteInfo, + /^(?:Test_suite_info|Test suite info):\s*/i, + "acs-meta-full" + )); + suiteInfo.classList.add("acs-metadata-source"); + suiteInfo.setAttribute("aria-hidden", "true"); + } + overview.appendChild(grid); + group.insertBefore(overview, group.firstChild); + } + + function buildCaseOverview(group) { + var suiteHeader = group.querySelector(".test-suite-header"); + var suiteDescription = group.querySelector(".test-suite-description"); + var caseHeader = group.querySelector(".test-case-header"); + var caseDescription = group.querySelector(".test-case-description"); + var suiteName = textWithoutPrefix(suiteHeader, /^Test Suite:\s*/i); + var overview = caseOverview( + textWithoutPrefix(caseHeader, /^Test Case:\s*/i), + textWithoutPrefix(caseDescription, /^Description:\s*/i), + "Test case", + "" + ); + overview.setAttribute("data-acs-jump-context", suiteName); + overview.setAttribute("data-acs-test-suite", suiteName); + overview.setAttribute( + "data-acs-test-case", + textWithoutPrefix(caseHeader, /^Test Case:\s*/i) + ); + var grid = document.createElement("div"); + grid.className = "acs-metadata-grid"; + grid.appendChild(metaItem("Test suite", suiteName)); + grid.appendChild(metaItem( + "Suite description", + textWithoutPrefix(suiteDescription, /^Description:\s*/i), + "acs-meta-wide" + )); + var suiteInfo = group.querySelector(".test-suite-info"); + if (suiteInfo) { + grid.appendChild(sourceMetaItem( + "Test suite info", + suiteInfo, + /^(?:Test_suite_info|Test suite info):\s*/i, + "acs-meta-full" + )); + } + overview.appendChild(grid); + [suiteHeader, suiteDescription, suiteInfo, caseHeader, caseDescription].forEach(function (element) { + if (element) { + element.classList.add("acs-metadata-source"); + element.setAttribute("aria-hidden", "true"); + } + }); + group.insertBefore(overview, group.firstChild); + } + + function buildSuiteOverview(group) { + var suiteHeader = group.querySelector(":scope > .test-suite-header, :scope > .suite-header"); + if (!suiteHeader) { + return; + } + var suiteDescription = group.querySelector(":scope > .test-suite-description"); + var suiteInfo = group.querySelector(":scope > .test-suite-info"); + var suiteReason = group.querySelector(":scope > .suite-reason"); + var suiteName = textWithoutPrefix(suiteHeader, /^Test Suite:\s*/i); + var description = textWithoutPrefix( + suiteDescription, + /^(?:Suite )?Description:\s*/i + ); + var overview = caseOverview(suiteName, description, "Test suite", ""); + overview.setAttribute("data-acs-test-suite", suiteName); + [ + "data-acs-summary-outcomes", + "data-acs-summary-failed", + "data-acs-summary-failed-with-waiver" + ].forEach(function (attribute) { + if (suiteHeader.hasAttribute(attribute)) { + overview.setAttribute(attribute, suiteHeader.getAttribute(attribute)); + } + }); + var grid = document.createElement("div"); + grid.className = "acs-metadata-grid"; + if (suiteInfo) { + grid.appendChild(sourceMetaItem( + "Test suite info", + suiteInfo, + /^(?:Test_suite_info|Test suite info):\s*/i, + "acs-meta-full" + )); + } + if (suiteReason) { + grid.appendChild(sourceMetaItem( + "Reason", + suiteReason, + /^Reason:\s*/i, + "acs-meta-full" + )); + } + if (grid.children.length) { + overview.appendChild(grid); + } + [suiteHeader, suiteDescription, suiteInfo, suiteReason].forEach(function (element) { + if (element) { + element.classList.add("acs-metadata-source"); + element.setAttribute("aria-hidden", "true"); + } + }); + group.insertBefore(overview, group.firstChild); + } + + function buildFlatOverview(detail, profile) { + var sectionTitle = detail.querySelector(":scope > h3"); + var heading = detail.querySelector(":scope > h2"); + var rawTitle = normalizeText(sectionTitle ? sectionTitle.textContent : ""); + var titleParts = rawTitle.match(/^([^:]+):\s*(.*)$/); + var title = titleParts ? titleParts[1] : (rawTitle || profile.label || "Test results"); + var description = titleParts ? titleParts[2] : ""; + var overview = caseOverview(title, description, "Test suite", ""); + overview.setAttribute("data-acs-test-suite", title); + [heading, sectionTitle].forEach(function (element) { + if (element) { + element.classList.add("acs-metadata-source"); + element.setAttribute("aria-hidden", "true"); + } + }); + detail.insertBefore(overview, detail.firstChild); + } + + function wrapThroughFirstTable(start, className) { + var parent = start.parentNode; + var group = document.createElement("article"); + group.className = "acs-result-group " + className; + parent.insertBefore(group, start); + var node = start; + while (node) { + var next = node.nextElementSibling; + group.appendChild(node); + if (node.tagName === "TABLE") { + break; + } + node = next; + } + return group; + } + + function wrapUntilNextSuite(start, selector) { + var parent = start.parentNode; + var group = document.createElement("section"); + group.className = "acs-result-group acs-suite-card"; + parent.insertBefore(group, start); + var node = start; + while (node) { + var next = node.nextElementSibling; + if (node !== start && node.matches(selector)) { + break; + } + group.appendChild(node); + node = next; + } + return group; + } + + function upgradeSuiteLayouts() { + var suiteType = document.body.getAttribute("data-acs-suite"); + var profile = SUITE_PROFILES[suiteType] || {}; + var detail = document.querySelector(".detailed-summary, .detailed-container"); + if (!detail || detail.classList.contains("acs-layout-ready")) { + return; + } + detail.classList.add("acs-layout-ready"); + + if (profile.layout === "metadata") { + Array.prototype.forEach.call(detail.querySelectorAll(":scope > .heading"), function (heading) { + if (heading.parentNode !== detail || + !/^Test Suite Name:/i.test(normalizeText(heading.textContent))) { + return; + } + var group = wrapThroughFirstTable(heading, "acs-case-card"); + buildMetadataOverview(group); + }); + } else if (profile.layout === "case") { + Array.prototype.forEach.call(detail.querySelectorAll(":scope > .test-suite-header"), function (header) { + if (header.parentNode !== detail) { + return; + } + var group = wrapThroughFirstTable(header, "acs-case-card"); + buildCaseOverview(group); + }); + } else if (profile.layout === "suite" || profile.layout === "hierarchy") { + var selector = ".test-suite-header, .suite-header"; + Array.prototype.forEach.call(detail.querySelectorAll(":scope > .test-suite-header, :scope > .suite-header"), function (header) { + if (header.parentNode !== detail) { + return; + } + buildSuiteOverview(wrapUntilNextSuite(header, selector)); + }); + } else if (profile.layout === "flat") { + detail.classList.add("acs-result-group", "acs-flat-card"); + buildFlatOverview(detail, profile); + } + } + + function columnClass(label) { + var text = normalizeText(label).toLowerCase(); + if (/result|status/.test(text)) { return "acs-col-status"; } + if (/guid/.test(text)) { return "acs-col-guid"; } + if (/path/.test(text)) { return "acs-col-path"; } + if (/waiver/.test(text)) { return "acs-col-waiver"; } + if (/reason/.test(text)) { return "acs-col-reason"; } + if (/description/.test(text)) { return "acs-col-description"; } + if (/number|^#$|test case/.test(text)) { return "acs-col-number"; } + return ""; + } + + function upgradeTableColumns() { + var suiteType = document.body.getAttribute("data-acs-suite"); + var collapseReasons = suiteType === "sct" || suiteType === "fwts"; + Array.prototype.forEach.call(document.querySelectorAll("table:not(.summary-table)"), function (table) { + if (!table.tHead || !table.tHead.rows.length) { + return; + } + var headings = Array.prototype.slice.call(table.tHead.rows[0].cells); + headings.forEach(function (heading, index) { + var className = columnClass(heading.textContent); + if (!className) { + return; + } + heading.classList.add(className); + Array.prototype.forEach.call(table.tBodies, function (body) { + Array.prototype.forEach.call(body.rows, function (row) { + var cell = row.cells[index]; + if (!cell) { + return; + } + cell.classList.add(className); + if (className === "acs-col-path" && + normalizeText(cell.textContent).length > 100 && + !cell.querySelector(".acs-long-value")) { + var fullPath = normalizeText(cell.textContent); + var disclosure = document.createElement("details"); + disclosure.className = "acs-long-value"; + var label = document.createElement("summary"); + label.textContent = "View source path"; + var code = document.createElement("code"); + code.textContent = fullPath; + disclosure.appendChild(label); + disclosure.appendChild(code); + cell.title = fullPath; + cell.textContent = ""; + cell.appendChild(disclosure); + } else if (className === "acs-col-reason" && + collapseReasons && + !cell.querySelector(".acs-long-reason")) { + var reasonDisclosure = document.createElement("details"); + reasonDisclosure.className = "acs-long-value acs-long-reason"; + var reasonLabel = document.createElement("summary"); + reasonLabel.textContent = "View full reason"; + var reasonText = document.createElement("span"); + reasonText.className = "acs-long-reason-text"; + while (cell.firstChild) { + reasonText.appendChild(cell.firstChild); + } + reasonDisclosure.appendChild(reasonLabel); + reasonDisclosure.appendChild(reasonText); + cell.appendChild(reasonDisclosure); + } + }); + }); + }); + }); + } + + function statusColumnIndex(table) { + if (!table) { + return -1; + } + if (typeof table.acsStatusColumnIndex === "number") { + return table.acsStatusColumnIndex; + } + var index = -1; + if (table.tHead && table.tHead.rows.length) { + var rows = Array.prototype.slice.call(table.tHead.rows).reverse(); + rows.some(function (headerRow) { + return Array.prototype.some.call(headerRow.cells, function (heading, cellIndex) { + var label = normalizeText(heading.textContent).toLowerCase(); + if (/\b(result|status|outcome)\b/.test(label) && + !/reason|summary|description/.test(label)) { + index = cellIndex; + return true; + } + return false; + }); + }); + } + table.acsStatusColumnIndex = index; + return index; + } + + function directStatusCell(row) { + var cells = Array.prototype.slice.call(row.cells || []); + var table = row.closest("table"); + var headerIndex = statusColumnIndex(table); + if (headerIndex >= 0 && cells[headerIndex]) { + var headerStatus = classifyStatusCell(cells[headerIndex], true); + if (headerStatus) { + return { + cell: cells[headerIndex], + status: headerStatus, + label: statusLabel(headerStatus, cells[headerIndex].textContent) + }; + } + } + for (var index = 0; index < cells.length; index += 1) { + var status = classifyStatusCell(cells[index], false); + if (status) { + return { + cell: cells[index], + status: status, + label: statusLabel(status, cells[index].textContent) + }; + } + } + return null; + } + + function upgradeStatusCells() { + Array.prototype.forEach.call(document.querySelectorAll("table:not(.summary-table) tbody > tr"), function (row) { + if (row.querySelector("table") || row.closest(".system-info, .acs-results-summary")) { + return; + } + var found = directStatusCell(row); + if (!found) { + return; + } + var cell = found.cell; + row.classList.add("acs-row-" + found.status); + row.setAttribute("data-acs-row-status", found.status); + row.style.setProperty("--acs-row-accent", statusAccent(found.status)); + cell.classList.add("acs-status-cell"); + cell.setAttribute("data-acs-status", found.status); + cell.setAttribute("data-acs-status-label", found.label); + if (cell.querySelector(".acs-status-pill")) { + return; + } + var text = normalizeText(cell.textContent); + var pill = document.createElement("span"); + pill.className = "acs-status-pill " + found.status; + pill.setAttribute("data-acs-status", found.status); + pill.setAttribute("data-acs-status-label", found.label); + pill.textContent = statusDisplay(found.status, text); + cell.textContent = ""; + cell.appendChild(pill); + }); + } + + function decorateGroupStatuses() { + var priority = [ + "fail", "fail-waiver", "warning", "passed-partial", "pass", + "skipped", "aborted", "ignored", "not-implemented", "not-tested", + "pal-not-supported", "unknown", "info" + ]; + Array.prototype.forEach.call(document.querySelectorAll(".acs-result-group"), function (group) { + var status = ""; + var overviewStatus = group.querySelector(".acs-case-status .acs-status-pill"); + if (overviewStatus) { + status = overviewStatus.getAttribute("data-acs-status") || ""; + } + if (!status) { + priority.some(function (candidate) { + if (group.querySelector(".acs-row-" + candidate)) { + status = candidate; + return true; + } + return false; + }); + } + if (!status) { + var firstStatusRow = group.querySelector("[data-acs-row-status]"); + status = firstStatusRow ? firstStatusRow.getAttribute("data-acs-row-status") : ""; + } + if (status) { + group.classList.add("acs-group-" + status); + group.setAttribute("data-acs-group-status", status); + group.style.borderLeftColor = statusAccent(status); + } + }); + } + + function removeDetailedCharts() { + if (document.body.getAttribute("data-acs-view") !== "detail") { + return; + } + Array.prototype.forEach.call(document.querySelectorAll(".chart-container"), function (chart) { + chart.remove(); + }); + Array.prototype.forEach.call(document.querySelectorAll(".acs-chart-panel"), function (panel) { + panel.remove(); + }); + } + + function decorateSummaryContext() { + if (document.body.getAttribute("data-acs-view") !== "detail") { + return; + } + Array.prototype.forEach.call(document.querySelectorAll(".result-summary p"), function (paragraph) { + if (paragraph.querySelector("a[href*='RuleBasedGuide']")) { + paragraph.classList.add("acs-reference-note"); + var link = paragraph.querySelector("a"); + link.textContent = "Rule result status guide \u2197"; + } + }); + } + + function addMobileStatusPreviews() { + Array.prototype.forEach.call(document.querySelectorAll("tr[data-acs-row-status]"), function (row) { + if (row.querySelector(".acs-mobile-status-preview")) { + return; + } + var statusCell = row.querySelector(".acs-status-cell"); + var pill = statusCell && statusCell.querySelector(".acs-status-pill"); + if (!pill) { + return; + } + var description = row.querySelector(".acs-col-description") || row.cells[1] || row.cells[0]; + if (!description || description === statusCell) { + return; + } + var preview = document.createElement("span"); + preview.className = "acs-mobile-status-preview"; + preview.appendChild(pill.cloneNode(true)); + description.appendChild(preview); + }); + } + + function hideLegacyNavigation() { + Array.prototype.forEach.call(document.querySelectorAll(".dropdown"), function (dropdown) { + dropdown.classList.add("acs-legacy-navigation"); + dropdown.setAttribute("aria-hidden", "true"); + }); + } + + function addMobileTableNote(toolbar) { + if (!toolbar || document.querySelector(".acs-mobile-table-note")) { + return; + } + var note = document.createElement("p"); + note.className = "acs-mobile-table-note"; + note.textContent = "Results are repeated beside descriptions on narrow screens. Swipe tables for paths, reasons, and waivers."; + toolbar.insertAdjacentElement("afterend", note); + } + + function groupSearchText(group) { + if (!group) { + return ""; + } + if (typeof group.acsSearchText === "string") { + return group.acsSearchText; + } + var contextParts = []; + Array.prototype.forEach.call(group.children, function (child) { + if (child.tagName !== "TABLE" && !child.querySelector("table")) { + contextParts.push(child.textContent); + } + }); + group.acsSearchText = normalizeText(contextParts.join(" ")).toLowerCase(); + return group.acsSearchText; + } + + function statusRows() { + var records = []; + Array.prototype.forEach.call(document.querySelectorAll("table:not(.summary-table) tbody > tr"), function (row) { + var table = row.closest("table"); + if (!table || table.closest(".system-info") || table.closest(".acs-results-summary") || + row.querySelector("table")) { + return; + } + var found = directStatusCell(row); + if (found) { + var group = row.closest(".acs-result-group"); + records.push({ + row: row, + group: group, + status: found.status, + label: found.label, + text: normalizeText(row.textContent).toLowerCase() + " " + groupSearchText(group), + match: true + }); + } + }); + Array.prototype.forEach.call(document.querySelectorAll(".acs-result-group"), function (group) { + var pill = group.querySelector(".acs-case-status .acs-status-pill[data-acs-status]"); + if (pill) { + group.acsOverviewStatus = pill.getAttribute("data-acs-status"); + group.acsOverviewStatusLabel = pill.getAttribute("data-acs-status-label") || + statusLabel(group.acsOverviewStatus, pill.textContent); + } + if (!pill) { + return; + } + var status = pill.getAttribute("data-acs-status"); + records.push({ + row: null, + group: group, + status: status, + label: pill.getAttribute("data-acs-status-label") || statusLabel(status, pill.textContent), + text: groupSearchText(group), + match: true + }); + }); + return records; + } + + function navigationTargets() { + var profile = SUITE_PROFILES[document.body.getAttribute("data-acs-suite")] || {}; + if (profile.layout === "flat") { + return {kind: "test suite", targets: []}; + } + var kind = profile.layout === "case" || profile.layout === "metadata" ? + "test case" : "test suite"; + var targets = []; + Array.prototype.forEach.call(document.querySelectorAll(".acs-result-group"), function (group, index) { + var title = group.querySelector(":scope > .acs-case-overview .acs-case-title"); + var label = normalizeText(title ? title.textContent : ""); + if (!label) { + return; + } + if (!group.id) { + group.id = "acs-" + kind.replace(/\s+/g, "-") + "-" + index; + } + var overview = group.querySelector(":scope > .acs-case-overview"); + var context = normalizeText( + overview ? overview.getAttribute("data-acs-jump-context") : "" + ); + targets.push({ + element: group, + label: kind === "test case" && context ? context + " · " + label : label + }); + }); + return {kind: kind, targets: targets}; + } + + function actionButton(label, className) { + var button = document.createElement("button"); + button.type = "button"; + button.className = "acs-button" + (className ? " " + className : ""); + button.textContent = label; + return button; + } + + function setGroupCollapsed(group, collapsed) { + group.classList.toggle("acs-collapsed", collapsed); + var toggle = group.querySelector(":scope > .acs-group-toggle"); + if (toggle) { + toggle.setAttribute("aria-expanded", collapsed ? "false" : "true"); + toggle.textContent = collapsed ? "Show details" : "Hide details"; + } + } + + function enableGroupCollapsing(groups) { + groups.forEach(function (group) { + if (group.classList.contains("acs-flat-card") || + !group.querySelector(":scope > .acs-case-overview")) { + return; + } + if (!group.classList.contains("acs-collapsible")) { + group.classList.add("acs-collapsible"); + var toggle = document.createElement("button"); + toggle.type = "button"; + toggle.className = "acs-group-toggle"; + toggle.addEventListener("click", function () { + setGroupCollapsed(group, !group.classList.contains("acs-collapsed")); + }); + group.appendChild(toggle); + } + group.setAttribute("data-acs-default-collapsed", "false"); + setGroupCollapsed(group, false); + }); + } + + function buildFailureSummary(groups) { + var suiteType = document.body.getAttribute("data-acs-suite"); + var profile = SUITE_PROFILES[suiteType] || {}; + var groupBySuite = suiteType === "sct" || + profile.layout === "suite" || profile.layout === "hierarchy" || + profile.layout === "flat"; + var unitKind = groupBySuite ? "test suite" : "test case"; + var units = []; + var unitMap = Object.create(null); + + groups.forEach(function (group) { + var overview = group.querySelector(":scope > .acs-case-overview"); + var title = normalizeText(overview && + overview.querySelector(".acs-case-title") ? + overview.querySelector(".acs-case-title").textContent : ""); + var suiteName = normalizeText( + overview ? overview.getAttribute("data-acs-test-suite") : "" + ); + var caseName = normalizeText( + overview ? overview.getAttribute("data-acs-test-case") : "" + ); + var subSuiteName = normalizeText( + overview ? overview.getAttribute("data-acs-sub-test-suite") : "" + ); + var label; + var key; + var unit; + + if (groupBySuite) { + label = suiteName && suiteName !== "N/A" ? suiteName : title; + label = label || "Unnamed test suite"; + key = "suite:" + normalizeText(label); + unit = unitMap[key]; + if (!unit) { + unit = { + label: label, + groups: [], + outcomes: 0, + failed: 0, + waived: 0 + }; + unitMap[key] = unit; + units.push(unit); + } + } else { + label = caseName && caseName !== "N/A" ? caseName : title; + label = label || "Unnamed test case"; + var context = []; + if (suiteName && suiteName !== "N/A" && suiteName !== label) { + context.push(suiteName); + } + if (subSuiteName && subSuiteName !== "N/A" && + subSuiteName !== label && subSuiteName !== suiteName) { + context.push(subSuiteName); + } + context.push(label); + label = context.join(" \u00b7 "); + key = "case:" + JSON.stringify([ + suiteName, subSuiteName, caseName || title + ].map(function (value) { return normalizeText(value); })); + unit = unitMap[key]; + if (!unit) { + unit = { + label: label, + groups: [], + outcomes: 0, + failed: 0, + waived: 0 + }; + unitMap[key] = unit; + units.push(unit); + } + } + unit.groups.push(group); + var groupStatuses = []; + var leafRowCount = 0; + Array.prototype.forEach.call( + group.querySelectorAll("table tbody > tr"), + function (row) { + if (row.closest(".acs-result-group") !== group || row.querySelector("table")) { + return; + } + leafRowCount += 1; + groupStatuses.push( + row.getAttribute("data-acs-row-status") || "unknown" + ); + } + ); + if (!leafRowCount) { + var overviewStatus = overview && overview.querySelector( + ".acs-case-status .acs-status-pill[data-acs-status]" + ); + if (overviewStatus) { + groupStatuses.push(overviewStatus.getAttribute("data-acs-status")); + } + } + var summaryOutcomes = overview && overview.hasAttribute( + "data-acs-summary-outcomes" + ) ? Number(overview.getAttribute("data-acs-summary-outcomes")) : NaN; + var summaryFailed = overview && overview.hasAttribute( + "data-acs-summary-failed" + ) ? Number(overview.getAttribute("data-acs-summary-failed")) : NaN; + var summaryWaived = overview && overview.hasAttribute( + "data-acs-summary-failed-with-waiver" + ) ? Number(overview.getAttribute( + "data-acs-summary-failed-with-waiver" + )) : NaN; + if (Number.isFinite(summaryOutcomes) && summaryOutcomes >= 0 && + Number.isFinite(summaryFailed) && summaryFailed >= 0 && + Number.isFinite(summaryWaived) && summaryWaived >= 0) { + unit.outcomes += summaryOutcomes; + unit.failed += summaryFailed; + unit.waived += summaryWaived; + } else { + unit.outcomes += groupStatuses.length; + unit.failed += groupStatuses.filter(function (status) { + return status === "fail"; + }).length; + unit.waived += groupStatuses.filter(function (status) { + return status === "fail-waiver"; + }).length; + } + }); + + var totalFailed = 0; + var totalWaived = 0; + units.forEach(function (unit) { + totalFailed += unit.failed; + totalWaived += unit.waived; + }); + + var panel = document.createElement("aside"); + panel.className = "acs-failure-summary"; + panel.setAttribute("aria-labelledby", "acs-failure-summary-title"); + panel.setAttribute("data-acs-breakdown-kind", unitKind); + panel.setAttribute("data-acs-unit-count", String(units.length)); + + var header = document.createElement("div"); + header.className = "acs-compact-summary-header"; + var heading = document.createElement("h2"); + heading.id = "acs-failure-summary-title"; + heading.textContent = "Failures by " + unitKind; + var total = document.createElement("span"); + total.className = "acs-compact-summary-total"; + total.textContent = units.length + " " + unitKind + + (units.length === 1 ? "" : "s") + " \u00b7 " + + totalFailed + " failed" + + (totalWaived ? " \u00b7 " + totalWaived + " waived" : ""); + header.appendChild(heading); + header.appendChild(total); + panel.appendChild(header); + + var list = document.createElement("div"); + list.className = "acs-progress-list"; + list.setAttribute("role", "list"); + list.setAttribute( + "aria-label", + "Failure counts for every " + unitKind + " in this report" + ); + units.forEach(function (unit) { + var outcomeCount = unit.outcomes; + var maximum = Math.max(outcomeCount, unit.failed + unit.waived, 1); + var failedWidth = outcomeCount > 0 ? + Math.min(100, (unit.failed / outcomeCount) * 100) : 0; + var waiverWidth = outcomeCount > 0 ? + Math.min(100 - failedWidth, (unit.waived / outcomeCount) * 100) : 0; + var item = document.createElement("div"); + item.className = "acs-progress-row acs-failure-row"; + item.setAttribute("role", "listitem"); + item.setAttribute("data-acs-group-count", String(unit.groups.length)); + item.setAttribute("data-acs-outcomes", String(outcomeCount)); + item.setAttribute("data-acs-failed", String(unit.failed)); + item.setAttribute("data-acs-failed-with-waiver", String(unit.waived)); + item.setAttribute( + "data-acs-zero", String(unit.failed === 0 && unit.waived === 0) + ); + + var label = document.createElement("span"); + label.className = "acs-progress-label"; + label.textContent = unit.label; + + var track = document.createElement("span"); + track.className = "acs-progress-track"; + track.setAttribute("role", "progressbar"); + track.setAttribute( + "aria-label", + unit.label + ": " + unit.failed + " failed, " + + unit.waived + " failed with waiver, " + outcomeCount + " outcomes" + ); + track.setAttribute("aria-valuemin", "0"); + track.setAttribute("aria-valuemax", String(maximum)); + track.setAttribute("aria-valuenow", String(unit.failed + unit.waived)); + var failedFill = document.createElement("span"); + failedFill.className = "acs-progress-fill acs-failure-fill"; + failedFill.setAttribute("aria-hidden", "true"); + failedFill.style.backgroundColor = statusAccent("fail"); + failedFill.style.width = failedWidth.toFixed(4).replace(/\.?0+$/, "") + "%"; + failedFill.style.minWidth = unit.failed > 0 ? "2px" : "0"; + track.appendChild(failedFill); + if (unit.waived) { + var waiverFill = document.createElement("span"); + waiverFill.className = "acs-progress-fill acs-failure-fill-waiver"; + waiverFill.setAttribute("aria-hidden", "true"); + waiverFill.style.width = waiverWidth.toFixed(4).replace(/\.?0+$/, "") + "%"; + waiverFill.style.minWidth = "2px"; + track.appendChild(waiverFill); + } + + var count = document.createElement("span"); + count.className = "acs-progress-count"; + count.appendChild(document.createTextNode(String(unit.failed))); + if (unit.waived) { + var waiverCount = document.createElement("span"); + waiverCount.className = "acs-failure-waiver-count"; + waiverCount.textContent = " + " + unit.waived + " waived"; + waiverCount.title = unit.waived + " failed with waiver"; + count.appendChild(waiverCount); + } + + item.appendChild(label); + item.appendChild(track); + item.appendChild(count); + list.appendChild(item); + }); + panel.appendChild(list); + return panel; + } + + function buildDetailedToolbar() { + var records = statusRows(); + var resultGroups = Array.prototype.slice.call(document.querySelectorAll(".acs-result-group")); + resultGroups.forEach(function (group) { group.acsFilterRecords = []; }); + records.forEach(function (record) { + if (record.group) { + record.group.acsFilterRecords.push(record); + } + }); + enableGroupCollapsing(resultGroups); + var toolbar = document.createElement("section"); + toolbar.className = "acs-toolbar"; + toolbar.setAttribute("aria-label", "Report controls"); + + var searchControl = document.createElement("label"); + searchControl.className = "acs-control acs-control-grow"; + var searchLabel = document.createElement("span"); + searchLabel.className = "acs-control-label"; + searchLabel.textContent = "Search results"; + var search = document.createElement("input"); + search.type = "search"; + search.placeholder = "Test, rule, description, reason\u2026"; + search.setAttribute("aria-label", "Search report rows"); + searchControl.appendChild(searchLabel); + searchControl.appendChild(search); + toolbar.appendChild(searchControl); + + var navigation = navigationTargets(); + if (navigation.targets.length > 1) { + var suiteControl = document.createElement("label"); + suiteControl.className = "acs-control"; + var suiteLabel = document.createElement("span"); + suiteLabel.className = "acs-control-label"; + suiteLabel.textContent = "Jump to " + navigation.kind; + var suiteSelect = document.createElement("select"); + suiteSelect.setAttribute("aria-label", "Jump to " + navigation.kind); + var prompt = document.createElement("option"); + prompt.value = ""; + prompt.textContent = "Choose a " + navigation.kind + "\u2026"; + suiteSelect.appendChild(prompt); + navigation.targets.forEach(function (header) { + var option = document.createElement("option"); + option.value = header.element.id; + option.textContent = header.label; + suiteSelect.appendChild(option); + }); + suiteSelect.addEventListener("change", function () { + var target = document.getElementById(suiteSelect.value); + if (target) { + target.scrollIntoView({behavior: "smooth", block: "start"}); + suiteSelect.value = ""; + } + }); + suiteControl.appendChild(suiteLabel); + suiteControl.appendChild(suiteSelect); + toolbar.appendChild(suiteControl); + } + + var actions = document.createElement("div"); + actions.className = "acs-actions"; + var reset = actionButton("Reset"); + var print = actionButton("Print all results / PDF"); + print.setAttribute("aria-label", "Print all result rows or save them as PDF"); + print.title = "Prints every result group and nested result row, including items collapsed or filtered on screen."; + actions.appendChild(reset); + actions.appendChild(print); + + var hasExpanders = document.querySelector(".testcase-toggle, .expand-subtests, .collapse-subtests, .acs-group-toggle"); + var expand; + var collapse; + if (hasExpanders) { + expand = actionButton("Expand all", "acs-button-primary"); + collapse = actionButton("Collapse all"); + actions.insertBefore(expand, reset); + actions.insertBefore(collapse, reset); + } + var count = document.createElement("div"); + count.className = "acs-filter-count"; + count.setAttribute("aria-live", "polite"); + toolbar.appendChild(count); + toolbar.appendChild(actions); + + var filters = document.createElement("div"); + filters.className = "acs-status-filters"; + filters.setAttribute("aria-label", "Filter by status"); + toolbar.appendChild(filters); + + var filterScope = document.createElement("div"); + filterScope.className = "acs-filter-scope"; + filterScope.textContent = "Case and subtest outcomes in this report"; + filters.appendChild(filterScope); + + var filterHelp = document.createElement("p"); + filterHelp.className = "acs-filter-help"; + filterHelp.textContent = "Choose one status to focus the report. Choose it again, or All, to restore every result."; + filters.appendChild(filterHelp); + + var printScopeNote = document.createElement("p"); + printScopeNote.className = "acs-print-scope-note"; + printScopeNote.textContent = "PDF includes every result group and nested result row, including items collapsed or filtered on screen. Summary totals remain unchanged."; + toolbar.appendChild(printScopeNote); + + var totals = {}; + var labels = {}; + var encountered = []; + var activeStatus = ""; + records.forEach(function (record) { + if (encountered.indexOf(record.status) < 0) { + encountered.push(record.status); + } + totals[record.status] = (totals[record.status] || 0) + 1; + labels[record.status] = labels[record.status] || record.label || statusLabel(record.status, record.status); + }); + var allButton = document.createElement("button"); + allButton.type = "button"; + allButton.className = "acs-status-filter info"; + allButton.setAttribute("aria-pressed", "true"); + allButton.textContent = "All outcomes " + records.length; + filters.appendChild(allButton); + var filterButtons = {}; + var orderedStatuses = STATUS_ORDER.filter(function (status) { return totals[status]; }); + encountered.forEach(function (status) { + if (orderedStatuses.indexOf(status) < 0) { + orderedStatuses.push(status); + } + }); + orderedStatuses.forEach(function (status) { + if (!totals[status]) { + return; + } + var button = document.createElement("button"); + button.type = "button"; + button.className = "acs-status-filter " + status; + button.setAttribute("aria-pressed", "false"); + button.textContent = labels[status] + " " + totals[status]; + button.addEventListener("click", function () { + activeStatus = activeStatus === status ? "" : status; + applyFilters(); + }); + filterButtons[status] = button; + filters.appendChild(button); + }); + + var anchor = document.querySelector(".detailed-summary, .detailed-container"); + var overview = null; + if (anchor) { + anchor.parentNode.insertBefore(toolbar, anchor); + var summary = anchor.parentNode.querySelector( + ":scope > .result-summary.acs-compact-summary, " + + ":scope > .summary-container.acs-compact-summary, " + + ":scope > .card.acs-compact-summary" + ); + if (summary) { + overview = document.createElement("section"); + overview.className = "acs-detail-overview"; + overview.setAttribute( + "aria-label", "Result summary and failure breakdown" + ); + summary.parentNode.insertBefore(overview, summary); + overview.appendChild(summary); + overview.appendChild(buildFailureSummary(resultGroups)); + } + } else { + document.body.insertBefore(toolbar, document.body.firstChild); + } + var empty = document.createElement("div"); + empty.className = "acs-empty-state"; + empty.textContent = "No report rows match the selected filters."; + toolbar.insertAdjacentElement("afterend", empty); + var printBanner = document.createElement("div"); + printBanner.className = "acs-print-banner"; + printBanner.textContent = "All-results PDF: every result group and nested result row is included. Suite-reported totals above still cover the complete run."; + empty.insertAdjacentElement("afterend", printBanner); + addMobileTableNote(toolbar); + + function applyFilters() { + var query = normalizeText(search.value).toLowerCase(); + var visible = 0; + records.forEach(function (record) { + record.match = (!activeStatus || record.status === activeStatus) && + (!query || record.text.indexOf(query) >= 0); + if (record.row) { + record.row.classList.toggle("acs-filter-hidden", !record.match); + record.row.classList.remove("acs-context-row"); + } + if (record.match) { + visible += 1; + } + }); + + Array.prototype.forEach.call(document.querySelectorAll("tr"), function (container) { + if (!container.querySelector("table")) { + return; + } + var childMatches = records.some(function (record) { + return record.match && container.contains(record.row); + }); + var parentRow = container.previousElementSibling; + var parentRecord = records.find(function (record) { return record.row === parentRow; }); + var parentMatches = parentRecord && parentRecord.match; + container.classList.toggle("acs-filter-hidden", !childMatches && !parentMatches); + if (childMatches && parentRecord && !parentMatches) { + parentRow.classList.remove("acs-filter-hidden"); + parentRow.classList.add("acs-context-row"); + } + }); + + var matchingGroupCount = resultGroups.filter(function (group) { + return (group.acsFilterRecords || []).some(function (record) { + return record.match; + }); + }).length; + var expandMatchingGroups = Boolean(activeStatus || query) && matchingGroupCount <= 40; + + resultGroups.forEach(function (group) { + var groupRecords = group.acsFilterRecords || []; + if (groupRecords.length) { + var groupMatches = groupRecords.some(function (record) { + return record.match; + }); + group.classList.toggle("acs-filter-hidden", !groupMatches); + if (group.classList.contains("acs-collapsible")) { + if (activeStatus || query) { + if (!groupMatches) { + setGroupCollapsed(group, true); + } else if (expandMatchingGroups) { + setGroupCollapsed(group, false); + } else { + setGroupCollapsed(group, true); + } + } else { + setGroupCollapsed( + group, + group.getAttribute("data-acs-default-collapsed") === "true" + ); + } + } + } + }); + + allButton.setAttribute("aria-pressed", activeStatus ? "false" : "true"); + Object.keys(filterButtons).forEach(function (status) { + filterButtons[status].setAttribute("aria-pressed", activeStatus === status ? "true" : "false"); + }); + count.textContent = "Matching " + visible + " of " + records.length + " outcomes"; + if ((activeStatus || query) && !expandMatchingGroups && matchingGroupCount > 40) { + count.textContent += " · groups kept collapsed for performance"; + } + empty.classList.toggle("visible", visible === 0); + } + + allButton.addEventListener("click", function () { + activeStatus = ""; + applyFilters(); + }); + search.addEventListener("input", applyFilters); + reset.addEventListener("click", function () { + search.value = ""; + activeStatus = ""; + applyFilters(); + search.focus(); + }); + print.addEventListener("click", function () { window.print(); }); + + if (expand && collapse) { + expand.addEventListener("click", function () { + Array.prototype.forEach.call(document.querySelectorAll(".testcase-toggle[aria-expanded='false']"), function (button) { + button.click(); + }); + Array.prototype.forEach.call(document.querySelectorAll(".expand-subtests:not(:disabled)"), function (button) { + if (!button.closest(".acs-toolbar")) { button.click(); } + }); + Array.prototype.forEach.call(document.querySelectorAll(".acs-result-group.acs-collapsed"), function (group) { + setGroupCollapsed(group, false); + }); + }); + collapse.addEventListener("click", function () { + Array.prototype.forEach.call(document.querySelectorAll(".testcase-toggle[aria-expanded='true']"), function (button) { + button.click(); + }); + Array.prototype.forEach.call(document.querySelectorAll(".collapse-subtests:not(:disabled)"), function (button) { + if (!button.closest(".acs-toolbar")) { button.click(); } + }); + Array.prototype.forEach.call(document.querySelectorAll(".acs-result-group.acs-collapsible:not(.acs-collapsed)"), function (group) { + setGroupCollapsed(group, true); + }); + }); + } + + document.addEventListener("keydown", function (event) { + var tag = document.activeElement && document.activeElement.tagName; + if (event.key === "/" && tag !== "INPUT" && tag !== "TEXTAREA" && tag !== "SELECT") { + event.preventDefault(); + search.focus(); + } + if (event.key === "Escape" && document.activeElement === search) { + search.value = ""; + applyFilters(); + } + }); + applyFilters(); + } + + function informationGroup(label) { + var text = normalizeText(label).toLowerCase(); + if (/source\s+code|flashing|website|operat(?:ed|ing)\s+systems?|test\s*lab|assistance|support|instructions/.test(text)) { + return "support"; + } + if (/firmware|uefi/.test(text)) { + return "firmware"; + } + if (text === "band") { + return "standards"; + } + if (/^(?:vendor|manufacturer|system(?:\s+name|\s+model)?|soc\s+family|platform)$/.test(text)) { + return "platform"; + } + if (/version|standard|specification|profile/.test(text)) { + return "standards"; + } + return "other"; + } + + function cloneCellContents(cell, target) { + Array.prototype.forEach.call(cell.childNodes, function (node) { + target.appendChild(node.cloneNode(true)); + }); + } + + function informationFieldPriority(source) { + var label = normalizeText(source.labelCell.textContent).toLowerCase(); + var priorities = {"band": 0, "acs version": 1, "srs version": 2}; + return Object.prototype.hasOwnProperty.call(priorities, label) ? + priorities[label] : 3 + source.index; + } + + function buildInformationOverview(section) { + var table = section.querySelector("table"); + if (!table) { + return false; + } + var definitions = [ + {key: "platform", label: "Platform", fields: []}, + {key: "firmware", label: "Firmware", fields: []}, + {key: "standards", label: "ACS & standards", fields: []}, + {key: "support", label: "Support", fields: []}, + {key: "other", label: "Other information", fields: []} + ]; + var groups = {}; + definitions.forEach(function (definition) { + groups[definition.key] = definition; + }); + var rows = Array.prototype.slice.call(table.querySelectorAll("tr")); + var fieldCount = 0; + var invalidRow = !rows.length; + rows.forEach(function (row, index) { + var labelCells = Array.prototype.filter.call(row.cells, function (cell) { + return cell.tagName === "TH"; + }); + var valueCells = Array.prototype.filter.call(row.cells, function (cell) { + return cell.tagName === "TD"; + }); + if (labelCells.length !== 1 || valueCells.length !== 1) { + invalidRow = true; + return; + } + var labelCell = labelCells[0]; + var valueCell = valueCells[0]; + var label = normalizeText(labelCell && labelCell.textContent); + if (!label) { + invalidRow = true; + return; + } + groups[informationGroup(label)].fields.push({ + labelCell: labelCell, + valueCell: valueCell, + index: index + }); + fieldCount += 1; + }); + if (invalidRow || !fieldCount || fieldCount !== rows.length) { + return false; + } + + var heading = section.querySelector("h2"); + if (!heading) { + heading = document.createElement("h2"); + heading.textContent = "System Information"; + section.insertBefore(heading, section.firstChild); + } + if (!heading.id) { + heading.id = "acs-system-information-heading"; + } + var headingRow = document.createElement("div"); + headingRow.className = "acs-overview-heading"; + heading.parentNode.insertBefore(headingRow, heading); + headingRow.appendChild(heading); + var count = document.createElement("span"); + count.className = "acs-overview-field-count"; + count.textContent = fieldCount + (fieldCount === 1 ? " field" : " fields"); + headingRow.appendChild(count); + + var bands = document.createElement("div"); + bands.className = "acs-information-bands"; + definitions.forEach(function (definition) { + if (!definition.fields.length) { + return; + } + var band = document.createElement("section"); + band.className = "acs-information-band"; + band.setAttribute("data-acs-information-group", definition.key); + var bandHeading = document.createElement("h3"); + bandHeading.textContent = definition.label; + band.appendChild(bandHeading); + var values = document.createElement("dl"); + values.className = "acs-information-values"; + var orderedFields = definition.fields.slice(); + if (definition.key === "standards") { + orderedFields.sort(function (left, right) { + return informationFieldPriority(left) - informationFieldPriority(right) || + left.index - right.index; + }); + } + orderedFields.forEach(function (source) { + var field = document.createElement("div"); + field.className = "acs-information-field"; + field.setAttribute("data-acs-source-row", String(source.index)); + field.setAttribute("data-acs-unknown", String( + normalizeText(source.valueCell.textContent).toLowerCase() === "unknown" + )); + var term = document.createElement("dt"); + cloneCellContents(source.labelCell, term); + var description = document.createElement("dd"); + cloneCellContents(source.valueCell, description); + field.appendChild(term); + field.appendChild(description); + values.appendChild(field); + }); + band.appendChild(values); + bands.appendChild(band); + }); + headingRow.insertAdjacentElement("afterend", bands); + table.classList.add("acs-overview-source"); + table.setAttribute("aria-hidden", "true"); + section.classList.add("acs-information-overview"); + section.setAttribute("aria-labelledby", heading.id); + section.setAttribute("data-acs-source-fields", String(fieldCount)); + return true; + } + + function collectResultEntries(table) { + var entries = []; + var current = null; + var valueIndex = 0; + Array.prototype.forEach.call(table.querySelectorAll("tr"), function (row) { + var labelCell = row.querySelector("th"); + var valueCells = Array.prototype.slice.call(row.querySelectorAll("td")); + if (labelCell) { + current = { + labelCell: labelCell, + label: normalizeText(labelCell.textContent), + values: [] + }; + entries.push(current); + } else if (!current && valueCells.length) { + current = {labelCell: null, label: "Additional information", values: []}; + entries.push(current); + } + if (!current) { + return; + } + valueCells.forEach(function (cell) { + current.values.push({cell: cell, index: valueIndex}); + valueIndex += 1; + }); + }); + return entries; + } + + function complianceTone(value) { + var text = normalizeText(value).toUpperCase(); + if (/NOT\s+COMPLIANT|FAIL(?:ED|URE)?/.test(text)) { + return "fail"; + } + if (/WAIVER|WARNING/.test(text)) { + return "warn"; + } + if (/^(?:COMPLIANT|PASS(?:ED)?)\b/.test(text)) { + return "pass"; + } + return "neutral"; + } + + function resultEntryIsStatus(entry) { + return /compliance|result/.test(entry.label.toLowerCase()) && entry.values.length; + } + + function buildResultOverview(section, index) { + var table = section.querySelector("table"); + var entries = table ? collectResultEntries(table) : []; + if (!table || !entries.length) { + return false; + } + var heading = section.querySelector("h2"); + if (!heading) { + heading = document.createElement("h2"); + heading.textContent = "Results"; + section.insertBefore(heading, section.firstChild); + } + if (!heading.id) { + heading.id = "acs-overview-result-heading-" + index; + } + var headingRow = document.createElement("div"); + headingRow.className = "acs-overview-heading"; + heading.parentNode.insertBefore(headingRow, heading); + headingRow.appendChild(heading); + + var isAcsResults = normalizeText(heading.textContent).toLowerCase() === + "acs results summary"; + var dateEntry = isAcsResults ? entries.find(function (entry) { + return normalizeText(entry.label).toLowerCase() === "date"; + }) : null; + if (dateEntry) { + var headingMeta = document.createElement("dl"); + headingMeta.className = "acs-overview-heading-meta"; + var dateItem = document.createElement("div"); + dateItem.className = "acs-overview-meta-item"; + var dateTerm = document.createElement("dt"); + if (dateEntry.labelCell) { + cloneCellContents(dateEntry.labelCell, dateTerm); + } else { + dateTerm.textContent = dateEntry.label; + } + dateItem.appendChild(dateTerm); + dateEntry.values.forEach(function (source) { + var dateValue = document.createElement("dd"); + dateValue.setAttribute("data-acs-source-result", String(source.index)); + cloneCellContents(source.cell, dateValue); + dateItem.appendChild(dateValue); + }); + headingMeta.appendChild(dateItem); + headingRow.appendChild(headingMeta); + } + + var content = document.createElement("div"); + content.className = "acs-overview-result-content"; + var systemBandFields = Array.prototype.filter.call( + document.querySelectorAll(".acs-information-overview .acs-information-field"), + function (field) { + var term = field.querySelector("dt"); + return term && normalizeText(term.textContent).toLowerCase() === "band"; + } + ); + var resultBandEntries = isAcsResults ? entries.filter(function (entry) { + return normalizeText(entry.label).toLowerCase() === "band"; + }) : []; + var duplicateBandEntry = null; + if (systemBandFields.length === 1 && resultBandEntries.length === 1 && + resultBandEntries[0].values.length === 1) { + var systemBandValue = systemBandFields[0].querySelector("dd"); + if (systemBandValue && normalizeText(systemBandValue.textContent) === + normalizeText(resultBandEntries[0].values[0].cell.textContent)) { + duplicateBandEntry = resultBandEntries[0]; + } + } + var metaEntries = entries.filter(function (entry) { + return !resultEntryIsStatus(entry) && entry !== dateEntry && + entry !== duplicateBandEntry; + }); + if (duplicateBandEntry) { + section.setAttribute("data-acs-duplicate-band-omitted", "true"); + } + if (metaEntries.length) { + var meta = document.createElement("dl"); + meta.className = "acs-overview-result-meta"; + metaEntries.forEach(function (entry) { + var item = document.createElement("div"); + item.className = "acs-overview-meta-item"; + var term = document.createElement("dt"); + if (entry.labelCell) { + cloneCellContents(entry.labelCell, term); + } else { + term.textContent = entry.label; + } + item.appendChild(term); + entry.values.forEach(function (source) { + var description = document.createElement("dd"); + description.setAttribute("data-acs-source-result", String(source.index)); + cloneCellContents(source.cell, description); + item.appendChild(description); + }); + meta.appendChild(item); + }); + content.appendChild(meta); + } + + var cardTone = "pass"; + var statusEntries = entries.filter(resultEntryIsStatus); + var toneRank = {pass: 0, neutral: 1, warn: 2, fail: 3}; + var statusList = document.createElement("dl"); + statusList.className = "acs-overview-status-list"; + statusEntries.forEach(function (entry) { + var result = document.createElement("div"); + result.className = "acs-overview-result-entry"; + var primaryText = normalizeText(entry.values[0].cell.textContent); + var tone = complianceTone(primaryText); + result.setAttribute("data-acs-tone", tone); + if (toneRank[tone] > toneRank[cardTone]) { + cardTone = tone; + } + var label = document.createElement("dt"); + label.className = "acs-overview-result-label"; + if (entry.labelCell) { + cloneCellContents(entry.labelCell, label); + } else { + label.textContent = entry.label; + } + result.appendChild(label); + entry.values.forEach(function (source, valuePosition) { + var value = document.createElement("dd"); + value.className = "acs-overview-result-value"; + value.setAttribute("data-acs-primary", String(valuePosition === 0)); + value.setAttribute("data-acs-source-result", String(source.index)); + cloneCellContents(source.cell, value); + result.appendChild(value); + }); + statusList.appendChild(result); + }); + if (!statusEntries.length) { + cardTone = "neutral"; + } else { + content.appendChild(statusList); + } + headingRow.insertAdjacentElement("afterend", content); + table.classList.add("acs-overview-source"); + table.setAttribute("aria-hidden", "true"); + section.classList.add("acs-overview-result-card"); + section.setAttribute("aria-labelledby", heading.id); + section.setAttribute("data-acs-card-tone", cardTone); + section.setAttribute("data-acs-source-results", String(table.querySelectorAll("td").length)); + return true; + } + + function upgradeAcsSummaryOverview() { + if (document.querySelector(".acs-summary-overview")) { + return; + } + var container = document.querySelector("body > .container"); + if (!container) { + return; + } + var systemInformation = null; + var resultSources = []; + Array.prototype.forEach.call(container.children, function (child) { + if (child.classList.contains("system-info")) { + systemInformation = child; + } else if (child.classList.contains("acs-results-summary")) { + resultSources.push(child); + } + }); + var emptyResultSections = resultSources.filter(function (section) { + var table = section.querySelector("table"); + var hasOtherContent = Array.prototype.some.call(section.childNodes, function (node) { + if (node.nodeType === 3) { + return Boolean(normalizeText(node.textContent)); + } + return node.nodeType === 1 && !/^(?:H[1-6]|TABLE)$/.test(node.tagName) && + Boolean(normalizeText(node.textContent)); + }); + return table && !table.querySelector("tr") && !hasOtherContent; + }); + var resultSections = resultSources.filter(function (section) { + var rows = Array.prototype.slice.call(section.querySelectorAll("table tr")); + return rows.length && rows.every(function (row) { return row.querySelector("td"); }); + }); + if (!systemInformation || !resultSections.length || + !systemInformation.querySelector("table tr th")) { + return; + } + if (!buildInformationOverview(systemInformation)) { + return; + } + var overview = document.createElement("div"); + overview.className = "acs-summary-overview"; + overview.setAttribute("data-acs-source-sections", String(resultSections.length + 1)); + systemInformation.parentNode.insertBefore(overview, systemInformation); + overview.appendChild(systemInformation); + var results = document.createElement("div"); + results.className = "acs-overview-results"; + resultSections.forEach(function (section, index) { + buildResultOverview(section, index + 1); + results.appendChild(section); + }); + emptyResultSections.forEach(function (section) { + section.classList.add("acs-overview-source"); + section.setAttribute("aria-hidden", "true"); + }); + results.setAttribute("data-acs-card-count", String(resultSections.length)); + overview.appendChild(results); + } + + function summaryLabel(section) { + var labels = { + "bsa_summary": "BSA", + "sbsa_summary": "SBSA", + "scmi_summary": "SCMI", + "sbmr_ib_summary": "SBMR-IB", + "sbmr_oob_summary": "SBMR-OOB", + "post_script_summary": "POST-SCRIPT", + "standalone_summary": "Standalone", + "bbsr_fwts_summary": "BBSR-FWTS", + "bbsr_sct_summary": "BBSR-SCT", + "bbsr_tpm_summary": "BBSR-TPM", + "pfdi_summary": "PFDI", + "OS_tests_summary": "OS Tests" + }; + if (labels[section.id]) { + return labels[section.id]; + } + var heading = section.querySelector("h1, h2, h3"); + if (heading) { + return normalizeText(heading.textContent) + .replace(/\s+(?:Test|Log) Summary$/i, ""); + } + return section.id.replace(/_summary$/i, "").replace(/_/g, " ").toUpperCase(); + } + + function buildSummaryNavigation() { + var sections = Array.prototype.slice.call(document.querySelectorAll(".summary[id]")); + if (sections.length < 2) { + return; + } + if (sections.length % 2) { + sections[sections.length - 1].classList.add("acs-summary-orphan"); + } + var nav = document.createElement("nav"); + nav.className = "acs-suite-nav"; + nav.setAttribute("aria-label", "Suite summaries"); + sections.forEach(function (section) { + var link = document.createElement("a"); + link.href = "#" + section.id; + link.textContent = summaryLabel(section); + nav.appendChild(link); + section.style.scrollMarginTop = "76px"; + }); + var summariesHeading = document.querySelector(".summary-section > h2"); + if (summariesHeading) { + summariesHeading.insertAdjacentElement("afterend", nav); + } else { + var header = document.querySelector("body > .header"); + if (header) { + header.insertAdjacentElement("afterend", nav); + } else { + document.body.insertBefore(nav, document.body.firstChild); + } + } + } + + function addSummaryPrintButton() { + var heading = document.querySelector(".summary-section > h2"); + if (!heading || heading.parentNode.querySelector(":scope > .acs-summary-heading-row")) { + return; + } + var row = document.createElement("div"); + row.className = "acs-summary-heading-row"; + heading.parentNode.insertBefore(row, heading); + row.appendChild(heading); + var print = actionButton("Print / PDF", "acs-button-primary acs-summary-print-button"); + print.setAttribute("aria-label", "Print the consolidated ACS summary or save it as PDF"); + print.addEventListener("click", function () { window.print(); }); + row.appendChild(print); + } + + function shortenDetailLinks() { + Array.prototype.forEach.call(document.querySelectorAll(".summary[id]"), function (section) { + var link = section.querySelector(".details-link a"); + if (!link) { + return; + } + var label = summaryLabel(section); + link.removeAttribute("target"); + link.setAttribute("aria-label", "Open detailed report for " + label); + link.textContent = "View " + label + " details \u2192"; + }); + } + + function addBackToTop() { + var button = document.createElement("button"); + button.type = "button"; + button.className = "acs-back-to-top"; + button.setAttribute("aria-label", "Back to top"); + button.title = "Back to top"; + button.textContent = "\u2191"; + button.addEventListener("click", function () { + window.scrollTo({top: 0, behavior: "smooth"}); + }); + document.body.appendChild(button); + function updateVisibility() { + button.classList.toggle("visible", window.scrollY > 600); + } + window.addEventListener("scroll", updateVisibility, {passive: true}); + updateVisibility(); + } + + function initialize() { + addReportHeading(); + upgradeSummaryTables(); + upgradeSuiteLayouts(); + upgradeTableColumns(); + upgradeStatusCells(); + addMobileStatusPreviews(); + decorateGroupStatuses(); + removeDetailedCharts(); + decorateSummaryContext(); + if (document.body.getAttribute("data-acs-report-kind") === "acs-summary") { + buildSummaryNavigation(); + addSummaryPrintButton(); + shortenDetailLinks(); + upgradeAcsSummaryOverview(); + } else { + buildDetailedToolbar(); + } + hideLegacyNavigation(); + addBackToTop(); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initialize); + } else { + initialize(); + } +}()); +""" + + +def _add_body_attributes(match, page_type, suite_type): + """Return a body tag with the shared class and page type attributes.""" + attributes = match.group(1) or "" + class_match = re.search(r'\bclass\s*=\s*(["\'])(.*?)\1', attributes, re.IGNORECASE) + if class_match: + classes = class_match.group(2).split() + if "acs-report-ui" not in classes: + classes.append("acs-report-ui") + replacement = f'class={class_match.group(1)}{" ".join(classes)}{class_match.group(1)}' + attributes = attributes[:class_match.start()] + replacement + attributes[class_match.end():] + else: + attributes += ' class="acs-report-ui"' + attributes += f' data-acs-report-kind="{page_type}"' + if suite_type: + attributes += f' data-acs-suite="{suite_type}"' + attributes += f" {UI_MARKER}" + return f"" + + +def enhance_html_report(html_content, page_type="suite", suite_type=None): + """Return HTML with the shared theme and progressive UI injected once.""" + if not isinstance(html_content, str): + raise TypeError("html_content must be a string") + if page_type not in VALID_PAGE_TYPES: + raise ValueError(f"unsupported report page type: {page_type}") + if suite_type is not None and suite_type not in VALID_SUITE_TYPES: + raise ValueError(f"unsupported report suite type: {suite_type}") + if page_type == "acs-summary" and suite_type is not None: + raise ValueError("suite_type is not valid for the consolidated summary") + if UI_MARKER in html_content: + return html_content + + output = html_content + is_detailed = re.search( + r"class\s*=\s*[\"'][^\"']*\bdetailed-(?:summary|container)\b", + output, + re.IGNORECASE, + ) + if is_detailed: + output = re.sub( + r"" + r"\s*]*>\s*", + "", + output, + flags=re.IGNORECASE | re.DOTALL, + ) + if not re.search(r']*name=["\']viewport["\']', output, re.IGNORECASE): + viewport = '\n' + output = re.sub( + r"(]*>)", + r"\1\n" + viewport, + output, + count=1, + flags=re.IGNORECASE, + ) + if not re.search(r']*charset=', output, re.IGNORECASE): + charset = '\n' + output = re.sub( + r"(]*>)", + r"\1\n" + charset, + output, + count=1, + flags=re.IGNORECASE, + ) + + style_block = f'\n\n' + if re.search(r"", output, re.IGNORECASE): + output = re.sub( + r"", + lambda _match: style_block + "", + output, + count=1, + flags=re.IGNORECASE, + ) + else: + output = style_block + output + + output, replacements = re.subn( + r"]*)>", + lambda match: _add_body_attributes(match, page_type, suite_type), + output, + count=1, + flags=re.IGNORECASE, + ) + if not replacements: + suite_attribute = f' data-acs-suite="{suite_type}"' if suite_type else "" + output = ( + f'\n{output}\n' + ) + + script_block = f'\n\n' + if re.search(r"", output, re.IGNORECASE): + output = re.sub( + r"", + lambda _match: script_block + "", + output, + count=1, + flags=re.IGNORECASE, + ) + else: + output += script_block + return output diff --git a/common/log_parser/sbmr/json_to_html.py b/common/log_parser/sbmr/json_to_html.py index 88b2edd7..f193bab3 100644 --- a/common/log_parser/sbmr/json_to_html.py +++ b/common/log_parser/sbmr/json_to_html.py @@ -14,27 +14,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import matplotlib.pyplot as plt +"""Render SBMR JSON results as detailed and summary HTML reports.""" + import base64 -from io import BytesIO -from jinja2 import Template +import importlib +import json import os import sys +from io import BytesIO + +from jinja2 import Template + +plt = importlib.import_module("matplotlib.pyplot") +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +enhance_html_report = importlib.import_module("report_ui").enhance_html_report # ---------------------------- # Helpers # ---------------------------- -def get_case_insensitive(d, key, default=0): - if not isinstance(d, dict): +def get_case_insensitive(data, key, default=0): + """Return a dictionary value without requiring exact key casing.""" + if not isinstance(data, dict): return default - for k, v in d.items(): - if k.lower() == key.lower(): - return v + for candidate_key, value in data.items(): + if candidate_key.lower() == key.lower(): + return value return default -def summarize_subtests_list(subtests): + +def summarize_subtests(subtests): + """Calculate a suite summary from a flat subtest list.""" total = { 'total_passed': 0, 'total_failed': 0, @@ -44,36 +54,39 @@ def summarize_subtests_list(subtests): 'total_warnings': 0, 'total_ignored': 0, } - for st in subtests or []: - res = st.get('sub_test_result') - if isinstance(res, dict): - total['total_passed'] += res.get('PASSED', 0) - total['total_failed'] += res.get('FAILED', 0) - total['total_failed_with_waiver'] += res.get('FAILED_WITH_WAIVER', 0) - total['total_aborted'] += res.get('ABORTED', 0) - total['total_skipped'] += res.get('SKIPPED', 0) - total['total_warnings'] += res.get('WARNINGS', 0) + for subtest in subtests or []: + result = subtest.get('sub_test_result') + if isinstance(result, dict): + total['total_passed'] += result.get('PASSED', 0) + total['total_failed'] += result.get('FAILED', 0) + total['total_failed_with_waiver'] += result.get( + 'FAILED_WITH_WAIVER', 0 + ) + total['total_aborted'] += result.get('ABORTED', 0) + total['total_skipped'] += result.get('SKIPPED', 0) + total['total_warnings'] += result.get('WARNINGS', 0) else: - u = (res or "").upper() - if 'PASS' in u: + normalized_result = (result or "").upper() + if 'PASS' in normalized_result: total['total_passed'] += 1 - elif 'FAIL' in u: - if '(WITH WAIVER)' in u: + elif 'FAIL' in normalized_result: + if '(WITH WAIVER)' in normalized_result: total['total_failed_with_waiver'] += 1 else: total['total_failed'] += 1 - elif 'ABORT' in u: + elif 'ABORT' in normalized_result: total['total_aborted'] += 1 - elif 'SKIP' in u: + elif 'SKIP' in normalized_result: total['total_skipped'] += 1 - elif 'WARN' in u: + elif 'WARN' in normalized_result: total['total_warnings'] += 1 else: total['total_ignored'] += 1 return total -def compute_suite_summary_from_results(test_results): - agg = { +def compute_suite_summary(test_results): + """Return combined totals for all SBMR result formats.""" + aggregate = { 'total_passed': 0, 'total_failed': 0, 'total_failed_with_waiver': 0, @@ -84,18 +97,22 @@ def compute_suite_summary_from_results(test_results): } for suite in test_results: if isinstance(suite, dict) and 'Test_cases' in suite: - ss = suite.get('test_suite_summary', {}) - for k in agg: - agg[k] += get_case_insensitive(ss, k, 0) + summary = suite.get('test_suite_summary', {}) + for summary_key in aggregate: + aggregate[summary_key] += get_case_insensitive( + summary, summary_key, 0 + ) else: - ss = suite.get('test_suite_summary') - if not ss: - ss = summarize_subtests_list(suite.get('subtests', [])) - for k in agg: - agg[k] += ss.get(k, 0) - return agg + summary = suite.get('test_suite_summary') + if not summary: + summary = summarize_subtests(suite.get('subtests', [])) + for summary_key in aggregate: + aggregate[summary_key] += summary.get(summary_key, 0) + return aggregate + def generate_bar_chart(suite_summary): + """Return a base64-encoded SBMR result-distribution chart.""" labels = ['Passed', 'Failed', 'Failed with Waiver', 'Aborted', 'Skipped', 'Warnings'] sizes = [ suite_summary.get('total_passed', 0), @@ -112,12 +129,12 @@ def generate_bar_chart(suite_summary): total_tests = sum(sizes) max_size = max(sizes) if sizes else 0 - for bar, size in zip(bars, sizes): - y = bar.get_height() + for chart_bar, size in zip(bars, sizes): + height = chart_bar.get_height() pct = (size / total_tests) * 100 if total_tests > 0 else 0 plt.text( - bar.get_x() + bar.get_width()/2, - y + (0.01 * max_size if max_size else 0.1), + chart_bar.get_x() + chart_bar.get_width()/2, + height + (0.01 * max_size if max_size else 0.1), f'{pct:.2f}%', ha='center', va='bottom', @@ -129,6 +146,7 @@ def generate_bar_chart(suite_summary): plt.xticks(fontsize=12) plt.yticks(fontsize=12) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) buffer = BytesIO() plt.savefig(buffer, format='png') @@ -372,11 +390,18 @@ def generate_bar_chart(suite_summary): # ---------------------------- def render_detail_html(dataset, output_html_path, page_title, report_link=None): - html = DETAIL_TEMPLATE.render(ds=dataset, page_title=page_title.upper(), report_link=report_link) - with open(output_html_path, "w") as f: - f.write(html) + """Render the detailed SBMR report.""" + html = DETAIL_TEMPLATE.render( + ds=dataset, + page_title=page_title.upper(), + report_link=report_link, + ) + with open(output_html_path, "w", encoding="utf-8") as file_handle: + file_handle.write(enhance_html_report(html, suite_type="sbmr")) + def render_summary_html(combined_summary, output_html_path, page_title): + """Render the SBMR summary report.""" total_tests = ( combined_summary.get("total_passed", 0) + combined_summary.get("total_failed", 0) @@ -395,14 +420,15 @@ def render_summary_html(combined_summary, output_html_path, page_title): total_skipped=combined_summary.get("total_skipped", 0), total_warnings=combined_summary.get("total_warnings", 0), ) - with open(output_html_path, "w") as f: - f.write(html) + with open(output_html_path, "w", encoding="utf-8") as file_handle: + file_handle.write(enhance_html_report(html, suite_type="sbmr")) # ---------------------------- # Main # ---------------------------- def friendly_label_from_filename(path): + """Return a readable report label derived from a JSON filename.""" base = os.path.splitext(os.path.basename(path))[0].upper() # Aim for user-friendly labels: # sbmr_ib.json -> SBMR IB | sbmr_oob.json -> SBMR OOB @@ -410,22 +436,27 @@ def friendly_label_from_filename(path): return base def uid_from_label(label): + """Return a stable lowercase HTML identifier for a report label.""" # Safe id for HTML element ids return "".join(ch for ch in label if ch.isalnum()).lower() def main(): + """Load SBMR JSON and write detailed and summary HTML reports.""" if len(sys.argv) < 4: - print("Usage: python json_to_html.py [report_html_abs_path]") + print( + "Usage: python json_to_html.py " + " [report_html_abs_path]" + ) sys.exit(1) input_json_file, detailed_html_file, summary_html_file = sys.argv[1:4] report_html_abs = sys.argv[4] if len(sys.argv) >= 5 else os.environ.get("SBMR_REPORT_HTML", "") - with open(input_json_file, "r") as jf: - data = json.load(jf) + with open(input_json_file, "r", encoding="utf-8") as json_file: + data = json.load(json_file) suites = data.get("test_results", []) - suite_summary = data.get("suite_summary") or compute_suite_summary_from_results(suites) + suite_summary = data.get("suite_summary") or compute_suite_summary(suites) total_tests = ( suite_summary.get("total_passed", 0) @@ -448,14 +479,16 @@ def main(): } page_title = label # keep simple - # If a report.html absolute path was provided, convert to a relative href from the detailed HTML dir + # Convert the report path into a link relative to the detailed HTML file. report_link = None if report_html_abs: try: report_html_abs = os.path.abspath(report_html_abs) if os.path.exists(report_html_abs): detail_dir = os.path.dirname(os.path.abspath(detailed_html_file)) - report_link = os.path.relpath(report_html_abs, start=detail_dir).replace(os.sep, "/") + report_link = os.path.relpath( + report_html_abs, start=detail_dir + ).replace(os.sep, "/") except Exception: report_link = None diff --git a/common/log_parser/scmi/json_to_html.py b/common/log_parser/scmi/json_to_html.py index 5c2d3914..9ca0d997 100644 --- a/common/log_parser/scmi/json_to_html.py +++ b/common/log_parser/scmi/json_to_html.py @@ -17,14 +17,18 @@ """Render SCMI JSON results into detailed and summary HTML reports.""" import base64 +import importlib import json import sys from io import BytesIO from pathlib import Path -import matplotlib.pyplot as plt from jinja2 import Template +plt = importlib.import_module("matplotlib.pyplot") +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +enhance_html_report = importlib.import_module("report_ui").enhance_html_report + def generate_bar_chart(summary_dict): """Return base64 PNG for the summary bar chart.""" @@ -59,6 +63,7 @@ def generate_bar_chart(summary_dict): plt.xticks(fontsize=12) plt.yticks(fontsize=12) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) buf = BytesIO() plt.savefig(buf, format="png") @@ -178,7 +183,9 @@ def build_html(overall_summary, test_results, chart_b64, dest_html, suite_name, **overall_summary, ) - Path(dest_html).write_text(html, encoding="utf-8") + Path(dest_html).write_text( + enhance_html_report(html, suite_type="scmi"), encoding="utf-8" + ) def _tally_from_testcases(test_results): diff --git a/common/log_parser/standalone_runner.py b/common/log_parser/standalone_runner.py index 3d60c2aa..aee66c17 100755 --- a/common/log_parser/standalone_runner.py +++ b/common/log_parser/standalone_runner.py @@ -94,6 +94,14 @@ def __init__(self, message, exit_code): self.exit_code = exit_code +class MissingSuiteInputError(StandaloneError): + """A selected suite has no collected input, but a summary can report it.""" + + +class SuiteNotRunError(StandaloneError): + """A parser recognized its input but found no runnable suite tests.""" + + @dataclass class ResolvedInput: path: Path @@ -308,7 +316,7 @@ def resolve_suite_inputs(canonical, execution, roots, direct_inputs=None): if missing_required: lines = "\n".join(f" - {path}" for path in missing_required) - raise StandaloneError( + raise MissingSuiteInputError( f"{canonical}: required input is missing:\n{lines}", EXIT_INPUT ) @@ -316,7 +324,7 @@ def resolve_suite_inputs(canonical, execution, roots, direct_inputs=None): if primary_found < minimum: expected = [item.path for item in resolved.values() if not item.supporting] lines = "\n".join(f" - {path}" for path in expected) - raise StandaloneError( + raise MissingSuiteInputError( f"{canonical}: requires at least {minimum} input log(s); found {primary_found}:\n{lines}", EXIT_INPUT, ) @@ -338,6 +346,35 @@ def registry_script(suite, key): return path +def parser_not_run_statuses(canonical, execution): + """Return validated parser statuses that mean a suite could not run.""" + configured = execution.get("not_run_exit_codes", {}) + if not isinstance(configured, dict): + raise StandaloneError( + f"{canonical}: not_run_exit_codes must map statuses to reasons.", + EXIT_INPUT, + ) + + statuses = {} + for raw_status, reason in configured.items(): + try: + status = int(raw_status) + except (TypeError, ValueError): + status = 0 + if status <= 0 or str(status) != str(raw_status): + raise StandaloneError( + f"{canonical}: invalid not-run parser status '{raw_status}'.", + EXIT_INPUT, + ) + if not isinstance(reason, str) or not reason.strip(): + raise StandaloneError( + f"{canonical}: not-run parser status {status} has no reason.", + EXIT_INPUT, + ) + statuses[status] = reason.strip() + return statuses + + def validate_registry(registry, standalone, registry_path=DEFAULT_REGISTRY): suite_map = {suite.get("canonical"): suite for suite in registry} execution_map = standalone.get("suite_execution", {}) @@ -385,6 +422,7 @@ def validate_registry(registry, standalone, registry_path=DEFAULT_REGISTRY): f"{canonical}: unsupported standalone handler '{execution.get('handler')}'.", EXIT_INPUT, ) + parser_not_run_statuses(canonical, execution) input_names = set() for input_spec in execution.get("inputs", []): input_name = input_spec.get("name") @@ -491,7 +529,7 @@ def check_dependencies(modules): ) -def run_command(label, command, failure_code): +def run_command(label, command, failure_code, accepted_return_codes=()): print(f"{label}") command = [str(item) for item in command] try: @@ -516,11 +554,12 @@ def run_command(label, command, failure_code): pass process.wait() raise - if return_code != 0: + if return_code != 0 and return_code not in accepted_return_codes: raise StandaloneError( f"Command failed with status {return_code}: {' '.join(command)}", failure_code, ) + return return_code def validate_json_output(canonical, path): @@ -596,7 +635,7 @@ def run_os_tests(canonical, suite, execution, resolved, mode, json_dir): result.boot_sources.append(boot_sources if boot_sources.is_file() else "Unknown") if not result.json_files: - raise StandaloneError( + raise MissingSuiteInputError( f"{canonical}: no linux*/ethtool_test.log inputs found under {os_logs}", EXIT_INPUT ) return result @@ -646,7 +685,20 @@ def run_suite(canonical, suite, execution, roots, mode, json_dir, direct_inputs= else: raise StandaloneError(f"{canonical}: unsupported registry handler '{handler}'.", EXIT_INPUT) - run_command(f"[{canonical}] Parsing logs", command, EXIT_PARSE) + not_run_statuses = parser_not_run_statuses(canonical, execution) + return_code = run_command( + f"[{canonical}] Parsing logs", + command, + EXIT_PARSE, + not_run_statuses, + ) + if return_code: + output.unlink(missing_ok=True) + raise SuiteNotRunError( + f"{canonical}: {not_run_statuses[return_code]} " + f"(parser status {return_code}).", + EXIT_PARSE, + ) validate_json_output(canonical, output) result.json_files.append(output) return result @@ -1277,46 +1329,67 @@ def main(): copy_run_configs(stage, args) results = [] + missing_suites = [] + not_runnable_suites = [] for canonical in selected: suite = get_suite(canonical, registry) - result = run_suite( - canonical, - suite, - execution_map[canonical], - roots, - args.mode, - json_dir, - direct_inputs, - ) + try: + result = run_suite( + canonical, + suite, + execution_map[canonical], + roots, + args.mode, + json_dir, + direct_inputs, + ) + except MissingSuiteInputError as error: + if direct_inputs or "summary" not in outputs: + raise + missing_suites.append(canonical) + print(f"WARNING: {error}", file=sys.stderr) + continue + except SuiteNotRunError as error: + if "summary" not in outputs: + raise + not_runnable_suites.append(canonical) + print(f"WARNING: {error}", file=sys.stderr) + continue for json_file in result.json_files: apply_waiver(canonical, suite, json_file, args.waiver, test_category) results.append(result) raw_jsons = [path for result in results for path in result.json_files] - run_command( - "[METADATA] Enriching raw suite JSON files", - [ - sys.executable, - BASE_DIR / "enrich_suite_json.py", - "--registry", DEFAULT_REGISTRY, - "--test-category", test_category, - *raw_jsons, - ], - EXIT_PARSE, - ) - - if args.schema: + if raw_jsons: run_command( - "[SCHEMA] Validating raw suite JSON files", + "[METADATA] Enriching raw suite JSON files", [ sys.executable, - SCHEMA_VALIDATOR, - "raw", + BASE_DIR / "enrich_suite_json.py", "--registry", DEFAULT_REGISTRY, + "--test-category", test_category, *raw_jsons, ], - EXIT_SCHEMA, + EXIT_PARSE, ) + else: + print("[METADATA] Skipped: no raw suite JSON files were generated.") + + if args.schema: + if raw_jsons: + run_command( + "[SCHEMA] Validating raw suite JSON files", + [ + sys.executable, + SCHEMA_VALIDATOR, + "raw", + "--registry", DEFAULT_REGISTRY, + *raw_jsons, + ], + EXIT_SCHEMA, + ) + else: + print("[SCHEMA] Skipped: no raw suite JSON files were generated.") if "html" in outputs: render_reports(results, html_dir) @@ -1338,6 +1411,10 @@ def main(): print("") print("Standalone run result: PASS") + if missing_suites: + print(f"Suites without collected logs: {', '.join(missing_suites)}") + if not_runnable_suites: + print(f"Suites reported as Not Run: {', '.join(not_runnable_suites)}") print(f"Output: {target}") return 0 diff --git a/common/log_parser/standalone_tests/json_to_html.py b/common/log_parser/standalone_tests/json_to_html.py index f020de2d..1e6df040 100755 --- a/common/log_parser/standalone_tests/json_to_html.py +++ b/common/log_parser/standalone_tests/json_to_html.py @@ -14,13 +14,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import matplotlib.pyplot as plt +import argparse import base64 +import importlib +import json +import os +import sys from io import BytesIO + from jinja2 import Template -import sys -import argparse + +plt = importlib.import_module("matplotlib.pyplot") +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +enhance_html_report = importlib.import_module("report_ui").enhance_html_report # 1) Detect which columns are used among all subtests in a given test def detect_columns_used(subtests): @@ -95,6 +101,7 @@ def generate_bar_chart(suite_summary): plt.title('Standalone test Results', fontsize=16, fontweight='bold', pad=20) plt.ylabel('Number of Standalone tests', fontsize=14) plt.tight_layout() + importlib.import_module("report_ui").center_matplotlib_plot(plt.gca()) buffer = BytesIO() plt.savefig(buffer, format='png') @@ -400,6 +407,7 @@ def generate_html(suite_summary, test_results_list, output_html_path, enumerate=enumerate ) + html_content = enhance_html_report(html_content, suite_type="standalone") with open(output_html_path, "w") as f: f.write(html_content) diff --git a/common/tools/acs-results-schema.json b/common/tools/acs-results-schema.json index f416795b..411afa48 100644 --- a/common/tools/acs-results-schema.json +++ b/common/tools/acs-results-schema.json @@ -26,10 +26,16 @@ "Suite_Name: BBSR-TPM": { "$ref": "#/definitions/tpm_suite" }, - "Suite_Name: FWTS": { + "Suite_Name: EBBR-FWTS": { "$ref": "#/definitions/fwts_suite" }, - "Suite_Name: SCT": { + "Suite_Name: EBBR-SCT": { + "$ref": "#/definitions/sct_suite" + }, + "Suite_Name: SBBR-FWTS": { + "$ref": "#/definitions/fwts_suite" + }, + "Suite_Name: SBBR-SCT": { "$ref": "#/definitions/sct_suite" }, "Suite_Name: PFDI": { @@ -66,6 +72,66 @@ "$ref": "#/definitions/os_tests_suite" } }, + "allOf": [ + { + "if": { + "properties": { + "Suite_Name: acs_info": { + "properties": { + "ACS Results Summary": { + "properties": { + "Band": { + "const": "SystemReady Devicetree band" + } + }, + "required": [ + "Band" + ] + } + }, + "required": [ + "ACS Results Summary" + ] + } + } + }, + "then": { + "properties": { + "Suite_Name: SBBR-FWTS": false, + "Suite_Name: SBBR-SCT": false + } + } + }, + { + "if": { + "properties": { + "Suite_Name: acs_info": { + "properties": { + "ACS Results Summary": { + "properties": { + "Band": { + "const": "SystemReady band" + } + }, + "required": [ + "Band" + ] + } + }, + "required": [ + "ACS Results Summary" + ] + } + } + }, + "then": { + "properties": { + "Suite_Name: EBBR-FWTS": false, + "Suite_Name: EBBR-SCT": false + } + } + } + ], "additionalProperties": false, "definitions": { "non_negative_int": { @@ -1576,7 +1642,16 @@ "Suite_Name: Mandatory : ETHTOOL_TEST_compliance": { "type": "string" }, - "Suite_Name: Mandatory : FWTS_compliance": { + "Suite_Name: Mandatory : EBBR-FWTS_compliance": { + "type": "string" + }, + "Suite_Name: Mandatory : EBBR-SCT_compliance": { + "type": "string" + }, + "Suite_Name: Mandatory : SBBR-FWTS_compliance": { + "type": "string" + }, + "Suite_Name: Mandatory : SBBR-SCT_compliance": { "type": "string" }, "Suite_Name: Mandatory : OS_ethtool_test_linux-Fedora-40_compliance": { @@ -1591,9 +1666,6 @@ "Suite_Name: Mandatory : READ_WRITE_CHECK_BLK_DEVICES_compliance": { "type": "string" }, - "Suite_Name: Mandatory : SCT_compliance": { - "type": "string" - }, "Suite_Name: Recommended : BSA_compliance": { "type": "string" }, @@ -1657,9 +1729,9 @@ "Suite_Name: Mandatory : Capsule Update_compliance", "Suite_Name: Mandatory : DT_VALIDATE_compliance", "Suite_Name: Mandatory : ETHTOOL_TEST_compliance", - "Suite_Name: Mandatory : FWTS_compliance", + "Suite_Name: Mandatory : EBBR-FWTS_compliance", "Suite_Name: Mandatory : READ_WRITE_CHECK_BLK_DEVICES_compliance", - "Suite_Name: Mandatory : SCT_compliance", + "Suite_Name: Mandatory : EBBR-SCT_compliance", "Suite_Name: Mandatory : OS_ethtool_test_linux-Fedora-40_compliance", "Suite_Name: Mandatory : OS_ethtool_test_linux-Tumbleweed_compliance", "Suite_Name: Mandatory : OS_ethtool_test_linux-ubuntu-24_compliance", @@ -1675,7 +1747,21 @@ "Suite_Name: Extension : BBSR-FWTS_compliance", "Suite_Name: Extension : BBSR-SCT_compliance", "Suite_Name: Extension : BBSR-TPM_compliance" - ] + ], + "not": { + "anyOf": [ + { + "required": [ + "Suite_Name: Mandatory : SBBR-FWTS_compliance" + ] + }, + { + "required": [ + "Suite_Name: Mandatory : SBBR-SCT_compliance" + ] + } + ] + } } }, { @@ -1692,9 +1778,9 @@ "then": { "required": [ "Suite_Name: Mandatory : BSA_compliance", - "Suite_Name: Mandatory : FWTS_compliance", + "Suite_Name: Mandatory : SBBR-FWTS_compliance", "Suite_Name: Mandatory : OS_TEST_compliance", - "Suite_Name: Mandatory : SCT_compliance", + "Suite_Name: Mandatory : SBBR-SCT_compliance", "Suite_Name: Extension : BBSR-FWTS_compliance", "Suite_Name: Extension : BBSR-SCT_compliance", "Suite_Name: Extension : BBSR-TPM_compliance" @@ -1721,6 +1807,20 @@ } } ], + "not": { + "anyOf": [ + { + "required": [ + "Suite_Name: Mandatory : EBBR-FWTS_compliance" + ] + }, + { + "required": [ + "Suite_Name: Mandatory : EBBR-SCT_compliance" + ] + } + ] + }, "anyOf": [ { "required": [ diff --git a/common/tools/suite_registry.json b/common/tools/suite_registry.json index 5d876e1c..e38d2bc6 100644 --- a/common/tools/suite_registry.json +++ b/common/tools/suite_registry.json @@ -421,6 +421,9 @@ "handler": "multi_log", "minimum_inputs": 1, "json_dependencies": ["chardet"], + "not_run_exit_codes": { + "2": "SCMI raw transport is unavailable" + }, "inputs": [ {"name": "log", "root": "results", "candidates": ["linux_acs/scmi_acs_app/arm_scmi_test_log.txt"]} ] diff --git a/docs/acs_schema_guide.md b/docs/acs_schema_guide.md index 7ec4c6df..e3b57256 100644 --- a/docs/acs_schema_guide.md +++ b/docs/acs_schema_guide.md @@ -66,6 +66,11 @@ common/tools/validate.py merged \ The default schema is `common/tools/acs-results-schema.json`. +Merged FWTS/SCT wrapper names are band-specific. SystemReady DT uses +`Suite_Name: EBBR-FWTS` and `Suite_Name: EBBR-SCT`; SystemReady SR uses +`Suite_Name: SBBR-FWTS` and `Suite_Name: SBBR-SCT`. Plain `FWTS`/`SCT`, obsolete +`BBR-*`, mixed-band, and opposite-band wrappers are rejected. + ### Use a Different Schema ```bash @@ -221,6 +226,16 @@ entries. The standalone child files use one wrapper because each raw child is an object while merged output combines those entries under `Suite_Name: Standalone`. +The FWTS/SCT compliance-summary keys are band-specific: + +- SystemReady DT requires `EBBR-FWTS_compliance` and `EBBR-SCT_compliance`. +- SystemReady SR requires `SBBR-FWTS_compliance` and `SBBR-SCT_compliance`. +- Plain `FWTS_compliance` and `SCT_compliance` keys are rejected. +- `Overall Compliance Result` uses the same EBBR/SBBR suite names in its + `not run` and `failed` lists. + +These blocks are strict; missing required fields causes schema errors. + Renaming a raw file to an unregistered basename prevents automatic schema selection. Keep the registered output name or update the registry deliberately. diff --git a/docs/log_parser_guide.md b/docs/log_parser_guide.md index d07b76f2..f8ab6e3d 100644 --- a/docs/log_parser_guide.md +++ b/docs/log_parser_guide.md @@ -717,7 +717,7 @@ directories beside that input directory. | `BBSR-SCT` | DT, SR | `bbsr/sct_results/Overall/Summary.log`; optional `edk2-test-parser/edk2-test-parser-bbsr.log` | Summary required | | `BBSR-TPM` | DT, SR | `bbsr/tpm2/verify_tpm_measurements.log` | Required | | `PFDI` | DT | `uefi/pfdiresults.log` | Required | -| `SCMI` | DT | `linux_acs/scmi_acs_app/arm_scmi_test_log.txt` | At least one | +| `SCMI` | DT | `linux_acs/scmi_acs_app/arm_scmi_test_log.txt` | At least one; unavailable raw transport is `Not Run` | | `SBMR-IB` | SR | `sbmr/sbmr_in_band_logs/output.xml`; optional `sbmr/sbmr_in_band_logs/report.html` | XML required | | `SBMR-OOB` | SR | `sbmr/sbmr_out_of_band_logs/output.xml`; optional `sbmr/sbmr_out_of_band_logs/report.html` | XML required | | `POST-SCRIPT` | DT | `post-script/post-script.log` | Required | @@ -932,10 +932,58 @@ Most suites create the registry's suite JSON filename. `OS-TESTS` creates one `ethtool_test_.json` file for each discovered `os-logs/linux*` directory instead. +For directory-input summary runs, a selected suite with no collected input is +recorded with its requirement-specific missing status in `merged_results.json` +and `acs_summary.html`. No raw suite JSON, suite HTML, or broken detailed-report +link is created for that suite. Missing inputs remain errors for `--doctor`, +direct-file input, and runs without the summary stage. + +If an SCMI log explicitly reports that its raw transport is unavailable, the +SCMI parser's status `2` is handled in the same way and SCMI is reported as +`Not Run`. Other nonzero SCMI parser statuses remain parser failures. + Standalone builds output in a temporary sibling directory. It publishes the final directory only after every requested stage succeeds. Failed or interrupted runs remove the temporary directory. +### Band-Specific Merged Results JSON + +```json +{ + "Suite_Name: acs_info": { + "ACS Results Summary": { + "Suite_Name: Mandatory : BSA_compliance": "Compliant", + "Suite_Name: Mandatory : SBBR-FWTS_compliance": "Not Compliant: Failed 2", + "Overall Compliance Result": "Not Compliant : Mandatory - (failed: SBBR-FWTS)" + } + }, + "Suite_Name: BSA": { + "...": "bsa.json content" + }, + "Suite_Name: SBBR-FWTS": { + "...": "fwts.json content" + } +} +``` + +SystemReady DT reports use `Suite_Name: EBBR-FWTS` and +`Suite_Name: EBBR-SCT`; SystemReady SR reports use the corresponding +`Suite_Name: SBBR-FWTS` and `Suite_Name: SBBR-SCT` keys. Their compliance +summary keys use the same band-specific prefix: `EBBR-FWTS_compliance` and +`EBBR-SCT_compliance` for DT, or `SBBR-FWTS_compliance` and +`SBBR-SCT_compliance` for SR. Plain `FWTS_compliance` and `SCT_compliance` +summary keys are not emitted or accepted. + +The suite names inside `Overall Compliance Result` use the same display +prefix. For example, a failing SCT run is reported as `EBBR-SCT` for DT or +`SBBR-SCT` for SR; compliant suites are not included in the failure list. + +Plain `Suite_Name: FWTS` and `Suite_Name: SCT` merged wrappers are rejected; +there is no compatibility fallback for those keys. The obsolete DT wrappers +`Suite_Name: BBR-FWTS` and `Suite_Name: BBR-SCT` are also rejected; DT uses +`EBBR` exclusively. Mixed EBBR/SBBR wrappers, or wrappers that disagree with +the report Band, are rejected rather than silently choosing a prefix. + ### What to Inspect After a Run For the default stages: @@ -951,11 +999,70 @@ For the default stages: If only `--outputs json` was requested, steps 1 through 3 are intentionally not available. +### Shared Report Interface + +`common/log_parser/report_ui.py` applies the same offline, self-contained +interface to normal and standalone suite reports. Detailed suite reports turn +their overview table into one compact test-result progress summary when +JavaScript is available, while the original table remains as the no-JavaScript +fallback. The consolidated ACS summary uses the same progress presentation and +places two suite summaries per row on wide screens and one per row on narrow +screens. + +- Detailed reports provide search, one-click status filters, matching-result + counts, test-suite or test-case navigation as appropriate, reset, print/PDF, + and expand/collapse controls. Small reports start expanded; large reports keep + their performance-oriented collapsed defaults. +- Status buttons are discovered from result, status, and outcome fields, so new + status text does not require a UI code change. `All` is selected initially. +- Suite profiles retain suite-specific data and hierarchy while presenting + suite, case, description, reason, and suite-information fields through one + consistent context-card layout, color, and type scale. Suite cards use a + `Test suite` label, while case cards use `Test case`; suite and sub-suite + names appear once in case metadata. Multi-line suite information remains a + list. +- Detailed reports omit the legacy result-distribution chart and count-card + overview. Their compact summary keeps every reported status, including zero + and future statuses, with the existing status colors. A companion breakdown + lists every test suite or test case, including zero-failure units, without an + internal scroll area. Tables remain left-aligned, status pills stay compact, + and wide evidence columns scroll on narrow screens. +- SCT source locations and FWTS/SCT reasons use disclosures without removing the + complete content from the HTML. +- Press `/` to focus report search and `Escape` to clear it. +- The consolidated summary embeds sanitized suite-summary bodies and places its + sticky suite navigator directly below `Test Summaries`, without nested HTML + documents or duplicated scripts. +- The responsive print layout requires no network resources. + +#### Report UI Browser Smoke Test + +The browser smoke test creates synthetic SCT, FWTS, BSA, Standalone, +Post-script, SBMR, and consolidated summary reports, applies the shared +interface, and opens them in headless Chromium. It verifies compact summary +totals, progress ratios, complete suite or case failure breakdowns, zero and +dynamic statuses, desktop and mobile layout, status filters and counts, +small-report collapse/expand behavior, test-suite and test-case navigation, +normalized suite information, reason disclosures, current-view printing, text +alignment, and detailed-report links. It tests browser behavior; the parser +suites separately verify JSON and parsed results. + +Run it from the repository root with `chromium` or `chromium-browser` available +on `PATH`: + +```bash +python3 common/acs_test_framework_runner/report_ui_browser_smoke.py +``` + +A successful run prints `report UI browser smoke: PASS`. + ### Compliance Result Versus Process Success -`Standalone run result: PASS` means parsing and requested report stages -completed. It does not mean every ACS test passed. Test failures remain in JSON, -HTML, and compliance output. +`Standalone run result: PASS` means the requested report stages completed. +For directory-input summary runs, selected missing or non-runnable suites may +be recorded as `Not Run` or `Not Compliant: not run`; inspect merged compliance. +It does not mean every ACS test passed. Test failures remain in JSON, HTML, and +compliance output. ## Schema Validation