1*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# Parses test results from a kernel dmesg log. 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun# Copyright (C) 2019, Google LLC. 6*4882a593Smuzhiyun# Author: Felix Guo <felixguoxiuping@gmail.com> 7*4882a593Smuzhiyun# Author: Brendan Higgins <brendanhiggins@google.com> 8*4882a593Smuzhiyun 9*4882a593Smuzhiyunimport re 10*4882a593Smuzhiyun 11*4882a593Smuzhiyunfrom collections import namedtuple 12*4882a593Smuzhiyunfrom datetime import datetime 13*4882a593Smuzhiyunfrom enum import Enum, auto 14*4882a593Smuzhiyunfrom functools import reduce 15*4882a593Smuzhiyunfrom typing import List, Optional, Tuple 16*4882a593Smuzhiyun 17*4882a593SmuzhiyunTestResult = namedtuple('TestResult', ['status','suites','log']) 18*4882a593Smuzhiyun 19*4882a593Smuzhiyunclass TestSuite(object): 20*4882a593Smuzhiyun def __init__(self): 21*4882a593Smuzhiyun self.status = None 22*4882a593Smuzhiyun self.name = None 23*4882a593Smuzhiyun self.cases = [] 24*4882a593Smuzhiyun 25*4882a593Smuzhiyun def __str__(self): 26*4882a593Smuzhiyun return 'TestSuite(' + self.status + ',' + self.name + ',' + str(self.cases) + ')' 27*4882a593Smuzhiyun 28*4882a593Smuzhiyun def __repr__(self): 29*4882a593Smuzhiyun return str(self) 30*4882a593Smuzhiyun 31*4882a593Smuzhiyunclass TestCase(object): 32*4882a593Smuzhiyun def __init__(self): 33*4882a593Smuzhiyun self.status = None 34*4882a593Smuzhiyun self.name = '' 35*4882a593Smuzhiyun self.log = [] 36*4882a593Smuzhiyun 37*4882a593Smuzhiyun def __str__(self): 38*4882a593Smuzhiyun return 'TestCase(' + self.status + ',' + self.name + ',' + str(self.log) + ')' 39*4882a593Smuzhiyun 40*4882a593Smuzhiyun def __repr__(self): 41*4882a593Smuzhiyun return str(self) 42*4882a593Smuzhiyun 43*4882a593Smuzhiyunclass TestStatus(Enum): 44*4882a593Smuzhiyun SUCCESS = auto() 45*4882a593Smuzhiyun FAILURE = auto() 46*4882a593Smuzhiyun TEST_CRASHED = auto() 47*4882a593Smuzhiyun NO_TESTS = auto() 48*4882a593Smuzhiyun FAILURE_TO_PARSE_TESTS = auto() 49*4882a593Smuzhiyun 50*4882a593Smuzhiyunkunit_start_re = re.compile(r'TAP version [0-9]+$') 51*4882a593Smuzhiyunkunit_end_re = re.compile('(List of all partitions:|' 52*4882a593Smuzhiyun 'Kernel panic - not syncing: VFS:)') 53*4882a593Smuzhiyun 54*4882a593Smuzhiyundef isolate_kunit_output(kernel_output): 55*4882a593Smuzhiyun started = False 56*4882a593Smuzhiyun for line in kernel_output: 57*4882a593Smuzhiyun line = line.rstrip() # line always has a trailing \n 58*4882a593Smuzhiyun if kunit_start_re.search(line): 59*4882a593Smuzhiyun prefix_len = len(line.split('TAP version')[0]) 60*4882a593Smuzhiyun started = True 61*4882a593Smuzhiyun yield line[prefix_len:] if prefix_len > 0 else line 62*4882a593Smuzhiyun elif kunit_end_re.search(line): 63*4882a593Smuzhiyun break 64*4882a593Smuzhiyun elif started: 65*4882a593Smuzhiyun yield line[prefix_len:] if prefix_len > 0 else line 66*4882a593Smuzhiyun 67*4882a593Smuzhiyundef raw_output(kernel_output): 68*4882a593Smuzhiyun for line in kernel_output: 69*4882a593Smuzhiyun print(line.rstrip()) 70*4882a593Smuzhiyun 71*4882a593SmuzhiyunDIVIDER = '=' * 60 72*4882a593Smuzhiyun 73*4882a593SmuzhiyunRESET = '\033[0;0m' 74*4882a593Smuzhiyun 75*4882a593Smuzhiyundef red(text): 76*4882a593Smuzhiyun return '\033[1;31m' + text + RESET 77*4882a593Smuzhiyun 78*4882a593Smuzhiyundef yellow(text): 79*4882a593Smuzhiyun return '\033[1;33m' + text + RESET 80*4882a593Smuzhiyun 81*4882a593Smuzhiyundef green(text): 82*4882a593Smuzhiyun return '\033[1;32m' + text + RESET 83*4882a593Smuzhiyun 84*4882a593Smuzhiyundef print_with_timestamp(message): 85*4882a593Smuzhiyun print('[%s] %s' % (datetime.now().strftime('%H:%M:%S'), message)) 86*4882a593Smuzhiyun 87*4882a593Smuzhiyundef format_suite_divider(message): 88*4882a593Smuzhiyun return '======== ' + message + ' ========' 89*4882a593Smuzhiyun 90*4882a593Smuzhiyundef print_suite_divider(message): 91*4882a593Smuzhiyun print_with_timestamp(DIVIDER) 92*4882a593Smuzhiyun print_with_timestamp(format_suite_divider(message)) 93*4882a593Smuzhiyun 94*4882a593Smuzhiyundef print_log(log): 95*4882a593Smuzhiyun for m in log: 96*4882a593Smuzhiyun print_with_timestamp(m) 97*4882a593Smuzhiyun 98*4882a593SmuzhiyunTAP_ENTRIES = re.compile(r'^(TAP|[\s]*ok|[\s]*not ok|[\s]*[0-9]+\.\.[0-9]+|[\s]*#).*$') 99*4882a593Smuzhiyun 100*4882a593Smuzhiyundef consume_non_diagnositic(lines: List[str]) -> None: 101*4882a593Smuzhiyun while lines and not TAP_ENTRIES.match(lines[0]): 102*4882a593Smuzhiyun lines.pop(0) 103*4882a593Smuzhiyun 104*4882a593Smuzhiyundef save_non_diagnositic(lines: List[str], test_case: TestCase) -> None: 105*4882a593Smuzhiyun while lines and not TAP_ENTRIES.match(lines[0]): 106*4882a593Smuzhiyun test_case.log.append(lines[0]) 107*4882a593Smuzhiyun lines.pop(0) 108*4882a593Smuzhiyun 109*4882a593SmuzhiyunOkNotOkResult = namedtuple('OkNotOkResult', ['is_ok','description', 'text']) 110*4882a593Smuzhiyun 111*4882a593SmuzhiyunOK_NOT_OK_SUBTEST = re.compile(r'^[\s]+(ok|not ok) [0-9]+ - (.*)$') 112*4882a593Smuzhiyun 113*4882a593SmuzhiyunOK_NOT_OK_MODULE = re.compile(r'^(ok|not ok) ([0-9]+) - (.*)$') 114*4882a593Smuzhiyun 115*4882a593Smuzhiyundef parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool: 116*4882a593Smuzhiyun save_non_diagnositic(lines, test_case) 117*4882a593Smuzhiyun if not lines: 118*4882a593Smuzhiyun test_case.status = TestStatus.TEST_CRASHED 119*4882a593Smuzhiyun return True 120*4882a593Smuzhiyun line = lines[0] 121*4882a593Smuzhiyun match = OK_NOT_OK_SUBTEST.match(line) 122*4882a593Smuzhiyun while not match and lines: 123*4882a593Smuzhiyun line = lines.pop(0) 124*4882a593Smuzhiyun match = OK_NOT_OK_SUBTEST.match(line) 125*4882a593Smuzhiyun if match: 126*4882a593Smuzhiyun test_case.log.append(lines.pop(0)) 127*4882a593Smuzhiyun test_case.name = match.group(2) 128*4882a593Smuzhiyun if test_case.status == TestStatus.TEST_CRASHED: 129*4882a593Smuzhiyun return True 130*4882a593Smuzhiyun if match.group(1) == 'ok': 131*4882a593Smuzhiyun test_case.status = TestStatus.SUCCESS 132*4882a593Smuzhiyun else: 133*4882a593Smuzhiyun test_case.status = TestStatus.FAILURE 134*4882a593Smuzhiyun return True 135*4882a593Smuzhiyun else: 136*4882a593Smuzhiyun return False 137*4882a593Smuzhiyun 138*4882a593SmuzhiyunSUBTEST_DIAGNOSTIC = re.compile(r'^[\s]+# .*?: (.*)$') 139*4882a593SmuzhiyunDIAGNOSTIC_CRASH_MESSAGE = 'kunit test case crashed!' 140*4882a593Smuzhiyun 141*4882a593Smuzhiyundef parse_diagnostic(lines: List[str], test_case: TestCase) -> bool: 142*4882a593Smuzhiyun save_non_diagnositic(lines, test_case) 143*4882a593Smuzhiyun if not lines: 144*4882a593Smuzhiyun return False 145*4882a593Smuzhiyun line = lines[0] 146*4882a593Smuzhiyun match = SUBTEST_DIAGNOSTIC.match(line) 147*4882a593Smuzhiyun if match: 148*4882a593Smuzhiyun test_case.log.append(lines.pop(0)) 149*4882a593Smuzhiyun if match.group(1) == DIAGNOSTIC_CRASH_MESSAGE: 150*4882a593Smuzhiyun test_case.status = TestStatus.TEST_CRASHED 151*4882a593Smuzhiyun return True 152*4882a593Smuzhiyun else: 153*4882a593Smuzhiyun return False 154*4882a593Smuzhiyun 155*4882a593Smuzhiyundef parse_test_case(lines: List[str]) -> Optional[TestCase]: 156*4882a593Smuzhiyun test_case = TestCase() 157*4882a593Smuzhiyun save_non_diagnositic(lines, test_case) 158*4882a593Smuzhiyun while parse_diagnostic(lines, test_case): 159*4882a593Smuzhiyun pass 160*4882a593Smuzhiyun if parse_ok_not_ok_test_case(lines, test_case): 161*4882a593Smuzhiyun return test_case 162*4882a593Smuzhiyun else: 163*4882a593Smuzhiyun return None 164*4882a593Smuzhiyun 165*4882a593SmuzhiyunSUBTEST_HEADER = re.compile(r'^[\s]+# Subtest: (.*)$') 166*4882a593Smuzhiyun 167*4882a593Smuzhiyundef parse_subtest_header(lines: List[str]) -> Optional[str]: 168*4882a593Smuzhiyun consume_non_diagnositic(lines) 169*4882a593Smuzhiyun if not lines: 170*4882a593Smuzhiyun return None 171*4882a593Smuzhiyun match = SUBTEST_HEADER.match(lines[0]) 172*4882a593Smuzhiyun if match: 173*4882a593Smuzhiyun lines.pop(0) 174*4882a593Smuzhiyun return match.group(1) 175*4882a593Smuzhiyun else: 176*4882a593Smuzhiyun return None 177*4882a593Smuzhiyun 178*4882a593SmuzhiyunSUBTEST_PLAN = re.compile(r'[\s]+[0-9]+\.\.([0-9]+)') 179*4882a593Smuzhiyun 180*4882a593Smuzhiyundef parse_subtest_plan(lines: List[str]) -> Optional[int]: 181*4882a593Smuzhiyun consume_non_diagnositic(lines) 182*4882a593Smuzhiyun match = SUBTEST_PLAN.match(lines[0]) 183*4882a593Smuzhiyun if match: 184*4882a593Smuzhiyun lines.pop(0) 185*4882a593Smuzhiyun return int(match.group(1)) 186*4882a593Smuzhiyun else: 187*4882a593Smuzhiyun return None 188*4882a593Smuzhiyun 189*4882a593Smuzhiyundef max_status(left: TestStatus, right: TestStatus) -> TestStatus: 190*4882a593Smuzhiyun if left == TestStatus.TEST_CRASHED or right == TestStatus.TEST_CRASHED: 191*4882a593Smuzhiyun return TestStatus.TEST_CRASHED 192*4882a593Smuzhiyun elif left == TestStatus.FAILURE or right == TestStatus.FAILURE: 193*4882a593Smuzhiyun return TestStatus.FAILURE 194*4882a593Smuzhiyun elif left != TestStatus.SUCCESS: 195*4882a593Smuzhiyun return left 196*4882a593Smuzhiyun elif right != TestStatus.SUCCESS: 197*4882a593Smuzhiyun return right 198*4882a593Smuzhiyun else: 199*4882a593Smuzhiyun return TestStatus.SUCCESS 200*4882a593Smuzhiyun 201*4882a593Smuzhiyundef parse_ok_not_ok_test_suite(lines: List[str], 202*4882a593Smuzhiyun test_suite: TestSuite, 203*4882a593Smuzhiyun expected_suite_index: int) -> bool: 204*4882a593Smuzhiyun consume_non_diagnositic(lines) 205*4882a593Smuzhiyun if not lines: 206*4882a593Smuzhiyun test_suite.status = TestStatus.TEST_CRASHED 207*4882a593Smuzhiyun return False 208*4882a593Smuzhiyun line = lines[0] 209*4882a593Smuzhiyun match = OK_NOT_OK_MODULE.match(line) 210*4882a593Smuzhiyun if match: 211*4882a593Smuzhiyun lines.pop(0) 212*4882a593Smuzhiyun if match.group(1) == 'ok': 213*4882a593Smuzhiyun test_suite.status = TestStatus.SUCCESS 214*4882a593Smuzhiyun else: 215*4882a593Smuzhiyun test_suite.status = TestStatus.FAILURE 216*4882a593Smuzhiyun suite_index = int(match.group(2)) 217*4882a593Smuzhiyun if suite_index != expected_suite_index: 218*4882a593Smuzhiyun print_with_timestamp( 219*4882a593Smuzhiyun red('[ERROR] ') + 'expected_suite_index ' + 220*4882a593Smuzhiyun str(expected_suite_index) + ', but got ' + 221*4882a593Smuzhiyun str(suite_index)) 222*4882a593Smuzhiyun return True 223*4882a593Smuzhiyun else: 224*4882a593Smuzhiyun return False 225*4882a593Smuzhiyun 226*4882a593Smuzhiyundef bubble_up_errors(to_status, status_container_list) -> TestStatus: 227*4882a593Smuzhiyun status_list = map(to_status, status_container_list) 228*4882a593Smuzhiyun return reduce(max_status, status_list, TestStatus.SUCCESS) 229*4882a593Smuzhiyun 230*4882a593Smuzhiyundef bubble_up_test_case_errors(test_suite: TestSuite) -> TestStatus: 231*4882a593Smuzhiyun max_test_case_status = bubble_up_errors(lambda x: x.status, test_suite.cases) 232*4882a593Smuzhiyun return max_status(max_test_case_status, test_suite.status) 233*4882a593Smuzhiyun 234*4882a593Smuzhiyundef parse_test_suite(lines: List[str], expected_suite_index: int) -> Optional[TestSuite]: 235*4882a593Smuzhiyun if not lines: 236*4882a593Smuzhiyun return None 237*4882a593Smuzhiyun consume_non_diagnositic(lines) 238*4882a593Smuzhiyun test_suite = TestSuite() 239*4882a593Smuzhiyun test_suite.status = TestStatus.SUCCESS 240*4882a593Smuzhiyun name = parse_subtest_header(lines) 241*4882a593Smuzhiyun if not name: 242*4882a593Smuzhiyun return None 243*4882a593Smuzhiyun test_suite.name = name 244*4882a593Smuzhiyun expected_test_case_num = parse_subtest_plan(lines) 245*4882a593Smuzhiyun if expected_test_case_num is None: 246*4882a593Smuzhiyun return None 247*4882a593Smuzhiyun while expected_test_case_num > 0: 248*4882a593Smuzhiyun test_case = parse_test_case(lines) 249*4882a593Smuzhiyun if not test_case: 250*4882a593Smuzhiyun break 251*4882a593Smuzhiyun test_suite.cases.append(test_case) 252*4882a593Smuzhiyun expected_test_case_num -= 1 253*4882a593Smuzhiyun if parse_ok_not_ok_test_suite(lines, test_suite, expected_suite_index): 254*4882a593Smuzhiyun test_suite.status = bubble_up_test_case_errors(test_suite) 255*4882a593Smuzhiyun return test_suite 256*4882a593Smuzhiyun elif not lines: 257*4882a593Smuzhiyun print_with_timestamp(red('[ERROR] ') + 'ran out of lines before end token') 258*4882a593Smuzhiyun return test_suite 259*4882a593Smuzhiyun else: 260*4882a593Smuzhiyun print('failed to parse end of suite' + lines[0]) 261*4882a593Smuzhiyun return None 262*4882a593Smuzhiyun 263*4882a593SmuzhiyunTAP_HEADER = re.compile(r'^TAP version 14$') 264*4882a593Smuzhiyun 265*4882a593Smuzhiyundef parse_tap_header(lines: List[str]) -> bool: 266*4882a593Smuzhiyun consume_non_diagnositic(lines) 267*4882a593Smuzhiyun if TAP_HEADER.match(lines[0]): 268*4882a593Smuzhiyun lines.pop(0) 269*4882a593Smuzhiyun return True 270*4882a593Smuzhiyun else: 271*4882a593Smuzhiyun return False 272*4882a593Smuzhiyun 273*4882a593SmuzhiyunTEST_PLAN = re.compile(r'[0-9]+\.\.([0-9]+)') 274*4882a593Smuzhiyun 275*4882a593Smuzhiyundef parse_test_plan(lines: List[str]) -> Optional[int]: 276*4882a593Smuzhiyun consume_non_diagnositic(lines) 277*4882a593Smuzhiyun match = TEST_PLAN.match(lines[0]) 278*4882a593Smuzhiyun if match: 279*4882a593Smuzhiyun lines.pop(0) 280*4882a593Smuzhiyun return int(match.group(1)) 281*4882a593Smuzhiyun else: 282*4882a593Smuzhiyun return None 283*4882a593Smuzhiyun 284*4882a593Smuzhiyundef bubble_up_suite_errors(test_suite_list: List[TestSuite]) -> TestStatus: 285*4882a593Smuzhiyun return bubble_up_errors(lambda x: x.status, test_suite_list) 286*4882a593Smuzhiyun 287*4882a593Smuzhiyundef parse_test_result(lines: List[str]) -> TestResult: 288*4882a593Smuzhiyun consume_non_diagnositic(lines) 289*4882a593Smuzhiyun if not lines or not parse_tap_header(lines): 290*4882a593Smuzhiyun return TestResult(TestStatus.NO_TESTS, [], lines) 291*4882a593Smuzhiyun expected_test_suite_num = parse_test_plan(lines) 292*4882a593Smuzhiyun if not expected_test_suite_num: 293*4882a593Smuzhiyun return TestResult(TestStatus.FAILURE_TO_PARSE_TESTS, [], lines) 294*4882a593Smuzhiyun test_suites = [] 295*4882a593Smuzhiyun for i in range(1, expected_test_suite_num + 1): 296*4882a593Smuzhiyun test_suite = parse_test_suite(lines, i) 297*4882a593Smuzhiyun if test_suite: 298*4882a593Smuzhiyun test_suites.append(test_suite) 299*4882a593Smuzhiyun else: 300*4882a593Smuzhiyun print_with_timestamp( 301*4882a593Smuzhiyun red('[ERROR] ') + ' expected ' + 302*4882a593Smuzhiyun str(expected_test_suite_num) + 303*4882a593Smuzhiyun ' test suites, but got ' + str(i - 2)) 304*4882a593Smuzhiyun break 305*4882a593Smuzhiyun test_suite = parse_test_suite(lines, -1) 306*4882a593Smuzhiyun if test_suite: 307*4882a593Smuzhiyun print_with_timestamp(red('[ERROR] ') + 308*4882a593Smuzhiyun 'got unexpected test suite: ' + test_suite.name) 309*4882a593Smuzhiyun if test_suites: 310*4882a593Smuzhiyun return TestResult(bubble_up_suite_errors(test_suites), test_suites, lines) 311*4882a593Smuzhiyun else: 312*4882a593Smuzhiyun return TestResult(TestStatus.NO_TESTS, [], lines) 313*4882a593Smuzhiyun 314*4882a593Smuzhiyundef print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]: 315*4882a593Smuzhiyun total_tests = 0 316*4882a593Smuzhiyun failed_tests = 0 317*4882a593Smuzhiyun crashed_tests = 0 318*4882a593Smuzhiyun for test_suite in test_result.suites: 319*4882a593Smuzhiyun if test_suite.status == TestStatus.SUCCESS: 320*4882a593Smuzhiyun print_suite_divider(green('[PASSED] ') + test_suite.name) 321*4882a593Smuzhiyun elif test_suite.status == TestStatus.TEST_CRASHED: 322*4882a593Smuzhiyun print_suite_divider(red('[CRASHED] ' + test_suite.name)) 323*4882a593Smuzhiyun else: 324*4882a593Smuzhiyun print_suite_divider(red('[FAILED] ') + test_suite.name) 325*4882a593Smuzhiyun for test_case in test_suite.cases: 326*4882a593Smuzhiyun total_tests += 1 327*4882a593Smuzhiyun if test_case.status == TestStatus.SUCCESS: 328*4882a593Smuzhiyun print_with_timestamp(green('[PASSED] ') + test_case.name) 329*4882a593Smuzhiyun elif test_case.status == TestStatus.TEST_CRASHED: 330*4882a593Smuzhiyun crashed_tests += 1 331*4882a593Smuzhiyun print_with_timestamp(red('[CRASHED] ' + test_case.name)) 332*4882a593Smuzhiyun print_log(map(yellow, test_case.log)) 333*4882a593Smuzhiyun print_with_timestamp('') 334*4882a593Smuzhiyun else: 335*4882a593Smuzhiyun failed_tests += 1 336*4882a593Smuzhiyun print_with_timestamp(red('[FAILED] ') + test_case.name) 337*4882a593Smuzhiyun print_log(map(yellow, test_case.log)) 338*4882a593Smuzhiyun print_with_timestamp('') 339*4882a593Smuzhiyun return total_tests, failed_tests, crashed_tests 340*4882a593Smuzhiyun 341*4882a593Smuzhiyundef parse_run_tests(kernel_output) -> TestResult: 342*4882a593Smuzhiyun total_tests = 0 343*4882a593Smuzhiyun failed_tests = 0 344*4882a593Smuzhiyun crashed_tests = 0 345*4882a593Smuzhiyun test_result = parse_test_result(list(isolate_kunit_output(kernel_output))) 346*4882a593Smuzhiyun if test_result.status == TestStatus.NO_TESTS: 347*4882a593Smuzhiyun print(red('[ERROR] ') + yellow('no tests run!')) 348*4882a593Smuzhiyun elif test_result.status == TestStatus.FAILURE_TO_PARSE_TESTS: 349*4882a593Smuzhiyun print(red('[ERROR] ') + yellow('could not parse test results!')) 350*4882a593Smuzhiyun else: 351*4882a593Smuzhiyun (total_tests, 352*4882a593Smuzhiyun failed_tests, 353*4882a593Smuzhiyun crashed_tests) = print_and_count_results(test_result) 354*4882a593Smuzhiyun print_with_timestamp(DIVIDER) 355*4882a593Smuzhiyun fmt = green if test_result.status == TestStatus.SUCCESS else red 356*4882a593Smuzhiyun print_with_timestamp( 357*4882a593Smuzhiyun fmt('Testing complete. %d tests run. %d failed. %d crashed.' % 358*4882a593Smuzhiyun (total_tests, failed_tests, crashed_tests))) 359*4882a593Smuzhiyun return test_result 360