diff --git a/SystemReady-band/build-scripts/build-buildroot.sh b/SystemReady-band/build-scripts/build-buildroot.sh
index 7909dfc4..bfdc295a 100755
--- a/SystemReady-band/build-scripts/build-buildroot.sh
+++ b/SystemReady-band/build-scripts/build-buildroot.sh
@@ -65,6 +65,12 @@ do_build ()
cp -r $TOP_DIR/edk2-test-parser root_fs_overlay/usr/bin/
fi
cp -r $TOP_DIR/../common/log_parser root_fs_overlay/usr/bin
+ mkdir -p root_fs_overlay/usr/bin/log_parser/tools
+ cp $TOP_DIR/../common/tools/acs-results-schema.json \
+ $TOP_DIR/../common/tools/suite_registry.json \
+ $TOP_DIR/../common/tools/suite_registry.py \
+ $TOP_DIR/../common/tools/validate.py \
+ root_fs_overlay/usr/bin/log_parser/tools/
cp -r $TOP_DIR/systemready-scripts root_fs_overlay/usr/bin
cp $TOP_DIR/ramdisk/linux-bsa/bsa root_fs_overlay/bin/
cp $TOP_DIR/ramdisk/linux-bsa/bsa_acs.ko root_fs_overlay/lib/modules/
diff --git a/SystemReady-devicetree-band/Yocto/build-scripts/get_source.sh b/SystemReady-devicetree-band/Yocto/build-scripts/get_source.sh
index 60254efe..2ad85c93 100755
--- a/SystemReady-devicetree-band/Yocto/build-scripts/get_source.sh
+++ b/SystemReady-devicetree-band/Yocto/build-scripts/get_source.sh
@@ -159,6 +159,12 @@ copy_recipes()
cp $TOP_DIR/../../common/linux_scripts/read_write_check_blk_devices.py $TOP_DIR/meta-woden/recipes-acs/install-files/files
cp -r $TOP_DIR/../../common/log_parser $TOP_DIR/meta-woden/recipes-acs/install-files/files/
+ mkdir -p $TOP_DIR/meta-woden/recipes-acs/install-files/files/log_parser/tools
+ cp $TOP_DIR/../../common/tools/acs-results-schema.json \
+ $TOP_DIR/../../common/tools/suite_registry.json \
+ $TOP_DIR/../../common/tools/suite_registry.py \
+ $TOP_DIR/../../common/tools/validate.py \
+ $TOP_DIR/meta-woden/recipes-acs/install-files/files/log_parser/tools/
popd
# copy any patches to linux src files directory
cp $COMMON_DIR_PATH/patches/tpm-tis-spi-Add-hardware-wait-polling.patch $TOP_DIR/meta-woden/recipes-kernel/linux/files
diff --git a/common/log_parser/bbr/sct/logs_to_json.py b/common/log_parser/bbr/sct/logs_to_json.py
index 8e1c3d53..f3c6d4ae 100644
--- a/common/log_parser/bbr/sct/logs_to_json.py
+++ b/common/log_parser/bbr/sct/logs_to_json.py
@@ -802,7 +802,11 @@ def main(input_file, output_file):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Parse an SCT Log file and save results to a JSON file.")
+ parser.add_argument("--mode", choices=["DT", "SR"],
+ help="Explicit parser mode; the legacy Yocto flag remains the default")
parser.add_argument("input_file", help="Input Log file")
parser.add_argument("output_file", help="Output JSON file")
args = parser.parse_args()
+ if args.mode:
+ DT_OR_SR_MODE = args.mode
main(args.input_file, args.output_file)
diff --git a/common/log_parser/bbr/tpm/logs_to_json.py b/common/log_parser/bbr/tpm/logs_to_json.py
index f2de9cfc..9f3554e1 100644
--- a/common/log_parser/bbr/tpm/logs_to_json.py
+++ b/common/log_parser/bbr/tpm/logs_to_json.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3i
-# Copyright (c) 2024-2025, Arm Limited or its affiliates. All rights reserved.
+# Copyright (c) 2024-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");
@@ -38,7 +38,10 @@ def parse_tpm_log(lines):
}
}
- pattern = re.compile(r'^Verify\s+.*:\s+(PASS|FAIL|ABORTED|SKIPPED|WARNING)', re.IGNORECASE)
+ pattern = re.compile(
+ r'^Verify\s+.*:\s+(PASS|FAIL|ABORTED|SKIPPED|WARNING|WARN)\b',
+ re.IGNORECASE
+ )
subtest_number = 0
i = 0
@@ -53,7 +56,9 @@ def parse_tpm_log(lines):
# We do a split by " : "
parts = line.split(':')
subtest_desc = parts[0].strip() # e.g. "Verify EV_POST_CODE events ... recommended strings"
- result_str = parts[-1].strip().upper() # e.g. "FAIL"
+ result_str = match.group(1).upper() # e.g. "FAIL"
+ if result_str == "WARN":
+ result_str = "WARNING"
# Grab any indented lines as "reason"
reason_lines = []
@@ -112,7 +117,10 @@ def main(input_file, output_file):
# If we found zero subtests, you can optionally handle that
if len(tpm_entry["subtests"]) == 0:
- print(f"WARNING: No 'Verify ... : PASS|FAIL' patterns found in {input_file}.")
+ print(
+ "WARNING: No 'Verify ... : "
+ f"PASS|FAIL|WARN|WARNING|ABORTED|SKIPPED' patterns found in {input_file}."
+ )
# Build the suite_summary from the single test_entry
summary = tpm_entry["test_case_summary"]
diff --git a/common/log_parser/enrich_suite_json.py b/common/log_parser/enrich_suite_json.py
new file mode 100644
index 00000000..6bf204c6
--- /dev/null
+++ b/common/log_parser/enrich_suite_json.py
@@ -0,0 +1,265 @@
+#!/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.
+
+"""Enrich raw suite JSON files with test category metadata."""
+
+import argparse
+import fnmatch
+import json
+import sys
+from pathlib import Path
+
+BASE_DIR = Path(__file__).resolve().parent
+TOOLS_CANDIDATES = (
+ BASE_DIR.parent / "tools",
+ BASE_DIR / "tools",
+)
+TOOLS_DIR = next(
+ (path for path in TOOLS_CANDIDATES if (path / "suite_registry.py").is_file()),
+ TOOLS_CANDIDATES[0],
+)
+sys.path.insert(0, str(TOOLS_DIR))
+
+from suite_registry import REGISTRY_PATH, load_registry
+
+
+DEFAULT_REGISTRY = Path(REGISTRY_PATH)
+
+REQUIRED_METADATA_FIELDS = [
+ "Main Readiness Grouping",
+ "SRS scope",
+ "Waivable",
+]
+
+ENTRY_ORDER = [
+ "Test_suite",
+ "Test_suite_name",
+ "Main Readiness Grouping",
+ "SRS scope",
+ "Test_case",
+ "Test_case_description",
+ "Test_suite_description",
+ "Test_suite_info",
+ "test_suite_summary",
+ "Waivable",
+ "Sub_test_suite",
+ "Test Entry Point GUID",
+ "Returned Status Code",
+ "test_result",
+ "reason",
+ "testcases",
+ "Test_cases",
+ "subtests",
+ "test_case_summary",
+]
+
+STANDALONE_SUITES = {
+ "CAPSULE-UPDATE",
+ "DT-KSELFTEST",
+ "DT-VALIDATE",
+ "ETHTOOL-TEST",
+ "NETWORK-BOOT",
+ "OS-TESTS",
+ "PSCI",
+ "READ-WRITE-CHECK-BLK-DEVICES",
+ "RUNTIME-DEV-MAP",
+ "SMBIOS",
+}
+
+LOOKUP_SUITE_OVERRIDES = {
+ "BBSR-TPM": "bbsr-standalone",
+ "SBMR-IB": "sbmr",
+ "SBMR-OOB": "sbmr",
+}
+
+
+def _load_json(path):
+ with open(path, "r", encoding="utf-8") as handle:
+ return json.load(handle)
+
+
+def _write_json(path, data):
+ with open(path, "w", encoding="utf-8") as handle:
+ json.dump(data, handle, indent=4)
+
+
+def _category_index(category_data):
+ index = {}
+ if not isinstance(category_data, dict):
+ return index
+
+ for rows in category_data.values():
+ if not isinstance(rows, list):
+ continue
+ for row in rows:
+ if not isinstance(row, dict):
+ continue
+ suite = (row.get("Suite") or "").strip().lower()
+ test_suite = (row.get("Test Suite") or "").strip().lower()
+ if suite and test_suite:
+ index.setdefault(suite, {})[test_suite] = row
+ return index
+
+
+def _file_suite_index(registry):
+ exact = {}
+ patterns = []
+
+ for suite in registry:
+ canonical = suite.get("canonical")
+ if not canonical:
+ continue
+
+ json_output = suite.get("json_output")
+ if json_output:
+ exact[json_output] = canonical
+
+ for pattern in suite.get("json_output_patterns", []):
+ patterns.append((pattern, canonical))
+
+ return exact, patterns
+
+
+def _canonical_for_file(path, exact, patterns):
+ name = Path(path).name
+ if name in exact:
+ return exact[name]
+
+ for pattern, canonical in patterns:
+ if fnmatch.fnmatch(name, pattern):
+ return canonical
+
+ return None
+
+
+def _lookup_suite(canonical):
+ if canonical in STANDALONE_SUITES:
+ return "standalone"
+ return LOOKUP_SUITE_OVERRIDES.get(canonical, canonical.lower())
+
+
+def _entry_list(data):
+ if isinstance(data, dict) and isinstance(data.get("test_results"), list):
+ return data["test_results"]
+ if isinstance(data, list):
+ return data
+ return []
+
+
+def _order_entry(entry):
+ ordered = {key: entry[key] for key in ENTRY_ORDER if key in entry}
+ for key, value in entry.items():
+ if key not in ordered:
+ ordered[key] = value
+ entry.clear()
+ entry.update(ordered)
+
+
+def _metadata_from_row(row):
+ metadata = {}
+ if "Main Readiness Grouping" in row:
+ metadata["Main Readiness Grouping"] = row["Main Readiness Grouping"]
+ if "SRS scope" in row:
+ metadata["SRS scope"] = row["SRS scope"]
+ if "Description" in row:
+ metadata["Test_suite_info"] = row["Description"]
+ if "Waivable" in row:
+ metadata["Waivable"] = row["Waivable"]
+ return metadata
+
+
+def enrich_file(json_file, canonical, category_rows):
+ data = _load_json(json_file)
+ entries = _entry_list(data)
+ if not entries:
+ return 0, 0
+
+ lookup_suite = _lookup_suite(canonical)
+ rows_for_suite = category_rows.get(lookup_suite, {})
+ enriched = 0
+ missing = 0
+
+ for entry in entries:
+ if not isinstance(entry, dict):
+ continue
+
+ test_suite = (entry.get("Test_suite") or entry.get("Test_suite_name") or "").strip().lower()
+ row = rows_for_suite.get(test_suite)
+ if not row:
+ missing += 1
+ continue
+
+ metadata = _metadata_from_row(row)
+ if not all(field in metadata for field in REQUIRED_METADATA_FIELDS):
+ missing += 1
+ continue
+
+ entry.update(metadata)
+ _order_entry(entry)
+ enriched += 1
+
+ if enriched:
+ _write_json(json_file, data)
+
+ return enriched, missing
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Enrich raw suite JSON files with test category metadata.")
+ parser.add_argument("json_files", nargs="+", help="Raw suite JSON files to enrich")
+ parser.add_argument("--registry", default=str(DEFAULT_REGISTRY), help="Path to suite_registry.json")
+ parser.add_argument("--test-category", required=True, help="Path to test_category.json or test_categoryDT.json")
+ parser.add_argument("--quiet", action="store_true", help="Suppress per-file enrichment messages")
+ args = parser.parse_args()
+
+ category_path = Path(args.test_category)
+ if not category_path.is_file():
+ print(f"ERROR: Test category file not found: {category_path}", file=sys.stderr)
+ return 1
+
+ registry = load_registry(args.registry)
+ exact, patterns = _file_suite_index(registry)
+ category_rows = _category_index(_load_json(category_path))
+
+ total_enriched = 0
+ total_missing = 0
+
+ for json_file in args.json_files:
+ json_path = Path(json_file)
+ canonical = _canonical_for_file(json_path, exact, patterns)
+ if not canonical:
+ continue
+
+ try:
+ enriched, missing = enrich_file(json_path, canonical, category_rows)
+ except Exception as exc:
+ print(f"ERROR: Failed to enrich {json_path}: {exc}", file=sys.stderr)
+ return 1
+
+ total_enriched += enriched
+ total_missing += missing
+
+ if not args.quiet and (enriched or missing):
+ print(f"{json_path}: enriched {enriched}, missing category match {missing}")
+
+ if not args.quiet:
+ print(f"Suite JSON enrichment result: {total_enriched} enriched, {total_missing} missing category matches")
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/common/log_parser/generate_acs_summary.py b/common/log_parser/generate_acs_summary.py
index 94f507ff..b0ded84d 100644
--- a/common/log_parser/generate_acs_summary.py
+++ b/common/log_parser/generate_acs_summary.py
@@ -16,6 +16,9 @@
"""Generate the consolidated ACS summary HTML report."""
+# Legacy HTML/template strings are intentionally kept intact for readability.
+# pylint: disable=line-too-long
+
import json
import argparse
import os
@@ -132,6 +135,34 @@ def read_acs_info_system_info(acs_info_json_path):
except Exception:
return {}
+def build_system_info(acs_config_path, system_config_path, uefi_version_log,
+ acs_info_json_path, use_acs_info_system_info=False):
+ """Build summary system information without changing the legacy default path."""
+ acs_info_system = read_acs_info_system_info(acs_info_json_path)
+ if use_acs_info_system_info:
+ system_info = dict(acs_info_system) if isinstance(acs_info_system, dict) else {}
+ summary_generated_date = system_info.pop('Summary Generated On', None)
+ legacy_date = system_info.pop('Summary Generated On Date/time', None)
+ return (
+ system_info,
+ summary_generated_date or legacy_date or 'Unknown',
+ system_info.get('Band', 'Unknown'),
+ )
+
+ system_info = get_system_info()
+ acs_config_info = parse_config(acs_config_path)
+ system_info.update(acs_config_info)
+ system_info.update(parse_config(system_config_path))
+ system_info['UEFI Version'] = get_uefi_version(uefi_version_log)
+
+ if isinstance(acs_info_system, dict) and "BMC Firmware Version" in acs_info_system:
+ system_info["BMC Firmware Version"] = acs_info_system.get("BMC Firmware Version", "N/A")
+ if isinstance(acs_info_system, dict) and "PSCI version" in acs_info_system:
+ system_info["PSCI version"] = acs_info_system.get("PSCI version", "Unknown")
+
+ summary_generated_date = system_info.pop('Summary Generated On Date/time', 'Unknown')
+ return system_info, summary_generated_date, acs_config_info.get('Band', 'Unknown')
+
def remove_result_summary_headings(content):
# Use regular expressions to remove any heading containing 'Result Summary'
pattern = r']*>\s*Result Summary\s*'
@@ -939,31 +970,21 @@ def generate_html(system_info, acs_results_summary,
parser.add_argument("--uefi_version_log", default="", help="Path to the uefi_version.log file")
parser.add_argument("--device_tree_dts", default="", help="Path to the device_tree.dts file")
parser.add_argument("--acs_info_json", default="", help="Path to acs_info.json for System Info fields")
+ parser.add_argument(
+ "--use-acs-info-system-info",
+ action="store_true",
+ help="Use acs_info.json as the complete System Information source",
+ )
args = parser.parse_args()
- # 1) Basic system info
- system_info = get_system_info()
-
- # 2) Merge data from ACS config & system config
- acs_config_info = parse_config(args.acs_config_path)
- system_config_info = parse_config(args.system_config_path)
- system_info.update(acs_config_info)
- system_info.update(system_config_info)
-
- # 3) UEFI version
- uefi_version = get_uefi_version(args.uefi_version_log)
- system_info['UEFI Version'] = uefi_version
-
- # 3b) BMC firmware version from acs_info.json
- acs_info_system = read_acs_info_system_info(args.acs_info_json)
- if isinstance(acs_info_system, dict) and "BMC Firmware Version" in acs_info_system:
- system_info["BMC Firmware Version"] = acs_info_system.get("BMC Firmware Version", "N/A")
- if isinstance(acs_info_system, dict) and "PSCI version" in acs_info_system:
- system_info["PSCI version"] = acs_info_system.get("PSCI version", "Unknown")
-
- # 4) Extract summary date from system_info
- summary_generated_date = system_info.pop('Summary Generated On Date/time', 'Unknown')
+ system_info, summary_generated_date, summary_band = build_system_info(
+ args.acs_config_path,
+ args.system_config_path,
+ args.uefi_version_log,
+ args.acs_info_json,
+ args.use_acs_info_system_info,
+ )
# 5) Read in the stand-alone & capsule summary, then combine them
standalone_summary_content = read_html_content(args.standalone_summary_path)
@@ -1007,7 +1028,7 @@ def generate_html(system_info, acs_results_summary,
# 9) Prepare the dictionary that will be used in the final HTML
acs_results_summary = {
- 'Band': acs_config_info.get('Band', 'Unknown'),
+ 'Band': summary_band,
'Date': summary_generated_date,
'Overall Compliance Results': overall_compliance,
'BBSR compliance results': bbsr_compliance,
diff --git a/common/log_parser/main_log_parser.sh b/common/log_parser/main_log_parser.sh
index 950ab49a..bb285f38 100755
--- a/common/log_parser/main_log_parser.sh
+++ b/common/log_parser/main_log_parser.sh
@@ -24,6 +24,28 @@ BASE_DIR=$(dirname "$(realpath "$0")")
# Determine paths
SCRIPTS_PATH="$BASE_DIR"
+if [ -f "$BASE_DIR/../tools/suite_registry.json" ]; then
+ TOOLS_PATH=$(realpath "$BASE_DIR/../tools")
+else
+ TOOLS_PATH="$BASE_DIR/tools"
+fi
+
+# Update this parser release version when publishing a new log parser release.
+LOG_PARSER_VERSION="1.0.0"
+
+if [ "${1:-}" = "--version" ]; then
+ printf "SystemReady ACS Log Parser %s\n" "$LOG_PARSER_VERSION"
+ exit 0
+fi
+
+# Standalone execution is opt-in. Without --standalone, the unmodified legacy
+# parser flow below handles all arguments exactly as before.
+for argument in "$@"; do
+ if [ "$argument" = "--standalone" ]; then
+ LOG_PARSER_VERSION="$LOG_PARSER_VERSION" \
+ exec python3 "$SCRIPTS_PATH/standalone_runner.py" "$@"
+ fi
+done
# Check for required arguments
if [ $# -lt 1 ]; then
@@ -842,6 +864,10 @@ else
fi
if [ ${#JSON_FILES[@]} -gt 0 ]; then
+ python3 "$SCRIPTS_PATH/enrich_suite_json.py" \
+ --registry "$TOOLS_PATH/suite_registry.json" \
+ --test-category "$test_category" \
+ "${JSON_FILES[@]}"
python3 "$SCRIPTS_PATH/merge_jsons.py" "$MERGED_JSON" "${JSON_FILES[@]}"
echo "ACS Merged JSON: $MERGED_JSON"
else
diff --git a/common/log_parser/merge_jsons.py b/common/log_parser/merge_jsons.py
index 004b27d6..68145b80 100755
--- a/common/log_parser/merge_jsons.py
+++ b/common/log_parser/merge_jsons.py
@@ -18,6 +18,25 @@
from collections import OrderedDict
import argparse
import os
+import sys
+from pathlib import Path
+
+BASE_DIR = Path(__file__).resolve().parent
+TOOLS_CANDIDATES = (
+ BASE_DIR.parent / "tools",
+ BASE_DIR / "tools",
+)
+TOOLS_DIR = next(
+ (path for path in TOOLS_CANDIDATES if (path / "suite_registry.py").is_file()),
+ TOOLS_CANDIDATES[0],
+)
+sys.path.insert(0, str(TOOLS_DIR))
+
+from suite_registry import (
+ normalize_suite_name as registry_normalize_suite_name,
+ requirement_table,
+ selected_requirement_keys,
+)
# Define color codes
RED = "\033[91m"
@@ -27,9 +46,12 @@
# Requirement map for each suite
_REQUIREMENT_MAP = {}
+_SELECTED_SUITE_FILTER = None
################################################################################
-# 1. Determine if we're in Device Tree (DT) mode or SR mode by checking yocto flag
+# 1. Determine if we're in Device Tree (DT) mode or SR mode.
+# main_log_parser.sh passes this explicitly. The flag check remains only for
+# direct legacy invocations of this helper.
################################################################################
YOCTO_FLAG_PATH = "/mnt/yocto_image.flag"
if os.path.isfile(YOCTO_FLAG_PATH):
@@ -46,45 +68,14 @@
# BSA, Kselftest, PSCI, post script are recommendation
# DT SRS scope table
-DT_SRS_SCOPE_TABLE = [
- ("SCT", "M"),
- ("FWTS", "M"),
- ("Capsule Update", "M"),
- ("DT_VALIDATE", "M"),
- ("READ_WRITE_CHECK_BLK_DEVICES", "M"),
- ("ETHTOOL_TEST", "M"),
- ("SCMI", "EM"),
- ("NETWORK_BOOT", "R"),
- ("BSA", "R"),
- ("BBSR-SCT", "EM"),
- ("BBSR-TPM", "EM"),
- ("BBSR-FWTS", "EM"),
- ("DT_KSELFTEST", "R"),
- ("SMBIOS", "R"),
- ("PSCI", "R"),
- ("RUNTIME_DEV_MAP","R"),
- ("POST_SCRIPT", "R"),
- ("OS_TEST", "M"),
- ("PFDI", "CM")
-]
+DT_SRS_SCOPE_TABLE = requirement_table("DT")
# SBSA is mandatory for servers only, default treat as recommended
# if SBSA is run, treat as mandatory
# BBSR is extension
# SR SRS scope table
-SR_SRS_SCOPE_TABLE = [
- ("SCT", "M"),
- ("FWTS", "M"),
- ("BSA", "M"),
- ("OS_TEST", "M"),
- ("BBSR-SCT", "EM"),
- ("BBSR-FWTS", "EM"),
- ("BBSR-TPM", "EM"),
- ("SBMR-IB", "R"),
- ("SBMR-OOB", "R"),
- ("SBSA", "R")
-]
+SR_SRS_SCOPE_TABLE = requirement_table("SR")
def compliance_label(suite_name: str) -> str:
req = _REQUIREMENT_MAP.get(suite_name, "R")
@@ -99,6 +90,21 @@ def compliance_label(suite_name: str) -> str:
# Match the console ordering: “Suite: : …”
return f"Suite_Name: {tag} : {suite_name}_compliance"
+def normalize_suite_name(suite_name: str) -> str:
+ return registry_normalize_suite_name(suite_name) or suite_name
+
+def build_selected_suite_filter(selected_suites):
+ selected = set(selected_requirement_keys(selected_suites))
+ return selected or None
+
+
+def suite_matches_selected_filter(suite_name):
+ if _SELECTED_SUITE_FILTER is None:
+ return True
+ if suite_name in _SELECTED_SUITE_FILTER:
+ return True
+ return "OS_TEST" in _SELECTED_SUITE_FILTER and suite_name.startswith("OS_")
+
def reformat_json(json_file_path):
"""
@@ -197,22 +203,22 @@ def _sum_suite_summary(a, b):
sa = _get_suite_summary(a); sb = _get_suite_summary(b)
return {k: int(sa.get(k, 0)) + int(sb.get(k, 0)) for k in keys}
-################################################################################
-# We will load the test_categoryDT.json data here, so we can enrich the
-# merged JSON with "Waivable", "SRS scope", and
-# "Main Readiness Grouping" fields for each test suite.
-################################################################################
-
-if DT_OR_SR_MODE == "DT":
- TEST_CATEGORY_PATH = "/usr/bin/log_parser/test_categoryDT.json"
-else:
- TEST_CATEGORY_PATH = "/usr/bin/log_parser/test_category.json"
+def load_test_category_data(mode, test_category_path=None):
+ """
+ Load test category metadata for the selected mode, so merged JSON entries can
+ be enriched with waivable, SRS scope, and readiness grouping fields.
+ """
+ if not test_category_path:
+ if mode == "DT":
+ test_category_path = "/usr/bin/log_parser/test_categoryDT.json"
+ else:
+ test_category_path = "/usr/bin/log_parser/test_category.json"
-try:
- with open(TEST_CATEGORY_PATH, "r") as catf:
- test_category_data = json.load(catf)
-except Exception:
- test_category_data = {}
+ try:
+ with open(test_category_path, "r") as catf:
+ return json.load(catf)
+ except Exception:
+ return {}
def build_testcategory_dict(category_data):
"""
@@ -239,8 +245,17 @@ def build_testcategory_dict(category_data):
result[s_lower][ts_lower] = row
return result
+test_category_data = load_test_category_data(DT_OR_SR_MODE)
test_cat_dict = build_testcategory_dict(test_category_data)
+
+def set_run_mode(mode, test_category_path=None):
+ global DT_OR_SR_MODE, test_category_data, test_cat_dict
+
+ DT_OR_SR_MODE = mode
+ test_category_data = load_test_category_data(DT_OR_SR_MODE, test_category_path)
+ test_cat_dict = build_testcategory_dict(test_category_data)
+
def recursive_sort(obj):
if isinstance(obj, dict):
# Maintain priority: "Test_suite" first, "Sub_test_suite" second, "subtests" last
@@ -494,16 +509,19 @@ def merge_json_files(json_files, output_file):
}
# --- ensure labels use the right Mandatory/Recommended tags for this mode ---
base_table = DT_SRS_SCOPE_TABLE if DT_OR_SR_MODE == "DT" else SR_SRS_SCOPE_TABLE
+ if _SELECTED_SUITE_FILTER is not None:
+ base_table = [(n, r) for (n, r) in base_table if suite_matches_selected_filter(n)]
+
for n, r in base_table:
_REQUIREMENT_MAP.setdefault(n, r)
# Step 3) Compute *per-suite* and overall compliance
# Base mandatory set
if DT_OR_SR_MODE == "DT":
- mandatory_suites = set(DT_SRS_SCOPE_TABLE)
+ mandatory_suites = set(base_table)
present = set(suite_fail_data.keys())
else:
- mandatory_suites = set(SR_SRS_SCOPE_TABLE)
+ mandatory_suites = set(base_table)
present = set(suite_fail_data.keys())
# Always consider SBSA mandatory if present (your existing rule)
@@ -597,6 +615,8 @@ def merge_json_files(json_files, output_file):
print(f"Suite: Extension : {suite_name}: {acs_results_summary[label]}")
else:
print(f"Suite: Recommended: {suite_name}: {acs_results_summary[label]}")
+ if _SELECTED_SUITE_FILTER is not None:
+ overall_comp = "Not Compliant"
recommended_non_waived_list.append(suite_name)
#Ensure suite-wise compliance lines for *all* discovered suites (including recommended)
@@ -653,6 +673,12 @@ def merge_json_files(json_files, output_file):
if "Overall Compliance Results" in acs_results_summary:
del acs_results_summary["Overall Compliance Results"]
+ bbsr_selected = (
+ _SELECTED_SUITE_FILTER is None
+ or bool({"BBSR-TPM", "BBSR-FWTS", "BBSR-SCT"} & _SELECTED_SUITE_FILTER)
+ )
+ scmi_selected = _SELECTED_SUITE_FILTER is None or "SCMI" in _SELECTED_SUITE_FILTER
+
bbsr_tpm = acs_results_summary.get(compliance_label("BBSR-TPM"), "")
bbsr_fwts = acs_results_summary.get(compliance_label("BBSR-FWTS"), "")
bbsr_sct = acs_results_summary.get(compliance_label("BBSR-SCT"), "")
@@ -663,7 +689,9 @@ def _is_missing(val: str) -> bool:
return (not val) or val.lower().startswith("not run")
_no_bbsr_logs = all(_is_missing(x) for x in (bbsr_tpm, bbsr_fwts, bbsr_sct))
- if _no_bbsr_logs:
+ if not bbsr_selected:
+ acs_results_summary.pop("BBSR compliance results", None)
+ elif _no_bbsr_logs:
acs_results_summary["BBSR compliance results"] = "Not run"
else:
# Gather which suites didn’t run vs. which failed non-waived
@@ -700,18 +728,19 @@ def _is_missing(val: str) -> bool:
acs_results_summary["BBSR compliance results"] = "Compliant"
# Persist + print BBSR result with color
- bbsr_comp_str = acs_results_summary["BBSR compliance results"]
- if bbsr_comp_str.lower().startswith("compliant with waivers"):
- print(f"{YELLOW}BBSR compliance results: {bbsr_comp_str}{RESET}\n")
- elif bbsr_comp_str.lower().startswith("compliant"):
- print(f"{GREEN}BBSR compliance results: {bbsr_comp_str}{RESET}\n")
- elif bbsr_comp_str.lower().startswith("not run"):
- print(f"BBSR compliance results: {bbsr_comp_str}\n")
- else:
- print(f"{RED}BBSR compliance results: {bbsr_comp_str}{RESET}\n")
+ if bbsr_selected:
+ bbsr_comp_str = acs_results_summary["BBSR compliance results"]
+ if bbsr_comp_str.lower().startswith("compliant with waivers"):
+ print(f"{YELLOW}BBSR compliance results: {bbsr_comp_str}{RESET}\n")
+ elif bbsr_comp_str.lower().startswith("compliant"):
+ print(f"{GREEN}BBSR compliance results: {bbsr_comp_str}{RESET}\n")
+ elif bbsr_comp_str.lower().startswith("not run"):
+ print(f"BBSR compliance results: {bbsr_comp_str}\n")
+ else:
+ print(f"{RED}BBSR compliance results: {bbsr_comp_str}{RESET}\n")
# --- handle SCMI result (DT only, separate from Overall Compliance) ---
- if DT_OR_SR_MODE == "DT":
+ if DT_OR_SR_MODE == "DT" and scmi_selected:
scmi_label = compliance_label("SCMI")
scmi_status = acs_results_summary.get(scmi_label, "")
if not scmi_status:
@@ -729,8 +758,9 @@ def _is_missing(val: str) -> bool:
else:
acs_results_summary["SCMI compliance results"] = scmi_status
- merged_results["Suite_Name: acs_info"]["ACS Results Summary"]["BBSR compliance results"] = (acs_results_summary.pop("BBSR compliance results", None))
- if DT_OR_SR_MODE == "DT":
+ if bbsr_selected:
+ merged_results["Suite_Name: acs_info"]["ACS Results Summary"]["BBSR compliance results"] = (acs_results_summary.pop("BBSR compliance results", None))
+ if DT_OR_SR_MODE == "DT" and scmi_selected:
merged_results["Suite_Name: acs_info"]["ACS Results Summary"]["SCMI compliance results"] = (acs_results_summary.pop("SCMI compliance results", None))
RENAME_SUITES_TO_STANDALONE = {
@@ -769,14 +799,27 @@ def _entry_to_list(entry):
json.dump(merged_results, outj, indent=4)
def main():
+ global _SELECTED_SUITE_FILTER
+
parser = argparse.ArgumentParser(
description="Merge suite JSONs + acs_info.json, store compliance lines inside 'ACS Results Summary'"
)
+ parser.add_argument("--mode", choices=["DT", "SR"], default=DT_OR_SR_MODE,
+ help="Explicit parser mode used for compliance and test category selection")
+ parser.add_argument("--selected-suites", default="",
+ help="Comma-separated suite names to include in compliance reporting")
+ parser.add_argument("--test-category", default="",
+ help="Explicit test category JSON path; legacy installed paths remain the default")
parser.add_argument("output_file", help="Output merged JSON file")
parser.add_argument("json_files", nargs='+',
help="List of JSON files to merge (including acs_info.json if present)")
args = parser.parse_args()
+ set_run_mode(args.mode, args.test_category or None)
+
+ selected_suites = [s.strip() for s in args.selected_suites.split(",") if s.strip()]
+ _SELECTED_SUITE_FILTER = build_selected_suite_filter(selected_suites)
+
merge_json_files(args.json_files, args.output_file)
if __name__ == "__main__":
diff --git a/common/log_parser/package_standalone.sh b/common/log_parser/package_standalone.sh
new file mode 100755
index 00000000..f5a5c654
--- /dev/null
+++ b/common/log_parser/package_standalone.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+# 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.
+
+set -euo pipefail
+
+SCRIPT_DIR=$(dirname "$(realpath "$0")")
+REPO_ROOT=$(realpath "$SCRIPT_DIR/../..")
+OUTPUT_PATH=${1:-"$PWD/systemready-log-parser-standalone.tar.gz"}
+
+required_paths=(
+ "common/log_parser/main_log_parser.sh"
+ "common/log_parser/standalone_runner.py"
+ "common/log_parser/requirements.txt"
+ "common/log_parser/test_category.json"
+ "common/log_parser/test_categoryDT.json"
+ "common/tools/acs-results-schema.json"
+ "common/tools/suite_registry.json"
+ "common/tools/suite_registry.py"
+ "common/tools/validate.py"
+ "docs/acs_schema_guide.md"
+ "docs/log_parser_guide.md"
+ "LICENSE.md"
+)
+
+for relative_path in "${required_paths[@]}"; do
+ if [ ! -f "$REPO_ROOT/$relative_path" ]; then
+ echo "ERROR: Required package file is missing: $REPO_ROOT/$relative_path" >&2
+ exit 1
+ fi
+done
+
+mkdir -p "$(dirname "$OUTPUT_PATH")"
+OUTPUT_PATH=$(realpath -m "$OUTPUT_PATH")
+TEMP_DIR=$(mktemp -d)
+TEMP_ARCHIVE="$TEMP_DIR/systemready-log-parser-standalone.tar.gz"
+trap 'rm -rf "$TEMP_DIR"' EXIT
+
+exclude_args=(
+ "--exclude=__pycache__"
+ "--exclude=*.pyc"
+ "--exclude=common/log_parser/tests"
+)
+output_relative=$(realpath --relative-to="$REPO_ROOT" "$OUTPUT_PATH")
+if [[ "$output_relative" != ../* ]]; then
+ exclude_args+=("--exclude=$output_relative" "--exclude=$output_relative.sha256")
+fi
+
+tar \
+ "${exclude_args[@]}" \
+ --transform='s,^,systemready-log-parser/,' \
+ -czf "$TEMP_ARCHIVE" \
+ -C "$REPO_ROOT" \
+ common/log_parser \
+ common/tools/acs-results-schema.json \
+ common/tools/suite_registry.json \
+ common/tools/suite_registry.py \
+ common/tools/validate.py \
+ docs/acs_schema_guide.md \
+ docs/log_parser_guide.md \
+ LICENSE.md
+
+mv "$TEMP_ARCHIVE" "$OUTPUT_PATH"
+output_dir=$(dirname "$OUTPUT_PATH")
+output_name=$(basename "$OUTPUT_PATH")
+(
+ cd "$output_dir"
+ sha256sum "$output_name" > "$output_name.sha256"
+)
+
+echo "Standalone package: $(realpath "$OUTPUT_PATH")"
+echo "SHA-256 checksum : $(realpath "$OUTPUT_PATH.sha256")"
diff --git a/common/log_parser/requirements.txt b/common/log_parser/requirements.txt
new file mode 100644
index 00000000..61754c9b
--- /dev/null
+++ b/common/log_parser/requirements.txt
@@ -0,0 +1,5 @@
+chardet
+Jinja2
+matplotlib
+jsonschema
+weasyprint
diff --git a/common/log_parser/standalone_runner.py b/common/log_parser/standalone_runner.py
new file mode 100755
index 00000000..3d60c2aa
--- /dev/null
+++ b/common/log_parser/standalone_runner.py
@@ -0,0 +1,1353 @@
+#!/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.
+
+"""Portable, strict suite-wise SystemReady log parser orchestration."""
+
+import argparse
+import json
+import os
+import re
+import signal
+import shutil
+import subprocess
+import sys
+import tempfile
+from dataclasses import dataclass, field
+from pathlib import Path
+
+BASE_DIR = Path(__file__).resolve().parent
+TOOLS_CANDIDATES = (
+ BASE_DIR.parent / "tools",
+ BASE_DIR / "tools",
+)
+TOOLS_DIR = next(
+ (path for path in TOOLS_CANDIDATES if (path / "suite_registry.py").is_file()),
+ TOOLS_CANDIDATES[0],
+)
+sys.path.insert(0, str(TOOLS_DIR))
+
+from suite_registry import (
+ expand_selected_suites,
+ get_suite,
+ list_suite_names,
+ load_registry,
+ normalize_suite_name,
+ suite_supports_mode,
+)
+
+
+DEFAULT_REGISTRY = TOOLS_DIR / "suite_registry.json"
+SCHEMA_VALIDATOR = TOOLS_DIR / "validate.py"
+DEFAULT_MODE = "SR"
+MINIMUM_PYTHON = (3, 8)
+MINIMUM_OUTPUT_FREE_BYTES = 10 * 1024 * 1024
+
+EXIT_INPUT = 3
+EXIT_DEPENDENCY = 4
+EXIT_PARSE = 5
+EXIT_SCHEMA = 6
+EXIT_REPORT = 7
+EXIT_OUTPUT = 8
+
+PACKAGE_NAMES = {
+ "chardet": "chardet",
+ "jinja2": "Jinja2",
+ "jsonschema": "jsonschema",
+ "matplotlib": "matplotlib",
+ "weasyprint": "weasyprint",
+}
+
+
+def get_log_parser_version():
+ version = os.environ.get("LOG_PARSER_VERSION")
+ if version:
+ return version
+
+ try:
+ main_script = (BASE_DIR / "main_log_parser.sh").read_text(encoding="utf-8")
+ except OSError:
+ return "unknown"
+ match = re.search(
+ r'^LOG_PARSER_VERSION="([0-9A-Za-z.+-]+)"$',
+ main_script,
+ flags=re.MULTILINE,
+ )
+ return match.group(1) if match else "unknown"
+
+
+class StandaloneError(Exception):
+ def __init__(self, message, exit_code):
+ super().__init__(message)
+ self.exit_code = exit_code
+
+
+@dataclass
+class ResolvedInput:
+ path: Path
+ exists: bool
+ supporting: bool = False
+
+
+@dataclass
+class SuiteResult:
+ canonical: str
+ suite: dict
+ execution: dict
+ inputs: dict
+ json_files: list = field(default_factory=list)
+ boot_sources: list = field(default_factory=list)
+ detailed_html: Path = None
+ summary_html: Path = None
+
+
+def load_registry_data(path):
+ try:
+ with open(path, "r", encoding="utf-8") as handle:
+ data = json.load(handle)
+ except (OSError, json.JSONDecodeError) as error:
+ raise StandaloneError(f"Cannot load suite registry '{path}': {error}", EXIT_INPUT) from error
+
+ standalone = data.get("standalone")
+ if not isinstance(standalone, dict):
+ raise StandaloneError("Suite registry has no standalone configuration.", EXIT_INPUT)
+ return data, standalone
+
+
+def split_values(values):
+ result = []
+ for group in values or []:
+ for value in group:
+ result.extend(item.strip() for item in value.split(",") if item.strip())
+ return result
+
+
+def flatten_values(values):
+ return [value for group in (values or []) for value in group]
+
+
+def parse_direct_inputs(values, canonical, execution):
+ supplied = flatten_values(values)
+ if not supplied:
+ return {}
+
+ specs = [
+ spec for spec in execution.get("inputs", [])
+ if spec.get("kind", "file") == "file"
+ ]
+ if not specs:
+ raise StandaloneError(
+ f"{canonical}: direct log files are not supported for this suite.", EXIT_INPUT
+ )
+
+ specs_by_name = {
+ spec["name"].strip().lower().replace("-", "_"): spec for spec in specs
+ }
+ named = {}
+ positional = []
+ for value in supplied:
+ possible_name, separator, possible_path = value.partition("=")
+ normalized_name = possible_name.strip().lower().replace("-", "_")
+ if separator and normalized_name in specs_by_name:
+ if not possible_path.strip():
+ raise StandaloneError(
+ f"{canonical}: direct input '{possible_name}' has an empty path.",
+ EXIT_INPUT,
+ )
+ input_name = specs_by_name[normalized_name]["name"]
+ if input_name in named:
+ raise StandaloneError(
+ f"{canonical}: direct input '{input_name}' was provided more than once.",
+ EXIT_INPUT,
+ )
+ named[input_name] = possible_path
+ elif separator and "/" not in possible_name and "\\" not in possible_name:
+ expected = ", ".join(spec["name"] for spec in specs)
+ raise StandaloneError(
+ f"{canonical}: unknown direct input name '{possible_name}'. "
+ f"Expected one of: {expected}.",
+ EXIT_INPUT,
+ )
+ else:
+ positional.append(value)
+
+ remaining = [spec for spec in specs if spec["name"] not in named]
+ if len(positional) > len(remaining):
+ expected = ", ".join(spec["name"] for spec in specs)
+ raise StandaloneError(
+ f"{canonical}: received {len(supplied)} direct log files, but the registry "
+ f"defines only {len(specs)} file inputs ({expected}).",
+ EXIT_INPUT,
+ )
+
+ if len(positional) == 1 and len(remaining) > 1:
+ required = [spec for spec in remaining if spec.get("required")]
+ targets = required if len(required) == 1 else remaining[:1]
+ else:
+ targets = remaining[:len(positional)]
+ for spec, value in zip(targets, positional):
+ named[spec["name"]] = value
+
+ resolved = {}
+ used_paths = set()
+ for spec in specs:
+ input_name = spec["name"]
+ if input_name not in named:
+ continue
+ path = Path(named[input_name]).expanduser().resolve()
+ if not path.is_file():
+ raise StandaloneError(
+ f"{canonical}: direct input '{input_name}' is not a file: {path}",
+ EXIT_INPUT,
+ )
+ if path in used_paths:
+ raise StandaloneError(
+ f"{canonical}: the same direct log was assigned more than once: {path}",
+ EXIT_INPUT,
+ )
+ used_paths.add(path)
+ resolved[input_name] = ResolvedInput(
+ path,
+ True,
+ bool(spec.get("supporting")),
+ )
+ return resolved
+
+
+def parse_outputs(value, defaults):
+ requested = []
+ for item in (value.split(",") if value else defaults):
+ name = item.strip().lower()
+ if name and name not in requested:
+ requested.append(name)
+ valid = {"json", "html", "summary", "pdf"}
+ invalid = [item for item in requested if item not in valid]
+ if invalid:
+ raise StandaloneError(
+ f"Unsupported output stage(s): {', '.join(invalid)}. Use json, html, summary, or pdf.",
+ EXIT_INPUT,
+ )
+ if "pdf" in requested and "summary" not in requested:
+ requested.append("summary")
+ if "summary" in requested and "html" not in requested:
+ requested.append("html")
+ if "json" not in requested:
+ requested.append("json")
+ return [stage for stage in ("json", "html", "summary", "pdf") if stage in requested]
+
+
+def build_input_roots(results_path=None):
+ roots = {}
+ if results_path:
+ roots.update({
+ "results": results_path,
+ "firmware": results_path.parent / "fw",
+ "os_logs": results_path.parent / "os-logs",
+ })
+ return roots
+
+
+def resolve_input(spec, roots):
+ root_name = spec.get("root")
+ if root_name not in roots:
+ raise StandaloneError(
+ f"Registry input '{spec.get('name', '?')}' uses unknown root '{root_name}'.",
+ EXIT_INPUT,
+ )
+ candidates = spec.get("candidates", [])
+ if not candidates:
+ raise StandaloneError(
+ f"Registry input '{spec.get('name', '?')}' has no candidate paths.", EXIT_INPUT
+ )
+
+ expected = None
+ for candidate in candidates:
+ path = (roots[root_name] / candidate).resolve()
+ expected = expected or path
+ kind = spec.get("kind", "file")
+ exists = path.is_dir() if kind == "directory" else path.is_file()
+ if exists:
+ return ResolvedInput(path, True, bool(spec.get("supporting")))
+ return ResolvedInput(expected, False, bool(spec.get("supporting")))
+
+
+def resolve_suite_inputs(canonical, execution, roots, direct_inputs=None):
+ direct_inputs = direct_inputs or {}
+ resolved = {}
+ missing_required = []
+ primary_found = 0
+
+ for spec in execution.get("inputs", []):
+ if spec["name"] in direct_inputs:
+ item = direct_inputs[spec["name"]]
+ elif spec.get("root") in roots:
+ item = resolve_input(spec, roots)
+ else:
+ item = ResolvedInput(
+ Path(f"<{spec.get('root', 'input')}>") / spec["candidates"][0],
+ False,
+ bool(spec.get("supporting")),
+ )
+ resolved[spec["name"]] = item
+ if item.exists and not item.supporting:
+ primary_found += 1
+ if spec.get("required") and not item.exists:
+ missing_required.append(item.path)
+
+ if missing_required:
+ lines = "\n".join(f" - {path}" for path in missing_required)
+ raise StandaloneError(
+ f"{canonical}: required input is missing:\n{lines}", EXIT_INPUT
+ )
+
+ minimum = int(execution.get("minimum_inputs", 0))
+ 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(
+ f"{canonical}: requires at least {minimum} input log(s); found {primary_found}:\n{lines}",
+ EXIT_INPUT,
+ )
+ return resolved
+
+
+def registry_script(suite, key):
+ relative = suite.get(key)
+ if not relative:
+ raise StandaloneError(
+ f"{suite.get('canonical', '?')}: registry field '{key}' is missing.", EXIT_INPUT
+ )
+ path = (BASE_DIR / relative).resolve()
+ if not path.is_file():
+ raise StandaloneError(
+ f"{suite.get('canonical', '?')}: registered script does not exist: {path}",
+ EXIT_INPUT,
+ )
+ return path
+
+
+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", {})
+ known_handlers = {
+ "capsule",
+ "multi_log",
+ "os_tests",
+ "psci",
+ "sbmr",
+ "sct",
+ "single_log",
+ "standalone_single",
+ }
+ known_roots = {"results", "firmware", "os_logs"}
+ alias_owners = {}
+
+ for suite in registry:
+ canonical = suite.get("canonical")
+ if not canonical:
+ raise StandaloneError("Registry contains a suite without a canonical name.", EXIT_INPUT)
+ for alias in [canonical] + suite.get("aliases", []):
+ token = "-".join(alias.strip().upper().replace("_", "-").split())
+ owner = alias_owners.get(token)
+ if owner and owner != canonical:
+ raise StandaloneError(
+ f"Registry alias '{alias}' is shared by {owner} and {canonical}.", EXIT_INPUT
+ )
+ alias_owners[token] = canonical
+
+ if suite.get("included_suites"):
+ for child in suite["included_suites"]:
+ if child not in suite_map:
+ raise StandaloneError(
+ f"{canonical}: included suite '{child}' is not registered.", EXIT_INPUT
+ )
+ continue
+
+ execution = execution_map.get(canonical)
+ if not execution:
+ raise StandaloneError(
+ f"{canonical}: standalone execution configuration is missing.", EXIT_INPUT
+ )
+ if execution.get("handler") not in known_handlers:
+ raise StandaloneError(
+ f"{canonical}: unsupported standalone handler '{execution.get('handler')}'.",
+ EXIT_INPUT,
+ )
+ input_names = set()
+ for input_spec in execution.get("inputs", []):
+ input_name = input_spec.get("name")
+ if not input_name or input_name in input_names:
+ raise StandaloneError(
+ f"{canonical}: input names must be present and unique.", EXIT_INPUT
+ )
+ input_names.add(input_name)
+ if input_spec.get("root") not in known_roots:
+ raise StandaloneError(
+ f"{canonical}: input '{input_name}' uses an unknown root.", EXIT_INPUT
+ )
+ candidates = input_spec.get("candidates")
+ if not candidates:
+ raise StandaloneError(
+ f"{canonical}: input '{input_name}' has no candidate path.", EXIT_INPUT
+ )
+ for candidate in candidates:
+ candidate_path = Path(candidate)
+ if candidate_path.is_absolute() or ".." in candidate_path.parts:
+ raise StandaloneError(
+ f"{canonical}: input '{input_name}' must stay within its registered root.",
+ EXIT_INPUT,
+ )
+ registry_script(suite, "logs_to_json")
+ registry_script(suite, "json_to_html")
+ if execution.get("handler") == "os_tests" and "SR" in suite.get("modes", []):
+ registry_script(suite, "sr_logs_to_json")
+ for support_script in suite.get("supporting_logs_to_json", []):
+ support_path = (BASE_DIR / support_script).resolve()
+ if not support_path.is_file():
+ raise StandaloneError(
+ f"{canonical}: registered supporting parser does not exist: {support_path}",
+ EXIT_INPUT,
+ )
+
+ schema_value = suite.get("schema", "")
+ if schema_value:
+ schema_path = schema_value.split("#", 1)[0]
+ resolved_schema = (Path(registry_path).resolve().parent / schema_path).resolve()
+ if not resolved_schema.is_file():
+ raise StandaloneError(
+ f"{canonical}: registered schema does not exist: {schema_path}", EXIT_INPUT
+ )
+
+ required_summary_inputs = {"uefi_version", "dmidecode", "ipmitool", "psci"}
+ summary_inputs = standalone.get("summary_inputs", {})
+ if set(summary_inputs) != required_summary_inputs:
+ raise StandaloneError(
+ "Standalone summary_inputs must define uefi_version, dmidecode, ipmitool, and psci.",
+ EXIT_INPUT,
+ )
+ for name, spec in summary_inputs.items():
+ candidates = spec.get("candidates")
+ if spec.get("root") not in known_roots or not candidates:
+ raise StandaloneError(
+ f"Standalone summary input '{name}' has an invalid root or candidates.",
+ EXIT_INPUT,
+ )
+ for candidate in candidates:
+ relative_path = Path(candidate)
+ if relative_path.is_absolute() or ".." in relative_path.parts:
+ raise StandaloneError(
+ f"Standalone summary input '{name}' must stay within its registered root.",
+ EXIT_INPUT,
+ )
+
+
+def validate_support_files(outputs, schema_requested, waiver):
+ paths = [BASE_DIR / "enrich_suite_json.py"]
+ if waiver:
+ paths.append(BASE_DIR / "apply_waivers.py")
+ if schema_requested:
+ paths.append(SCHEMA_VALIDATOR)
+ if "summary" in outputs:
+ paths.extend(
+ BASE_DIR / name
+ for name in ("acs_info.py", "merge_jsons.py", "generate_acs_summary.py")
+ )
+
+ missing = [path for path in paths if not path.is_file()]
+ if missing:
+ lines = "\n".join(f" - {path}" for path in missing)
+ raise StandaloneError(f"Required standalone support file is missing:\n{lines}", EXIT_INPUT)
+
+
+def check_dependencies(modules):
+ missing = []
+ for module in sorted(set(modules)):
+ command = [sys.executable, "-c", f"import {module}"]
+ result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
+ if result.returncode != 0:
+ missing.append((module, result.stderr.strip().splitlines()[-1:] or ["import failed"]))
+
+ if missing:
+ packages = ", ".join(PACKAGE_NAMES.get(module, module) for module, _ in missing)
+ install_command = (
+ f"{sys.executable} -m pip install -r {BASE_DIR / 'requirements.txt'}"
+ )
+ raise StandaloneError(
+ f"Missing or unusable Python dependencies: {packages}. "
+ f"Install with: {install_command}",
+ EXIT_DEPENDENCY,
+ )
+
+
+def run_command(label, command, failure_code):
+ print(f"{label}")
+ command = [str(item) for item in command]
+ try:
+ process = subprocess.Popen(command, start_new_session=True)
+ except OSError as error:
+ raise StandaloneError(
+ f"Cannot start command: {' '.join(command)}: {error}", failure_code
+ ) from error
+ try:
+ return_code = process.wait()
+ except KeyboardInterrupt:
+ try:
+ os.killpg(process.pid, signal.SIGTERM)
+ except ProcessLookupError:
+ pass
+ try:
+ process.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ try:
+ os.killpg(process.pid, signal.SIGKILL)
+ except ProcessLookupError:
+ pass
+ process.wait()
+ raise
+ if return_code != 0:
+ raise StandaloneError(
+ f"Command failed with status {return_code}: {' '.join(command)}",
+ failure_code,
+ )
+
+
+def validate_json_output(canonical, path):
+ if not path.is_file():
+ raise StandaloneError(f"{canonical}: parser did not create {path}", EXIT_PARSE)
+ try:
+ with open(path, "r", encoding="utf-8") as handle:
+ data = json.load(handle)
+ except (OSError, json.JSONDecodeError) as error:
+ raise StandaloneError(f"{canonical}: invalid generated JSON '{path}': {error}", EXIT_PARSE) from error
+ if not isinstance(data, dict) or not isinstance(data.get("test_results"), list):
+ raise StandaloneError(
+ f"{canonical}: generated JSON has no test_results list: {path}", EXIT_PARSE
+ )
+
+
+def apply_waiver(canonical, suite, json_file, waiver, test_category):
+ if not waiver:
+ return
+ command = [
+ sys.executable,
+ BASE_DIR / "apply_waivers.py",
+ suite.get("waiver_suite", canonical),
+ json_file,
+ waiver,
+ test_category,
+ "--quiet",
+ ]
+ run_command(f"[{canonical}] Applying waivers", command, EXIT_PARSE)
+ validate_json_output(canonical, json_file)
+
+
+def primary_inputs(resolved):
+ return [item.path for item in resolved.values() if item.exists and not item.supporting]
+
+
+def run_os_tests(canonical, suite, execution, resolved, mode, json_dir):
+ result = SuiteResult(canonical, suite, execution, resolved)
+ os_logs = resolved["os_logs"].path
+
+ if mode == "SR":
+ output = json_dir / suite["json_output"]
+ post_script = resolved["post_script"].path
+ command = [
+ sys.executable,
+ registry_script(suite, "sr_logs_to_json"),
+ os_logs,
+ post_script,
+ output,
+ ]
+ run_command(f"[{canonical}] Parsing SR OS logs", command, EXIT_PARSE)
+ validate_json_output(canonical, output)
+ result.json_files.append(output)
+ return result
+
+ os_directories = sorted(path for path in os_logs.glob("linux*") if path.is_dir())
+ for os_directory in os_directories:
+ ethtool_log = os_directory / "ethtool_test.log"
+ if not ethtool_log.is_file():
+ continue
+ output = json_dir / f"ethtool_test_{os_directory.name}.json"
+ command = [
+ sys.executable,
+ registry_script(suite, "logs_to_json"),
+ ethtool_log,
+ output,
+ os_directory.name,
+ ]
+ run_command(f"[{canonical}] Parsing {os_directory.name}", command, EXIT_PARSE)
+ validate_json_output(canonical, output)
+ result.json_files.append(output)
+ boot_sources = os_directory / "boot_sources.log"
+ result.boot_sources.append(boot_sources if boot_sources.is_file() else "Unknown")
+
+ if not result.json_files:
+ raise StandaloneError(
+ f"{canonical}: no linux*/ethtool_test.log inputs found under {os_logs}", EXIT_INPUT
+ )
+ return result
+
+
+def run_suite(canonical, suite, execution, roots, mode, json_dir, direct_inputs=None):
+ resolved = resolve_suite_inputs(canonical, execution, roots, direct_inputs)
+ handler = execution.get("handler")
+ if handler == "os_tests":
+ return run_os_tests(canonical, suite, execution, resolved, mode, json_dir)
+
+ output = json_dir / suite["json_output"]
+ parser_script = registry_script(suite, "logs_to_json")
+ result = SuiteResult(canonical, suite, execution, resolved)
+
+ if handler in {"multi_log", "single_log", "standalone_single", "sbmr"}:
+ command = [sys.executable, parser_script, *primary_inputs(resolved), output]
+ elif handler == "sct":
+ supporting = resolved.get("edk2")
+ if supporting and supporting.exists:
+ support_scripts = suite.get("supporting_logs_to_json", [])
+ if not support_scripts:
+ raise StandaloneError(f"{canonical}: supporting SCT parser is not registered.", EXIT_INPUT)
+ support_output_name = next(
+ spec.get("output") for spec in execution["inputs"] if spec.get("name") == "edk2"
+ )
+ support_output = json_dir / support_output_name
+ support_script = (BASE_DIR / support_scripts[0]).resolve()
+ run_command(
+ f"[{canonical}] Parsing supporting EDK2 log",
+ [sys.executable, support_script, supporting.path, support_output],
+ EXIT_PARSE,
+ )
+ command = [sys.executable, parser_script, "--mode", mode, resolved["log"].path, output]
+ elif handler == "capsule":
+ command = [
+ sys.executable,
+ parser_script,
+ "capsule_update",
+ resolved["update"].path,
+ resolved["on_disk"].path,
+ resolved["results"].path,
+ output,
+ ]
+ elif handler == "psci":
+ command = [sys.executable, parser_script, "psci_check", resolved["log"].path, output]
+ else:
+ raise StandaloneError(f"{canonical}: unsupported registry handler '{handler}'.", EXIT_INPUT)
+
+ run_command(f"[{canonical}] Parsing logs", command, EXIT_PARSE)
+ validate_json_output(canonical, output)
+ result.json_files.append(output)
+ return result
+
+
+def render_reports(results, html_dir):
+ standalone_results = [
+ result for result in results
+ if result.execution.get("handler") in {"standalone_single", "capsule", "psci"}
+ ]
+ regular_results = [result for result in results if result not in standalone_results]
+
+ for result in regular_results:
+ suite = result.suite
+ detailed = html_dir / suite["detailed_html"]
+ summary = html_dir / suite["summary_html"]
+ command = [
+ sys.executable,
+ registry_script(suite, "json_to_html"),
+ *result.json_files,
+ detailed,
+ summary,
+ ]
+ handler = result.execution.get("handler")
+ if handler == "sbmr":
+ report = result.inputs.get("report")
+ command.append(report.path if report and report.exists else "")
+ elif handler == "os_tests":
+ command.append("--include-drop-down")
+ if result.boot_sources:
+ command.extend(["--boot-sources-paths", *result.boot_sources])
+ run_command(f"[{result.canonical}] Generating HTML", command, EXIT_REPORT)
+ if not detailed.is_file() or not summary.is_file():
+ raise StandaloneError(
+ f"{result.canonical}: HTML renderer did not create expected outputs.", EXIT_REPORT
+ )
+ result.detailed_html = detailed
+ result.summary_html = summary
+
+ if standalone_results:
+ registry = load_registry()
+ group = get_suite("STANDALONE", registry)
+ detailed = html_dir / group["detailed_html"]
+ summary = html_dir / group["summary_html"]
+ json_files = [path for result in standalone_results for path in result.json_files]
+ command = [
+ sys.executable,
+ registry_script(group, "json_to_html"),
+ *json_files,
+ detailed,
+ summary,
+ "--include-drop-down",
+ ]
+ run_command("[STANDALONE] Generating combined HTML", command, EXIT_REPORT)
+ if not detailed.is_file() or not summary.is_file():
+ raise StandaloneError(
+ "STANDALONE: HTML renderer did not create expected outputs.", EXIT_REPORT
+ )
+ for result in standalone_results:
+ result.detailed_html = detailed
+ result.summary_html = summary
+
+
+def generate_acs_info(standalone, roots, args, json_dir):
+ summary_inputs = standalone.get("summary_inputs", {})
+
+ def summary_path(name):
+ spec = summary_inputs[name]
+ if spec["root"] not in roots:
+ return None
+ paths = [
+ (roots[spec["root"]] / candidate).resolve()
+ for candidate in spec["candidates"]
+ ]
+ return next((path for path in paths if path.is_file()), paths[0])
+
+ dmidecode = summary_path("dmidecode")
+ empty_dmidecode = None
+ if not dmidecode or not dmidecode.is_file():
+ empty_dmidecode = json_dir / ".empty_dmidecode.log"
+ empty_dmidecode.touch()
+ dmidecode = empty_dmidecode
+
+ command = [
+ sys.executable,
+ BASE_DIR / "acs_info.py",
+ "--acs_config_path", args.acs_config or "",
+ "--system_config_path", args.system_config or "",
+ "--uefi_version_log", summary_path("uefi_version") or "",
+ "--dmidecode_log", dmidecode,
+ "--ipmitool_log", summary_path("ipmitool") or "",
+ "--psci_kernel_log", summary_path("psci") or "",
+ "--output_dir", json_dir,
+ ]
+ try:
+ run_command("[SUMMARY] Gathering ACS information", command, EXIT_REPORT)
+ finally:
+ if empty_dmidecode:
+ empty_dmidecode.unlink(missing_ok=True)
+ acs_info = json_dir / "acs_info.json"
+ if not acs_info.is_file():
+ raise StandaloneError("ACS information generation did not create acs_info.json.", EXIT_REPORT)
+ return acs_info
+
+
+def merge_results(mode, selected, test_category, json_files, output):
+ command = [
+ sys.executable,
+ BASE_DIR / "merge_jsons.py",
+ "--mode", mode,
+ "--test-category", test_category,
+ "--selected-suites", ",".join(selected),
+ output,
+ *json_files,
+ ]
+ run_command("[SUMMARY] Merging selected suite JSON files", command, EXIT_REPORT)
+ if not output.is_file():
+ raise StandaloneError("Merge did not create merged_results.json.", EXIT_REPORT)
+
+
+def generate_combined_summary(results, roots, args, html_dir, merged_json, acs_info):
+ summaries = {result.canonical: result.summary_html for result in results if result.summary_html}
+ standalone_summary = next(
+ (
+ result.summary_html
+ for result in results
+ if result.execution.get("handler") in {"standalone_single", "capsule", "psci"}
+ ),
+ None,
+ )
+
+ ordered = [
+ summaries.get("BSA"),
+ summaries.get("SBSA"),
+ summaries.get("FWTS"),
+ summaries.get("SCT"),
+ summaries.get("BBSR-FWTS"),
+ summaries.get("BBSR-SCT"),
+ summaries.get("BBSR-TPM"),
+ summaries.get("PFDI"),
+ summaries.get("POST-SCRIPT"),
+ standalone_summary,
+ summaries.get("OS-TESTS"),
+ None,
+ summaries.get("SBMR-IB"),
+ summaries.get("SBMR-OOB"),
+ summaries.get("SCMI"),
+ ]
+ output = html_dir / "acs_summary.html"
+ command = [
+ sys.executable,
+ BASE_DIR / "generate_acs_summary.py",
+ *(str(path) if path else "" for path in ordered),
+ output,
+ "--merged_json", merged_json,
+ "--acs_info_json", acs_info,
+ "--use-acs-info-system-info",
+ ]
+ if args.acs_config:
+ command.extend(["--acs_config_path", args.acs_config])
+ if args.system_config:
+ command.extend(["--system_config_path", args.system_config])
+ results_root = roots.get("results")
+ uefi_version = results_root / "uefi_dump/uefi_version.log" if results_root else None
+ if uefi_version and uefi_version.is_file():
+ command.extend(["--uefi_version_log", uefi_version])
+
+ run_command("[SUMMARY] Generating combined HTML", command, EXIT_REPORT)
+ if not output.is_file():
+ raise StandaloneError("Combined report did not create acs_summary.html.", EXIT_REPORT)
+ return output
+
+
+def generate_pdf(html_path, pdf_path):
+ code = (
+ "import sys; from weasyprint import CSS, HTML; "
+ "HTML(sys.argv[1]).write_pdf(sys.argv[2], stylesheets=[CSS(string='@page { margin: 0; }')])"
+ )
+ run_command(
+ "[SUMMARY] Generating PDF",
+ [sys.executable, "-c", code, html_path, pdf_path],
+ EXIT_REPORT,
+ )
+ if not pdf_path.is_file():
+ raise StandaloneError("PDF generation did not create acs_summary.pdf.", EXIT_REPORT)
+
+
+def lexical_absolute(path):
+ return Path(os.path.abspath(os.path.expanduser(str(path))))
+
+
+def paths_overlap(first, second):
+ return first == second or first in second.parents or second in first.parents
+
+
+def validate_output_target(target, roots, direct_inputs=()):
+ target = lexical_absolute(target)
+ if target.is_symlink():
+ raise StandaloneError(f"Refusing to use a symlink output path: {target}", EXIT_OUTPUT)
+
+ resolved_target = target.resolve(strict=False)
+ results_root = roots.get("results")
+ results_path = Path(results_root).resolve() if results_root else None
+ default_output = (
+ (results_path / "acs_summary").resolve(strict=False)
+ if results_path else None
+ )
+ for root_name, root_path in roots.items():
+ resolved_root = Path(root_path).resolve(strict=False)
+ if not paths_overlap(resolved_target, resolved_root):
+ continue
+ if root_name == "results" and resolved_target == default_output:
+ continue
+ allowed_note = (
+ f" Only '{default_output}' may be inside results."
+ if root_name == "results"
+ else " Input and output paths must be separate."
+ )
+ raise StandaloneError(
+ f"Output '{target}' overlaps the effective {root_name} input root "
+ f"'{resolved_root}'.{allowed_note}",
+ EXIT_OUTPUT,
+ )
+
+ for input_path in direct_inputs:
+ resolved_input = Path(input_path).resolve()
+ if paths_overlap(resolved_target, resolved_input):
+ raise StandaloneError(
+ f"Output '{target}' overlaps direct input log '{resolved_input}'. "
+ "Input and output paths must be separate.",
+ EXIT_OUTPUT,
+ )
+
+ if target.exists():
+ raise StandaloneError(
+ f"Output '{target}' already exists. Delete or move the stale output, "
+ "or choose a different --output path.",
+ EXIT_OUTPUT,
+ )
+
+
+def check_output_readiness(target):
+ target = lexical_absolute(target)
+ ancestor = target.parent
+ while not ancestor.exists() and ancestor != ancestor.parent:
+ ancestor = ancestor.parent
+ if not ancestor.is_dir() or ancestor.is_symlink():
+ raise StandaloneError(
+ f"Output parent is not a usable directory: {ancestor}", EXIT_OUTPUT
+ )
+
+ try:
+ free_bytes = shutil.disk_usage(ancestor).free
+ except OSError as error:
+ raise StandaloneError(
+ f"Cannot inspect free space for output parent '{ancestor}': {error}", EXIT_OUTPUT
+ ) from error
+ if free_bytes < MINIMUM_OUTPUT_FREE_BYTES:
+ raise StandaloneError(
+ f"Output filesystem has less than {MINIMUM_OUTPUT_FREE_BYTES // (1024 * 1024)} MiB "
+ f"free at '{ancestor}'.",
+ EXIT_OUTPUT,
+ )
+
+ probe = None
+ try:
+ probe = Path(tempfile.mkdtemp(prefix=".standalone-write-test-", dir=ancestor))
+ (probe / "write-test").write_text("ok", encoding="ascii")
+ except OSError as error:
+ raise StandaloneError(
+ f"Output parent is not writable: {ancestor}: {error}", EXIT_OUTPUT
+ ) from error
+ finally:
+ if probe:
+ shutil.rmtree(probe, ignore_errors=True)
+
+
+def prepare_output(output_path, roots, direct_inputs=()):
+ target = lexical_absolute(output_path)
+ validate_output_target(target, roots, direct_inputs)
+ check_output_readiness(target)
+ stage = None
+ try:
+ target.parent.mkdir(parents=True, exist_ok=True)
+ stage = Path(tempfile.mkdtemp(prefix=f".{target.name}.tmp-", dir=target.parent))
+ except OSError as error:
+ if stage:
+ shutil.rmtree(stage, ignore_errors=True)
+ raise StandaloneError(f"Cannot prepare output '{target}': {error}", EXIT_OUTPUT) from error
+ return target, stage
+
+
+def copy_run_configs(stage, args):
+ config_sources = []
+ if args.acs_config:
+ config_sources.append(
+ (Path(args.acs_config), "acs_config_dt.txt" if args.mode == "DT" else "acs_config.txt")
+ )
+ if args.system_config:
+ config_sources.append(
+ (
+ Path(args.system_config),
+ "system_config_dt.txt" if args.mode == "DT" else "system_config.txt",
+ )
+ )
+ if not config_sources:
+ return
+
+ config_dir = stage / "config"
+ try:
+ config_dir.mkdir()
+ for source, output_name in config_sources:
+ shutil.copy2(source, config_dir / output_name)
+ except OSError as error:
+ raise StandaloneError(f"Cannot copy current run configuration: {error}", EXIT_OUTPUT) from error
+
+
+def publish_output(target, stage, roots, direct_inputs=()):
+ try:
+ validate_output_target(target, roots, direct_inputs)
+ stage.replace(target)
+ except OSError as error:
+ raise StandaloneError(f"Cannot publish output '{target}': {error}", EXIT_OUTPUT) from error
+
+
+def build_parser():
+ parser = argparse.ArgumentParser(
+ description="Run selected SystemReady log parser suites without installed /usr/bin or /mnt state."
+ )
+ parser.add_argument("--standalone", action="store_true", help=argparse.SUPPRESS)
+ parser.add_argument(
+ "--mode",
+ choices=["DT", "SR"],
+ help=f"Parser mode (default: {DEFAULT_MODE})",
+ )
+ parser.add_argument(
+ "--input-log",
+ action="append",
+ nargs="+",
+ default=[],
+ dest="input_logs",
+ metavar="[NAME=]PATH",
+ help=(
+ "ACS results directory or direct suite log file; repeat files or provide "
+ "multiple paths in registry input order"
+ ),
+ )
+ parser.add_argument(
+ "--output",
+ help=(
+ "Output directory; defaults to /acs_summary and is "
+ "required for direct log files"
+ ),
+ )
+ parser.add_argument("--suite", "--suites", action="append", nargs="+", dest="suites",
+ help="Selected suite names; repeat or use comma-separated values")
+ parser.add_argument("--acs-config", "--acs_config", dest="acs_config")
+ parser.add_argument("--system-config", "--system_config", dest="system_config")
+ parser.add_argument("--waiver", "--waiver-json", "--waiver_json", dest="waiver")
+ parser.add_argument("--test-category", help="Override the bundled mode-specific test category JSON")
+ parser.add_argument("--outputs", help="Comma-separated stages: json,html,summary,pdf")
+ parser.add_argument("--schema", action="store_true", help="Validate generated raw suite JSON files")
+ parser.add_argument(
+ "--doctor",
+ action="store_true",
+ help="Validate registry, dependencies, inputs, and output readiness, then exit",
+ )
+ parser.add_argument("--list-suites", action="store_true", help="List suites and exit")
+ parser.add_argument(
+ "--version",
+ action="version",
+ version=f"SystemReady ACS Log Parser {get_log_parser_version()}",
+ help="Print the complete log parser release version and exit",
+ )
+ return parser
+
+
+def apply_default_mode(args):
+ if args.mode:
+ return False
+
+ args.mode = DEFAULT_MODE
+ print(
+ f"INFO: --mode was not provided; standalone parser will run in "
+ f"{DEFAULT_MODE} mode by default."
+ )
+ return True
+
+
+def validate_waiver_json(path):
+ try:
+ with open(path, "r", encoding="utf-8") as handle:
+ waiver_data = json.load(handle)
+ except (OSError, json.JSONDecodeError) as error:
+ raise StandaloneError(
+ f"Waiver file is not valid JSON: {path}: {error}", EXIT_INPUT
+ ) from error
+
+ suites = waiver_data.get("Suites") if isinstance(waiver_data, dict) else None
+ if not isinstance(suites, list):
+ raise StandaloneError(
+ "Waiver JSON must be an object containing a 'Suites' array: "
+ f"{path}",
+ EXIT_INPUT,
+ )
+
+
+def validate_cli_paths(args):
+ missing = [name for name in ("suites", "input_logs") if not getattr(args, name)]
+ if missing:
+ raise StandaloneError(
+ "Standalone mode requires --suite/--suites and --input-log.",
+ EXIT_INPUT,
+ )
+
+ supplied_inputs = flatten_values(args.input_logs)
+ directory_inputs = []
+ for value in supplied_inputs:
+ if "=" in value:
+ continue
+ path = Path(value).expanduser().resolve()
+ if path.is_dir():
+ directory_inputs.append(path)
+
+ if directory_inputs:
+ if len(supplied_inputs) != 1:
+ raise StandaloneError(
+ "An ACS results directory must be the only --input-log value.",
+ EXIT_INPUT,
+ )
+ results = directory_inputs[0]
+ args.results = str(results)
+ args.input_logs = []
+ else:
+ results = None
+ args.results = None
+
+ if not results and not args.output:
+ raise StandaloneError(
+ "--output is required when --input-log contains direct log files.",
+ EXIT_INPUT,
+ )
+
+ args.output = str(
+ lexical_absolute(args.output) if args.output else results / "acs_summary"
+ )
+
+ for label, value in (
+ ("ACS config", args.acs_config),
+ ("system config", args.system_config),
+ ("waiver", args.waiver),
+ ):
+ if value and not Path(value).expanduser().is_file():
+ raise StandaloneError(f"{label} file does not exist: {value}", EXIT_INPUT)
+ if args.acs_config:
+ args.acs_config = str(Path(args.acs_config).expanduser().resolve())
+ if args.system_config:
+ args.system_config = str(Path(args.system_config).expanduser().resolve())
+ if args.waiver:
+ args.waiver = str(Path(args.waiver).expanduser().resolve())
+ validate_waiver_json(args.waiver)
+
+
+def validate_mode_configs(args):
+ if args.acs_config:
+ band = ""
+ try:
+ with open(args.acs_config, "r", encoding="utf-8", errors="replace") as handle:
+ for line in handle:
+ key, separator, value = line.partition(":")
+ if separator and key.strip().lower() == "band":
+ band = value.strip()
+ break
+ except OSError as error:
+ raise StandaloneError(f"Cannot read ACS config '{args.acs_config}': {error}", EXIT_INPUT) from error
+ is_dt_band = "devicetree" in band.lower()
+ if band and args.mode == "DT" and not is_dt_band:
+ raise StandaloneError(
+ f"Selected DT mode conflicts with ACS config Band '{band}'.", EXIT_INPUT
+ )
+ if band and args.mode == "SR" and is_dt_band:
+ raise StandaloneError(
+ f"Selected SR mode conflicts with ACS config Band '{band}'.", EXIT_INPUT
+ )
+
+ expected_suffix = "_dt.txt" if args.mode == "DT" else ".txt"
+ for label, value in (("ACS", args.acs_config), ("system", args.system_config)):
+ if not value:
+ continue
+ name = Path(value).name.lower()
+ looks_dt = name.endswith("_dt.txt")
+ if args.mode == "DT" and not looks_dt:
+ print(f"WARNING: {label} config '{Path(value).name}' does not use the expected {expected_suffix} name.")
+ elif args.mode == "SR" and looks_dt:
+ print(f"WARNING: {label} config '{Path(value).name}' looks like a DT-mode config.")
+
+
+def validate_python_version():
+ if sys.version_info < MINIMUM_PYTHON:
+ required = ".".join(str(item) for item in MINIMUM_PYTHON)
+ current = ".".join(str(item) for item in sys.version_info[:3])
+ raise StandaloneError(
+ f"Python {required} or newer is required; current interpreter is {current}.",
+ EXIT_DEPENDENCY,
+ )
+
+
+def handle_termination(_signum, _frame):
+ raise KeyboardInterrupt
+
+
+def main():
+ if hasattr(sys.stdout, "reconfigure"):
+ sys.stdout.reconfigure(line_buffering=True)
+ validate_python_version()
+ signal.signal(signal.SIGTERM, handle_termination)
+ _, standalone = load_registry_data(DEFAULT_REGISTRY)
+ parser = build_parser()
+ args = parser.parse_args()
+ registry = load_registry()
+
+ if args.list_suites:
+ print("\n".join(list_suite_names(registry)))
+ return 0
+
+ apply_default_mode(args)
+ validate_cli_paths(args)
+ validate_mode_configs(args)
+ validate_registry(registry, standalone)
+
+ requested = split_values(args.suites)
+ normalized = []
+ for name in requested:
+ canonical = normalize_suite_name(name, registry)
+ if not canonical:
+ raise StandaloneError(f"Unsupported suite '{name}'. Use --list-suites.", EXIT_INPUT)
+ normalized.append(canonical)
+ selected = expand_selected_suites(normalized, registry)
+ if not selected:
+ raise StandaloneError("No executable suites were selected.", EXIT_INPUT)
+
+ execution_map = standalone["suite_execution"]
+ direct_inputs = {}
+ if flatten_values(args.input_logs):
+ if len(selected) != 1:
+ raise StandaloneError(
+ "Direct --input-log values require exactly one executable suite. "
+ "Run each suite separately or pass one ACS results directory to "
+ "--input-log for a multi-suite run.",
+ EXIT_INPUT,
+ )
+ direct_inputs = parse_direct_inputs(
+ args.input_logs,
+ selected[0],
+ execution_map[selected[0]],
+ )
+
+ for canonical in selected:
+ if not suite_supports_mode(canonical, args.mode, registry):
+ raise StandaloneError(
+ f"{canonical} is not available in {args.mode} mode.", EXIT_INPUT
+ )
+
+ outputs = parse_outputs(args.outputs, standalone.get("default_outputs", []))
+ test_category = Path(args.test_category).expanduser().resolve() if args.test_category else (
+ BASE_DIR / ("test_categoryDT.json" if args.mode == "DT" else "test_category.json")
+ )
+ if not test_category.is_file():
+ raise StandaloneError(f"Test category file does not exist: {test_category}", EXIT_INPUT)
+
+ roots = build_input_roots(Path(args.results) if args.results else None)
+ direct_paths = [item.path for item in direct_inputs.values()]
+ validate_output_target(Path(args.output), roots, direct_paths)
+ validate_support_files(outputs, args.schema, args.waiver)
+
+ modules = []
+ for canonical in selected:
+ modules.extend(execution_map[canonical].get("json_dependencies", []))
+ dependency_groups = standalone.get("dependency_modules", {})
+ if "html" in outputs:
+ modules.extend(dependency_groups.get("html", []))
+ if "summary" in outputs:
+ modules.extend(dependency_groups.get("summary", []))
+ if args.schema:
+ modules.extend(dependency_groups.get("schema", []))
+ if "pdf" in outputs:
+ modules.extend(dependency_groups.get("pdf", []))
+
+ check_dependencies(modules)
+
+ print("Standalone SystemReady log parser")
+ print(f" Mode : {args.mode}")
+ print(f" Input directory: {args.results or 'not provided (direct log mode)'}")
+ for input_name, item in direct_inputs.items():
+ print(f" Direct input : {input_name}={item.path}")
+ print(f" Output : {args.output}")
+ print(f" Selected suites: {', '.join(selected)}")
+ print(f" Output stages : {', '.join(outputs)}")
+ print(f" Test category : {test_category}")
+
+ if args.doctor:
+ for canonical in selected:
+ resolved = resolve_suite_inputs(
+ canonical,
+ execution_map[canonical],
+ roots,
+ direct_inputs,
+ )
+ if execution_map[canonical].get("handler") == "os_tests" and args.mode == "DT":
+ os_logs = resolved["os_logs"].path
+ if not any(path.is_file() for path in os_logs.glob("linux*/ethtool_test.log")):
+ raise StandaloneError(
+ f"{canonical}: no linux*/ethtool_test.log inputs found under {os_logs}",
+ EXIT_INPUT,
+ )
+ print(f" {canonical:<28} READY")
+ check_output_readiness(Path(args.output))
+ print(" Output destination READY")
+ print("Standalone preflight result: PASS")
+ return 0
+
+ target, stage = prepare_output(Path(args.output), roots, direct_paths)
+ try:
+ json_dir = stage / "acs_jsons"
+ html_dir = stage / "html_detailed_summaries"
+ json_dir.mkdir(parents=True)
+ html_dir.mkdir(parents=True)
+ copy_run_configs(stage, args)
+
+ results = []
+ for canonical in selected:
+ suite = get_suite(canonical, registry)
+ result = run_suite(
+ canonical,
+ suite,
+ execution_map[canonical],
+ roots,
+ args.mode,
+ json_dir,
+ direct_inputs,
+ )
+ 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:
+ run_command(
+ "[SCHEMA] Validating raw suite JSON files",
+ [
+ sys.executable,
+ SCHEMA_VALIDATOR,
+ "raw",
+ "--registry", DEFAULT_REGISTRY,
+ *raw_jsons,
+ ],
+ EXIT_SCHEMA,
+ )
+
+ if "html" in outputs:
+ render_reports(results, html_dir)
+
+ if "summary" in outputs:
+ acs_info = generate_acs_info(standalone, roots, args, json_dir)
+ merged = json_dir / "merged_results.json"
+ merge_results(args.mode, selected, test_category, [acs_info, *raw_jsons], merged)
+ combined_html = generate_combined_summary(
+ results, roots, args, html_dir, merged, acs_info
+ )
+ if "pdf" in outputs:
+ generate_pdf(combined_html, stage / "acs_summary.pdf")
+
+ publish_output(target, stage, roots, direct_paths)
+ finally:
+ if stage.exists():
+ shutil.rmtree(stage, ignore_errors=True)
+
+ print("")
+ print("Standalone run result: PASS")
+ print(f"Output: {target}")
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except StandaloneError as error:
+ print(f"ERROR: {error}", file=sys.stderr)
+ sys.exit(error.exit_code)
+ except KeyboardInterrupt:
+ print("ERROR: Standalone run interrupted; staged output was removed.", file=sys.stderr)
+ sys.exit(130)
diff --git a/common/tools/acs-results-schema.json b/common/tools/acs-results-schema.json
index 09b856a0..f416795b 100644
--- a/common/tools/acs-results-schema.json
+++ b/common/tools/acs-results-schema.json
@@ -23,6 +23,9 @@
"Suite_Name: BBSR-SCT": {
"$ref": "#/definitions/bbsr_sct_suite"
},
+ "Suite_Name: BBSR-TPM": {
+ "$ref": "#/definitions/tpm_suite"
+ },
"Suite_Name: FWTS": {
"$ref": "#/definitions/fwts_suite"
},
@@ -41,6 +44,12 @@
"Suite_Name: SBMR": {
"$ref": "#/definitions/sbmr_suite"
},
+ "Suite_Name: SBMR-IB": {
+ "$ref": "#/definitions/sbmr_suite"
+ },
+ "Suite_Name: SBMR-OOB": {
+ "$ref": "#/definitions/sbmr_suite"
+ },
"Suite_Name: SCMI": {
"$ref": "#/definitions/scmi_suite"
},
@@ -84,6 +93,8 @@
"IGNORED",
"KNOWN ACS LIMITATION",
"KNOWN U-BOOT LIMITATION",
+ "TEST NOT IMPLEMENTED",
+ "PAL NOT SUPPORTED",
"NOT TESTED (TEST NOT IMPLEMENTED)",
"NOT TESTED (PAL NOT SUPPORTED)",
"FAILED (WITH WAIVER)",
@@ -399,6 +410,12 @@
"sub_Test_GUID": {
"type": "string"
},
+ "sub_Test_Level": {
+ "$ref": "#/definitions/non_negative_int"
+ },
+ "sub_Test_Path": {
+ "type": "string"
+ },
"sub_Rule_ID": {
"type": "string"
},
@@ -411,11 +428,16 @@
"$ref": "#/definitions/sub_test_result_object"
}
]
+ },
+ "subtests": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/bsa_subtest"
+ }
}
},
"required": [
"sub_Test_Number",
- "sub_Rule_ID",
"sub_Test_Description",
"sub_test_result"
],
@@ -443,6 +465,9 @@
},
"reason": {
"type": "string"
+ },
+ "waiver_reason": {
+ "type": "string"
}
},
"required": [
@@ -858,94 +883,222 @@
"unevaluatedProperties": false
},
"sbmr_test_case": {
+ "type": "object",
+ "required": [
+ "Test_case",
+ "test_case_summary",
+ "subtests"
+ ],
+ "properties": {
+ "Test_case": {
+ "type": "string"
+ },
+ "Test_case_description": {
+ "type": "string"
+ },
+ "test_case_summary": {
+ "$ref": "#/definitions/summary_totals"
+ },
+ "subtests": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/sbmr_subtest"
+ }
+ },
+ "waiver_reason": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "tpm_summary_totals": {
"allOf": [
{
- "$ref": "#/definitions/test_case_base"
+ "$ref": "#/definitions/summary_totals"
},
{
- "type": "object",
"required": [
- "Test_case",
- "test_case_summary",
- "subtests"
- ],
- "properties": {
- "Test_case": {
- "type": "string"
- },
- "test_case_summary": {
- "$ref": "#/definitions/summary_totals"
- },
- "subtests": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/sbmr_subtest"
- }
- }
- },
- "additionalProperties": false
+ "total_ignored"
+ ]
}
]
},
- "sbmr_test_result": {
- "allOf": [
- {
- "$ref": "#/definitions/test_category_base"
+ "tpm_subtest": {
+ "type": "object",
+ "required": [
+ "sub_Test_Number",
+ "sub_Test_Description",
+ "sub_test_result"
+ ],
+ "properties": {
+ "sub_Test_Number": {
+ "type": "string"
},
- {
- "$ref": "#/definitions/test_result_base"
+ "sub_Test_Description": {
+ "type": "string"
},
- {
- "type": "object",
- "required": [
- "Test_suite",
- "Test_cases",
- "test_suite_summary"
- ],
- "properties": {
- "Test_suite": {
- "type": "string"
- },
- "test_suite_summary": {
- "$ref": "#/definitions/summary_totals"
- },
- "Test_cases": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/sbmr_test_case"
- }
- }
- }
+ "sub_test_result": {
+ "type": "string",
+ "enum": [
+ "PASS",
+ "FAIL",
+ "FAIL (WITH WAIVER)",
+ "ABORTED",
+ "SKIPPED",
+ "WARNING"
+ ]
+ },
+ "reason": {
+ "$ref": "#/definitions/reason_value"
+ },
+ "waiver_reason": {
+ "$ref": "#/definitions/reason_value"
}
+ },
+ "additionalProperties": false
+ },
+ "tpm_test_result": {
+ "type": "object",
+ "required": [
+ "Test_suite",
+ "Sub_test_suite",
+ "Test_case",
+ "Test_case_description",
+ "subtests",
+ "test_case_summary"
],
- "unevaluatedProperties": false
+ "properties": {
+ "Test_suite": {
+ "type": "string"
+ },
+ "Sub_test_suite": {
+ "type": "string"
+ },
+ "Test_case": {
+ "type": "string"
+ },
+ "Test_case_description": {
+ "type": "string"
+ },
+ "subtests": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/tpm_subtest"
+ }
+ },
+ "test_case_summary": {
+ "$ref": "#/definitions/tpm_summary_totals"
+ },
+ "waiver_reason": {
+ "$ref": "#/definitions/reason_value"
+ },
+ "Main Readiness Grouping": {
+ "type": "string"
+ },
+ "SRS scope": {
+ "type": "string"
+ },
+ "Waivable": {
+ "type": "string"
+ },
+ "Test_suite_info": {
+ "$ref": "#/definitions/string_list"
+ }
+ },
+ "additionalProperties": false
},
- "sbmr_suite": {
+ "tpm_suite": {
"allOf": [
{
"$ref": "#/definitions/suite_base"
},
{
"type": "object",
+ "required": [
+ "suite_summary",
+ "test_results"
+ ],
"properties": {
"suite_summary": {
- "$ref": "#/definitions/summary_totals"
+ "$ref": "#/definitions/tpm_summary_totals"
},
"test_results": {
"type": "array",
"items": {
- "$ref": "#/definitions/sbmr_test_result"
+ "$ref": "#/definitions/tpm_test_result"
}
}
- },
- "required": [
- "suite_summary",
- "test_results"
- ]
+ }
}
],
"unevaluatedProperties": false
},
+ "sbmr_test_result": {
+ "type": "object",
+ "required": [
+ "Test_suite",
+ "Test_cases",
+ "test_suite_summary"
+ ],
+ "properties": {
+ "Test_suite": {
+ "type": "string"
+ },
+ "Test_suite_description": {
+ "type": "string"
+ },
+ "Sub_test_suite": {
+ "type": "string"
+ },
+ "Main Readiness Grouping": {
+ "type": "string"
+ },
+ "SRS scope": {
+ "type": "string"
+ },
+ "Waivable": {
+ "type": "string"
+ },
+ "Test_suite_info": {
+ "$ref": "#/definitions/string_list"
+ },
+ "test_suite_summary": {
+ "$ref": "#/definitions/summary_totals"
+ },
+ "Test_cases": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/sbmr_test_case"
+ }
+ },
+ "waiver_reason": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "sbmr_suite": {
+ "type": "object",
+ "properties": {
+ "Suite_Name": {
+ "type": "string"
+ },
+ "suite_summary": {
+ "$ref": "#/definitions/summary_totals"
+ },
+ "test_results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/sbmr_test_result"
+ }
+ }
+ },
+ "required": [
+ "suite_summary",
+ "test_results"
+ ],
+ "additionalProperties": false
+ },
"standalone_test_result": {
"allOf": [
{
@@ -1161,6 +1314,74 @@
],
"unevaluatedProperties": false
},
+ "reason_value": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ },
+ "raw_bsa_suite": {
+ "$ref": "#/definitions/bsa_suite"
+ },
+ "raw_fwts_suite": {
+ "$ref": "#/definitions/fwts_suite"
+ },
+ "raw_post_script_suite": {
+ "$ref": "#/definitions/post_script_suite"
+ },
+ "raw_sct_suite": {
+ "$ref": "#/definitions/sct_suite"
+ },
+ "raw_tpm_suite": {
+ "$ref": "#/definitions/tpm_suite"
+ },
+ "raw_sbmr_suite": {
+ "$ref": "#/definitions/sbmr_suite"
+ },
+ "raw_scmi_suite": {
+ "$ref": "#/definitions/scmi_suite"
+ },
+ "raw_standalone_suite": {
+ "type": "object",
+ "required": [
+ "test_results",
+ "suite_summary"
+ ],
+ "properties": {
+ "Suite_Name": {
+ "type": "string"
+ },
+ "test_results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/standalone_test_result"
+ }
+ },
+ "suite_summary": {
+ "$ref": "#/definitions/summary_totals"
+ }
+ },
+ "additionalProperties": false
+ },
+ "raw_os_tests_suite": {
+ "$ref": "#/definitions/os_tests_suite"
+ },
"system_info": {
"type": "object",
"required": [
@@ -1406,6 +1627,12 @@
"Suite_Name: Recommended : SBMR-OOB_compliance": {
"type": "string"
},
+ "Suite_Name: Mandatory : SBMR-IB_compliance": {
+ "type": "string"
+ },
+ "Suite_Name: Mandatory : SBMR-OOB_compliance": {
+ "type": "string"
+ },
"Suite_Name: Recommended : SBSA_compliance": {
"type": "string"
},
@@ -1468,12 +1695,32 @@
"Suite_Name: Mandatory : FWTS_compliance",
"Suite_Name: Mandatory : OS_TEST_compliance",
"Suite_Name: Mandatory : SCT_compliance",
- "Suite_Name: Recommended : SBMR-IB_compliance",
- "Suite_Name: Recommended : SBMR-OOB_compliance",
"Suite_Name: Extension : BBSR-FWTS_compliance",
"Suite_Name: Extension : BBSR-SCT_compliance",
"Suite_Name: Extension : BBSR-TPM_compliance"
],
+ "oneOf": [
+ {
+ "required": [
+ "Suite_Name: Recommended : SBMR-IB_compliance",
+ "Suite_Name: Recommended : SBMR-OOB_compliance"
+ ],
+ "properties": {
+ "Suite_Name: Mandatory : SBMR-IB_compliance": false,
+ "Suite_Name: Mandatory : SBMR-OOB_compliance": false
+ }
+ },
+ {
+ "required": [
+ "Suite_Name: Mandatory : SBMR-IB_compliance",
+ "Suite_Name: Mandatory : SBMR-OOB_compliance"
+ ],
+ "properties": {
+ "Suite_Name: Recommended : SBMR-IB_compliance": false,
+ "Suite_Name: Recommended : SBMR-OOB_compliance": false
+ }
+ }
+ ],
"anyOf": [
{
"required": [
diff --git a/common/tools/suite_registry.json b/common/tools/suite_registry.json
new file mode 100644
index 00000000..5d876e1c
--- /dev/null
+++ b/common/tools/suite_registry.json
@@ -0,0 +1,513 @@
+{
+ "suites": [
+ {
+ "canonical": "BSA",
+ "aliases": ["BSA"],
+ "modes": ["SR", "DT"],
+ "requirement_key": "BSA",
+ "requirements": {"SR": "M", "DT": "R"},
+ "waiver_suite": "BSA",
+ "logs_to_json": "bsa/logs_to_json.py",
+ "json_to_html": "bsa/json_to_html.py",
+ "json_output": "bsa.json",
+ "schema": "acs-results-schema.json#/definitions/bsa_suite",
+ "detailed_html": "bsa_detailed.html",
+ "summary_html": "bsa_summary.html"
+ },
+ {
+ "canonical": "SBSA",
+ "aliases": ["SBSA"],
+ "modes": ["SR"],
+ "requirement_key": "SBSA",
+ "requirements": {"SR": "R"},
+ "waiver_suite": "SBSA",
+ "logs_to_json": "bsa/logs_to_json.py",
+ "json_to_html": "bsa/json_to_html.py",
+ "json_output": "sbsa.json",
+ "schema": "acs-results-schema.json#/definitions/bsa_suite",
+ "detailed_html": "sbsa_detailed.html",
+ "summary_html": "sbsa_summary.html"
+ },
+ {
+ "canonical": "FWTS",
+ "aliases": ["FWTS"],
+ "modes": ["SR", "DT"],
+ "requirement_key": "FWTS",
+ "requirements": {"SR": "M", "DT": "M"},
+ "waiver_suite": "FWTS",
+ "logs_to_json": "bbr/fwts/logs_to_json.py",
+ "json_to_html": "bbr/fwts/json_to_html.py",
+ "json_output": "fwts.json",
+ "schema": "acs-results-schema.json#/definitions/fwts_suite",
+ "detailed_html": "fwts_detailed.html",
+ "summary_html": "fwts_summary.html"
+ },
+ {
+ "canonical": "SCT",
+ "aliases": ["SCT"],
+ "modes": ["SR", "DT"],
+ "requirement_key": "SCT",
+ "requirements": {"SR": "M", "DT": "M"},
+ "waiver_suite": "SCT",
+ "logs_to_json": "bbr/sct/logs_to_json.py",
+ "supporting_logs_to_json": ["bbr/sct/logs_to_json_edk2.py"],
+ "json_to_html": "bbr/sct/json_to_html.py",
+ "json_output": "sct.json",
+ "schema": "acs-results-schema.json#/definitions/sct_suite",
+ "detailed_html": "sct_detailed.html",
+ "summary_html": "sct_summary.html"
+ },
+ {
+ "canonical": "BBSR-FWTS",
+ "aliases": ["BBSR-FWTS", "BBSRFWTS"],
+ "modes": ["SR", "DT"],
+ "requirement_key": "BBSR-FWTS",
+ "requirements": {"SR": "EM", "DT": "EM"},
+ "waiver_suite": "BBSR-FWTS",
+ "logs_to_json": "bbr/fwts/logs_to_json.py",
+ "json_to_html": "bbr/fwts/json_to_html.py",
+ "json_output": "bbsr_fwts.json",
+ "schema": "acs-results-schema.json#/definitions/bbsr_fwts_suite",
+ "detailed_html": "bbsr_fwts_detailed.html",
+ "summary_html": "bbsr_fwts_summary.html"
+ },
+ {
+ "canonical": "BBSR-SCT",
+ "aliases": ["BBSR-SCT", "BBSRSCT"],
+ "modes": ["SR", "DT"],
+ "requirement_key": "BBSR-SCT",
+ "requirements": {"SR": "EM", "DT": "EM"},
+ "waiver_suite": "BBSR-SCT",
+ "logs_to_json": "bbr/sct/logs_to_json.py",
+ "supporting_logs_to_json": ["bbr/sct/logs_to_json_edk2.py"],
+ "json_to_html": "bbr/sct/json_to_html.py",
+ "json_output": "bbsr_sct.json",
+ "schema": "acs-results-schema.json#/definitions/bbsr_sct_suite",
+ "detailed_html": "bbsr_sct_detailed.html",
+ "summary_html": "bbsr_sct_summary.html"
+ },
+ {
+ "canonical": "BBSR-TPM",
+ "aliases": ["BBSR-TPM", "BBSRTPM"],
+ "modes": ["SR", "DT"],
+ "requirement_key": "BBSR-TPM",
+ "requirements": {"SR": "EM", "DT": "EM"},
+ "waiver_suite": "BBSR-TPM",
+ "logs_to_json": "bbr/tpm/logs_to_json.py",
+ "json_to_html": "bbr/tpm/json_to_html.py",
+ "json_output": "bbsr_tpm.json",
+ "schema": "acs-results-schema.json#/definitions/tpm_suite",
+ "detailed_html": "bbsr_tpm_detailed.html",
+ "summary_html": "bbsr_tpm_summary.html"
+ },
+ {
+ "canonical": "PFDI",
+ "aliases": ["PFDI"],
+ "modes": ["DT"],
+ "requirement_key": "PFDI",
+ "requirements": {"DT": "CM"},
+ "waiver_suite": "PFDI",
+ "logs_to_json": "bsa/logs_to_json.py",
+ "json_to_html": "bsa/json_to_html.py",
+ "json_output": "pfdi.json",
+ "schema": "acs-results-schema.json#/definitions/pfdi_suite",
+ "detailed_html": "pfdi_detailed.html",
+ "summary_html": "pfdi_summary.html"
+ },
+ {
+ "canonical": "SCMI",
+ "aliases": ["SCMI"],
+ "modes": ["DT"],
+ "requirement_key": "SCMI",
+ "requirements": {"DT": "EM"},
+ "waiver_suite": "SCMI",
+ "logs_to_json": "scmi/logs_to_json.py",
+ "json_to_html": "scmi/json_to_html.py",
+ "json_output": "scmi.json",
+ "schema": "acs-results-schema.json#/definitions/scmi_suite",
+ "detailed_html": "scmi_detailed.html",
+ "summary_html": "scmi_summary.html"
+ },
+ {
+ "canonical": "SBMR",
+ "aliases": ["SBMR"],
+ "modes": ["SR"],
+ "requirement_key": "SBMR",
+ "requirements": {},
+ "waiver_suite": "SBMR",
+ "included_suites": ["SBMR-IB", "SBMR-OOB"]
+ },
+ {
+ "canonical": "SBMR-IB",
+ "aliases": ["SBMR-IB", "SBMRIB", "SBMR-IN-BAND", "SBMR-INBAND"],
+ "modes": ["SR"],
+ "requirement_key": "SBMR-IB",
+ "requirements": {"SR": "R"},
+ "waiver_suite": "SBMR",
+ "logs_to_json": "sbmr/logs_to_json.py",
+ "json_to_html": "sbmr/json_to_html.py",
+ "json_output": "sbmr_ib.json",
+ "schema": "acs-results-schema.json#/definitions/sbmr_suite",
+ "detailed_html": "sbmr_ib_detailed.html",
+ "summary_html": "sbmr_ib_summary.html"
+ },
+ {
+ "canonical": "SBMR-OOB",
+ "aliases": ["SBMR-OOB", "SBMROOB", "SBMR-OUT-OF-BAND", "SBMR-OUTOFBAND"],
+ "modes": ["SR"],
+ "requirement_key": "SBMR-OOB",
+ "requirements": {"SR": "R"},
+ "waiver_suite": "SBMR",
+ "logs_to_json": "sbmr/logs_to_json.py",
+ "json_to_html": "sbmr/json_to_html.py",
+ "json_output": "sbmr_oob.json",
+ "schema": "acs-results-schema.json#/definitions/sbmr_suite",
+ "detailed_html": "sbmr_oob_detailed.html",
+ "summary_html": "sbmr_oob_summary.html"
+ },
+ {
+ "canonical": "POST-SCRIPT",
+ "aliases": ["POST-SCRIPT", "POSTSCRIPT", "POST_SCRIPT"],
+ "modes": ["DT"],
+ "requirement_key": "POST_SCRIPT",
+ "requirements": {"DT": "R"},
+ "waiver_suite": "POST_SCRIPT",
+ "logs_to_json": "post_script/logs_to_json.py",
+ "json_to_html": "post_script/json_to_html.py",
+ "json_output": "post_script.json",
+ "schema": "acs-results-schema.json#/definitions/post_script_suite",
+ "detailed_html": "post_script_detailed.html",
+ "summary_html": "post_script_summary.html"
+ },
+ {
+ "canonical": "STANDALONE",
+ "aliases": ["STANDALONE", "STANDALONE-TEST", "STANDALONE-TESTS", "STANDALONE_TEST", "STANDALONE_TESTS"],
+ "modes": ["DT"],
+ "requirement_key": "STANDALONE",
+ "requirements": {},
+ "waiver_suite": "Standalone",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html",
+ "included_suites": [
+ "DT-KSELFTEST",
+ "DT-VALIDATE",
+ "ETHTOOL-TEST",
+ "READ-WRITE-CHECK-BLK-DEVICES",
+ "CAPSULE-UPDATE",
+ "PSCI",
+ "SMBIOS",
+ "NETWORK-BOOT",
+ "RUNTIME-DEV-MAP"
+ ]
+ },
+ {
+ "canonical": "DT-KSELFTEST",
+ "aliases": ["DT-KSELFTEST", "DTKSELFTEST", "DT_KSELFTEST"],
+ "modes": ["DT"],
+ "requirement_key": "DT_KSELFTEST",
+ "requirements": {"DT": "R"},
+ "waiver_suite": "Standalone",
+ "logs_to_json": "standalone_tests/logs_to_json.py",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "json_output": "dt_kselftest.json",
+ "schema": "acs-results-schema.json#/definitions/raw_standalone_suite",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html"
+ },
+ {
+ "canonical": "DT-VALIDATE",
+ "aliases": ["DT-VALIDATE", "DTVALIDATE", "DT_VALIDATE"],
+ "modes": ["DT"],
+ "requirement_key": "DT_VALIDATE",
+ "requirements": {"DT": "M"},
+ "waiver_suite": "Standalone",
+ "logs_to_json": "standalone_tests/logs_to_json.py",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "json_output": "dt_validate.json",
+ "schema": "acs-results-schema.json#/definitions/raw_standalone_suite",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html"
+ },
+ {
+ "canonical": "ETHTOOL-TEST",
+ "aliases": ["ETHTOOL-TEST", "ETHTOOLTEST", "ETHTOOL_TEST"],
+ "modes": ["DT"],
+ "requirement_key": "ETHTOOL_TEST",
+ "requirements": {"DT": "M"},
+ "waiver_suite": "Standalone",
+ "logs_to_json": "standalone_tests/logs_to_json.py",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "json_output": "ethtool_test.json",
+ "schema": "acs-results-schema.json#/definitions/raw_standalone_suite",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html"
+ },
+ {
+ "canonical": "READ-WRITE-CHECK-BLK-DEVICES",
+ "aliases": ["READ-WRITE-CHECK-BLK-DEVICES", "READWRITECHECKBLKDEVICES", "READ-WRITE-CHECK", "READ_WRITE_CHECK_BLK_DEVICES"],
+ "modes": ["DT"],
+ "requirement_key": "READ_WRITE_CHECK_BLK_DEVICES",
+ "requirements": {"DT": "M"},
+ "waiver_suite": "Standalone",
+ "logs_to_json": "standalone_tests/logs_to_json.py",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "json_output": "read_write_check_blk_devices.json",
+ "schema": "acs-results-schema.json#/definitions/raw_standalone_suite",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html"
+ },
+ {
+ "canonical": "CAPSULE-UPDATE",
+ "aliases": ["CAPSULE", "CAPSULE-UPDATE", "CAPSULEUPDATE", "CAPSULE_UPDATE"],
+ "modes": ["DT"],
+ "requirement_key": "Capsule Update",
+ "requirements": {"DT": "M"},
+ "waiver_suite": "Standalone",
+ "logs_to_json": "standalone_tests/logs_to_json.py",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "json_output": "capsule_update.json",
+ "schema": "acs-results-schema.json#/definitions/raw_standalone_suite",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html"
+ },
+ {
+ "canonical": "PSCI",
+ "aliases": ["PSCI", "PSCI-CHECK", "PSCICHECK"],
+ "modes": ["DT"],
+ "requirement_key": "PSCI",
+ "requirements": {"DT": "R"},
+ "waiver_suite": "Standalone",
+ "logs_to_json": "standalone_tests/logs_to_json.py",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "json_output": "psci.json",
+ "schema": "acs-results-schema.json#/definitions/raw_standalone_suite",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html"
+ },
+ {
+ "canonical": "SMBIOS",
+ "aliases": ["SMBIOS", "SMBIOS-CHECK", "SMBIOSCHECK"],
+ "modes": ["DT"],
+ "requirement_key": "SMBIOS",
+ "requirements": {"DT": "R"},
+ "waiver_suite": "Standalone",
+ "logs_to_json": "standalone_tests/logs_to_json.py",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "json_output": "smbios_check.json",
+ "schema": "acs-results-schema.json#/definitions/raw_standalone_suite",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html"
+ },
+ {
+ "canonical": "NETWORK-BOOT",
+ "aliases": ["NETWORK-BOOT", "NETWORKBOOT", "NETWORK_BOOT"],
+ "modes": ["DT"],
+ "requirement_key": "NETWORK_BOOT",
+ "requirements": {"DT": "R"},
+ "waiver_suite": "Standalone",
+ "logs_to_json": "standalone_tests/logs_to_json.py",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "json_output": "network_boot.json",
+ "schema": "acs-results-schema.json#/definitions/raw_standalone_suite",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html"
+ },
+ {
+ "canonical": "RUNTIME-DEV-MAP",
+ "aliases": ["RUNTIME-DEV-MAP", "RUNTIME-DEVICE-MAPPING", "RUNTIME-DEVICE-MAPPING-CHECK", "RUNTIMEDEVMAP", "RUNTIME_DEV_MAP"],
+ "modes": ["DT"],
+ "requirement_key": "RUNTIME_DEV_MAP",
+ "requirements": {"DT": "R"},
+ "waiver_suite": "Standalone",
+ "logs_to_json": "standalone_tests/logs_to_json.py",
+ "json_to_html": "standalone_tests/json_to_html.py",
+ "json_output": "runtime_dev_map.json",
+ "schema": "acs-results-schema.json#/definitions/raw_standalone_suite",
+ "detailed_html": "standalone_tests_detailed.html",
+ "summary_html": "standalone_tests_summary.html"
+ },
+ {
+ "canonical": "OS-TESTS",
+ "aliases": ["OS", "OS-TEST", "OS-TESTS", "OSTEST", "OSTESTS", "OS_TEST"],
+ "modes": ["SR", "DT"],
+ "requirement_key": "OS_TEST",
+ "requirements": {"SR": "M", "DT": "M"},
+ "waiver_suite": "os Tests",
+ "logs_to_json": "os_tests/logs_to_json.py",
+ "sr_logs_to_json": "os_tests/sr_logs_to_json.py",
+ "json_to_html": "os_tests/json_to_html.py",
+ "json_output": "os_test.json",
+ "json_output_patterns": ["ethtool_test_*.json"],
+ "schema": "acs-results-schema.json#/definitions/os_tests_suite",
+ "detailed_html": "os_tests_detailed.html",
+ "summary_html": "os_tests_summary.html"
+ }
+ ],
+ "standalone": {
+ "default_outputs": ["json", "html", "summary"],
+ "dependency_modules": {
+ "html": ["jinja2", "matplotlib"],
+ "schema": ["jsonschema"],
+ "pdf": ["weasyprint"]
+ },
+ "summary_inputs": {
+ "uefi_version": {"root": "results", "candidates": ["uefi_dump/uefi_version.log"]},
+ "dmidecode": {"root": "results", "candidates": ["linux_dump/dmidecode.txt", "linux_dump/dmidecode.log"]},
+ "ipmitool": {"root": "results", "candidates": ["linux_dump/ipmitool.txt", "linux_dump/ipmitool.log"]},
+ "psci": {"root": "results", "candidates": ["linux_tools/psci/psci_kernel.log"]}
+ },
+ "suite_execution": {
+ "BSA": {
+ "handler": "multi_log",
+ "minimum_inputs": 1,
+ "json_dependencies": ["chardet"],
+ "inputs": [
+ {"name": "uefi", "root": "results", "candidates": ["uefi/BsaResults.log"]},
+ {"name": "kernel", "root": "results", "candidates": ["linux_acs/bsa_acs_app/BsaResultsKernel.log", "linux/BsaResultsKernel.log"]}
+ ]
+ },
+ "SBSA": {
+ "handler": "multi_log",
+ "minimum_inputs": 1,
+ "json_dependencies": ["chardet"],
+ "inputs": [
+ {"name": "uefi", "root": "results", "candidates": ["uefi/SbsaResults.log"]},
+ {"name": "kernel", "root": "results", "candidates": ["linux/SbsaResultsKernel.log"]}
+ ]
+ },
+ "FWTS": {
+ "handler": "single_log",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["fwts/FWTSResults.log"], "required": true}
+ ]
+ },
+ "SCT": {
+ "handler": "sct",
+ "json_dependencies": ["chardet"],
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["sct_results/Overall/Summary.log"], "required": true},
+ {"name": "edk2", "root": "results", "candidates": ["edk2-test-parser/edk2-test-parser.log"], "supporting": true, "output": "edk2_test_parser.json"}
+ ]
+ },
+ "BBSR-FWTS": {
+ "handler": "single_log",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["bbsr/fwts/FWTSResults.log"], "required": true}
+ ]
+ },
+ "BBSR-SCT": {
+ "handler": "sct",
+ "json_dependencies": ["chardet"],
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["bbsr/sct_results/Overall/Summary.log"], "required": true},
+ {"name": "edk2", "root": "results", "candidates": ["edk2-test-parser/edk2-test-parser-bbsr.log"], "supporting": true, "output": "edk2_test_parser-bbsr.json"}
+ ]
+ },
+ "BBSR-TPM": {
+ "handler": "single_log",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["bbsr/tpm2/verify_tpm_measurements.log"], "required": true}
+ ]
+ },
+ "PFDI": {
+ "handler": "single_log",
+ "json_dependencies": ["chardet"],
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["uefi/pfdiresults.log"], "required": true}
+ ]
+ },
+ "SCMI": {
+ "handler": "multi_log",
+ "minimum_inputs": 1,
+ "json_dependencies": ["chardet"],
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["linux_acs/scmi_acs_app/arm_scmi_test_log.txt"]}
+ ]
+ },
+ "SBMR-IB": {
+ "handler": "sbmr",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["sbmr/sbmr_in_band_logs/output.xml"], "required": true},
+ {"name": "report", "root": "results", "candidates": ["sbmr/sbmr_in_band_logs/report.html"], "supporting": true}
+ ]
+ },
+ "SBMR-OOB": {
+ "handler": "sbmr",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["sbmr/sbmr_out_of_band_logs/output.xml"], "required": true},
+ {"name": "report", "root": "results", "candidates": ["sbmr/sbmr_out_of_band_logs/report.html"], "supporting": true}
+ ]
+ },
+ "POST-SCRIPT": {
+ "handler": "single_log",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["post-script/post-script.log"], "required": true}
+ ]
+ },
+ "DT-KSELFTEST": {
+ "handler": "standalone_single",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["linux_tools/dt_kselftest.log"], "required": true}
+ ]
+ },
+ "DT-VALIDATE": {
+ "handler": "standalone_single",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["linux_tools/dt-validate-parser.log"], "required": true}
+ ]
+ },
+ "ETHTOOL-TEST": {
+ "handler": "standalone_single",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["linux_tools/ethtool-test.log"], "required": true}
+ ]
+ },
+ "READ-WRITE-CHECK-BLK-DEVICES": {
+ "handler": "standalone_single",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["linux_tools/read_write_check_blk_devices.log"], "required": true}
+ ]
+ },
+ "CAPSULE-UPDATE": {
+ "handler": "capsule",
+ "inputs": [
+ {"name": "update", "root": "firmware", "candidates": ["capsule-update.log"]},
+ {"name": "on_disk", "root": "firmware", "candidates": ["capsule-on-disk.log"]},
+ {"name": "results", "root": "firmware", "candidates": ["capsule_test_results.log"], "required": true}
+ ]
+ },
+ "PSCI": {
+ "handler": "psci",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["linux_tools/psci/psci_kernel.log"], "required": true}
+ ]
+ },
+ "SMBIOS": {
+ "handler": "standalone_single",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["sct_results/Overall/Summary.log"], "required": true}
+ ]
+ },
+ "NETWORK-BOOT": {
+ "handler": "standalone_single",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["network_boot/network_boot_results.log"], "required": true}
+ ]
+ },
+ "RUNTIME-DEV-MAP": {
+ "handler": "standalone_single",
+ "inputs": [
+ {"name": "log", "root": "results", "candidates": ["linux_tools/runtime_device_mapping_conflict_test.log"], "required": true}
+ ]
+ },
+ "OS-TESTS": {
+ "handler": "os_tests",
+ "inputs": [
+ {"name": "os_logs", "root": "os_logs", "candidates": ["."], "kind": "directory", "required": true},
+ {"name": "post_script", "root": "results", "candidates": ["post-script/post-script.log"], "supporting": true}
+ ]
+ }
+ }
+ }
+}
diff --git a/common/tools/suite_registry.py b/common/tools/suite_registry.py
new file mode 100755
index 00000000..648c97d9
--- /dev/null
+++ b/common/tools/suite_registry.py
@@ -0,0 +1,201 @@
+#!/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.
+
+"""Shared suite registry helpers for log parser orchestration and merging."""
+
+import argparse
+import json
+import sys
+from pathlib import Path
+
+
+REGISTRY_PATH = str(Path(__file__).resolve().with_name("suite_registry.json"))
+
+
+def _token(value):
+ return "-".join(
+ part
+ for part in (value or "").strip().upper().replace("_", "-").replace(" ", "-").split("-")
+ if part
+ )
+
+
+def load_registry(path=REGISTRY_PATH):
+ with open(path, "r", encoding="utf-8") as registry_file:
+ data = json.load(registry_file)
+ suites = data.get("suites", [])
+ if not isinstance(suites, list):
+ raise ValueError("suite_registry.json must contain a 'suites' list")
+ return suites
+
+
+def _suite_by_canonical(registry):
+ return {suite["canonical"]: suite for suite in registry}
+
+
+def _alias_map(registry):
+ aliases = {}
+ for suite in registry:
+ canonical = suite.get("canonical")
+ if not canonical:
+ continue
+ aliases[_token(canonical)] = canonical
+ for alias in suite.get("aliases", []):
+ aliases[_token(alias)] = canonical
+ return aliases
+
+
+def normalize_suite_name(name, registry=None):
+ registry = registry or load_registry()
+ return _alias_map(registry).get(_token(name), "")
+
+
+def list_suite_names(registry=None):
+ registry = registry or load_registry()
+ return [suite["canonical"] for suite in registry]
+
+
+def get_suite(canonical, registry=None):
+ registry = registry or load_registry()
+ return _suite_by_canonical(registry).get(canonical)
+
+
+def suite_modes(canonical, registry=None):
+ suite = get_suite(canonical, registry)
+ return suite.get("modes", []) if suite else []
+
+
+def suite_supports_mode(canonical, mode, registry=None):
+ return mode.upper() in suite_modes(canonical, registry)
+
+
+def suite_includes(selected_canonical, wanted_canonical, registry=None):
+ registry = registry or load_registry()
+ selected = get_suite(selected_canonical, registry)
+ if not selected:
+ return False
+ return wanted_canonical in selected.get("included_suites", [])
+
+
+def expand_selected_suites(selected_suites, registry=None):
+ registry = registry or load_registry()
+ expanded = []
+ seen = set()
+ for selected in selected_suites or []:
+ canonical = normalize_suite_name(selected, registry)
+ if not canonical:
+ continue
+ suite = get_suite(canonical, registry)
+ values = suite.get("included_suites", []) if suite else []
+ if not values:
+ values = [canonical]
+ for value in values:
+ if value not in seen:
+ seen.add(value)
+ expanded.append(value)
+ return expanded
+
+
+def requirement_table(mode, registry=None):
+ registry = registry or load_registry()
+ table = []
+ mode = mode.upper()
+ for suite in registry:
+ requirements = suite.get("requirements", {})
+ if mode in requirements:
+ table.append((suite["requirement_key"], requirements[mode]))
+ return table
+
+
+def selected_requirement_keys(selected_suites, registry=None):
+ registry = registry or load_registry()
+ selected = []
+ seen = set()
+ for canonical in expand_selected_suites(selected_suites, registry):
+ suite = get_suite(canonical, registry)
+ requirement_key = suite.get("requirement_key") if suite else None
+ if requirement_key and requirement_key not in seen:
+ seen.add(requirement_key)
+ selected.append(requirement_key)
+ return selected
+
+
+def _main():
+ parser = argparse.ArgumentParser(description="Query the SystemReady log parser suite registry")
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ subparsers.add_parser("list", help="List canonical suite names")
+
+ normalize_parser = subparsers.add_parser("normalize", help="Normalize a suite name or alias")
+ normalize_parser.add_argument("suite")
+
+ includes_parser = subparsers.add_parser("includes", help="Check whether one suite group includes another suite")
+ includes_parser.add_argument("selected")
+ includes_parser.add_argument("wanted")
+
+ supports_parser = subparsers.add_parser("supports-mode", help="Check whether a suite supports a mode")
+ supports_parser.add_argument("suite")
+ supports_parser.add_argument("mode", choices=["SR", "DT", "sr", "dt"])
+
+ modes_parser = subparsers.add_parser("modes", help="Print suite modes")
+ modes_parser.add_argument("suite")
+
+ selected_keys_parser = subparsers.add_parser("selected-requirement-keys", help="Print selected requirement keys")
+ selected_keys_parser.add_argument("suites", nargs="*")
+
+ args = parser.parse_args()
+ registry = load_registry()
+
+ if args.command == "list":
+ print("\n".join(list_suite_names(registry)))
+ return 0
+
+ if args.command == "normalize":
+ normalized = normalize_suite_name(args.suite, registry)
+ if normalized:
+ print(normalized)
+ return 0
+ return 1
+
+ if args.command == "includes":
+ selected = normalize_suite_name(args.selected, registry)
+ wanted = normalize_suite_name(args.wanted, registry)
+ if selected and wanted and suite_includes(selected, wanted, registry):
+ return 0
+ return 1
+
+ if args.command == "supports-mode":
+ suite = normalize_suite_name(args.suite, registry)
+ if suite and suite_supports_mode(suite, args.mode, registry):
+ return 0
+ return 1
+
+ if args.command == "modes":
+ suite = normalize_suite_name(args.suite, registry)
+ if not suite:
+ return 1
+ print(" ".join(suite_modes(suite, registry)))
+ return 0
+
+ if args.command == "selected-requirement-keys":
+ print("\n".join(selected_requirement_keys(args.suites, registry)))
+ return 0
+
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(_main())
diff --git a/common/tools/validate.py b/common/tools/validate.py
new file mode 100755
index 00000000..00c278fa
--- /dev/null
+++ b/common/tools/validate.py
@@ -0,0 +1,779 @@
+#!/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.
+
+"""Validate merged ACS results or individual suite JSON files."""
+
+import argparse
+import fnmatch
+import json
+import re
+import sys
+import warnings
+from pathlib import Path
+
+try:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ from jsonschema import Draft202012Validator, RefResolver
+ from jsonschema.exceptions import best_match
+except ImportError:
+ print("ERROR: Missing required Python package: jsonschema", file=sys.stderr)
+ print("Install it with: python3 -m pip install jsonschema", file=sys.stderr)
+ sys.exit(2)
+
+SCRIPT_DIR = Path(__file__).resolve().parent
+
+from suite_registry import expand_selected_suites, load_registry
+
+
+RED = "\033[0;31m"
+GREEN = "\033[0;32m"
+YELLOW = "\033[1;33m"
+BLUE = "\033[0;34m"
+NC = "\033[0m"
+
+DEFAULT_REGISTRY = SCRIPT_DIR / "suite_registry.json"
+DEFAULT_SCHEMA = SCRIPT_DIR / "acs-results-schema.json"
+
+
+def _suite_by_canonical(registry):
+ return {suite.get("canonical"): suite for suite in registry if suite.get("canonical")}
+
+
+def _schema_location(registry_path, suite):
+ schema_ref = suite.get("schema")
+ if not schema_ref:
+ return None, None, None
+
+ if "#" in schema_ref:
+ schema, fragment = schema_ref.split("#", 1)
+ fragment = f"#{fragment}"
+ else:
+ schema = schema_ref
+ fragment = ""
+
+ schema_path = Path(schema)
+ if schema_path.is_absolute():
+ return schema_path, fragment, schema_ref
+ return Path(registry_path).resolve().parent / schema_path, fragment, schema_ref
+
+
+def _build_file_schema_index(registry, registry_path):
+ exact = {}
+ patterns = []
+
+ for suite in registry:
+ schema_path, schema_fragment, schema_ref = _schema_location(registry_path, suite)
+ if not schema_path:
+ continue
+
+ suite_info = {
+ "canonical": suite.get("canonical", "UNKNOWN"),
+ "schema": schema_path,
+ "schema_fragment": schema_fragment,
+ "schema_ref": schema_ref,
+ }
+
+ json_output = suite.get("json_output")
+ if json_output:
+ exact[json_output] = suite_info
+
+ for pattern in suite.get("json_output_patterns", []):
+ patterns.append((pattern, suite_info))
+
+ return exact, patterns
+
+
+def _find_schema_for_file(json_file, exact, patterns):
+ basename = Path(json_file).name
+ if basename in exact:
+ return exact[basename]
+
+ for pattern, suite_info in patterns:
+ if fnmatch.fnmatch(basename, pattern):
+ return suite_info
+
+ return None
+
+
+def _discover_selected_files(selected_suites, json_dir, registry, registry_path):
+ suites_by_name = _suite_by_canonical(registry)
+ discovered = []
+ missing = []
+ seen = set()
+
+ for canonical in expand_selected_suites(selected_suites, registry):
+ suite = suites_by_name.get(canonical)
+ if not suite or not suite.get("schema"):
+ continue
+
+ candidates = []
+ json_output = suite.get("json_output")
+ if json_output:
+ candidates.append(Path(json_dir) / json_output)
+
+ for pattern in suite.get("json_output_patterns", []):
+ candidates.extend(sorted(Path(json_dir).glob(pattern)))
+
+ existing = [candidate for candidate in candidates if candidate.is_file()]
+ if not existing:
+ expected = [str(candidate) for candidate in candidates] or [""]
+ missing.append((canonical, expected))
+ continue
+
+ for path in existing:
+ resolved = str(path.resolve())
+ if resolved not in seen:
+ seen.add(resolved)
+ discovered.append(path)
+
+ return discovered, missing
+
+
+def _load_json(path):
+ with open(path, "r", encoding="utf-8") as handle:
+ return json.load(handle)
+
+
+def _load_schema(schema_path, schema_fragment):
+ with open(schema_path, "r", encoding="utf-8") as handle:
+ schema = json.load(handle)
+ Draft202012Validator.check_schema(schema)
+
+ schema_uri = schema_path.resolve().as_uri()
+ if schema_fragment:
+ # Keep the selected fragment and its local references in one resource.
+ # New jsonschema releases otherwise resolve nested #/definitions refs
+ # against the external-ref wrapper instead of the complete schema.
+ validator_schema = {
+ "$schema": schema.get(
+ "$schema", "https://json-schema.org/draft/2020-12/schema"
+ ),
+ "$ref": schema_fragment,
+ }
+ for definitions_key in ("definitions", "$defs"):
+ if definitions_key in schema:
+ validator_schema[definitions_key] = schema[definitions_key]
+ Draft202012Validator.check_schema(validator_schema)
+ return schema, Draft202012Validator(validator_schema)
+
+ base_uri = schema_path.resolve().parent.as_uri() + "/"
+ resolver = RefResolver(
+ base_uri=base_uri,
+ referrer=schema,
+ store={schema_uri: schema},
+ )
+ return schema, Draft202012Validator(schema, resolver=resolver)
+
+
+def _format_path(path):
+ parts = []
+ for item in path:
+ if isinstance(item, int):
+ parts.append(f"[{item}]")
+ elif parts:
+ parts.append(f".{item}")
+ else:
+ parts.append(str(item))
+ return "".join(parts) if parts else ""
+
+
+def _suite_from_path(path, default_suite=""):
+ if path:
+ first = path[0]
+ if isinstance(first, str) and first.startswith("Suite_Name:"):
+ return first
+ return default_suite
+
+
+def _collect_key_issues(error):
+ missing = set()
+ unexpected = set()
+
+ def handle(candidate):
+ if candidate.validator == "required" and isinstance(candidate.instance, dict):
+ required = candidate.validator_value
+ if isinstance(required, list):
+ missing.update(key for key in required if key not in candidate.instance)
+ elif candidate.validator == "additionalProperties" and isinstance(candidate.message, str):
+ unexpected.update(re.findall(r"'([^']+)'", candidate.message))
+
+ if error.context:
+ for suberror in error.context:
+ handle(suberror)
+ else:
+ handle(error)
+
+ return missing, unexpected
+
+
+def _best_suberror(error):
+ if error.validator not in ("anyOf", "oneOf") or not error.context:
+ return None
+
+ non_additional = [
+ suberror for suberror in error.context if suberror.validator != "additionalProperties"
+ ]
+ candidate = best_match(non_additional) if non_additional else None
+ if candidate is not None and candidate.validator == "type":
+ for suberror in error.context:
+ if suberror.validator == "additionalProperties":
+ return suberror
+ return candidate if candidate is not None else best_match(error.context)
+
+
+def _error_tag(error, missing, unexpected):
+ if missing:
+ return "MISSING_KEY"
+ if unexpected:
+ return "UNEXPECTED_KEY"
+ if error.validator == "not":
+ return "DISALLOWED_VALUE"
+ if error.validator == "type":
+ return "TYPE_MISMATCH"
+ if error.validator == "enum":
+ return "ENUM"
+ if error.validator:
+ return str(error.validator).upper()
+ return "VALIDATION"
+
+
+def _shorten_message(error):
+ message = error.message
+ if isinstance(error.instance, dict) and message.startswith("{") and " is " in message:
+ return "object" + message[message.find(" is "):]
+ if isinstance(error.instance, list) and message.startswith("[") and " is " in message:
+ return "array" + message[message.find(" is "):]
+ if error.validator == "not" and isinstance(error.schema, dict):
+ disallowed = error.schema.get("not")
+ if isinstance(disallowed, dict) and disallowed.get("enum"):
+ return f"value '{error.instance}' is not allowed"
+ return message
+
+
+def _subtest_result_unexpected_keys(instance, schema):
+ try:
+ allowed = set(schema["definitions"]["sub_test_result_object"]["properties"])
+ except (KeyError, TypeError):
+ return set()
+
+ if not isinstance(instance, dict) or not isinstance(instance.get("subtests"), list):
+ return set()
+
+ unexpected = set()
+ for subtest in instance["subtests"]:
+ if not isinstance(subtest, dict):
+ continue
+ result = subtest.get("sub_test_result")
+ if isinstance(result, dict):
+ unexpected.update(set(result) - allowed)
+ return unexpected
+
+
+def _is_prefix_path(prefix, full):
+ return len(prefix) <= len(full) and all(left == right for left, right in zip(prefix, full))
+
+
+def _filter_cascading_errors(errors, default_suite):
+ by_suite = {}
+ for error in errors:
+ path = list(error.absolute_path)
+ suite = _suite_from_path(path, default_suite)
+ by_suite.setdefault(suite, []).append(error)
+
+ filtered = []
+ for suite_errors in by_suite.values():
+ specific_paths = [
+ list(error.absolute_path)
+ for error in suite_errors
+ if error.validator != "unevaluatedProperties"
+ ]
+ for error in suite_errors:
+ if error.validator == "unevaluatedProperties" and specific_paths:
+ path = list(error.absolute_path)
+ if any(_is_prefix_path(path, specific) for specific in specific_paths):
+ continue
+ filtered.append(error)
+ return sorted(filtered, key=lambda item: list(item.absolute_path))
+
+
+def _report_path(error, default_suite, path_prefix=None):
+ path = list(error.absolute_path)
+ formatted = _format_path(path)
+ if default_suite != "" and _suite_from_path(path) == "":
+ prefix = path_prefix or default_suite
+ return prefix if formatted == "" else f"{prefix}.{formatted}"
+ return formatted
+
+
+def _validation_report(
+ instance,
+ schema,
+ errors,
+ default_suite="",
+ path_prefix=None,
+ max_paths=5,
+):
+ errors = _filter_cascading_errors(errors, default_suite)
+ grouped = {}
+
+ for error in errors:
+ suite = _suite_from_path(list(error.absolute_path), default_suite)
+ suberror = _best_suberror(error)
+ base_error = suberror if suberror is not None else error
+ message = _shorten_message(base_error)
+ missing, unexpected = _collect_key_issues(base_error)
+
+ if base_error.validator == "unevaluatedProperties" and not missing and not unexpected:
+ nested_unexpected = _subtest_result_unexpected_keys(error.instance, schema)
+ if nested_unexpected:
+ unexpected = nested_unexpected
+ names = ", ".join(f"'{key}' was unexpected" for key in sorted(unexpected))
+ message = f"Additional properties are not allowed ({names})"
+
+ details = []
+ if missing:
+ details.append(f"{RED}missing{NC}: " + ", ".join(sorted(missing)))
+ if unexpected:
+ details.append(f"{BLUE}unexpected{NC}: " + ", ".join(sorted(unexpected)))
+ if details:
+ message = f"{message} ({'; '.join(details)})"
+
+ tag = _error_tag(base_error, missing, unexpected)
+ message = f"{YELLOW}{tag}{NC}: {message}"
+ grouped.setdefault((suite, message), []).append(error)
+
+ if default_suite == "" and isinstance(instance, dict):
+ suites = sorted(
+ key for key in instance if isinstance(key, str) and key.startswith("Suite_Name:")
+ )
+ elif default_suite != "":
+ suites = [default_suite]
+ else:
+ suites = []
+
+ error_suites = sorted({suite for suite, _ in grouped if suite != ""})
+ suites.extend(suite for suite in error_suites if suite not in suites)
+
+ lines = []
+ counts = {suite: 0 for suite in suites}
+ for suite in suites:
+ suite_groups = [
+ (message, group_errors)
+ for (group_suite, message), group_errors in grouped.items()
+ if group_suite == suite
+ ]
+ if not suite_groups:
+ lines.append(f"{GREEN}*suite={suite} no errors{NC}")
+ continue
+
+ for message, group_errors in suite_groups:
+ counts[suite] += len(group_errors)
+ paths = [
+ _report_path(error, default_suite, path_prefix) for error in group_errors
+ ]
+ lines.append(f"{RED}*suite={suite} issue={message} count={len(paths)}{NC}")
+ lines.extend(f"{YELLOW} *at={path}{NC}" for path in paths[:max_paths])
+ if len(paths) > max_paths:
+ lines.append(f"{YELLOW} *... and {len(paths) - max_paths} more{NC}")
+ lines.append("")
+
+ root_groups = [
+ (message, group_errors)
+ for (suite, message), group_errors in grouped.items()
+ if suite == ""
+ ]
+ if root_groups:
+ counts[""] = 0
+ for message, group_errors in root_groups:
+ counts[""] += len(group_errors)
+ paths = [
+ _report_path(error, default_suite, path_prefix) for error in group_errors
+ ]
+ lines.append(f"{RED}*suite= issue={message} count={len(paths)}{NC}")
+ lines.extend(f"{YELLOW} *at={path}{NC}" for path in paths[:max_paths])
+ if len(paths) > max_paths:
+ lines.append(f"{YELLOW} *... and {len(paths) - max_paths} more{NC}")
+ lines.append("")
+
+ while lines and not lines[-1]:
+ lines.pop()
+ return lines, counts
+
+
+def _count_report_lines(counts):
+ lines = [f"{BLUE}--- Error Counts by Suite ---{NC}"]
+ for suite, count in counts.items():
+ color = GREEN if count == 0 else RED
+ lines.append(f"{color}{suite}: {count}{NC}")
+ return lines
+
+
+def _fatal_report(suite, tag, message, path):
+ lines = [
+ f"{RED}*suite={suite} issue={YELLOW}{tag}{NC}: {message} count=1{NC}",
+ f"{YELLOW} *at={path}{NC}",
+ ]
+ return lines, {suite: 1}
+
+
+def _validate_one(json_file, suite_info):
+ json_path = Path(json_file)
+ result = {
+ "canonical": suite_info["canonical"],
+ "json_path": json_path,
+ "schema_ref": suite_info["schema_ref"],
+ }
+ schema_path = suite_info["schema"]
+ if not schema_path.is_file():
+ result["fatal"] = ("SCHEMA_FILE", f"schema not found: {suite_info['schema_ref']}")
+ return result
+
+ try:
+ result["data"] = _load_json(json_path)
+ except Exception as exc:
+ result["fatal"] = ("JSON_FILE", f"failed to read JSON: {exc}")
+ return result
+
+ try:
+ result["schema"], validator = _load_schema(
+ schema_path, suite_info["schema_fragment"]
+ )
+ except Exception as exc:
+ result["fatal"] = ("SCHEMA_FILE", f"failed to load schema: {exc}")
+ return result
+
+ result["errors"] = sorted(
+ validator.iter_errors(result["data"]),
+ key=lambda item: list(item.absolute_path),
+ )
+ return result
+
+
+def _print_heading(title):
+ print(f"{BLUE}====================================={NC}")
+ print(f"{BLUE}{title}{NC}")
+ print(f"{BLUE}====================================={NC}\n")
+
+
+def _run_merged_validation(json_file, schema_file, max_paths):
+ json_path = Path(json_file)
+ schema_path = Path(schema_file)
+ if not json_path.is_file():
+ print(f"{RED}Error: File not found: {json_path}{NC}")
+ return 1
+ if not schema_path.is_file():
+ print(f"{RED}Error: Schema file not found: {schema_path}{NC}")
+ return 1
+
+ _print_heading("JSON Schema Validation")
+
+ try:
+ instance = _load_json(json_path)
+ except Exception as exc:
+ lines, counts = _fatal_report(
+ "", "JSON_FILE", f"failed to read JSON: {exc}", ""
+ )
+ print(f"{RED}✗ Schema validation FAILED{NC}\n")
+ print(f"{RED}Errors:{NC}")
+ print("\n".join([*lines, "", *_count_report_lines(counts)]))
+ print(f"\n{BLUE}File: {json_path}{NC}")
+ print(f"{BLUE}Schema: {schema_path}{NC}\n")
+ return 1
+
+ try:
+ schema, validator = _load_schema(schema_path, "")
+ except Exception as exc:
+ lines, counts = _fatal_report(
+ "", "SCHEMA_FILE", f"failed to load schema: {exc}", ""
+ )
+ print(f"{RED}✗ Schema validation FAILED{NC}\n")
+ print(f"{RED}Errors:{NC}")
+ print("\n".join([*lines, "", *_count_report_lines(counts)]))
+ print(f"\n{BLUE}File: {json_path}{NC}")
+ print(f"{BLUE}Schema: {schema_path}{NC}\n")
+ return 1
+
+ errors = sorted(validator.iter_errors(instance), key=lambda item: list(item.absolute_path))
+ if not errors:
+ print(f"{GREEN}✓ Schema validation PASSED{NC}\n")
+ print(f"{BLUE}File: {json_path}{NC}")
+ print(f"{BLUE}Schema: {schema_path}{NC}\n")
+ return 0
+
+ lines, counts = _validation_report(instance, schema, errors, max_paths=max_paths)
+ print(f"{RED}✗ Schema validation FAILED{NC}\n")
+ print(f"{RED}Errors:{NC}")
+ print("\n".join([*lines, "", *_count_report_lines(counts)]))
+ print(f"\n{BLUE}File: {json_path}{NC}")
+ print(f"{BLUE}Schema: {schema_path}{NC}\n")
+ return 1
+
+
+def _split_selected_suites(value):
+ suites = []
+ for chunk in value or []:
+ for item in chunk.split(","):
+ item = item.strip()
+ if item:
+ suites.append(item)
+ return suites
+
+
+def _run_raw_validation(args):
+ max_paths = max(args.max_paths, 1)
+ registry_path = Path(args.registry)
+ try:
+ registry = load_registry(str(registry_path))
+ except Exception as exc:
+ print(f"{RED}ERROR:{NC} failed to load registry '{registry_path}': {exc}")
+ return 2
+ exact, patterns = _build_file_schema_index(registry, registry_path)
+
+ json_files = [Path(path) for path in args.json_files]
+ missing = []
+ selected_suites = _split_selected_suites(args.selected_suites)
+
+ if not json_files and selected_suites:
+ if not args.json_dir:
+ print(
+ f"{RED}ERROR:{NC} --json-dir is required when --selected-suites "
+ "is used without JSON files."
+ )
+ return 2
+ json_files, missing = _discover_selected_files(
+ selected_suites, args.json_dir, registry, registry_path
+ )
+ elif not json_files:
+ print(
+ f"{RED}ERROR:{NC} raw validation requires JSON files, or "
+ "--json-dir with --selected-suites."
+ )
+ return 2
+
+ results = []
+ skipped_paths = []
+ seen = set()
+ for json_file in json_files:
+ resolved = str(json_file.resolve())
+ if resolved in seen:
+ continue
+ seen.add(resolved)
+
+ suite_info = _find_schema_for_file(json_file, exact, patterns)
+ if not suite_info:
+ skipped_paths.append(json_file)
+ continue
+ results.append(_validate_one(json_file, suite_info))
+
+ if not results and not missing:
+ _print_heading("Suite JSON Schema Validation")
+ print(f"{RED}✗ Schema validation NOT RUN{NC}\n")
+ print(f"{RED}ERROR:{NC} no files matched a registered raw suite schema.")
+ if skipped_paths:
+ print(f"\n{BLUE}--- Skipped Files (no registered suite schema) ---{NC}")
+ for skipped_path in skipped_paths:
+ print(f"{YELLOW}{skipped_path}{NC}")
+ print(f"\n{BLUE}Registry: {registry_path}{NC}")
+ print(f"Schema validation result: {RED}NOT RUN{NC} (0 validated)")
+ return 2
+
+ detail_lines = []
+ counts = {}
+ failed = len(missing)
+ passed = 0
+ canonical_counts = {}
+ for result in results:
+ canonical = result["canonical"]
+ canonical_counts[canonical] = canonical_counts.get(canonical, 0) + 1
+
+ for canonical, expected_paths in missing:
+ suite = f"Suite_Name: {canonical}"
+ detail_lines.append(
+ f"{RED}*suite={suite} issue={YELLOW}MISSING_JSON{NC}: "
+ f"generated JSON not found count=1{NC}"
+ )
+ detail_lines.extend(
+ f"{YELLOW} *at={expected_path}{NC}" for expected_path in expected_paths[:max_paths]
+ )
+ if len(expected_paths) > max_paths:
+ detail_lines.append(
+ f"{YELLOW} *... and {len(expected_paths) - max_paths} more expected paths{NC}"
+ )
+ detail_lines.append("")
+ counts[suite] = counts.get(suite, 0) + 1
+
+ for result in results:
+ path_prefix = f"Suite_Name: {result['canonical']}"
+ suite = path_prefix
+ if canonical_counts[result["canonical"]] > 1:
+ suite += f" [{result['json_path'].name}]"
+ if "fatal" in result:
+ tag, message = result["fatal"]
+ error_path = (
+ str(result["json_path"])
+ if tag == "JSON_FILE"
+ else result["schema_ref"]
+ )
+ lines, result_counts = _fatal_report(suite, tag, message, error_path)
+ failed += 1
+ else:
+ lines, result_counts = _validation_report(
+ result["data"],
+ result["schema"],
+ result["errors"],
+ default_suite=suite,
+ path_prefix=path_prefix,
+ max_paths=max_paths,
+ )
+ if result["errors"]:
+ failed += 1
+ else:
+ passed += 1
+ detail_lines.extend([*lines, ""])
+ for name, count in result_counts.items():
+ counts[name] = counts.get(name, 0) + count
+
+ while detail_lines and not detail_lines[-1]:
+ detail_lines.pop()
+
+ _print_heading("Suite JSON Schema Validation")
+
+ if failed:
+ print(f"{RED}✗ Schema validation FAILED{NC}\n")
+ print(f"{RED}Errors:{NC}")
+ if detail_lines:
+ print("\n".join(detail_lines))
+ print()
+ print("\n".join(_count_report_lines(counts)))
+ else:
+ print(f"{GREEN}✓ Schema validation PASSED{NC}")
+ if not results:
+ print(f"{YELLOW}SKIP{NC} no suite JSON files with registered schemas were found")
+
+ if results:
+ print(f"\n{BLUE}--- Files Checked ---{NC}")
+ for result in results:
+ print(f"{BLUE}{result['canonical']}: {result['json_path']}{NC}")
+
+ if skipped_paths:
+ print(f"\n{BLUE}--- Skipped Files (no registered suite schema) ---{NC}")
+ for skipped_path in skipped_paths:
+ print(f"{YELLOW}{skipped_path}{NC}")
+
+ print(f"\n{BLUE}Registry: {registry_path}{NC}")
+ skipped = len(skipped_paths)
+ if failed:
+ print(
+ f"Schema validation result: {RED}FAIL{NC} "
+ f"({failed} failed, {passed} passed, {skipped} skipped)"
+ )
+ return 1
+
+ print(
+ f"Schema validation result: {GREEN}PASS{NC} "
+ f"({len(results)} validated, {skipped} skipped)"
+ )
+ return 0
+
+
+def _build_parser():
+ parser = argparse.ArgumentParser(
+ description="Validate SystemReady merged results or individual suite JSON files.",
+ epilog=(
+ "examples:\n"
+ " validate.py merged /path/to/merged_results.json\n"
+ " validate.py raw /path/to/bsa.json /path/to/fwts.json\n"
+ " validate.py raw --json-dir /path/to/acs_jsons "
+ "--selected-suites BSA,FWTS"
+ ),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ merged_parser = subparsers.add_parser(
+ "merged",
+ help="Validate one complete merged_results.json file",
+ description="Validate one complete merged_results.json file.",
+ )
+ merged_parser.add_argument("json_file", help="Path to merged_results.json")
+ merged_parser.add_argument(
+ "--schema",
+ default=str(DEFAULT_SCHEMA),
+ help=f"Merged schema path (default: {DEFAULT_SCHEMA})",
+ )
+ merged_parser.add_argument(
+ "--max-paths",
+ "--max-errors",
+ dest="max_paths",
+ type=int,
+ default=5,
+ help="Maximum example paths per grouped issue (default: 5)",
+ )
+
+ raw_parser = subparsers.add_parser(
+ "raw",
+ help="Validate one or more individual suite JSON files",
+ description=(
+ "Validate individual suite JSON files using filename-to-schema "
+ "mappings from the suite registry."
+ ),
+ )
+ raw_parser.add_argument("json_files", nargs="*", help="Suite JSON files to validate")
+ raw_parser.add_argument(
+ "--registry",
+ default=str(DEFAULT_REGISTRY),
+ help=f"Suite registry path (default: {DEFAULT_REGISTRY})",
+ )
+ raw_parser.add_argument(
+ "--json-dir",
+ help="Directory containing generated suite JSON files",
+ )
+ raw_parser.add_argument(
+ "--selected-suites",
+ action="append",
+ default=[],
+ metavar="NAMES",
+ help="Suite name or comma-separated names to discover in --json-dir",
+ )
+ raw_parser.add_argument(
+ "--max-paths",
+ "--max-errors",
+ dest="max_paths",
+ type=int,
+ default=5,
+ help="Maximum example paths per grouped issue (default: 5)",
+ )
+ return parser
+
+
+def main():
+ parser = _build_parser()
+ args = parser.parse_args()
+
+ if args.command == "merged":
+ return _run_merged_validation(
+ args.json_file,
+ args.schema,
+ max(args.max_paths, 1),
+ )
+ if args.command == "raw":
+ return _run_raw_validation(args)
+ parser.error(f"unsupported validation mode: {args.command}")
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/common/tools/validate.sh b/common/tools/validate.sh
deleted file mode 100755
index 4596eb42..00000000
--- a/common/tools/validate.sh
+++ /dev/null
@@ -1,338 +0,0 @@
-#!/bin/bash
-# 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.
-
-SCHEMA_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-SCHEMA_FILE="$SCHEMA_DIR/acs-merged-schema.json"
-
-# Color codes
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m' # No Color
-
-usage() {
- echo "Usage:"
- echo " $0 [schema.json]"
- echo ""
- echo "Arguments:"
- echo " Path to merged results JSON (required)"
- echo " [schema.json] Path to JSON schema (optional)"
- echo ""
- echo "Notes:"
- echo " If schema.json is omitted, defaults to:"
- echo " $SCHEMA_FILE"
- echo ""
- echo "Examples:"
- echo " $0 /path/to/merged_results.json"
- echo " $0 /path/to/merged_results.json /path/to/acs-merged-schema-doc.json"
- echo ""
- echo "Options:"
- echo " -h, --help Show this help"
-}
-
-if [ $# -eq 0 ]; then
- echo -e "${RED}Error: Missing JSON file argument${NC}"
- usage
- exit 1
-fi
-
-case "$1" in
- -h|--help)
- usage
- exit 0
- ;;
-esac
-
-JSON_FILE="$1"
-if [ $# -ge 2 ]; then
- SCHEMA_FILE="$2"
-fi
-
-if [ ! -f "$JSON_FILE" ]; then
- echo -e "${RED}Error: File not found: $JSON_FILE${NC}"
- exit 1
-fi
-
-if [ ! -f "$SCHEMA_FILE" ]; then
- echo -e "${RED}Error: Schema file not found: $SCHEMA_FILE${NC}"
- exit 1
-fi
-
-echo -e "${BLUE}=====================================${NC}"
-echo -e "${BLUE}JSON Schema Validation${NC}"
-echo -e "${BLUE}=====================================${NC}\n"
-
-# Run validation with concise, readable errors (path + message)
-output=$(python3 - "$JSON_FILE" "$SCHEMA_FILE" <<'PY'
-import json
-import re
-import sys
-from jsonschema import Draft202012Validator
-from jsonschema.exceptions import best_match
-
-# ANSI colors
-RED = "\033[0;31m"
-GREEN = "\033[0;32m"
-YELLOW = "\033[1;33m"
-BLUE = "\033[0;34m"
-NC = "\033[0m"
-
-def fmt_path(path):
- parts = []
- for p in path:
- if isinstance(p, int):
- parts.append(f"[{p}]")
- else:
- if parts:
- parts.append("." + str(p))
- else:
- parts.append(str(p))
- return "".join(parts) if parts else ""
-
-def suite_from_path(path):
- if not path:
- return ""
- first = path[0]
- if isinstance(first, str) and first.startswith("Suite_Name:"):
- return first
- return ""
-
-def collect_key_issues(error):
- missing = set()
- unexpected = set()
-
- def handle(err):
- if err.validator == "required" and isinstance(err.instance, dict):
- required = err.validator_value if isinstance(err.validator_value, list) else []
- for key in required:
- if key not in err.instance:
- missing.add(key)
- elif err.validator == "additionalProperties" and isinstance(err.message, str):
- for key in re.findall(r"'([^']+)'", err.message):
- unexpected.add(key)
-
- if error.context:
- for sub in error.context:
- handle(sub)
- else:
- handle(error)
-
- return missing, unexpected
-
-def best_suberror(error):
- if error.validator not in ("anyOf", "oneOf") or not error.context:
- return None
- # Prefer a concrete schema failure over the fallback additionalProperties noise.
- non_additional = [e for e in error.context if e.validator != "additionalProperties"]
- candidate = best_match(non_additional) if non_additional else None
- # If best match is a type mismatch but additionalProperties exists, surface the key error.
- if candidate is not None and candidate.validator == "type":
- for e in error.context:
- if e.validator == "additionalProperties":
- return e
- if candidate is not None:
- return candidate
- return best_match(error.context)
-
-
-def error_tag(error, missing, unexpected):
- if missing:
- return "MISSING_KEY"
- if unexpected:
- return "UNEXPECTED_KEY"
- if error.validator == "not":
- return "DISALLOWED_VALUE"
- if error.validator == "type":
- return "TYPE_MISMATCH"
- if error.validator == "enum":
- return "ENUM"
- if error.validator:
- return str(error.validator).upper()
- return "VALIDATION"
-
-def shorten_message(error, message):
- # Reduce noisy object/array dumps in messages like "{...} is not of type ..."
- if isinstance(error.instance, dict) and message.startswith("{") and " is " in message:
- return "object" + message[message.find(" is "):]
- if isinstance(error.instance, list) and message.startswith("[") and " is " in message:
- return "array" + message[message.find(" is "):]
- # Normalize "not" errors when a value is explicitly disallowed via enum.
- if error.validator == "not":
- enum_vals = []
- if isinstance(error.schema, dict):
- not_schema = error.schema.get("not")
- if isinstance(not_schema, dict):
- enum_vals = not_schema.get("enum") or []
- if enum_vals:
- return f"value '{error.instance}' is not allowed"
- return message
-
-def subtest_result_unexpected_keys(instance, schema):
- # Generic: detect unexpected keys inside sub_test_result objects.
- try:
- allowed = set(schema["definitions"]["sub_test_result_object"]["properties"].keys())
- except Exception:
- return set()
- if not isinstance(instance, dict):
- return set()
- subtests = instance.get("subtests")
- if not isinstance(subtests, list):
- return set()
- unexpected = set()
- for sub in subtests:
- if not isinstance(sub, dict):
- continue
- sr = sub.get("sub_test_result")
- if isinstance(sr, dict):
- unexpected.update(set(sr.keys()) - allowed)
- return unexpected
-
-json_file = sys.argv[1]
-schema_file = sys.argv[2]
-
-with open(schema_file, "r", encoding="utf-8") as sf:
- schema = json.load(sf)
-with open(json_file, "r", encoding="utf-8") as jf:
- instance = json.load(jf)
-
-v = Draft202012Validator(schema)
-errors = sorted(v.iter_errors(instance), key=lambda e: list(e.path))
-if not errors:
- sys.exit(0)
-
-def is_prefix_path(prefix, full):
- if len(prefix) > len(full):
- return False
- return all(p == f for p, f in zip(prefix, full))
-
-# Suppress cascading unevaluatedProperties when a more specific child error exists.
-by_suite = {}
-for e in errors:
- s = suite_from_path(list(e.path))
- by_suite.setdefault(s, []).append(e)
-
-filtered = []
-for suite, errs in by_suite.items():
- other_paths = [list(e.path) for e in errs if e.validator != "unevaluatedProperties"]
- for e in errs:
- if e.validator == "unevaluatedProperties" and other_paths:
- ep = list(e.path)
- if any(is_prefix_path(ep, op) for op in other_paths):
- continue
- filtered.append(e)
-
-errors = sorted(filtered, key=lambda e: list(e.path))
-
-suite_errors = {}
-for err in errors:
- suite = suite_from_path(list(err.path))
- sub = best_suberror(err)
- base_err = sub if sub is not None else err
- msg = shorten_message(base_err, base_err.message)
- missing, unexpected = collect_key_issues(base_err)
-
- # If only a cascading unevaluatedProperties error, surface unexpected keys
- # from nested sub_test_result objects (generic across suites).
- if base_err.validator == "unevaluatedProperties" and not missing and not unexpected:
- sub_unexpected = subtest_result_unexpected_keys(err.instance, schema)
- if sub_unexpected:
- unexpected = sub_unexpected
- msg = "Additional properties are not allowed (" + ", ".join(
- f"'{k}' was unexpected" for k in sorted(sub_unexpected)
- ) + ")"
- details = []
- if missing:
- details.append(f"{RED}missing{NC}: " + ", ".join(sorted(missing)))
- if unexpected:
- details.append(f"{BLUE}unexpected{NC}: " + ", ".join(sorted(unexpected)))
- if details:
- msg = f"{msg} ({'; '.join(details)})"
- tag = error_tag(base_err, missing, unexpected)
- msg = f"{YELLOW}{tag}{NC}: {msg}"
- key = (suite, msg)
- suite_errors.setdefault(key, []).append(err)
-
-suites = [k for k in instance.keys() if isinstance(k, str) and k.startswith("Suite_Name:")]
-suites = sorted(suites)
-suite_counts = {s: 0 for s in suites}
-
-reported = set()
-for suite in suites:
- any_err = False
- for (s, message), errs in suite_errors.items():
- if s != suite:
- continue
- any_err = True
- suite_counts[suite] += len(errs)
- reported.add((s, message))
- paths = [fmt_path(e.path) for e in errs]
- print(f"{RED}*suite={suite} issue={message} count={len(paths)}{NC}")
- for p in paths[:5]:
- print(f"{YELLOW} *at={p}{NC}")
- if len(paths) > 5:
- print(f"{YELLOW} *... and {len(paths) - 5} more{NC}")
- print()
- if not any_err:
- print(f"{GREEN}*suite={suite} no errors{NC}")
-
-# Root-level errors (not tied to a suite)
-for (s, message), errs in suite_errors.items():
- if s != "":
- continue
- if (s, message) in reported:
- continue
- paths = [fmt_path(e.path) for e in errs]
- print(f"{RED}*suite= issue={message} count={len(paths)}{NC}")
- for p in paths[:5]:
- print(f"{YELLOW} *at={p}{NC}")
- if len(paths) > 5:
- print(f"{YELLOW} *... and {len(paths) - 5} more{NC}")
-
-print()
-print(f"{BLUE}--- Error Counts by Suite ---{NC}")
-for s in suites:
- count = suite_counts.get(s, 0)
- color = GREEN if count == 0 else RED
- print(f"{color}{s}: {count}{NC}")
-
-if any(k[0] == "" for k in suite_errors):
- root_count = sum(len(errs) for (s, _), errs in suite_errors.items() if s == "")
- color = GREEN if root_count == 0 else RED
- print(f"{color}: {root_count}{NC}")
-
-sys.exit(1)
-PY
-)
-exit_code=$?
-
-if [ $exit_code -eq 0 ]; then
- echo -e "${GREEN}✓ Schema validation PASSED${NC}\n"
-else
- echo -e "${RED}✗ Schema validation FAILED${NC}\n"
- if [ -n "$output" ]; then
- echo -e "${RED}Errors:${NC}"
- echo "$output" | while read line; do
- echo "$line"
- done
- echo
- fi
-fi
-
-echo -e "${BLUE}File: $JSON_FILE${NC}"
-echo -e "${BLUE}Schema: $SCHEMA_FILE${NC}\n"
-
-exit $exit_code
diff --git a/docs/acs_schema_guide.md b/docs/acs_schema_guide.md
index 7edc2510..7ec4c6df 100644
--- a/docs/acs_schema_guide.md
+++ b/docs/acs_schema_guide.md
@@ -1,192 +1,268 @@
-# ACS Merged Schema - Beginner Guide
+# ACS JSON Schema Validation Guide
-This document explains the merged results JSON schema in plain language. It is meant for readers seeing the schema for the first time.
+This guide explains how to validate complete ACS merged results and individual
+suite JSON files with the single SystemReady schema validator.
-## 1) What this schema validates
+## Contents
-The schema validates a single JSON file (merged results) that contains multiple test suites. Each suite appears under a key like:
+1. [Files and Prerequisites](#files-and-prerequisites)
+2. [Choose Merged or Raw Validation](#choose-merged-or-raw-validation)
+3. [Validate Merged Results](#validate-merged-results)
+4. [Validate Raw Suite JSON](#validate-raw-suite-json)
+5. [Understand the Report](#understand-the-report)
+6. [How Schema Selection Works](#how-schema-selection-works)
+7. [Schema Rules and Error Tags](#schema-rules-and-error-tags)
+8. [Exit Codes](#exit-codes)
+9. [Maintainer Notes](#maintainer-notes)
-- `Suite_Name: BSA`
-- `Suite_Name: FWTS`
-- `Suite_Name: SCT`
-- `Suite_Name: SBMR`
-- `Suite_Name: Standalone`
-- `Suite_Name: SCMI`
-- `Suite_Name: OS Tests - `
-- `Suite_Name: acs_info`
+## Files and Prerequisites
-All suite objects are **strict**. Any unexpected key causes a schema error.
+The schema tools are kept together:
-## 2) Top-level structure
+| File | Purpose |
+|---|---|
+| `common/tools/validate.py` | Validates merged or raw JSON and formats errors |
+| `common/tools/acs-results-schema.json` | Draft 2020-12 merged and suite contracts |
+| `common/tools/suite_registry.json` | Maps raw filenames and suites to schema definitions |
+| `common/tools/suite_registry.py` | Shared registry lookup helpers |
-Top-level object:
-- Keys are suite names (strings). Each key maps to a suite object.
-- Some suite names are fixed (BSA, FWTS, SCT, SBMR, Standalone, SCMI, PFDI, POST_SCRIPT, BBSR-*).
-- OS Tests are flexible and use a pattern: `Suite_Name: OS Tests - `.
+Run commands in this guide from the repository root. Python 3 and the
+`jsonschema` package are required:
-## 3) Shared definitions (common building blocks)
+```bash
+python3 -m pip install -r common/log_parser/requirements.txt
+common/tools/validate.py --help
+```
-### 3.1 Summary totals (lowercase)
+Paths containing spaces must be quoted.
-Most suites use lowercase totals:
+## Choose Merged or Raw Validation
-- `summary_totals_base` (optional fields):
- - `total_aborted`, `total_failed`, `total_failed_with_waiver`, `total_ignored`, `total_passed`, `total_skipped`, `total_warnings`
+Use an explicit command so the validator never guesses the JSON type:
-- `summary_totals` (required core fields):
- - `total_aborted`, `total_failed`, `total_failed_with_waiver`, `total_passed`, `total_skipped`, `total_warnings`
- - `total_ignored` is optional here
+| Input | Command | What is checked |
+|---|---|---|
+| Complete `merged_results.json` | `validate.py merged` | Full root, `acs_info`, suite keys, compliance summary, and every included suite |
+| Individual suite JSON such as `bsa.json` | `validate.py raw` | The suite definition registered for that filename |
-For SCT only, `total_ignored` is **required** by wrapping `summary_totals` with an extra `required` at the SCT usage sites.
+Here, **complete** means the normal parser's full compliance matrix. A
+selected-suite standalone `merged_results.json` intentionally omits unselected
+compliance entries and is therefore not a full merged-schema validation target.
+Use standalone `--schema` or `validate.py raw` for standalone suite JSON.
-### 3.2 BSA suite summary (capital keys)
+Schema validation checks JSON structure and field values. It does not decide
+whether ACS tests passed. A structurally valid JSON file may contain failed ACS
+tests, and a schema failure means the generated JSON does not satisfy its data
+contract.
-BSA/SBSA/PFDI use capitalized keys:
+## Validate Merged Results
-- `bsa_suite_summary`:
- - `Total Rules Run`, `Passed`, `Passed (Partial)`, `Warnings`, `Skipped`, `Failed`, `PAL Not Supported`, `Not Implemented`, `Total_failed_with_waiver`
+### Standard Command
-### 3.3 Test category metadata
+```bash
+common/tools/validate.py merged \
+ "/path/to/acs_results/acs_summary/acs_jsons/merged_results.json"
+```
-Many suites require three test category fields on each test result:
+The default schema is `common/tools/acs-results-schema.json`.
-- `test_category_base` (required on test_results items)
- - `Main Readiness Grouping`, `SRS scope`, `Waivable`
+### Use a Different Schema
-### 3.4 Base shapes
+```bash
+common/tools/validate.py merged \
+ "/path/to/merged_results.json" \
+ --schema "/path/to/candidate-schema.json"
+```
-- `suite_base`:
- - `Suite_Name`
+### Show Fewer Example Paths
-- `test_result_base`:
- - `Test_suite`, `Test_suite_description`, `Sub_test_suite` (optional)
+The complete error count is always retained. This option changes only how many
+example locations are printed for each grouped issue:
-- `test_case_base`:
- - `Test_case`, `Test_case_description`, `Test_result`, `Returned Status Code`, `reason`, `subtests` (as applicable)
+```bash
+common/tools/validate.py merged \
+ "/path/to/merged_results.json" \
+ --max-paths 2
+```
-- `subtest_base`:
- - Subtest fields used by multiple suites
+## Validate Raw Suite JSON
-## 4) Suite-specific shapes
+Raw validation uses `common/tools/suite_registry.json` to choose the schema
+definition from each file's basename.
-### 4.1 BSA / SBSA / PFDI (BSA-style suites)
+### One Suite
-Suite object:
-- `suite_summary` (bsa_suite_summary)
-- `test_results` (array of `bsa_test_result`)
+```bash
+common/tools/validate.py raw "/path/to/acs_jsons/bsa.json"
+```
-Each `bsa_test_result`:
-- `Test_suite`
-- `testcases` (array)
-- `test_suite_summary` (bsa_suite_summary)
-- **Requires** `Main Readiness Grouping`, `SRS scope`, `Waivable` via `test_category_base`
+### Multiple Suites
-Each `bsa_test_case`:
-- `Test_case`, `Test_case_description`, `Test_result`, `Test_case_summary`
-- Optional: `subtests`, `waiver_reason`
-- If `Test_result` is `FAILED (WITH WAIVER)`, `waiver_reason` is required
-- BSA/SBSA `subtests` may be nested recursively. Nested subtests use `sub_Test_Number`, `sub_Test_Description`, `sub_test_result`, `sub_Test_Level`, `sub_Test_Path`, and optional child `subtests`.
-- New BSA/SBSA JSON does not emit `sub_Rule_ID`; waiver files may still use `sub_Rule_ID` as a legacy matcher.
+```bash
+common/tools/validate.py raw \
+ "/path/to/acs_jsons/bsa.json" \
+ "/path/to/acs_jsons/fwts.json" \
+ "/path/to/acs_jsons/sct.json"
+```
-### 4.2 FWTS / BBSR-FWTS
+### Discover Selected Suites in a Directory
-Suite object:
-- `suite_summary` (summary_totals)
-- `test_results` (array of `fwts_test_result`)
+```bash
+common/tools/validate.py raw \
+ --json-dir "/path/to/acs_jsons" \
+ --selected-suites BSA,FWTS,SCT
+```
-Each `fwts_test_result`:
-- `Test_suite`, `Test_suite_description`, `subtests`, `test_suite_summary` (summary_totals)
-- **Requires** `Main Readiness Grouping`, `SRS scope`, `Waivable` via `test_category_base`
+`--selected-suites` accepts canonical suite names and registered aliases. The
+validator expands grouped suites and reports a missing generated JSON as an
+error.
-### 4.3 SCT / BBSR-SCT
+List the accepted canonical names with:
-Suite object:
-- `suite_summary` (summary_totals **with required** `total_ignored`)
-- `test_results` (array of `sct_test_result`)
+```bash
+common/tools/suite_registry.py list
+```
-Each `sct_test_result`:
-- SCT-required fields: `Returned Status Code`, `Sub_test_suite`, `Test Entry Point GUID`, `Test_case`, `Test_case_description`, `Test_suite`, `reason`, `subtests`, `test_case_summary`, `test_result`
-- `test_case_summary` uses summary_totals with **required** `total_ignored`
-- **Requires** `Main Readiness Grouping`, `SRS scope`, `Waivable` via `test_category_base`
+### Validate Every JSON in a Directory
-### 4.4 SBMR
+```bash
+common/tools/validate.py raw "/path/to/acs_jsons/"*.json
+```
-Suite object:
-- `suite_summary` (summary_totals)
-- `test_results` (array of `sbmr_test_result`)
+Only filenames registered as raw suite outputs are validated. Files such as
+`acs_info.json` and `merged_results.json` are listed under `Skipped Files`. Use
+the `merged` command only for a complete normal-parser `merged_results.json`;
+validate selected standalone suite files with `raw`.
-Each `sbmr_test_result`:
-- `Test_suite`, `Test_cases`, `test_suite_summary` (summary_totals)
-- **Requires** `Main Readiness Grouping`, `SRS scope`, `Waivable` via `test_category_base`
+### During a Standalone Parser Run
-### 4.5 Standalone
+The standalone parser invokes the same raw validator when `--schema` is used:
-Standalone suite is an array of independent test entries.
+```bash
+cd common/log_parser
+./main_log_parser.sh \
+ --standalone \
+ --mode DT \
+ --input-log "/path/to/acs_results" \
+ --suite BSA \
+ --output "/path/to/new-output" \
+ --schema
+```
-Each entry is either:
-- A `standalone_test_result`, or
-- A summary-only object containing `suite_summary`
+The standalone flow parses and enriches the suite JSON before validating it.
+Running `validate.py raw` directly validates the file as it exists; it does not
+parse logs, add category metadata, apply waivers, or modify JSON.
-`standalone_test_result` includes:
-- `Test_suite`, `Test_suite_description`, `Test_case`, `Test_case_description`, `subtests`, `test_suite_summary`
-- **Requires** `Main Readiness Grouping`, `SRS scope`, `Waivable` via `test_category_base`
+## Understand the Report
-### 4.6 OS Tests
+Repeated errors are grouped by suite and issue. A report entry has this form:
-OS Tests suites are **pattern-based**:
-- `Suite_Name: OS Tests - `
+```text
+*suite=Suite_Name: BSA issue=MISSING_KEY: ... count=9
+ *at=Suite_Name: BSA.test_results[0]
+ *at=Suite_Name: BSA.test_results[1]
+ *... and 7 more
+```
-Suite object:
-- `suite_summary` (summary_totals)
-- `test_results` (array of `os_tests_test_result`)
+Interpret it as follows:
-Each `os_tests_test_result`:
-- `Test_suite`, `Test_suite_description`, `Test_case`, `Test_case_description`, `subtests`, `test_suite_summary`
-- **Requires** `Main Readiness Grouping`, `SRS scope`, `Waivable` via `test_category_base`
-
-### 4.7 SCMI
-
-Suite object:
-- `suite_summary` (summary_totals)
-- `test_results` (array of `scmi_test_result`)
-
-Each `scmi_test_result`:
-- `Test_suite`, `test_suite_summary`, `testcases`
-- **Requires** `Main Readiness Grouping`, `SRS scope`, `Waivable` via `test_category_base`
-
-Each `scmi_test_case`:
-- `Test_case`, `Test_case_description`, `Test_result`
-- Optional `reason`
-
-### 4.8 POST_SCRIPT
-
-Suite object:
-- `suite_summary` (summary_totals)
-- `test_results` (array of `standard_test_result`)
-
-`standard_test_result`:
-- `Test_suite`, `Test_suite_description`, `Sub_test_suite` (optional), `test_suite_summary` (summary_totals)
-- **Does not require** `Main Readiness Grouping`, `SRS scope`, `Waivable`
-
-### 4.9 acs_info
-
-`Suite_Name: acs_info` contains:
-- `System Info`
-- `ACS Results Summary`
-
-These blocks are strict; missing required fields causes schema errors.
-
-## 5) Common error patterns
-
-- Wrong key case: `Total_failed_with_waiver` vs `total_failed_with_waiver`
-- Unexpected plural: `total_failed_with_waivers`
-- Missing required keys because a suite uses a different structure
-- Extra fields anywhere (schema is strict)
-
-## 6) How to validate
-
-Run:
-
-`/data_nvme1n1/ashsha06/schema_changes/syscomp_systemready/common/log_parser/validate.sh \
- /data_nvme1n1/ashsha06/acs_results_template/acs_results/acs_summary/acs_jsons/merged_results.json \
- /data_nvme1n1/ashsha06/schema_changes/syscomp_systemready/common/log_parser/acs-merged-schema-doc.json`
+- `suite` identifies the affected merged section or raw suite.
+- `issue` identifies the schema rule that failed.
+- `count` is the complete number of matching schema violations.
+- `*at` shows up to five example JSON paths by default.
+- `*... and N more` means the remaining paths were hidden, not ignored.
+- `Error Counts by Suite` shows complete totals, including hidden paths.
+- `Files Checked` records every raw file that was actually validated.
+- `Skipped Files` records inputs whose filenames have no registry mapping.
+
+A large `count` does not mean thousands of lines were printed. It means the
+same structural problem occurs in many JSON entries.
+
+## How Schema Selection Works
+
+### Merged Mode
+
+`merged` validates the whole document against the schema root. The root requires
+`Suite_Name: acs_info`, permits only declared suite keys, and validates each
+included suite with its referenced definition. OS suite keys may also match the
+declared `Suite_Name: OS Tests - ` pattern.
+
+### Raw Mode
+
+`raw` follows this flow:
+
+```text
+raw filename
+ -> suite_registry.json filename match
+ -> registered schema fragment
+ -> acs-results-schema.json suite definition
+ -> grouped PASS or FAIL report
+```
+
+Examples:
+
+| Raw filename | Schema definition |
+|---|---|
+| `bsa.json`, `sbsa.json` | `bsa_suite` |
+| `fwts.json` | `fwts_suite` |
+| `sct.json` | `sct_suite` |
+| `bbsr_fwts.json` | `bbsr_fwts_suite` |
+| `bbsr_sct.json` | `bbsr_sct_suite` |
+| `bbsr_tpm.json` | `tpm_suite` |
+| `pfdi.json` | `pfdi_suite` |
+| `scmi.json` | `scmi_suite` |
+| `sbmr_ib.json`, `sbmr_oob.json` | `sbmr_suite` |
+| `post_script.json` | `post_script_suite` |
+| `os_test.json`, `ethtool_test_*.json` | `os_tests_suite` |
+| Registered standalone child JSON files | `raw_standalone_suite` |
+
+The regular raw suites reuse the same definitions used by their merged suite
+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`.
+
+Renaming a raw file to an unregistered basename prevents automatic schema
+selection. Keep the registered output name or update the registry deliberately.
+
+## Schema Rules and Error Tags
+
+The schema is intentionally strict. Most objects reject undeclared fields, and
+required fields must have the expected type and spelling.
+
+Common issue tags are:
+
+| Issue | Meaning |
+|---|---|
+| `MISSING_KEY` | A required property is absent |
+| `UNEXPECTED_KEY` | The JSON contains a property the contract does not permit |
+| `TYPE_MISMATCH` | A value is the wrong JSON type |
+| `ENUM` or `DISALLOWED_VALUE` | A value is outside the permitted set |
+| `JSON_FILE` | The input is unreadable or is not valid JSON |
+| `SCHEMA_FILE` | The schema is missing, unreadable, or invalid |
+
+## Exit Codes
+
+| Code | Meaning |
+|---|---|
+| `0` | Every requested validation passed |
+| `1` | JSON failed schema validation, or a requested JSON/schema file was unusable |
+| `2` | Invalid command, missing dependency, invalid registry, or no raw file could be validated |
+
+Use the exit code in automation; do not search terminal text for `PASS`.
+
+## Maintainer Notes
+
+When adding or renaming a suite output:
+
+1. Define or update the suite contract in
+ `common/tools/acs-results-schema.json`.
+2. Update the suite entry, output filename, and schema fragment in
+ `common/tools/suite_registry.json`.
+3. Keep parser script paths in the registry relative to
+ `common/log_parser`.
+4. Test the raw file with `validate.py raw`.
+5. Test a complete merged artifact with `validate.py merged`.
+6. Test the standalone package and the installed `log_parser/tools` layout.
+
+Do not add a second validator for a new suite. Extend the schema and registry so
+the single validator handles it.
diff --git a/docs/log_parser_guide.md b/docs/log_parser_guide.md
index e309cb56..d07b76f2 100644
--- a/docs/log_parser_guide.md
+++ b/docs/log_parser_guide.md
@@ -1,1522 +1,1103 @@
-# SystemReady ACS Log Parser - Comprehensive Documentation
+# SystemReady ACS Log Parser Guide
-## Table of Contents
-1. [Overview](#overview)
-2. [Architecture](#architecture)
-3. [Prerequisites](#prerequisites)
-4. [Usage](#usage)
-5. [Log Parser Flow](#log-parser-flow)
-6. [Supported Test Suites](#supported-test-suites)
-7. [Waiver System](#waiver-system)
-8. [Output Structure](#output-structure)
-9. [Configuration Files](#configuration-files)
-10. [Detailed Component Breakdown](#detailed-component-breakdown)
-11. [Compliance Determination](#compliance-determination)
-12. [Test Category System](#test-category-system)
-13. [Troubleshooting](#troubleshooting)
+## Contents
----
+1. [Purpose](#purpose)
+2. [Before You Start](#before-you-start)
+3. [Choose an Execution Mode](#choose-an-execution-mode)
+4. [Prerequisites](#prerequisites)
+5. [Normal Parser](#normal-parser)
+6. [Standalone Parser](#standalone-parser)
+7. [Supported Suites and Inputs](#supported-suites-and-inputs)
+8. [Configuration and Waivers](#configuration-and-waivers)
+9. [Output Files](#output-files)
+10. [Schema Validation](#schema-validation)
+11. [Packaging for a Partner](#packaging-for-a-partner)
+12. [Main Components](#main-components)
+13. [Exit Codes](#exit-codes)
-## Overview
+## Purpose
-The SystemReady ACS (Architecture Compliance Suite) Log Parser is a comprehensive tool designed to parse, analyze, and generate reports from various test suite logs for Arm SystemReady certification. It processes logs from multiple test suites, applies waivers, and generates detailed HTML/PDF summaries with compliance status.
+The SystemReady ACS log parser converts collected suite logs into JSON, suite
+HTML reports, a merged compliance result, and a combined ACS summary.
-### Key Features
-- **Multi-suite parsing**: Supports BSA, SBSA, FWTS, SCT, BBSR, SBMR, PFDI, and more
-- **Waiver management**: Apply test-level waivers with justifications
-- **Compliance tracking**: Determines pass/fail status based on mandatory/recommended tests
-- **HTML/PDF reports**: Generates detailed and summary reports
-- **Dual mode support**: SystemReady (SR) and DeviceTree (DT) bands
+The parser provides two separate interfaces:
----
+1. **Normal parser:** the original environment-integrated parser from the main
+ branch. It auto-detects DT/SR and runs every applicable suite.
+2. **Standalone parser:** an opt-in portable runner for selected suites. It uses
+ explicit input options, defaults to SR mode, and does not require the original
+ ACS target environment.
-## Architecture
+### Why Standalone Support Was Added
-```
-┌─────────────────────────────────────────────────────────────────┐
-│ main_log_parser.sh │
-│ (Orchestration Layer) │
-└──────────────────────┬──────────────────────────────────────────┘
- │
- ┌───────────────┼───────────────┐
- │ │ │
- ▼ ▼ ▼
-┌──────────┐ ┌──────────┐ ┌──────────────┐
-│ acs_info │ │ Suite │ │apply_waivers │
-│ .py │ │ Parsers │ │ .py │
-└──────────┘ └──────────┘ └──────────────┘
- │ │ │
- │ ┌────────┴────────┐ │
- │ ▼ ▼ │
- │ logs_to_json.py json_to_html.py
- │ │ │
- └──────┼─────────────────┼──────┘
- │ │
- ▼ ▼
- ┌──────────────────────────────┐
- │ generate_acs_summary.py │
- │ merge_jsons.py │
- └──────────────────────────────┘
- │
- ▼
- ┌──────────────────────────────┐
- │ HTML Summary + PDF Report │
- └──────────────────────────────┘
-```
+The normal parser is designed to run in the installed ACS environment. A
+partner may instead have only copied results or individual logs on another
+Linux system.
----
+Standalone support allows that user to:
-## Prerequisites
+- run one selected suite or several selected suites;
+- parse an ACS results directory or direct log files;
+- run without `/usr/bin/log_parser`, `/mnt/acs_tests`, or
+ `/mnt/yocto_image.flag`;
+- select DT or SR explicitly when needed instead of using the parser host to
+ infer it;
+- check inputs and dependencies before parsing with `--doctor`;
+- generate only the required output stages;
+- validate raw suite JSON with `--schema`;
+- apply waivers and include configuration metadata;
+- publish output only after all requested stages succeed;
+- package the parser and verify the package checksum.
-### System Requirements
-- **OS**: Linux (bash shell)
-- **Python**: 3.6+
-- **Root Access**: Required for system information extraction (dmidecode)
+The standalone addition does not replace the normal parser. Commands without
+`--standalone` continue through the original main-branch flow.
+
+## Before You Start
+
+### How to Read Commands
+
+- Replace text inside angle brackets, such as ``, with a
+ real path. Do not type the angle brackets.
+- Text inside square brackets is optional. Do not type the square brackets.
+- Keep paths containing spaces inside quotes.
+- A standalone `--output` path must not exist before the run starts. Its parent
+ directory may already exist.
+
+### Open the Parser Directory
+
+For a source checkout:
-### Python Dependencies
```bash
-pip3 install jinja2 weasyprint
+cd /path/to/syscomp_systemready/common/log_parser
```
-### Directory Structure
-```
-/
-├── uefi/
-│ ├── BsaResults.log
-│ ├── SbsaResults.log
-│ ├── pfdiresults.log
-├── uefi_dump/
-│ └── uefi_version.log
-├── linux_acs/
-│ └── bsa_acs_app/BsaResultsKernel.log
-│ └── scmi_acs_app/arm_scmi_test_log.txt
-├── linux/
-│ └── BsaResultsKernel.log
-├── fwts/
-│ └── FWTSResults.log
-├── sct_results/
-│ └── Overall/Summary.log
-├── bbsr/
-│ ├── fwts/FWTSResults.log
-│ ├── sct_results/Overall/Summary.log
-│ └── tpm2/verify_tpm_measurements.log
-├── sbmr/
-│ ├── sbmr_in_band_logs/console.log
-│ └── sbmr_out_of_band_logs/console.log
-├── post-script/
-│ └── post-script.log
-├── linux_tools/
-│ ├── dt_kselftest.log
-│ ├── dt-validate-parser.log
-│ ├── ethtool-test.log
-│ └── read_write_check_blk_devices.log
-│ └── psci/psci_kernel.log
-├── network_boot/
-│ └── network_boot_results.log
-└── ../fw/
- ├── capsule-update.log
- ├── capsule-on-disk.log
- └── capsule_test_results.log
-
-/os-logs/
-└── linux*/
- ├── ethtool_test.log
- └── boot_sources.log
+For an extracted partner package:
+
+```bash
+cd /path/to/systemready-log-parser/common/log_parser
```
----
+Unless a section explicitly says **repository root**, every
+`./main_log_parser.sh` and dependency command in this guide runs from this
+`common/log_parser` directory.
-## Usage
+Confirm that the entry point is available:
-### Basic Command Syntax
```bash
-sudo ./main_log_parser.sh [acs_config.txt] [system_config.txt] [waiver.json]
+./main_log_parser.sh --standalone --help
```
-### Example Command
-```bash
-sudo ./main_log_parser.sh \
- \
- \
- \
-
+## Choose an Execution Mode
+
+| Requirement | Normal parser | Standalone parser |
+|---|---|---|
+| Original full parser behavior | Yes | No |
+| Run every applicable suite | Yes | Only when explicitly selected |
+| Run selected suites | No | Yes |
+| Mode selection | Auto-detected | Optional; defaults to SR, or use `--mode DT`/`--mode SR` |
+| Input | ACS results directory | ACS results directory or direct files |
+| Installed ACS paths | Expected | Not required |
+| Custom output directory | No | Yes |
+| Dependency/input preflight | No | `--doctor` |
+| Raw suite schema validation | No | Optional `--schema` |
+| Output stages | Fixed | Selectable |
+
+Use the normal parser when reproducing the existing complete ACS parser flow.
+Use standalone when parsing copied results, selecting suites, supplying direct
+logs, or running outside the installed ACS environment.
+
+Use this decision rule:
+
+```text
+Need the original full run in its installed ACS environment?
+ Yes -> normal parser
+ No -> standalone parser
+
+Need one suite, several selected suites, direct files, or an explicit mode?
+ Yes -> standalone parser
```
-### Parameters
+Do not combine normal positional syntax with standalone options. In particular,
+`--suite`, `--mode`, and `--input-log` work only when `--standalone` is present.
-| Parameter | Required | Description |
-|-----------|----------|-------------|
-| `acs_results_directory` | **Yes** | Path to directory containing test results |
-| `acs_config.txt` | No | ACS configuration file for test info |
-| `system_config.txt` | No | System configuration file for metadata |
-| `waiver.json` | No | Waiver file to mark known issues |
+### Three Different Standalone Decisions
-### Command Line Flags
+```text
+--standalone = how to run: use the portable runner
+--mode = what kind of results: DT or SR (optional; SR is the default)
+--suite = which tests to parse
+```
-The parser automatically detects the mode:
-- **SR Mode**: If `/mnt/yocto_image.flag` does NOT exist
-- **DT Mode**: If `/mnt/yocto_image.flag` exists
+`--standalone` does not mean DT or SR. `--mode` does not select a suite. All
+three decisions are independent. If `--mode` is omitted, the runner selects SR
+and prints a notice before continuing. DT results must use `--mode DT`.
----
+## Prerequisites
-## Log Parser Flow
+### System Requirements
+
+- Linux
+- Bash
+- Python 3.8 or newer
+- Read access to collected logs
+- Write access to the output parent directory
+- At least 10 MiB free space for a standalone run
-### High-Level Flow
+### Python Packages
+From `common/log_parser`, install all parser packages with:
+
+```bash
+python3 -m pip install -r requirements.txt
```
-1. Initialize Environment
- ├── Check arguments
- ├── Detect SR/DT mode (yocto_image.flag)
- ├── Select test_category file (test_category.json or test_categoryDT.json)
- └── Create output directories
-
-2. Gather System Information (acs_info.py)
- ├── Extract system metadata (dmidecode)
- ├── Parse config files
- └── Generate acs_info.json
-
-3. Parse Individual Test Suites
- ├── BSA/SBSA Parsing
- │ ├── logs_to_json.py → bsa.json
- │ ├── apply_waivers.py (with test_category)
- │ └── json_to_html.py
- ├── FWTS Parsing
- │ ├── logs_to_json.py → fwts.json
- │ ├── apply_waivers.py (with test_category)
- │ └── json_to_html.py
- ├── SCT Parsing
- ├── BBSR Suite Parsing
- ├── SBMR Parsing (SR mode only)
- ├── PFDI Parsing (DT mode only)
- ├── Standalone Tests (DT mode)
- └── OS Tests
-
-4. Apply Waivers (Suite-by-Suite)
- ├── Load waiver.json
- ├── Load selected test_category file for waivability checks
- ├── Match waivers to failed tests
- └── Mark as "FAILED (WITH WAIVER)"
-
-5. Merge JSONs and Generate Summary
- ├── merge_jsons.py → merged_results.json
- ├── Load test_categoryDT.json metadata (current implementation)
- ├── Enrich with Waivable/SRS scope/Readiness grouping
- ├── Determine compliance status
- └── generate_acs_summary.py → acs_summary/html_detailed_summaries/acs_summary.html
-
-6. PDF Generation (DT mode only)
- └── Convert HTML to PDF using weasyprint
+
+The equivalent command from the repository or extracted package root is:
+
+```bash
+python3 -m pip install -r common/log_parser/requirements.txt
```
-### Detailed Step-by-Step Flow
-
-#### Step 1: System Info Gathering
-```python
-# acs_info.py extracts:
-- Vendor (from dmidecode -t system)
-- System Name (Product Name)
-- SoC Family
-- Firmware Version (from dmidecode -t bios)
-- Timestamp
-- Config file parameters
+The requirements file installs every supported stage. Individual packages are
+required only when the selected suites or output stages use them:
+
+| Package | When it is mandatory |
+|---|---|
+| `chardet` | Parsing BSA, SBSA, SCT, BBSR-SCT, PFDI, or SCMI |
+| `Jinja2` | Generating HTML, including the default standalone output |
+| `matplotlib` | Generating HTML, including the default standalone output |
+| `jsonschema` | Using `--schema` |
+| `weasyprint` | Requesting the `pdf` output stage |
+
+WeasyPrint may also require operating-system Cairo and Pango packages. A
+JSON-only run needs only the dependencies used by its selected suite. Installing
+the complete requirements file is recommended for a portable partner setup.
+
+Using a virtual environment is recommended:
+
+```bash
+mkdir -p "$HOME/.venvs"
+python3 -m venv "$HOME/.venvs/systemready-log-parser"
+source "$HOME/.venvs/systemready-log-parser/bin/activate"
+python3 -m pip install --upgrade pip
+python3 -m pip install -r requirements.txt
```
-#### Step 2: Test Suite Parsing Loop
-For each test suite (BSA, FWTS, SCT, etc.):
+Keeping the virtual environment outside `common/log_parser` also prevents it
+from being included in a partner archive.
+
+## Normal Parser
+
+The normal parser is the original main-branch behavior. It accepts positional
+arguments, auto-detects mode, and considers every suite applicable to that
+mode.
-1. **Check if log file exists**
- - Mandatory logs (marked "M"): Print error if missing and continue
- - Optional logs: Warn and continue
+The normal flow in `main_log_parser.sh` uses host state and hard-coded suite
+paths and does not use the standalone suite registry. The important boundaries
+are:
-2. **Parse log to JSON** (`logs_to_json.py`)
- - Extract test cases, results, descriptions
- - Structure as hierarchical JSON
+- `/mnt/yocto_image.flag` selects normal DT or SR behavior and the installed
+ category file;
+- the ACS config `Band`, not the host flag by itself, controls whether
+ `acs_info.py` adds BMC firmware or PSCI version metadata;
+- compliance is calculated for the full requirement set of the detected mode,
+ including applicable suites that were not run.
-3. **Apply waivers** (`apply_waivers.py`)
- - Match failed tests against waiver.json
- - Update status to "FAILED (WITH WAIVER)"
+### Normal Parser Syntax
-4. **Generate HTML reports** (`json_to_html.py`)
- - Detailed HTML: All test details
- - Summary HTML: Pass/Fail counts
+Run from `common/log_parser`:
-#### Step 3: Merging and Compliance
-```python
-# merge_jsons.py:
-1. Load all suite JSONs
-2. Apply suite compliance rules (Mandatory/Recommended)
-3. Calculate overall compliance
-4. Generate merged_results.json
+```bash
+./main_log_parser.sh \
+ \
+ [acs_config] \
+ [system_config] \
+ [waiver_json]
```
-#### Step 4: Final Summary Generation
-```python
-# generate_acs_summary.py:
-1. Load merged_results.json
-2. Extract system info
-3. Render HTML template with:
- - System information table
- - Per-suite summaries
- - Overall compliance status
- - Links to detailed reports
+Square brackets mean optional. Do not type the brackets.
+
+### Mandatory Normal Argument
+
+| Position | Value | Meaning |
+|---|---|---|
+| 1 | ACS results directory | Directory containing collected ACS suite logs |
+
+### Optional Normal Arguments
+
+| Position | Value | Meaning |
+|---|---|---|
+| 2 | ACS config | Adds ACS metadata to the summary |
+| 3 | System config | Adds system metadata to the summary |
+| 4 | Waiver JSON | Applies approved waivers |
+
+Because these arguments are positional, keep their order. To provide a waiver
+without configs, pass empty placeholders for positions 2 and 3.
+
+Example with a waiver but no configs:
+
+```bash
+./main_log_parser.sh \
+ /path/to/acs_results \
+ "" \
+ "" \
+ /path/to/waiver.json
```
----
-
-## Supported Test Suites
-
-### SystemReady (SR) Band
-
-| Suite | Compliance Level | Description |
-|-------|-----------------|-------------|
-| **BSA** | Mandatory | Base System Architecture tests (UEFI + Kernel) |
-| **SBSA** | Recommended* | Server Base System Architecture |
-| **FWTS** | Mandatory | Firmware Test Suite |
-| **SCT** | Mandatory | Self Certification Test (UEFI) |
-| **BBSR-FWTS** | Extension-Mandatory | BBR Security Recipe - FWTS |
-| **BBSR-SCT** | Extension-Mandatory | BBR Security Recipe - SCT |
-| **BBSR-TPM** | Extension-Mandatory | BBR Security Recipe - TPM |
-| **SBMR-IB** | Recommended* | System BMC Management Recipe - In-Band |
-| **SBMR-OOB** | Recommended* | System BMC Management Recipe - Out-of-Band |
-| **OS Tests** | Mandatory | SR OS tests (os_test.json from sr_logs_to_json.py) |
-
-*If SBSA is present it is treated as Mandatory; if any SBMR logs are present both SBMR-IB/OOB are treated as Mandatory.
-
-### DeviceTree (DT) Band
-
-| Suite | Compliance Level | Description |
-|-------|-----------------|-------------|
-| **BSA** | Recommended | Base System Architecture |
-| **FWTS** | Mandatory | Firmware Test Suite |
-| **SCT** | Mandatory | Self Certification Test |
-| **BBSR-FWTS** | Extension-Mandatory | BBR Security Recipe - FWTS |
-| **BBSR-SCT** | Extension-Mandatory | BBR Security Recipe - SCT |
-| **BBSR-TPM** | Extension-Mandatory | BBR Security Recipe - TPM |
-| **DT_VALIDATE** | Mandatory | DeviceTree Validation |
-| **DT_KSELFTEST** | Recommended | Kernel Selftest for DT |
-| **ETHTOOL_TEST** | Mandatory | Ethernet Tool Tests |
-| **READ_WRITE_CHECK_BLK_DEVICES** | Mandatory | Block Device R/W Check |
-| **Capsule Update** | Mandatory | UEFI Capsule Update |
-| **NETWORK_BOOT** | Recommended | Network Boot Tests |
-| **OS Tests** | Mandatory | OS-level tests across distros |
-| **PFDI** | Conditional-Mandatory | Platform Fault Detection Interface |
-| **POST_SCRIPT** | Recommended | Post-boot validation scripts |
-| **PSCI** | Recommended | Power State Coordination Interface |
-| **SMBIOS** | Recommended | SMBIOS validation |
-| **SCMI** | Extension-Mandatory | System Control and Management Interface (DT only) |
-
----
-
-## Waiver System
-
-### Overview
-The waiver system allows marking known test failures with justifications. Waivers are applied at multiple granularity levels.
-
-### Waiver Hierarchy
+### Basic Normal Run
+```bash
+./main_log_parser.sh /path/to/acs_results
```
-Suite Level
- └── TestSuite Level
- └── TestCase Level
- └── SubTest Level
+
+### Normal Run With DT Configs and Waiver
+
+```bash
+./main_log_parser.sh \
+ /path/to/acs_results \
+ /path/to/acs_config_dt.txt \
+ /path/to/system_config_dt.txt \
+ /path/to/waiver.json
```
-### Waiver JSON Structure
+### Normal Run With SR Configs and Waiver
-```json
-{
- "Suites": [
- {
- "Suite": "",
- "Reason": "Optional: Suite-level waiver applies to all tests",
- "TestSuites": [
- {
- "TestSuite": "",
- "Reason": "Optional: TestSuite-level waiver",
- "TestCases": [
- {
- "Test_case": "",
- "Reason": "Required: TestCase-level waiver",
- "SubTests": [
- {
- "sub_Test_Path": "",
- "sub_Test_Number": "",
- "sub_Test_Description": "",
- "Reason": "Required: SubTest-level waiver"
- }
- ]
- }
- ]
- }
- ]
- }
- ]
-}
+```bash
+./main_log_parser.sh \
+ /path/to/acs_results \
+ /path/to/acs_config.txt \
+ /path/to/system_config.txt \
+ /path/to/waiver.json
```
-For BSA/SBSA nested subtests, prefer `sub_Test_Path`. It uniquely identifies the branch when the same rule or subtest number appears under multiple parents. The waiver parser also accepts `sub_Test_Number`, legacy `sub_Rule_ID`, and exact `sub_Test_Description` for compatibility.
+### How Normal Mode Is Selected
-### Waiver Examples
+The normal parser checks the parser machine:
-#### 1. Suite-Level Waiver (applies to all failed tests in suite)
-```json
-{
- "Suites": [
- {
- "Suite": "BSA",
- "Reason": "All BSA tests waived for this pre-production variant",
- "TestSuites": []
- }
- ]
-}
+```text
+/mnt/yocto_image.flag exists -> DT mode
+/mnt/yocto_image.flag is absent -> SR mode
```
-#### 2. TestSuite-Level Waiver
-```json
-{
- "Suite": "BSA",
- "TestSuites": [
- {
- "TestSuite": "TIMER",
- "Reason": "All PE architectural timer checks waived for this product"
- }
- ]
-}
-```
+It then uses the installed category file:
-#### 3. TestCase-Level Waiver
-```json
-{
- "Suite": "BSA",
- "TestSuites": [
- {
- "TestSuite": "GIC",
- "TestCases": [
- {
- "Test_case": "B_PPI_00",
- "Reason": "PPI assignments not required on this platform configuration"
- }
- ]
- }
- ]
-}
+```text
+DT -> /usr/bin/log_parser/test_categoryDT.json
+SR -> /usr/bin/log_parser/test_category.json
```
-#### 4. SubTest-Level Waiver
-```json
-{
- "Suite": "SBSA",
- "TestSuites": [
- {
- "TestSuite": "PCIE",
- "TestCases": [
- {
- "Test_case": "S_L6PCI_1",
- "SubTests": [
- {
- "sub_Test_Path": "S_L6PCI_1 : - / B_REP_1 : - / JKZMT : - / PCI_MM_01 : -",
- "Reason": "Device memory decode not supported on this platform"
- }
- ]
- }
- ]
- }
- ]
-}
+Check what the normal parser will select before running it:
+
+```bash
+if [ -f /mnt/yocto_image.flag ]; then
+ echo "Normal parser will use DT mode"
+else
+ echo "Normal parser will use SR mode"
+fi
```
-BSA/SBSA waivers using `sub_Test_Number` or legacy `sub_Rule_ID` still work, but they can match every nested occurrence of that number or rule under the testcase. Use `sub_Test_Path` when only one branch should be waived.
+The ACS config filename and its `Band` value do **not** select normal mode. For
+example, passing `acs_config_dt.txt` on a machine without
+`/mnt/yocto_image.flag` still runs the normal parser in SR mode. Use standalone
+with explicit `--mode DT` when parsing copied DT results on such a machine.
-#### 5. Standalone/OS Tests Waiver Format
-```json
-{
- "Suite": "Standalone",
- "TestSuites": [
- {
- "TestCase": {
- "Test_case": "dt_kselftest",
- "SubTests": [
- {
- "sub_Test_Description": "/fw-cfg@9020000",
- "Reason": "ADC node not required on this platform"
- }
- ]
- }
- }
- ]
-}
-```
+The normal interface does not accept standalone options such as `--mode`,
+`--suite`, `--input-log`, `--doctor`, `--schema`, or `--output`.
+Without `--standalone`, the original script treats its first argument as the ACS
+results path, so do not try to pass named standalone options to a normal run.
-#### 6. Complete Multi-Suite Waiver Example
-```json
-{
- "Suites": [
- {
- "Suite": "BSA",
- "TestSuites": [
- {
- "TestSuite": "TIMER",
- "Reason": "Timer checks waived"
- },
- {
- "TestSuite": "PCIE",
- "TestCases": [
- {
- "Test_case": "B_PER_08",
- "SubTests": [
- {
- "sub_Test_Path": "B_PER_08 : - / PCI_MM_01 : -",
- "Reason": "Device memory decode not supported"
- }
- ]
- }
- ]
- }
- ]
- },
- {
- "Suite": "SBSA",
- "TestSuites": [
- {
- "TestSuite": "PE",
- "TestCases": [
- {
- "Test_case": "S_L5PE_02",
- "Reason": "PE feature not available on this variant"
- }
- ]
- }
- ]
- },
- {
- "Suite": "Standalone",
- "TestSuites": [
- {
- "TestCase": {
- "TestCase": "ethtool_test",
- "SubTests": [
- {
- "sub_Test_Description": "Ping to www.arm.com on eth0",
- "Reason": "Network stack excluded for this SKU"
- }
- ]
- }
- }
- ]
- }
- ]
-}
-```
+### What the Normal Parser Does
-### Waiver Application Logic
+1. Creates or reuses `/acs_summary`.
+2. Gathers ACS and system information.
+3. Checks and parses every mode-applicable suite log.
+4. Applies waivers when the fourth argument was supplied.
+5. Generates suite JSON and HTML.
+6. Merges all generated suite JSON.
+7. Calculates compliance for the full mode requirement set.
+8. Generates the combined HTML summary.
+9. Attempts PDF generation on the DT path.
-1. **Load waivers** from waiver.json
-2. **Match waivers** to failed tests based on hierarchy
-3. **Apply waivers**:
- - Suite-level: Applies to ALL failed tests in suite
- - TestSuite-level: Applies to ALL failed tests in that TestSuite
- - TestCase-level: Applies to the specific TestCase and its nested failed SubTests
- - SubTest-level: Applies only to specific SubTests; BSA/SBSA matching uses `sub_Test_Path`, then `sub_Test_Number`, then legacy `sub_Rule_ID`, then exact description
+The normal parser writes to `/acs_summary`. Remove or move stale
+output before a clean rerun when old artifacts must not be retained.
-4. **Mark results**:
- - Original: `FAILED`
- - After waiver: `FAILED (WITH WAIVER)`
- - Add `waiver_reason` field to JSON
+## Standalone Parser
-5. **Compliance impact**:
- - Tests marked "FAILED (WITH WAIVER)" do NOT count as failures
- - Suite can still pass if all failures are waived
+Standalone is selected only by adding `--standalone`. It requires input and
+suite selection. Mode is optional: SR is used by default, while DT must be
+selected with `--mode DT`.
-### Important Waiver Rules
+`--doctor` follows the preflight branch and exits without parsing or creating
+output. A real run writes into a temporary directory and publishes the requested
+output path only after every selected stage succeeds.
-- **Reason is REQUIRED** for each waiver entry; missing reasons are skipped (quietly unless verbose)
-- Waivers cascade down (Suite → TestSuite → TestCase → SubTest)
-- Waivers only apply to **failed** tests (passing tests are not affected)
-- For nested BSA/SBSA failures, if all failed nested children under a failed parent are waived, the parent subtest/testcase is also marked `FAILED (WITH WAIVER)`
-- Waiver reasons appear in both detailed HTML and summary reports
-- Multiple waivers can be applied to the same suite
+### Standalone Syntax
-### Waivability Enforcement
+```bash
+./main_log_parser.sh \
+ --standalone \
+ [--mode ] \
+ --input-log \
+ --suite \
+ [optional_arguments]
+```
-**What happens if "Waivable" is "no" in test_category.json?**
+### Mandatory Standalone Arguments
-The apply_waivers.py script **enforces waivability** based on test_category metadata:
+| Argument | Meaning |
+|---|---|
+| `--standalone` | Select the portable standalone runner |
+| `--input-log PATH` | Supply one ACS results directory or direct log files |
+| `--suite NAME` or `--suites NAMES...` | Select suites to execute |
-1. **When test_category.json is provided**:
- - Script checks each test suite's "Waivable" field
- - If `"Waivable": "no"`, the script **skips** that test suite entirely
- - Waivers in waiver.json are **silently ignored** for non-waivable suites
- - Tests remain as "FAILED" (waivers are NOT applied)
+`--output PATH` is also mandatory when direct files are supplied. It is
+optional when `--input-log` is an ACS results directory.
-2. **When test_category.json is NOT provided**:
- - All waivers are applied (no waivability enforcement)
- - This is a fallback mode
+The informational commands `--help`, `--list-suites`, and `--version` exit
+without parsing, so they do not require mode, input, suite, or output options.
-**Example Behavior**:
+### Optional Standalone Arguments
-```json
-// test_categoryDT.json
-{
- "catID: 1": [
- {
- "Suite": "BSA",
- "Test Suite": "PE",
- "Waivable": "no" // ← Critical test, cannot be waived
- }
- ]
-}
-```
+| Argument | Meaning |
+|---|---|
+| `--mode DT\|SR` | Select DT or SR; if omitted, SR is used and a notice is printed |
+| `--output PATH` | Write to this new directory |
+| `--acs-config PATH` | Add ACS metadata and validate its Band against mode |
+| `--system-config PATH` | Add system metadata |
+| `--waiver PATH` | Apply this waiver JSON to selected suites |
+| `--test-category PATH` | Override the bundled mode category file |
+| `--outputs STAGES` | Select `json`, `html`, `summary`, and/or `pdf` |
+| `--schema` | Validate generated raw suite JSON |
+| `--doctor` | Check readiness and exit without parsing |
+| `--list-suites` | Print canonical suite names and exit |
+| `--version` | Print the complete log parser release version and exit |
-```json
-// waiver.json (user attempts to waive PE)
-{
- "Suites": [
- {
- "Suite": "BSA",
- "TestSuites": [
- {
- "TestSuite": "PE",
- "Reason": "Attempting to waive critical test"
- }
- ]
- }
- ]
-}
-```
+Compatibility spellings `--acs_config`, `--system_config`, `--waiver-json`,
+and `--waiver_json` are accepted. New commands should use the hyphenated forms
+shown above.
-**Result**:
-- The waiver for "PE" is **NOT applied**
-- Tests in PE remain "FAILED"
-- No error message is shown (silent skip)
-- Compliance will fail if PE has failures
-
-**Code Reference** (apply_waivers.py):
-```python
-# Determine if waivers should be applied based on test_category.json
-if output_json_data is None:
- # test_category.json not provided, apply all waivers
- waivable = True
-else:
- # Check if the test suite is waivable according to test_category.json
- waivable = False
- for catID, catData in output_json_data.items():
- for row in catData:
- if row.get("Suite", "").lower() == suite_name.lower() and \
- row.get("Test Suite", "").lower() == test_suite_name.lower():
- if row.get("Waivable", "").lower() == "yes":
- waivable = True
- break
- if waivable:
- break
-
-if not waivable:
- # Do not process non-waivable test suites
- continue # Skip this test suite, no waivers applied
-```
+### How `--input-log` Works
-**Best Practice**:
-- Review test_category.json before creating waivers
-- Don't attempt to waive critical/non-waivable tests
-- Focus waiver efforts on tests marked "Waivable": "yes"
+`--input-log` has two clear forms.
----
+#### Form 1: ACS Results Directory
-## Output Structure
+Pass exactly one directory:
-### Generated Directory Structure
-```
-/acs_summary/
-├── acs_jsons/
-│ ├── acs_info.json
-│ ├── bsa.json
-│ ├── sbsa.json
-│ ├── fwts.json
-│ ├── sct.json
-│ ├── bbsr_fwts.json
-│ ├── bbsr_sct.json
-│ ├── bbsr_tpm.json
-│ ├── sbmr_ib.json
-│ ├── sbmr_oob.json
-│ ├── pfdi.json
-│ ├── post_script.json
-│ ├── dt_kselftest.json
-│ ├── dt_validate.json
-│ ├── ethtool_test.json
-│ ├── read_write_check_blk_devices.json
-│ ├── capsule_update.json
-│ ├── psci.json
-│ ├── smbios_check.json
-│ ├── network_boot.json
-│ ├── ethtool_test_.json
-│ └── merged_results.json
-├── html_detailed_summaries/
-│ ├── bsa_detailed.html
-│ ├── bsa_summary.html
-│ ├── fwts_detailed.html
-│ ├── fwts_summary.html
-│ ├── sct_detailed.html
-│ ├── sct_summary.html
-│ ├── ... (one per suite)
-│ ├── standalone_tests_detailed.html
-│ ├── standalone_tests_summary.html
-│ ├── os_tests_detailed.html
-│ ├── os_tests_summary.html
-│ └── acs_summary.html (Main Report)
-└── acs_summary.pdf (DT mode only)
+```bash
+--input-log /path/to/acs_results
```
-### JSON Schema Examples
+The registry checks the exact relative paths registered for each selected suite.
+It does not recursively search the directory for matching filenames. A log that
+exists at a different path is treated as missing. This form supports one suite,
+multiple suites, and suite groups.
-#### BSA/SBSA JSON Structure
-```json
-{
- "test_results": [
- {
- "Test_suite": "PCIE",
- "testcases": [
- {
- "Test_case": "S_L6PCI_1 : -",
- "Test_case_description": "Check PCIe On-chip Peripherals",
- "Test_result": "FAILED",
- "subtests": [
- {
- "sub_Test_Number": "B_REP_1 : -",
- "sub_Test_Description": "Check RCiEP Devices",
- "sub_test_result": "FAILED",
- "sub_Test_Level": 1,
- "sub_Test_Path": "S_L6PCI_1 : - / B_REP_1 : -",
- "subtests": [
- {
- "sub_Test_Number": "JKZMT : -",
- "sub_Test_Description": "",
- "sub_test_result": "FAILED",
- "sub_Test_Level": 2,
- "sub_Test_Path": "S_L6PCI_1 : - / B_REP_1 : - / JKZMT : -",
- "subtests": [
- {
- "sub_Test_Number": "PCI_MM_01 : -",
- "sub_Test_Description": "PCIe Device Memory mapping support",
- "sub_test_result": "FAILED",
- "sub_Test_Level": 3,
- "sub_Test_Path": "S_L6PCI_1 : - / B_REP_1 : - / JKZMT : - / PCI_MM_01 : -"
- }
- ]
- }
- ]
- }
- ],
- "Test_case_summary": {
- "Total Rules Run": 1,
- "Passed": 0,
- "Failed": 1,
- "Total_failed_with_waiver": 0
- }
- }
- ],
- "test_suite_summary": {
- "Total Rules Run": 1,
- "Passed": 0,
- "Failed": 1,
- "Total_failed_with_waiver": 0
- }
- }
- ],
- "suite_summary": {
- "Total Rules Run": 1,
- "Passed": 0,
- "Failed": 1,
- "Total_failed_with_waiver": 0
- }
-}
+The parser also automatically discovers the two external roots relative to the
+results directory:
+
+```text
+/
+|-- acs_results/ <- value passed to --input-log
+|-- fw/ <- capsule-update logs
+`-- os-logs/ <- OS test directories
```
-BSA/SBSA `subtests` can recurse to any depth. `sub_Test_Path` is built from the visible log nesting and is intended for partners to compare directly with the log and for precise waiver matching. New BSA/SBSA JSON does not emit `sub_Rule_ID`; waiver files may still use it as a legacy matcher.
+For example, with:
-#### FWTS/SCT JSON Structure
-```json
-{
- "test_results": [
- {
- "Test_suite": "UEFI Services",
- "subtests": [
- {
- "sub_Test_Description": "BootServices",
- "sub_test_result": {
- "PASSED": 10,
- "FAILED": 1,
- "FAILED_WITH_WAIVER": 0,
- "fail_reasons": ["Boot order check failed"]
- }
- }
- ],
- "test_suite_summary": {
- "total_passed": 10,
- "total_failed": 1,
- "total_failed_with_waiver": 0
- }
- }
- ]
-}
+```text
+--input-log /data/run/acs_results
```
-#### Merged Results JSON
-```json
-{
- "Suite_Name: acs_info": {
- "ACS Results Summary": {
- "Suite_Name: Mandatory : BSA_compliance": "Compliant",
- "Suite_Name: Mandatory : FWTS_compliance": "Not Compliant: Failed 2",
- "Overall Compliance Result": "Not Compliant : Mandatory - (failed: FWTS)"
- }
- },
- "Suite_Name: BSA": {
- "...": "bsa.json content"
- },
- "Suite_Name: FWTS": {
- "...": "fwts.json content"
- }
-}
+the parser automatically uses:
+
+```text
+results logs -> /data/run/acs_results
+capsule logs -> /data/run/fw
+OS logs -> /data/run/os-logs
```
----
+No extra root options are required. Keep `acs_results`, `fw`, and `os-logs` as
+sibling directories in the collected run layout.
-## Configuration Files
+If one suite's logs use a different layout, supply those files directly. If
+several suites use a different layout, either arrange them in the registered
+layout or update their standalone paths in
+`common/tools/suite_registry.json` before packaging the parser. The normal
+parser does not read this registry.
-### 1. system_config.txt
-Contains system hardware information.
+#### Form 2: Direct Log Files
-**Format**: `Key: Value` pairs
+Pass one or more files for exactly one executable suite:
-**Example**:
-```
-Vendor: ARM
-System Name: RD-Aspen Platform
-SoC Family: Neoverse
-Firmware Version: 1.2.3
+```bash
+--input-log /path/to/BsaResults.log
```
-### 2. acs_config_dt.txt / acs_config.txt
-Contains ACS test configuration.
+For suites with more than one possible input, named values avoid ambiguity:
-**Example**:
-```
-BSA Version: 1.0.8
-SBSA Version: 7.1.5
-SCT Version: 2.9.0
-FWTS Version: 24.01.00
-Test Date: 2025-12-15
+```bash
+--input-log uefi=/path/to/BsaResults.log \
+--input-log kernel=/path/to/BsaResultsKernel.log
```
-### 3. waiver.json
-See [Waiver System](#waiver-system) section above.
+Direct-file mode requires `--output`. It cannot select multiple suites or a
+suite group. Use an ACS results directory for those cases.
-### 4. test_category.json / test_categoryDT.json
-Defines test metadata for enriching merged results with additional test suite properties.
+With several unnamed files, direct inputs are assigned in the registry's input
+order. With one unnamed file, the runner assigns it to the suite's only required
+file input when exactly one exists; otherwise it uses the first registered file
+input. Use the names in the [Direct Input Names](#direct-input-names) table
+whenever a suite has multiple inputs. This prevents a kernel or supporting log
+from being assigned incorrectly.
-**Purpose**:
-- Provides metadata about each test suite
-- Used by `merge_jsons.py` to enrich merged results
-- Automatically selected based on SR/DT mode
+`OS-TESTS` consumes an OS-log directory, so run it from the ACS results
+directory layout rather than direct files.
-**Location**:
-- SR mode: `/usr/bin/log_parser/test_category.json`
-- DT mode: `/usr/bin/log_parser/test_categoryDT.json`
+### Standalone Mode Selection
-**Fields**:
-- **Suite**: Test suite name (e.g., "BSA", "FWTS", "SCT")
-- **Test Suite**: Test subsuite name (e.g., "PE", "GIC", "Timer")
-- **specName**: Specification name (BSA, SBSA, etc.)
-- **rel Import. to main readiness**: Relative importance (Critical/Major/Minor)
-- **Waivable**: Whether tests can be waived ("yes"/"no")
-- **SRS scope**: Compliance scope (Mandatory/Recommended/Extension)
-- **Main Readiness Grouping**: Functional category
-- **FunctionID**: Function identifier for grouping
+```text
+--mode DT -> SystemReady Devicetree collected results
+--mode SR -> non-Devicetree SystemReady collected results
+omitted -> SR mode by default
+```
-**Example (test_categoryDT.json)**:
-```json
-{
- "catID: 1": [
- {
- "Suite": "BSA",
- "Test Suite": "PE",
- "specName": "BSA",
- "rel Import. to main readiness": "Critical",
- "Waivable": "yes",
- "SRS scope": "Recommended",
- "FunctionID": 9,
- "Main Readiness Grouping": "Physical readiness"
- }
- ],
- "catID: 4": [
- {
- "Suite": "BSA",
- "Test Suite": "PCIe",
- "specName": "BSA",
- "rel Import. to main readiness": "Minor",
- "Waivable": "yes",
- "SRS scope": "Recommended",
- "FunctionID": 9,
- "Main Readiness Grouping": "Physical readiness"
- }
- ]
-}
+Mode controls suite availability, bundled category selection, requirement
+levels, config Band validation, mode-sensitive parsers, and merged compliance.
+Standalone never guesses mode from the parser host. When `--mode` is omitted,
+it uses the deterministic SR default and prints:
+
+```text
+INFO: --mode was not provided; standalone parser will run in SR mode by default.
```
-**Usage in Log Parser**:
-1. **Automatic Selection**: Based on yocto_image.flag presence
-2. **Passed to apply_waivers.py**: As 4th argument for waivability checks
-3. **Used by merge_jsons.py**: Currently only loads `test_categoryDT.json` to enrich merged_results.json with metadata
-4. **Enrichment Process**:
- - Builds lookup dictionary from test_category data
- - Matches suite/testsuite names (case-insensitive)
- - Adds "Waivable", "SRS scope", and "Main Readiness Grouping" to merged JSON
-
----
-
-## Detailed Component Breakdown
-
-### 1. main_log_parser.sh
-**Purpose**: Orchestrates the entire parsing workflow
-
-**Key Functions**:
-- `check_file()`: Validates log file existence (Mandatory/Optional)
-- `apply_waivers()`: Calls apply_waivers.py for each suite
-- Determines SR vs DT mode via yocto_image.flag
-
-**Processing Order**:
-1. System info gathering
-2. BSA/SBSA parsing
-3. FWTS parsing
-4. SCT parsing
-5. BBSR suite parsing
-6. SBMR parsing (SR mode)
-7. PFDI parsing (DT mode)
-8. Standalone tests (DT mode)
-9. OS tests
-10. JSON merging
-11. Summary generation
-12. PDF conversion (DT mode)
-
-### 2. acs_info.py
-**Purpose**: Extract system and ACS information
-
-**Inputs**:
-- `--acs_config_path`: ACS config file
-- `--system_config_path`: System config file
-- `--uefi_version_log`: UEFI version log
-- `--output_dir`: Output directory for JSON
-
-**Outputs**:
-- `acs_info.json`: System metadata
-
-**Functions**:
-- `get_system_info()`: Uses dmidecode to extract system info
-- `parse_config()`: Parses key:value config files
-
-### 3. apply_waivers.py
-**Purpose**: Apply waivers to failed tests
-
-**Usage**:
+For an SR run, `--mode` may therefore be omitted:
+
```bash
-python3 apply_waivers.py [--quiet]
+./main_log_parser.sh \
+ --standalone \
+ --input-log /path/to/acs_results \
+ --suite SBSA \
+ --output /path/to/new-sbsa-output
```
-**Process**:
-1. Load waiver.json
-2. Extract suite-specific waivers
-3. Load test results JSON
-4. Match waivers to failed tests (hierarchy-based; recursive for BSA/SBSA nested subtests)
-5. Update test status to "FAILED (WITH WAIVER)"
-6. Add waiver_reason field
-7. Save updated JSON
-
-**Waiver Levels** (in order of precedence):
-- Suite-level
-- TestSuite-level
-- SubSuite-level (SCT/Standalone)
-- TestCase-level
-- SubTest-level
-
-For BSA/SBSA subtest-level waivers, matching priority is `sub_Test_Path`, `sub_Test_Number`, legacy `sub_Rule_ID`, then exact `sub_Test_Description`.
-
-### 4. logs_to_json.py (per suite)
-**Purpose**: Parse raw log files into structured JSON
-
-**Located in**:
-- `bsa/logs_to_json.py`
-- `bbr/fwts/logs_to_json.py`
-- `bbr/sct/logs_to_json.py`
-- `standalone_tests/logs_to_json.py`
-- `os_tests/logs_to_json.py`
-- `sbmr/logs_to_json.py`
-- `pfdi/logs_to_json.py`
-
-**Common Pattern**:
-```python
-1. Read log file
-2. Parse test cases using regex/patterns
-3. Extract:
- - Test ID
- - Description
- - Result (PASS/FAIL/SKIP)
- - Additional metadata
-4. Structure as JSON
-5. Calculate summary statistics
-6. Write to output JSON
-```
+Examples:
+
+- `SBSA` is SR-only.
+- `PFDI` is DT-only.
+- Selecting a suite in the wrong mode fails before parsing.
-**BSA/SBSA nested rules**:
-- The BSA/SBSA parser uses a rule stack so any number of nested rule groups can be represented.
-- A top-level rule is emitted as a testcase. Rules that run inside it are emitted under recursive `subtests`.
-- Each BSA/SBSA subtest contains `sub_Test_Number`, `sub_Test_Description`, `sub_test_result`, `sub_Test_Level`, and `sub_Test_Path`.
-- `sub_Test_Path` mirrors the log nesting and is stable for comparison with the log and precise waiver matching.
-- Only rules with a completed `Result:` line are emitted as completed JSON entries.
-
-### 5. json_to_html.py (per suite)
-**Purpose**: Generate HTML reports from JSON
-
-**Outputs**:
-- Detailed HTML: Complete test breakdown
-- Summary HTML: Pass/Fail counts, embedded in main summary
-- BSA/SBSA HTML renders nested `subtests` recursively with indentation.
-
-**Template Variables**:
-- Test suite name
-- Test counts (Pass/Fail/Skip/Waived)
-- Individual test details
-- Waiver reasons
-
-### 6. merge_jsons.py
-**Purpose**: Combine all suite JSONs into merged_results.json
-
-**Process**:
-1. Detect SR/DT mode
-2. Load test_categoryDT.json (used for enrichment in both modes)
-3. Build test category lookup dictionary
-4. Load compliance scope table
-5. Load all suite JSONs
-6. For each suite:
- - Extract pass/fail counts
- - Determine compliance level (M/R/EM/CM)
- - Enrich with test_category metadata
- - Calculate suite status
-7. Determine overall compliance:
- - **Not Compliant**: Any M/CM suite fails or is missing (DT mode: missing R suites also mark Not Compliant)
- - **Compliant with waivers**: Only waived failures in M/CM suites
- - **Compliant**: No failures in M/CM suites
-
-**Test Category Enrichment**:
-The script loads test metadata from `test_categoryDT.json` and enriches each test suite entry with:
-- **Waivable**: Whether the test suite allows waivers
-- **SRS scope**: Compliance scope (Mandatory/Recommended/Extension)
-- **Main Readiness Grouping**: Functional category for reporting
-
-This metadata helps in:
-- Better reporting and categorization
-- Understanding test importance
-- Grouping related test suites
-
-**Compliance Rules (as implemented)**:
-- **Mandatory (M)**: Missing or failing marks overall Not Compliant
-- **Conditional-Mandatory (CM)**: Missing does not change overall; failing marks overall Not Compliant
-- **Extension (EM)**: Missing or failing is reported but does not change overall
-- **Recommended (R)**:
- - DT mode: missing is treated as Not Compliant
- - SR mode: missing is reported as Not Run and does not change overall
-
-### 7. generate_acs_summary.py
-**Purpose**: Generate final HTML summary report
-
-**Inputs**:
-- All suite summary HTMLs
-- merged_results.json
-- System config files
-
-**Output**:
-- `acs_summary/html_detailed_summaries/acs_summary.html`: Main compliance report
-
-**Report Sections**:
-1. **System Information Table**
- - Vendor, System Name, SoC Family
- - Firmware Version
- - ACS versions
- - Test date
-
-2. **Overall Compliance Status**
- - PASS/FAIL badge
- - Compliance percentage
-
-3. **Suite-by-Suite Summary**
- - Suite name
- - Compliance level (M/R/EM/CM)
- - Pass/Fail/Waived counts
- - Links to detailed reports
-
-4. **Footer**
- - Generated timestamp
- - ACS version info
-
----
-
-## Compliance Determination
-
-### Decision Tree
+If an ACS config is available, inspect its `Band` field:
+```bash
+grep -i '^Band:' /path/to/acs_config.txt
```
-For each suite:
- ├── Is suite Mandatory?
- │ ├── YES → Any unwaived failures? → FAIL
- │ └── NO → Continue
- │
- ├── Is suite Extension (EM)?
- │ ├── YES → Report status but do NOT change overall compliance
- │ └── NO → Continue
- │
- ├── Is suite Conditional-Mandatory?
- │ ├── YES → Is suite run?
- │ │ ├── YES → Any unwaived failures? → FAIL
- │ │ └── NO → Not Run (no overall impact)
- │ └── NO → Continue
- │
- └── Is suite Recommended?
- └── Failures do NOT affect compliance (DT mode: missing suites are treated as Not Compliant)
-
-Overall Compliance:
- └── Based on the implemented rules above (M/CM failures are decisive; EM is informational; DT recommended-missing is treated as Not Compliant)
+
+Use `--mode DT` when the value contains `SystemReady Devicetree`. Use
+`--mode SR` for the non-Devicetree `SystemReady band`. If no config is
+available, obtain the band from whoever collected the results; do not infer it
+from the machine that is only parsing the copied logs.
+
+### List Suites
+
+```bash
+./main_log_parser.sh --standalone --list-suites
```
-### Compliance Examples
+### Check the Log Parser Version
-#### Example 1: SR Mode - PASS
+```bash
+./main_log_parser.sh --version
```
-BSA (M): 45 PASS, 0 FAIL, 1 FAIL_WAIVED → PASS
-SBSA (R): 30 PASS, 2 FAIL, 0 FAIL_WAIVED → PASS (Recommended)
-FWTS (M): 38 PASS, 0 FAIL, 0 FAIL_WAIVED → PASS
-SCT (M): 120 PASS, 0 FAIL, 2 FAIL_WAIVED → PASS
-BBSR-FWTS (EM): NOT RUN → SKIP
-SBMR-IB (M*): 10 PASS, 1 FAIL, 0 FAIL_WAIVED → PASS (Promoted to Mandatory when present)
-
-Overall: PASS (All mandatory suites passed)
+
+Example when the parser release version is `1.0.0`:
+
+```text
+SystemReady ACS Log Parser 1.0.0
```
-#### Example 2: DT Mode - FAIL
+The standalone form reports the same whole-parser version:
+
+```bash
+./main_log_parser.sh --standalone --version
```
-BSA (R): 45 PASS, 1 FAIL, 0 FAIL_WAIVED → FAIL (but Recommended)
-FWTS (M): 38 PASS, 0 FAIL, 0 FAIL_WAIVED → PASS
-SCT (M): 120 PASS, 1 FAIL, 0 FAIL_WAIVED → FAIL ❌
-DT_VALIDATE (M): 25 PASS, 0 FAIL, 0 FAIL_WAIVED → PASS
-ETHTOOL_TEST (M): 5 PASS, 0 FAIL, 1 FAIL_WAIVED → PASS
-Capsule Update (M): 3 PASS, 0 FAIL, 0 FAIL_WAIVED → PASS
-
-Overall: FAIL (SCT is mandatory and has unwaived failure)
+
+`LOG_PARSER_VERSION` is hardcoded once near the top of `main_log_parser.sh`.
+Release owners must update that value when publishing a new log parser release.
+The version identifies the complete packaged parser, not an individual suite,
+ACS release, Git commit, or SystemReady specification version.
+
+### First Standalone Run
+
+Start with one suite and a new output path. These shell variables make it clear
+which paths must be replaced:
+
+```bash
+RESULTS=/path/to/acs_results
+OUTPUT=/path/to/new-bsa-output
```
-#### Example 3: Extension Suite Handling
+Check the exact intended run first:
+
+```bash
+./main_log_parser.sh \
+ --standalone \
+ --mode DT \
+ --input-log "$RESULTS" \
+ --suite BSA \
+ --output "$OUTPUT" \
+ --doctor
```
-BSA (M): PASS
-FWTS (M): PASS
-SCT (M): PASS
-BBSR-FWTS (EM): PASS (Extension implemented and passed)
-BBSR-SCT (EM): NOT RUN (Extension not implemented - OK)
-BBSR-TPM (EM): 1 FAIL (Extension implemented but failed) → Reported, overall compliance unchanged
-
-Overall: PASS (EM failures do not affect overall compliance)
+
+If the preflight result is `PASS`, run the same command without `--doctor`:
+
+```bash
+./main_log_parser.sh \
+ --standalone \
+ --mode DT \
+ --input-log "$RESULTS" \
+ --suite BSA \
+ --output "$OUTPUT"
```
----
+Do not create `OUTPUT` yourself. Standalone publishes that directory after the
+requested parsing and reporting stages finish successfully.
-## Test Category System
+### BSA From an ACS Results Directory
-### Overview
+When both registered BSA logs exist, both are parsed into one `bsa.json`:
-The test_category system provides metadata enrichment for test suites, enabling better reporting, categorization, and understanding of test importance. It automatically loads based on the operating mode (SR or DT).
+```text
+uefi/BsaResults.log
+linux_acs/bsa_acs_app/BsaResultsKernel.log
+```
-### File Selection
+Command:
```bash
-# In main_log_parser.sh
-if [ $YOCTO_FLAG_PRESENT -eq 1 ]; then
- test_category="/usr/bin/log_parser/test_categoryDT.json" # DT mode
-else
- test_category="/usr/bin/log_parser/test_category.json" # SR mode
-fi
+./main_log_parser.sh \
+ --standalone \
+ --mode DT \
+ --input-log /path/to/acs_results \
+ --suite BSA \
+ --output /path/to/new-bsa-output
```
-### Metadata Fields
+BSA can run when at least one registered BSA log exists. If only the kernel log
+is provided directly, name it explicitly with `kernel=`.
-Each test suite entry in test_category.json contains:
+### Multiple Suites From an ACS Results Directory
-| Field | Description | Example Values |
-|-------|-------------|----------------|
-| **Suite** | Top-level test suite name | "BSA", "SBSA", "FWTS", "SCT" |
-| **Test Suite** | Sub-suite or test group | "PE", "GIC", "Timer", "PCIe" |
-| **specName** | Specification reference | "BSA", "SBSA", "UEFI" |
-| **rel Import. to main readiness** | Criticality level | "Critical", "Major", "Minor" |
-| **Waivable** | Can failures be waived? | "yes", "no" |
-| **SRS scope** | Compliance requirement | "Mandatory", "Recommended", "Extension" |
-| **Main Readiness Grouping** | Functional category | "Physical readiness", "Firmware readiness" |
-| **FunctionID** | Numeric category ID | 1, 2, 9, etc. |
+```bash
+./main_log_parser.sh \
+ --standalone \
+ --mode DT \
+ --input-log /path/to/acs_results \
+ --suites BSA FWTS SCT \
+ --output /path/to/new-multi-suite-output
+```
-### Usage Flow
+These selection forms are equivalent:
+```text
+--suites BSA FWTS SCT
+--suite BSA,FWTS,SCT
+--suite BSA --suite FWTS --suite SCT
```
-1. main_log_parser.sh selects test_category file
- └── Based on yocto_image.flag
-2. Passed to apply_waivers.py
- ├── Used for waivability validation
- └── Ensures only waivable tests are waived
+### BSA From Two Direct Logs
-3. Loaded by merge_jsons.py (currently `test_categoryDT.json` only)
- ├── Builds lookup dictionary: suite → testsuite → metadata
- ├── Enriches merged_results.json with metadata
- └── Adds context for reporting
+```bash
+./main_log_parser.sh \
+ --standalone \
+ --mode DT \
+ --input-log uefi=/path/to/BsaResults.log \
+ --input-log kernel=/path/to/BsaResultsKernel.log \
+ --suite BSA \
+ --output /path/to/new-bsa-output
```
-### Example: Metadata Lookup
-
-```python
-# merge_jsons.py builds a lookup structure:
-test_cat_dict = {
- "bsa": {
- "pe": {
- "Suite": "BSA",
- "Test Suite": "PE",
- "Waivable": "yes",
- "SRS scope": "Recommended",
- "rel Import. to main readiness": "Critical",
- "Main Readiness Grouping": "Physical readiness"
- },
- "pcie": {
- "Suite": "BSA",
- "Test Suite": "PCIe",
- "Waivable": "yes",
- "SRS scope": "Recommended",
- "rel Import. to main readiness": "Minor",
- "Main Readiness Grouping": "Physical readiness"
- }
- }
-}
+### Complete Standalone BSA Example
+
+```bash
+./main_log_parser.sh \
+ --standalone \
+ --mode DT \
+ --input-log /path/to/acs_results \
+ --suite BSA \
+ --output /path/to/new-bsa-output \
+ --acs-config /path/to/acs_config_dt.txt \
+ --system-config /path/to/system_config_dt.txt \
+ --waiver /path/to/waiver.json \
+ --schema
```
-### Enrichment in Merged Results
+This command discovers BSA logs, applies BSA waivers, enriches metadata,
+validates raw BSA JSON, generates suite HTML, and creates selected-only merged
+and combined summaries.
-When merge_jsons.py processes test results, it adds metadata:
+### Doctor Preflight
-```json
-{
- "suites": [
- {
- "suite_name": "BSA",
- "test_suite": "PE",
- "compliance_level": "Recommended",
- "waivable": "yes",
- "srs_scope": "Recommended",
- "main_readiness_grouping": "Physical readiness",
- "relative_importance": "Critical",
- "suite_status": "PASSED",
- "total_passed": 45,
- "total_failed": 0
- }
- ]
-}
-```
+`--doctor` checks suite/mode compatibility, the registry, parser files, required
+dependencies, required input paths, output safety, write access, and free space.
+It does not parse log contents or create final output.
-### Benefits
-
-1. **Enhanced Reporting**: Categorize tests by function and importance
-2. **Waiver Validation**: Ensure only waivable tests accept waivers
-3. **Waiver Enforcement**: Non-waivable tests cannot be waived (enforced by apply_waivers.py)
-4. **Readiness Tracking**: Group tests by readiness categories
-5. **Compliance Context**: Understand why tests are mandatory/recommended
-6. **Criticality Awareness**: Identify high-impact vs. minor tests
-
-### Waivability Enforcement Details
-
-The test_category system provides **hard enforcement** of waivability:
-
-**Process**:
-1. apply_waivers.py loads test_category file (passed as 4th argument)
-2. For each test suite in the JSON, checks if "Waivable" == "yes"
-3. If "Waivable" == "no":
- - Skips the entire test suite
- - Ignores any waivers defined in waiver.json
- - Leaves test results unchanged (failures remain as "FAILED")
-4. If "Waivable" == "yes":
- - Proceeds with waiver application
- - Matches waivers from waiver.json
- - Updates results to "FAILED (WITH WAIVER)"
-
-**Why This Matters**:
-- **Critical tests cannot be bypassed**: Ensures compliance integrity
-- **Prevents accidental waivers**: Can't waive important architectural checks
-- **Enforces certification rules**: Aligns with SystemReady specification requirements
-
-**Example Enforcement**:
-```
-Test Suite: BSA PE (Waivable: no)
-├── Test B_PE_01: FAILED
-│ └── Waiver exists in waiver.json
-│ └── Result: FAILED (waiver NOT applied) ❌
-│
-Test Suite: BSA Timer (Waivable: yes)
-├── Test B_TIMER_01: FAILED
-│ └── Waiver exists in waiver.json
-│ └── Result: FAILED (WITH WAIVER) ✅
+```bash
+./main_log_parser.sh \
+ --standalone \
+ --mode DT \
+ --input-log /path/to/acs_results \
+ --suite BSA \
+ --output /path/to/new-output \
+ --schema \
+ --doctor
```
-### Differences: SR vs DT Mode
+A doctor PASS means the run can start. It does not guarantee that log contents
+or generated JSON will pass parsing and schema validation.
-**test_category.json (SR mode)**:
-- Focuses on server/system requirements
-- Includes SBSA test suites
-- SBMR test metadata
+Use the same mode, input, suites, output stages, schema flag, configs, waiver,
+and output path planned for the real run. Then remove only `--doctor`. This
+ensures the preflight checks the dependencies and files for the intended run.
-**test_categoryDT.json (DT mode)**:
-- Focuses on DeviceTree-specific tests
-- Includes DT validation metadata
-- Standalone test categories
-- OS test groupings
+### Output Selection
----
+Default standalone stages are:
+
+```text
+json,html,summary
+```
+
+| Command value | Effective stages | Main result |
+|---|---|---|
+| Omit `--outputs` | JSON, HTML, summary | Raw suite JSON, suite HTML, merged JSON, combined HTML |
+| `--outputs json` | JSON | Raw suite JSON only |
+| `--outputs html` | JSON, HTML | Raw suite JSON and suite HTML |
+| `--outputs json,html` | JSON, HTML | Same as `--outputs html` |
+| `--outputs summary` | JSON, HTML, summary | Default output without naming every stage |
+| `--outputs pdf` | JSON, HTML, summary, PDF | Complete output including `acs_summary.pdf` |
+
+JSON is always included. Summary automatically includes HTML, and PDF
+automatically includes summary. Separate multiple stage names with commas, for
+example `--outputs json,html`, not spaces. PDF requires WeasyPrint.
+
+An HTML-stage run (`--outputs html` or `--outputs json,html`) does not create
+`merged_results.json` or the combined `acs_summary.html`; those files belong to
+the summary stage.
+
+### Existing Output
+
+Standalone requires a new output path. If the requested path already exists,
+the command fails before parsing.
+
+To rerun, do one of these before starting the parser:
+
+1. Delete the stale output directory after confirming it is no longer needed.
+2. Move the stale output directory to an archive location.
+3. Choose a different `--output` path.
+
+The parser never deletes or replaces an existing directory.
+
+When `--input-log` is an ACS results directory and `--output` is omitted, the
+default is `/acs_summary`. That default must not already exist.
+
+## Supported Suites and Inputs
+
+Paths in this table are relative to the directory passed through `--input-log`.
+Entries beginning with `../fw/` or `../os-logs/` resolve through sibling
+directories beside that input directory.
+
+| Canonical suite | Mode | Registered input | Input rule |
+|---|---|---|---|
+| `BSA` | DT, SR | `uefi/BsaResults.log`; kernel alternatives | At least one |
+| `SBSA` | SR | `uefi/SbsaResults.log`; `linux/SbsaResultsKernel.log` | At least one |
+| `FWTS` | DT, SR | `fwts/FWTSResults.log` | Required |
+| `SCT` | DT, SR | `sct_results/Overall/Summary.log`; optional `edk2-test-parser/edk2-test-parser.log` | Summary required |
+| `BBSR-FWTS` | DT, SR | `bbsr/fwts/FWTSResults.log` | Required |
+| `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 |
+| `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 |
+| `DT-KSELFTEST` | DT | `linux_tools/dt_kselftest.log` | Required |
+| `DT-VALIDATE` | DT | `linux_tools/dt-validate-parser.log` | Required |
+| `ETHTOOL-TEST` | DT | `linux_tools/ethtool-test.log` | Required |
+| `READ-WRITE-CHECK-BLK-DEVICES` | DT | `linux_tools/read_write_check_blk_devices.log` | Required |
+| `CAPSULE-UPDATE` | DT | Required `../fw/capsule_test_results.log`; optional `../fw/capsule-update.log` and `../fw/capsule-on-disk.log` | Results required |
+| `PSCI` | DT | `linux_tools/psci/psci_kernel.log` | Required |
+| `SMBIOS` | DT | `sct_results/Overall/Summary.log` | Required |
+| `NETWORK-BOOT` | DT | `network_boot/network_boot_results.log` | Required |
+| `RUNTIME-DEV-MAP` | DT | `linux_tools/runtime_device_mapping_conflict_test.log` | Required |
+| `OS-TESTS` | DT, SR | `../os-logs/` directory; optional `post-script/post-script.log` | Directory required |
+
+The **Input rule** column describes what the standalone parser must find when
+that suite is selected. It is not the suite's Mandatory, Recommended, or
+Extension compliance classification.
+
+BSA kernel alternatives are:
+
+```text
+linux_acs/bsa_acs_app/BsaResultsKernel.log
+linux/BsaResultsKernel.log
+```
+### How the Registry Is Used
+
+`common/tools/suite_registry.json` is the standalone runner's suite directory.
+For each suite it defines:
+
+- canonical name, accepted aliases, and DT/SR availability;
+- exact input roots and candidate paths;
+- parser and HTML-generator scripts;
+- raw JSON and HTML filenames;
+- waiver/compliance mapping and the matching merged suite schema definition;
+- grouped-suite expansion and required Python modules.
+
+When a user selects BSA, for example, the runner reads the BSA registry entry,
+checks its UEFI and kernel candidates, runs the registered BSA parser, and
+writes the registered outputs. `--doctor` uses the same entry and prints an
+error containing the expected path when a required input is missing.
+
+Changing a registered input path changes standalone discovery for that suite.
+It does not change the normal parser, which retains its original paths. Changing
+a canonical suite name is a larger compatibility change because category files,
+schemas, waivers, parser-emitted names, and report labels may also contain that
+name.
+
+Registered candidate paths must remain relative to their assigned `results`,
+`firmware`, or `os_logs` root. Absolute paths and paths containing `..` are
+rejected. Use direct `--input-log` files instead of placing machine-specific
+absolute paths in a shared registry.
+
+### Suite Groups
+
+| Group | Mode | Expansion |
+|---|---|---|
+| `SBMR` | SR | `SBMR-IB`, `SBMR-OOB` |
+| `STANDALONE` | DT | The nine DT tests listed below |
+
+`STANDALONE` expands to:
+
+```text
+DT-KSELFTEST
+DT-VALIDATE
+ETHTOOL-TEST
+READ-WRITE-CHECK-BLK-DEVICES
+CAPSULE-UPDATE
+PSCI
+SMBIOS
+NETWORK-BOOT
+RUNTIME-DEV-MAP
+```
-
-
-
+The uppercase `STANDALONE` value is a suite group. It is different from the
+lowercase `--standalone` option that selects the portable runner.
-### 📊 Auto-Detected Component Status
+### Direct Input Names
-**Test Suite Parsers Detected:** 9
+| Suite | Names accepted before `=` |
+|---|---|
+| `BSA`, `SBSA` | `uefi`, `kernel` |
+| `SCT`, `BBSR-SCT` | `log`, `edk2` |
+| `SBMR-IB`, `SBMR-OOB` | `log`, `report` |
+| `CAPSULE-UPDATE` | `update`, `on_disk`, `results` |
+| Other direct-file suites | `log` |
-| Suite | Parser | HTML Generator | Path |
-|-------|--------|----------------|------|
-| bsa | ✅ | ✅ | `bsa/` |
-| bbr_fwts | ✅ | ✅ | `bbr/fwts/` |
-| bbr_sct | ✅ | ✅ | `bbr/sct/` |
-| bbr_tpm | ✅ | ✅ | `bbr/tpm/` |
-| os_tests | ✅ | ✅ | `os_tests/` |
-| pfdi | ✅ | ✅ | `pfdi/` |
-| post_script | ✅ | ✅ | `post_script/` |
-| sbmr | ✅ | ✅ | `sbmr/` |
-| standalone_tests | ✅ | ✅ | `standalone_tests/` |
+## Configuration and Waivers
-**Waiver-Supported Suites:** BBSR-FWTS, BBSR-SCT, BBSR-TPM, BSA, FWTS, PFDI, SBSA, SBMR, SCT, STANDALONE
+### ACS Config
-**Test Category Files:**
+ACS config is optional. It supplies summary metadata. Standalone validates a
+provided `Band` against the selected mode, including the default SR mode.
-- Runtime paths: `/usr/bin/log_parser/test_category.json`, `/usr/bin/log_parser/test_categoryDT.json`
-- Repo copies: `test_category.json`, `test_categoryDT.json` (content format may differ from packaged files)
+Expected names:
-
+```text
+DT -> acs_config_dt.txt
+SR -> acs_config.txt
+```
----
+`--mode` selects DT or SR; the filename does not. If `--mode` is omitted, SR is
+selected. The runner reads the ACS config's `Band` value and stops with exit
+code 3 if that value conflicts with the selected mode. A nonstandard filename
+produces only a naming warning when its content matches the selected mode.
-## Troubleshooting
+### System Config
-### Common Issues and Solutions
+System config is optional and supplies system metadata.
-#### 1. Missing Log Files
-**Error**: `ERROR: Log file "" is missing`
+Expected names:
-**Solutions**:
-- Verify test suite actually ran
-- Check file path matches expected structure
-- For optional suites, ignore warning
-- For mandatory suites, rerun tests
+```text
+DT -> system_config_dt.txt
+SR -> system_config.txt
+```
-#### 2. JSON Parsing Errors
-**Error**: `ERROR: BSA logs parsing to json failed`
+These filenames are conventions used by the collection environment. The runner
+copies a supplied config into the output using the mode-appropriate filename.
-**Solutions**:
-- Check log file format/encoding
-- Verify log file is complete (not truncated)
-- Look for parsing script errors in terminal output
-- Validate log file matches expected format
+The standalone runner reads machine information only from supplied configs and
+archived result logs. It does not inspect the parser host with `dmidecode`.
-#### 3. Waiver Not Applied
-**Issue**: Test still shows FAILED instead of FAILED (WITH WAIVER)
+### Waiver JSON
-**Solutions**:
-- Check waiver.json syntax (use JSON validator)
-- Verify suite name matches exactly (case-sensitive)
-- Ensure TestSuite/TestCase names match log output
-- Check that "Reason" field is present and non-empty
-- Review apply_waivers.py output for matching errors
+Waivers are optional. They apply only when a waiver file is supplied. A waiver
+does not remove a test; it records the approved reason and updates compliance
+according to the existing waiver logic and category waivability.
-#### 4. System Info Shows "Unknown"
-**Issue**: System information fields show "Unknown"
+Minimal structure:
-**Solutions**:
-- Run with `sudo` (dmidecode requires root)
-- Verify dmidecode is installed: `which dmidecode`
-- Check system_config.txt is provided and readable
-- Manually populate system_config.txt with info
+```json
+{
+ "Suites": [
+ {
+ "Suite": "BSA",
+ "TestSuites": [
+ {
+ "TestSuite": "TIMER",
+ "TestCases": [
+ {
+ "Test_case": "B_TIME_02 : 407",
+ "Reason": "Approved platform-specific waiver reason."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+```
-#### 5. PDF Generation Fails
-**Error**: PDF not created in DT mode
+Use precise suite, test-suite, test-case, and subtest identifiers from generated
+JSON. Keep waiver reasons explicit and reviewable.
-**Solutions**:
-- Install weasyprint: `pip3 install weasyprint`
-- Check HTML file exists and is valid
-- Verify sufficient disk space
-- Review weasyprint dependencies
+Before parsing, standalone verifies that the waiver file is valid JSON with a
+top-level `Suites` array. An unreadable, malformed, or incorrectly structured
+waiver file stops with exit code 3 and no output is published. This input check
+does not guarantee that valid waiver entries match the selected suite results.
-#### 6. Compliance Status Incorrect
-**Issue**: Expected PASS but shows FAIL (or vice versa)
+### Category Files
-**Solutions**:
-- Review merged_results.json for suite statuses
-- Check compliance scope table (M/R/EM/CM)
-- Verify all mandatory suite failures are waived
-- Check for unintended extension suite failures
+Standalone defaults to bundled category files:
-#### 7. Performance Issues
-**Issue**: Parser takes very long time
+```text
+DT -> common/log_parser/test_categoryDT.json
+SR -> common/log_parser/test_category.json
+```
-**Solutions**:
-- Large log files can slow parsing
-- Check disk I/O performance
-- Reduce unnecessary debug output
-- Consider running on faster storage
+Use `--test-category` when logs belong to a release with a different category
+file. The selected file is used by waiver handling, raw metadata enrichment,
+and merged compliance generation.
-#### 8. Permission Denied Errors
-**Error**: Cannot write to output directory
+The normal parser uses the installed category file selected by its auto-detected
+mode. That file is also applied to individual suite JSON files before they are
+merged.
-**Solutions**:
-- Run with `sudo`
-- Verify write permissions on acs_results directory
-- Check disk space availability
-- Ensure parent directories exist
+## Output Files
-### Debug Mode
+### Normal Output
-To enable verbose output in waiver application:
-```bash
-# Edit apply_waivers.py, set:
-verbose = True # Near line 22
+```text
+/acs_summary/
```
-To enable path printing:
-```bash
-# Edit main_log_parser.sh, set:
-print_path=1 # Near line 900
+Final individual suite JSON files are stored in
+`/acs_summary/acs_jsons/`. When a suite and test-suite entry matches
+the selected category file, each corresponding test result receives the same
+available `Test_suite_info`, `Waivable`, `SRS scope`, and
+`Main Readiness Grouping` metadata used in `merged_results.json`. For example,
+`Test_suite_info` is added only when the matching category row provides a
+`Description`. An entry without a category match is left unchanged and is
+reported by the enrichment step.
+
+### Standalone Output
+
+```text
+