diff --git a/common/log_parser/apply_waivers.py b/common/log_parser/apply_waivers.py
index 94ad92f0..d79599da 100755
--- a/common/log_parser/apply_waivers.py
+++ b/common/log_parser/apply_waivers.py
@@ -14,23 +14,152 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+"""Apply waiver files to parsed ACS JSON results."""
+
import json
-import sys
import re
import argparse
def clean_description(desc):
+ """Normalize a description string for older description-based waiver matching."""
desc = desc.strip().lower()
desc = re.sub(r'\s+', ' ', desc) # Replace multiple spaces with a single space
desc = re.sub(r'[^\w\s-]', '', desc) # Remove special characters except hyphens
return desc
-def _as_list(v):
- if v is None:
+def _as_list(value):
+ """Return a value as a list so callers can handle one or many entries."""
+ if value is None:
return []
- return v if isinstance(v, list) else [v]
+ return value if isinstance(value, list) else [value]
+
+# New BSA/SBSA JSON no longer needs a separate sub_Rule_ID field. When only
+# sub_Test_Number is present, the rule id is the text before the first ':'.
+def _rule_id_from_test_number(test_number):
+ """Extract the rule id from a visible test number such as 'ITS_04 : 1535'."""
+ if not isinstance(test_number, str):
+ return ''
+ return test_number.split(':', 1)[0].strip()
+
+def _subtest_rule_id(subtest):
+ """Return the best available rule id for old and new subtest JSON."""
+ return subtest.get('sub_Rule_ID') or _rule_id_from_test_number(subtest.get('sub_Test_Number'))
+
+# Walk waiver input and parser output the same way. Both "SubTests" from waiver
+# files and "subtests" from parsed JSON can now contain children at any depth.
+def _iter_nested_subtests(subtests):
+ """Yield every subtest from a recursive subtest tree."""
+ for subtest in subtests or []:
+ yield subtest
+ yield from _iter_nested_subtests(
+ subtest.get('subtests', []) or subtest.get('SubTests', [])
+ )
+
+def _is_failed_result(result):
+ """Return True when a result string represents any failed status."""
+ if not isinstance(result, str):
+ return False
+ result_upper = result.upper()
+ return 'FAILED' in result_upper or 'FAILURE' in result_upper or 'FAIL' in result_upper
+
+def _has_waiver_result(result):
+ """Return True when a result string is already marked with waiver status."""
+ return isinstance(result, str) and 'WITH WAIVER' in result.upper()
+
+def _append_waiver_to_result(result):
+ """Add the waiver suffix once to an existing result string."""
+ if not isinstance(result, str) or _has_waiver_result(result):
+ return result
+ return result + ' (WITH WAIVER)'
+
+# BSA/SBSA use Test_result, while some older parsers use test_result. Keep the
+# marking in one place so suite/testsuite/testcase waivers behave consistently.
+def _mark_failed_case_waived(testcase, reason):
+ """Mark a failed testcase as waived when it is not already waived."""
+ result_key = 'Test_result' if 'Test_result' in testcase else 'test_result'
+ test_result = testcase.get(result_key, '')
+ if _is_failed_result(test_result) and not _has_waiver_result(test_result):
+ testcase[result_key] = _append_waiver_to_result(test_result)
+ testcase['waiver_reason'] = reason
+ return True
+ return False
+
+# Suite, testsuite, and testcase waivers apply to the whole failed branch. Mark
+# every failed nested child so HTML and JSON show the waiver at the exact rule.
+def _mark_failed_subtests_waived(subtests, reason):
+ """Mark every failed subtest in a nested branch as waived."""
+ marked = False
+ for subtest in _iter_nested_subtests(subtests):
+ sub_result = subtest.get('sub_test_result')
+ if _is_failed_result(sub_result) and not _has_waiver_result(sub_result):
+ subtest['sub_test_result'] = _append_waiver_to_result(sub_result)
+ subtest['waiver_reason'] = reason
+ marked = True
+ return marked
+
+# If a failed parent only contains waived failed children, mark that parent as
+# failed-with-waiver too. Passing/skipped/not-implemented children do not block
+# this because they are not unwaived failures.
+def _propagate_nested_bsa_waivers(testcase):
+ """Move waiver state upward through failed BSA/SBSA nested parents."""
+ def propagate_subtest(subtest):
+ """Return failed, unwaived failed, and waived failed state for one branch."""
+ child_has_failed = False
+ child_has_unwaived_failed = False
+ child_has_waived_failed = False
+
+ for child in subtest.get('subtests', []) or []:
+ child_failed, child_unwaived, child_waived = propagate_subtest(child)
+ child_has_failed = child_has_failed or child_failed
+ child_has_unwaived_failed = child_has_unwaived_failed or child_unwaived
+ child_has_waived_failed = child_has_waived_failed or child_waived
+
+ result = subtest.get('sub_test_result', '')
+ is_failed = _is_failed_result(result)
+ has_waiver = _has_waiver_result(result)
+
+ if (
+ is_failed
+ and not has_waiver
+ and subtest.get('subtests')
+ and child_has_waived_failed
+ and not child_has_unwaived_failed
+ ):
+ subtest['sub_test_result'] = _append_waiver_to_result(result)
+ subtest.setdefault('waiver_reason', 'All failed nested subtests were waived.')
+ has_waiver = True
+
+ return (
+ is_failed or child_has_failed,
+ (is_failed and not has_waiver) or child_has_unwaived_failed,
+ (is_failed and has_waiver) or child_has_waived_failed
+ )
+
+ has_failed = False
+ has_unwaived_failed = False
+ has_waived_failed = False
+
+ for subtest in testcase.get('subtests', []) or []:
+ sub_failed, sub_unwaived, sub_waived = propagate_subtest(subtest)
+ has_failed = has_failed or sub_failed
+ has_unwaived_failed = has_unwaived_failed or sub_unwaived
+ has_waived_failed = has_waived_failed or sub_waived
+
+ test_result = testcase.get('Test_result', testcase.get('test_result', ''))
+ if (
+ _is_failed_result(test_result)
+ and not _has_waiver_result(test_result)
+ and has_failed
+ and has_waived_failed
+ and not has_unwaived_failed
+ ):
+ _mark_failed_case_waived(
+ testcase,
+ 'All failed nested subtests were waived.'
+ )
def load_waivers(waiver_data, suite_name):
+ """Collect waiver entries by scope for the requested suite."""
suite_level_waivers = []
testsuite_level_waivers = []
subsuite_level_waivers = [] # Added for SubSuite-level waivers
@@ -80,20 +209,26 @@ def load_waivers(waiver_data, suite_name):
# Also load SubTest-level waivers for this testcase
if 'SubTests' in testcase_entry:
- for subtest_entry in testcase_entry['SubTests']:
+ # Nested waiver entries are flattened here, but
+ # each entry still keeps its testcase scope.
+ for subtest_entry in _iter_nested_subtests(testcase_entry['SubTests']):
subtest_id = subtest_entry.get('sub_Rule_ID')
+ subtest_number = subtest_entry.get('sub_Test_Number')
+ subtest_path = subtest_entry.get('sub_Test_Path')
subtest_desc = subtest_entry.get('sub_Test_Description')
subtest_reason = subtest_entry.get('Reason')
- if (subtest_id or subtest_desc) and subtest_reason:
+ if (subtest_id or subtest_number or subtest_path or subtest_desc) and subtest_reason:
subtest_level_waivers.append({
'Test_case': testcase_name,
'sub_Rule_ID': subtest_id,
+ 'sub_Test_Number': subtest_number,
+ 'sub_Test_Path': subtest_path,
'sub_Test_Description': subtest_desc,
'Reason': subtest_reason
})
- elif (subtest_id or subtest_desc) and not subtest_reason:
+ elif (subtest_id or subtest_number or subtest_path or subtest_desc) and not subtest_reason:
if verbose:
- subtest_key = subtest_id or subtest_desc
+ subtest_key = subtest_path or subtest_number or subtest_id or subtest_desc
print(f"ERROR: Waiver for SubTest '{subtest_key}' under Test_case '{testcase_name}' is missing 'Reason'. Skipping SubTest-level waiver.")
if suite_name.upper() == 'SCMI':
if 'TestCases' in test_suite:
@@ -143,7 +278,7 @@ def load_waivers(waiver_data, suite_name):
# Collect SubTest-level waivers
subtests = test_suite.get('TestCase', {}).get('SubTests', []) or test_suite.get('SubSuite', {}).get('TestCase', {}).get('SubTests', [])
- for subtest in subtests:
+ for subtest in _iter_nested_subtests(subtests):
if 'Reason' not in subtest or not subtest['Reason']:
subtest_id_or_desc = subtest.get('SubTestID') or subtest.get('sub_Test_Description') or 'Unknown'
if verbose:
@@ -175,19 +310,17 @@ def load_waivers(waiver_data, suite_name):
return suite_level_waivers, testsuite_level_waivers, subsuite_level_waivers, testcase_level_waivers, subtest_level_waivers
def apply_suite_level_waivers(test_suite_entry, suite_waivers):
+ """Apply suite-level waivers to failed results below a suite."""
# Apply waivers to all applicable failed subtests in the suite
for waiver in suite_waivers:
reason = waiver['Reason']
# Apply to testcases (BSA/SBSA/SCMI)
for testcase in test_suite_entry.get('testcases', []):
- test_result = testcase.get('test_result', '')
- if isinstance(test_result, str):
- if 'FAILED' in test_result.upper() and '(WITH WAIVER)' not in test_result.upper():
- testcase['test_result'] = test_result + ' (WITH WAIVER)'
- testcase['waiver_reason'] = reason
- if verbose:
- print(f"Suite-level waiver applied to testcase '{testcase.get('Test_case')}' with reason: {reason}")
+ if _mark_failed_case_waived(testcase, reason):
+ _mark_failed_subtests_waived(testcase.get('subtests', []), reason)
+ if verbose:
+ print(f"Suite-level waiver applied to testcase '{testcase.get('Test_case')}' with reason: {reason}")
# For other structures: apply to subtests
for subtest in test_suite_entry.get('subtests', []):
@@ -229,13 +362,14 @@ def apply_suite_level_waivers(test_suite_entry, suite_waivers):
updated_fail_reasons = [fr + ' (WITH WAIVER)' for fr in existing_fail_reasons]
sub_test_result['fail_reasons'] = updated_fail_reasons
elif isinstance(sub_test_result, str):
- ru = sub_test_result.upper()
- if 'FAILED' in ru or 'FAILURE' in ru or 'FAIL' in ru:
- if '(WITH WAIVER)' not in ru:
+ result_upper = sub_test_result.upper()
+ if 'FAILED' in result_upper or 'FAILURE' in result_upper or 'FAIL' in result_upper:
+ if '(WITH WAIVER)' not in result_upper:
subtest['sub_test_result'] += ' (WITH WAIVER)'
subtest['waiver_reason'] = reason
def apply_testsuite_level_waivers(test_suite_entry, testsuite_waivers):
+ """Apply testsuite-level waivers to matching failed testcases or subtests."""
# Get the test_suite_name considering different keys
test_suite_name = test_suite_entry.get('Test_suite') or test_suite_entry.get('Test_suite_name')
# Apply waivers to all applicable failed tests within specific TestSuites
@@ -245,13 +379,10 @@ def apply_testsuite_level_waivers(test_suite_entry, testsuite_waivers):
if test_suite_name == target_testsuite:
# For BSA/SBSA: apply to testcases
for testcase in test_suite_entry.get('testcases', []):
- test_result = testcase.get('Test_result', '')
- if isinstance(test_result, str):
- if 'FAILED' in test_result.upper() and '(WITH WAIVER)' not in test_result.upper():
- testcase['Test_result'] = test_result + ' (WITH WAIVER)'
- testcase['waiver_reason'] = reason
- if verbose:
- print(f"TestSuite-level waiver applied to testcase '{testcase.get('Test_case')}' in TestSuite '{target_testsuite}' with reason: {reason}")
+ if _mark_failed_case_waived(testcase, reason):
+ _mark_failed_subtests_waived(testcase.get('subtests', []), reason)
+ if verbose:
+ print(f"TestSuite-level waiver applied to testcase '{testcase.get('Test_case')}' in TestSuite '{target_testsuite}' with reason: {reason}")
# For other structures: apply to subtests
for subtest in test_suite_entry.get('subtests', []):
@@ -295,13 +426,14 @@ def apply_testsuite_level_waivers(test_suite_entry, testsuite_waivers):
updated_fail_reasons = [fr + ' (WITH WAIVER)' for fr in existing_fail_reasons]
sub_test_result['fail_reasons'] = updated_fail_reasons
elif isinstance(sub_test_result, str):
- ru = sub_test_result.upper()
- if 'FAILED' in ru or 'FAILURE' in ru:
- if '(WITH WAIVER)' not in ru:
+ result_upper = sub_test_result.upper()
+ if 'FAILED' in result_upper or 'FAILURE' in result_upper:
+ if '(WITH WAIVER)' not in result_upper:
subtest['sub_test_result'] += ' (WITH WAIVER)'
subtest['waiver_reason'] = reason
def apply_subsuite_level_waivers(test_suite_entry, subsuite_waivers):
+ """Apply subsuite-level waivers where the parsed JSON has subsuite results."""
# Apply waivers to all applicable failed subtests within specific SubSuites
for waiver in subsuite_waivers:
target_subsuite = waiver['SubSuite']
@@ -331,6 +463,7 @@ def apply_subsuite_level_waivers(test_suite_entry, subsuite_waivers):
print(f"SubSuite-level waiver applied to subtest '{subtest.get('sub_Test_Description')}' with reason: {reason}")
def apply_testcase_level_waivers(test_suite_entry, testcase_waivers):
+ """Apply testcase-level waivers to matching failed testcases."""
# Apply waivers to all applicable failed subtests within specific Test_cases
for waiver in testcase_waivers:
target_testcase = waiver['Test_case']
@@ -343,13 +476,10 @@ def apply_testcase_level_waivers(test_suite_entry, testcase_waivers):
test_case_id = test_case_name.split(':')[0].strip() if ':' in test_case_name else test_case_name
if test_case_id == target_testcase or test_case_name == target_testcase:
- test_result = testcase.get('Test_result', '')
- if isinstance(test_result, str):
- if 'FAILED' in test_result.upper() and '(WITH WAIVER)' not in test_result.upper():
- testcase['Test_result'] = test_result + ' (WITH WAIVER)'
- testcase['waiver_reason'] = reason
- if verbose:
- print(f"Test_case-level waiver applied to testcase '{test_case_name}' with reason: {reason}")
+ if _mark_failed_case_waived(testcase, reason):
+ _mark_failed_subtests_waived(testcase.get('subtests', []), reason)
+ if verbose:
+ print(f"Test_case-level waiver applied to testcase '{test_case_name}' with reason: {reason}")
# For other structures: apply to subtests
if test_suite_entry.get('Test_case') == target_testcase:
@@ -394,21 +524,26 @@ def apply_testcase_level_waivers(test_suite_entry, testcase_waivers):
updated_fail_reasons = [fr + ' (WITH WAIVER)' for fr in existing_fail_reasons]
sub_test_result['fail_reasons'] = updated_fail_reasons
elif isinstance(sub_test_result, str):
- ru = sub_test_result.upper()
- if 'FAILED' in ru or 'FAILURE' in ru:
- if '(WITH WAIVER)' not in ru:
+ result_upper = sub_test_result.upper()
+ if 'FAILED' in result_upper or 'FAILURE' in result_upper:
+ if '(WITH WAIVER)' not in result_upper:
subtest['sub_test_result'] += ' (WITH WAIVER)'
subtest['waiver_reason'] = reason
def apply_subtest_level_waivers(test_suite_entry, subtest_waivers, suite_name):
+ """Apply subtest-level waivers, including nested BSA/SBSA subtests."""
# For BSA/SBSA: apply waivers to subtests within testcases
if suite_name.upper() in ['BSA', 'SBSA']:
for testcase in test_suite_entry.get('testcases', []):
testcase_name = testcase.get('Test_case', '')
testcase_id = testcase_name.split(':')[0].strip() if ':' in testcase_name else testcase_name
- for subtest in testcase.get('subtests', []):
- sub_rule_id = subtest.get('sub_Rule_ID', '')
+ # Check every nested subtest, not only direct children of the
+ # testcase, so deeper SBSA/BSA rule failures can be waived.
+ for subtest in _iter_nested_subtests(testcase.get('subtests', [])):
+ sub_rule_id = _subtest_rule_id(subtest)
+ sub_test_number = subtest.get('sub_Test_Number', '')
+ sub_test_path = subtest.get('sub_Test_Path', '')
sub_test_desc = subtest.get('sub_Test_Description', '')
sub_test_result = subtest.get('sub_test_result', '')
@@ -421,13 +556,22 @@ def apply_subtest_level_waivers(test_suite_entry, subtest_waivers, suite_name):
if waiver_testcase_id != testcase_id and waiver_testcase != testcase_id:
continue
- # Match by sub_Rule_ID or sub_Test_Description
+ # sub_Test_Path is safest for nested logs because the same
+ # sub_Test_Number can appear in different parent branches.
+ # Number, rule id, and description are kept as fallbacks for
+ # old waiver files and hand-written waivers.
waiver_id = waiver.get('sub_Rule_ID', '')
+ waiver_number = waiver.get('sub_Test_Number', '')
+ waiver_path = waiver.get('sub_Test_Path', '')
waiver_desc = waiver.get('sub_Test_Description', '')
reason = waiver.get('Reason', '')
match = False
- if waiver_id and waiver_id == sub_rule_id:
+ if waiver_path and waiver_path == sub_test_path:
+ match = True
+ elif waiver_number and waiver_number == sub_test_number:
+ match = True
+ elif waiver_id and waiver_id == sub_rule_id:
match = True
elif waiver_desc and waiver_desc == sub_test_desc:
match = True
@@ -627,32 +771,33 @@ def apply_subtest_level_waivers(test_suite_entry, subtest_waivers, suite_name):
break
def apply_waivers(suite_name, json_file, waiver_file='waiver.json', output_json_file=None):
+ """Apply all matching waivers to one parsed JSON file."""
# Load the JSON data
try:
- with open(json_file, 'r') as f:
- json_data = json.load(f)
- except Exception as e:
+ with open(json_file, 'r', encoding='utf-8') as json_handle:
+ json_data = json.load(json_handle)
+ except Exception as err:
if verbose:
- print(f"WARNING: Failed to read or parse {json_file}: {e}")
+ print(f"WARNING: Failed to read or parse {json_file}: {err}")
return
# Load waiver.json
try:
- with open(waiver_file, 'r') as f:
- waiver_data = json.load(f)
- except Exception as e:
+ with open(waiver_file, 'r', encoding='utf-8') as waiver_handle:
+ waiver_data = json.load(waiver_handle)
+ except Exception as err:
if verbose:
- print(f"INFO: Failed to read or parse {waiver_file}: {e}")
+ print(f"INFO: Failed to read or parse {waiver_file}: {err}")
return
# Load test_category.json if provided
if output_json_file:
try:
- with open(output_json_file, 'r') as f:
- output_json_data = json.load(f)
- except Exception as e:
+ with open(output_json_file, 'r', encoding='utf-8') as output_handle:
+ output_json_data = json.load(output_handle)
+ except Exception as err:
if verbose:
- print(f"WARNING: Failed to read or parse {output_json_file}: {e}")
+ print(f"WARNING: Failed to read or parse {output_json_file}: {err}")
output_json_data = None
else:
output_json_data = None
@@ -692,9 +837,9 @@ def apply_waivers(suite_name, json_file, waiver_file='waiver.json', output_json_
else:
# Check if the test suite is waivable according to test_category.json
waivable = False
- for catID, catData in output_json_data.items():
- # catData is a list
- for row in catData:
+ for _category_id, category_rows in output_json_data.items():
+ # category_rows is a list from test_category.json.
+ for row in category_rows:
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
@@ -741,36 +886,12 @@ def apply_waivers(suite_name, json_file, waiver_file='waiver.json', output_json_
if subtest_level_waivers:
apply_subtest_level_waivers(test_suite_entry, subtest_level_waivers, suite_name)
- # === FIX: Recompute testcase-level Test_result based on subtests (BSA/SBSA) ===
- # After waivers are applied to subtests, testcase result must reflect the actual status
+ # Subtest waivers are applied at the leaf/branch where they match. This
+ # pass then updates failed parents only when no failed child remains
+ # unwaived, keeping mixed waived/unwaived failures visible.
if suite_name.upper() in ('BSA', 'SBSA'):
for testcase in test_suite_entry.get('testcases', []):
- subtests = testcase.get('subtests', [])
- if not subtests:
- continue # No subtests to check, keep original
-
- # Check subtest results to determine testcase result
- has_non_waivered_failed = False
- has_waivered_failed = False
- has_passed = False
- has_skipped = False
-
- for subtest in subtests:
- sub_result = subtest.get('sub_test_result', '')
- if isinstance(sub_result, str):
- sub_upper = sub_result.upper()
- if 'FAILED' in sub_upper:
- if '(WITH WAIVER)' in sub_upper:
- has_waivered_failed = True
- else:
- has_non_waivered_failed = True
- elif 'PASSED' in sub_upper:
- has_passed = True
- elif 'SKIPPED' in sub_upper:
- has_skipped = True
-
- # Keep the original Test_result from the log file
- # Parent testcase result is always set by the parser from the END line
+ _propagate_nested_bsa_waivers(testcase)
# Update test suite summary
# Determine the summary field based on suite name
@@ -797,25 +918,25 @@ def apply_waivers(suite_name, json_file, waiver_file='waiver.json', output_json_
# Process results from testcases only
for testcase in testcases:
- test_result = testcase.get('test_result', '')
+ test_result = testcase.get('Test_result', testcase.get('test_result', ''))
if isinstance(test_result, str):
- r = test_result.upper()
- if 'FAILED' in r:
- if 'WITH WAIVER' in r:
+ result_upper = test_result.upper()
+ if 'FAILED' in result_upper:
+ if 'WITH WAIVER' in result_upper:
waived += 1
else:
failed += 1
- elif 'PAL NOT SUPPORTED' in r:
+ elif 'PAL NOT SUPPORTED' in result_upper:
pal += 1
- elif 'TEST NOT IMPLEMENTED' in r:
+ elif 'TEST NOT IMPLEMENTED' in result_upper:
notimpl += 1
- elif 'SKIPPED' in r:
+ elif 'SKIPPED' in result_upper:
skipped += 1
- elif 'STATUS' in r:
+ elif 'STATUS' in result_upper:
warnings += 1
- elif 'PARTIAL' in r:
+ elif 'PARTIAL' in result_upper:
partial += 1
- elif 'PASS' in r:
+ elif 'PASS' in result_upper:
passed += 1
test_suite_entry['test_suite_summary'] = {
@@ -841,19 +962,19 @@ def apply_waivers(suite_name, json_file, waiver_file='waiver.json', output_json_
for testcase in test_suite_entry.get('testcases', []):
test_result = testcase.get('Test_result', '')
if isinstance(test_result, str):
- r = test_result.upper()
- if 'FAILED' in r:
- if '(WITH WAIVER)' in r:
+ result_upper = test_result.upper()
+ if 'FAILED' in result_upper:
+ if '(WITH WAIVER)' in result_upper:
total_failed_with_waiver += 1
else:
total_failed += 1
- elif 'ABORTED' in r:
+ elif 'ABORTED' in result_upper:
total_aborted += 1
- elif 'SKIPPED' in r:
+ elif 'SKIPPED' in result_upper:
total_skipped += 1
- elif 'WARNING' in r:
+ elif 'WARNING' in result_upper:
total_warnings += 1
- elif 'PASS' in r:
+ elif 'PASS' in result_upper:
total_passed += 1
test_suite_entry['test_suite_summary'] = {
@@ -884,19 +1005,19 @@ def apply_waivers(suite_name, json_file, waiver_file='waiver.json', output_json_
total_skipped += sub_test_result.get('SKIPPED', 0)
total_warnings += sub_test_result.get('WARNINGS', 0)
elif isinstance(sub_test_result, str):
- r = sub_test_result.upper()
- if 'PASS' in r:
+ result_upper = sub_test_result.upper()
+ if 'PASS' in result_upper:
total_passed += 1
- elif 'FAIL' in r:
- if '(WITH WAIVER)' in r:
+ elif 'FAIL' in result_upper:
+ if '(WITH WAIVER)' in result_upper:
total_failed_with_waiver += 1
else:
total_failed += 1
- elif 'ABORTED' in r:
+ elif 'ABORTED' in result_upper:
total_aborted += 1
- elif 'SKIPPED' in r:
+ elif 'SKIPPED' in result_upper:
total_skipped += 1
- elif 'WARNING' in r:
+ elif 'WARNING' in result_upper:
total_warnings += 1
else:
# anything else (IGNORED, NOT SUPPORTED, etc.)
@@ -1123,15 +1244,16 @@ def apply_waivers(suite_name, json_file, waiver_file='waiver.json', output_json_
# Write the updated JSON data back to the file
try:
- with open(json_file, 'w') as f:
- json.dump(json_data, f, indent=4)
+ with open(json_file, 'w', encoding='utf-8') as json_handle:
+ json.dump(json_data, json_handle, indent=4)
print(f"Waivers successfully applied and '{json_file}' has been updated.")
- except Exception as e:
+ except Exception as err:
if verbose:
- print(f"ERROR: Failed to write updated data to {json_file}: {e}")
+ print(f"ERROR: Failed to write updated data to {json_file}: {err}")
return
def main():
+ """Parse command line arguments and apply waivers."""
parser = argparse.ArgumentParser(description='Apply waivers to test suite JSON results.')
parser.add_argument('suite_name', help='Name of the test suite')
parser.add_argument('json_file', help='Path to the JSON file')
diff --git a/common/log_parser/bsa/json_to_html.py b/common/log_parser/bsa/json_to_html.py
index 161b7198..9154c36f 100755
--- a/common/log_parser/bsa/json_to_html.py
+++ b/common/log_parser/bsa/json_to_html.py
@@ -14,22 +14,44 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import json
-import matplotlib.pyplot as plt
+"""Generate BSA/SBSA HTML reports from parsed JSON results."""
+
import base64
+import json
+import os
+import sys
from io import BytesIO
+
from jinja2 import Template
-import os
+import matplotlib.pyplot as plt # pylint: disable=import-error
# Helper function to retrieve dictionary values in a case-insensitive manner
-def get_case_insensitive(d, key, default=0):
- for k, v in d.items():
- if k.lower() == key.lower():
- return v
+def get_case_insensitive(data, key, default=0):
+ """Return a dictionary value by key without requiring exact key casing."""
+ for dict_key, value in data.items():
+ if dict_key.lower() == key.lower():
+ return value
return default
+def has_nested_subtests(subtests):
+ """Return True when any subtest contains child subtests."""
+ for subtest in subtests or []:
+ child_subtests = subtest.get("subtests", [])
+ if child_subtests or has_nested_subtests(child_subtests):
+ return True
+ return False
+
+def annotate_nested_subtests(test_results):
+ """Mark each testcase that needs expand/collapse controls in HTML."""
+ for test_suite in test_results:
+ for testcase in test_suite.get("testcases", []):
+ testcase["has_nested_subtests"] = has_nested_subtests(
+ testcase.get("subtests", [])
+ )
+
# Function to generate bar chart for test results
def generate_bar_chart(suite_summary):
+ """Build a base64 encoded bar chart image for suite summary counts."""
labels = [
'Passed',
'Failed',
@@ -69,11 +91,11 @@ def generate_bar_chart(suite_summary):
# Add percentage labels on top of the bars
total_tests = suite_summary.get('total_rules_run', 0) or sum(sizes)
- for bar, size in zip(bars, sizes):
- yval = bar.get_height()
+ for chart_bar, size in zip(bars, sizes):
+ yval = chart_bar.get_height()
percentage = (size / total_tests) * 100 if total_tests > 0 else 0
plt.text(
- bar.get_x() + bar.get_width()/2,
+ chart_bar.get_x() + chart_bar.get_width()/2,
yval + max(sizes)*0.01,
f'{percentage:.2f}%',
ha='center',
@@ -95,7 +117,16 @@ def generate_bar_chart(suite_summary):
return base64.b64encode(buffer.getvalue()).decode('utf-8')
# Function to generate HTML content for both summary and detailed pages
-def generate_html(suite_summary, test_results, chart_data, output_html_path, test_suite_name, is_summary_page=True):
+def generate_html( # pylint: disable=too-many-arguments,too-many-positional-arguments
+ suite_summary,
+ test_results,
+ chart_data,
+ output_html_path,
+ test_suite_name,
+ is_summary_page=True):
+ """Render either the detailed or summary BSA/SBSA HTML report."""
+ annotate_nested_subtests(test_results)
+
# Template for both summary and detailed pages
template = Template("""
@@ -225,9 +256,186 @@ def generate_html(suite_summary, test_results, chart_data, output_html_path, tes
text-align: center;
font-weight: normal;
}
+ /* Keep rule numbers readable while indentation shows nesting. */
+ .subtest-number {
+ white-space: nowrap;
+ }
+ .testcase-toggle,
+ .subtest-toggle,
+ .subtest-control {
+ border: 1px solid #6c7a89;
+ cursor: pointer;
+ font-weight: 600;
+ transition: background-color 0.15s ease, border-color 0.15s ease,
+ box-shadow 0.15s ease, color 0.15s ease;
+ }
+ .testcase-toggle,
+ .subtest-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ margin-right: 8px;
+ border-radius: 50%;
+ background: #f7fbff;
+ border-color: #6fa8dc;
+ color: #1f5f99;
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1;
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.08);
+ }
+ .testcase-toggle:hover,
+ .subtest-toggle:hover {
+ background: #e8f3ff;
+ border-color: #2f80c7;
+ }
+ .testcase-toggle[aria-expanded="true"],
+ .subtest-toggle[aria-expanded="true"] {
+ background: #edf7ed;
+ border-color: #58a55c;
+ color: #256b2a;
+ }
+ .testcase-toggle[aria-expanded="false"],
+ .subtest-toggle[aria-expanded="false"] {
+ background: #fff7e6;
+ border-color: #d89614;
+ color: #8a5a00;
+ }
+ .testcase-toggle-placeholder,
+ .subtest-toggle-placeholder {
+ display: inline-block;
+ width: 22px;
+ height: 22px;
+ margin-right: 8px;
+ }
+ .testcase-number {
+ white-space: nowrap;
+ }
+ .subtest-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 8px;
+ }
+ .subtest-title {
+ color: #2c3e50;
+ font-weight: bold;
+ }
+ .subtest-controls {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 8px;
+ }
+ .subtest-control {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 1.2;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
+ }
+ .expand-subtests {
+ background: #2f80c7;
+ border-color: #2878b5;
+ color: #fff;
+ }
+ .expand-subtests:hover:not(:disabled) {
+ background: #236a9f;
+ border-color: #1f5f99;
+ }
+ .collapse-subtests {
+ background: #fff;
+ border-color: #9aa9b5;
+ color: #2c3e50;
+ }
+ .collapse-subtests:hover:not(:disabled) {
+ background: #eef3f7;
+ border-color: #6c7a89;
+ }
+ .subtest-control:disabled {
+ cursor: default;
+ opacity: 0.45;
+ box-shadow: none;
+ }
+ .control-symbol {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.25);
+ font-weight: bold;
+ line-height: 1;
+ }
+ .collapse-subtests .control-symbol {
+ background: #edf2f7;
+ color: #34495e;
+ }
+ .testcase-toggle:focus-visible,
+ .subtest-toggle:focus-visible,
+ .subtest-control:focus-visible {
+ outline: 2px solid #1f77b4;
+ outline-offset: 2px;
+ }
+ @media (max-width: 700px) {
+ .subtest-header {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+ .subtest-controls {
+ justify-content: flex-start;
+ }
+ }
+ .subtest-row-hidden {
+ display: none;
+ }
+ {# Render BSA/SBSA subtests recursively so the HTML follows the same
+ parent-child order as the JSON and original nested log. #}
+ {% macro render_subtest_rows(subtests, parent_path='') %}
+ {% for subtest in subtests %}
+ {% set nesting_level = subtest.sub_Test_Level | default(1) | int %}
+ {% set subtest_path = subtest.sub_Test_Path | default(subtest.sub_Test_Number) %}
+ {% set has_children = subtest.subtests is defined and subtest.subtests %}
+ {# The row tooltip contains the full sub_Test_Path for comparison
+ with logs and for precise waiver targeting. #}
+