From 6c5691ac2b1947aa003e51ea39f568630604fbec Mon Sep 17 00:00:00 2001 From: Ashish Sharma Date: Mon, 7 Sep 2026 07:53:11 +0000 Subject: [PATCH] (improv): recover truncated SCT subtest records - Keep Summary.log as the primary SCT source and use aligned Summary.ekl fields only when records are proven truncated or joined. - Recover subtest descriptions and separate source paths and reasons without changing GUIDs, results, or suite totals. - Preserve validated SCT reasons when EDK overrides provide an empty reason, while keeping non-empty EDK reasons authoritative. - Add regression coverage for corrupted records, Summary-first behavior, and EDK reason precedence. Signed-off-by: Ashish Sharma Change-Id: I229bec1ff1e084a6fd3f7d31382e375e272f88ac --- .../logs-to-json.yaml | 94 +++++++ common/log_parser/bbr/sct/logs_to_json.py | 241 +++++++++++++++++- 2 files changed, 332 insertions(+), 3 deletions(-) diff --git a/common/acs_test_framework_manifests/logs-to-json.yaml b/common/acs_test_framework_manifests/logs-to-json.yaml index 271c8716..5a52efde 100644 --- a/common/acs_test_framework_manifests/logs-to-json.yaml +++ b/common/acs_test_framework_manifests/logs-to-json.yaml @@ -246,6 +246,100 @@ suites: sub_Test_Path: "RULE : 1" + - name: sct_logs_to_json_specific + files: + - common/log_parser/bbr/sct/logs_to_json.py + + cases: + - name: cli_sct_json_has_no_empty_subtest_description + type: cli + command: "./run_case.sh" + timeout_sec: 5 + scripts: + run_case.sh: | + #!/bin/sh + set -eu + + cat > Summary.log < Summary.ekl < edk2_test_parser.json < 8 else "" + test_entry_guid = candidate.upper() + continue + + record_header, record_separator, payload = line.partition("|") + subtest_guid, field_separator, result = record_header.partition(":") + if ( + not record_separator + or not field_separator + or not subtest_guid + or not test_entry_guid + ): + continue + + normalized_result = normalize_result(result) + if normalized_result not in {"PASSED", "FAILED", "WARNING", "NOT SUPPORTED"}: + continue + records.append(( + test_entry_guid, + subtest_guid.upper(), + normalized_result, + payload, + )) + except (OSError, UnicodeError, LookupError): + return [] + + return records + +def build_sct_field_updates(results, input_file): + """Prefer Summary fields and use aligned EKL data only for faulty fields.""" + log_records = [] + for test in results: + test_entry_guid = test.get("Test Entry Point GUID", "").strip().upper() + for subtest in test.get("subtests", []): + log_records.append(( + test_entry_guid, + subtest.get("sub_Test_GUID", "").strip().upper(), + normalize_result(subtest.get("sub_test_result", "")), + subtest, + subtest.pop("_raw_test_description", ""), + subtest.pop("_summary_detail_limit", None), + )) + + ekl_records = read_ekl_description_records(input_file) + # GUIDs repeat, so occurrence order and result are part of the identity. + # If the complete streams do not align, keep the legacy Summary output. + if ( + len(log_records) != len(ekl_records) + or any(log[:3] != ekl[:3] for log, ekl in zip(log_records, ekl_records)) + ): + return [] + + updates = [] + for log, ekl in zip(log_records, ekl_records): + subtest = log[3] + raw_description = log[4] + summary_description = subtest.get("sub_Test_Description", "") + summary_detail = subtest.get("sub_Test_Path", "") + summary_payload = f"{summary_description}:{summary_detail}" + + description = summary_description + normalized_summary_detail, path, reason = find_sct_path_and_reason( + summary_payload, + summary_description, + ) + + ekl_description = find_ekl_description(ekl[3], raw_description) + if ekl_description is not None: + normalized_ekl_detail, ekl_path, ekl_reason = ( + find_sct_path_and_reason(ekl[3], ekl_description) + ) + description_is_faulty = raw_description != ekl_description + detail_limit = log[5] + if detail_limit is not None: + detail_limit += len(raw_description) - len(ekl_description) + # At the old 510-character boundary, accept EKL only when the + # surviving Summary prefix proves later diagnostic text was joined. + appended_detail = ( + detail_limit is not None + and detail_limit >= 0 + and len(normalized_summary_detail) > detail_limit + and not normalized_summary_detail.startswith(normalized_ekl_detail) + and normalized_ekl_detail.startswith( + normalized_summary_detail[:detail_limit].rstrip() + ) + ) + # A result marker, strict prefix, or proven appended tail makes the + # Summary detail unsafe; arbitrary Summary/EKL differences do not. + detail_is_faulty = bool(FUSED_RESULT_PATTERN.search( + normalized_summary_detail + )) or ( + normalized_summary_detail != normalized_ekl_detail + and ( + normalized_ekl_detail.startswith(normalized_summary_detail) + or appended_detail + ) + ) + + if description_is_faulty: + description = ekl_description + if detail_is_faulty: + path, reason = ekl_path, ekl_reason + + updates.append(( + log[3], + description, + path, + reason, + )) + return updates + def find_test_suite_and_subsuite(test_case_name): for test_suite, sub_suites in test_mapping.items(): for sub_suite, test_cases in sub_suites.items(): @@ -582,7 +793,8 @@ def main(input_file, output_file): # Sub-test detection from lines like "FooTest -- PASS" if re.search(r'--\s*(PASS|FAIL|FAILURE|WARNING|NOT SUPPORTED)', line, re.IGNORECASE): parts = line.rsplit(' -- ', 1) - test_desc = clean_test_description(parts[0]) + raw_test_desc = parts[0] + test_desc = clean_test_description(raw_test_desc) result_str = normalize_result(parts[1]) # Tally in test_case_summary *before* overrides @@ -606,6 +818,12 @@ def main(input_file, output_file): test_guid = lines[i+1].strip() if i+1 < len(lines) else "" file_path = lines[i+2].strip() if i+2 < len(lines) else "" + # Old SCT records keep at most 510 UTF-16 characters. Account + # for the result line, GUID, and their two CRLF separators so + # appended diagnostic text can be distinguished from a clean, + # longer Summary detail. + summary_detail_limit = 510 - len(line) - len(test_guid) - 4 + sub_test_number += 1 reason = "" @@ -620,7 +838,9 @@ def main(input_file, output_file): "sub_Test_GUID": test_guid, "sub_test_result": result_str, "sub_Test_Path": file_path, - "reason": reason + "reason": reason, + "_raw_test_description": raw_test_desc, + "_summary_detail_limit": summary_detail_limit } test_entry["subtests"].append(sub_test) @@ -628,6 +848,15 @@ def main(input_file, output_file): if test_entry: results.append(test_entry) + field_updates = build_sct_field_updates(results, input_file) + + # Track reasons validated through the aligned Summary/EKL stream. An empty + # EDK reason must not erase these; a non-empty EDK reason still wins. + trusted_reason_subtests = set() + for subtest, _, _, reason in field_updates: + subtest["reason"] = reason + trusted_reason_subtests.add(id(subtest)) + # Skip SMBIOS tests in DT mode if DT_OR_SR_MODE == "DT": results = [test for test in results if not is_smbios_test(test.get("Test_case", ""))] @@ -695,7 +924,13 @@ def main(input_file, output_file): reason_val = match_record.get("reason", "").strip() if result_val: subtest["sub_test_result"] = normalize_result(result_val) - subtest["reason"] = reason_val + if reason_val or id(subtest) not in trusted_reason_subtests: + subtest["reason"] = reason_val + + # Apply canonical descriptions and paths after the existing EDK overrides. + for subtest, description, path, _ in field_updates: + subtest["sub_Test_Description"] = description + subtest["sub_Test_Path"] = path # Reorder final dictionary so "test_result" & "reason" appear after "Returned Status Code" for i, test_obj in enumerate(results):