18c2ecf20Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0
28c2ecf20Sopenharmony_ci#
38c2ecf20Sopenharmony_ci# Parses test results from a kernel dmesg log.
48c2ecf20Sopenharmony_ci#
58c2ecf20Sopenharmony_ci# Copyright (C) 2019, Google LLC.
68c2ecf20Sopenharmony_ci# Author: Felix Guo <felixguoxiuping@gmail.com>
78c2ecf20Sopenharmony_ci# Author: Brendan Higgins <brendanhiggins@google.com>
88c2ecf20Sopenharmony_ci
98c2ecf20Sopenharmony_ciimport re
108c2ecf20Sopenharmony_ci
118c2ecf20Sopenharmony_cifrom collections import namedtuple
128c2ecf20Sopenharmony_cifrom datetime import datetime
138c2ecf20Sopenharmony_cifrom enum import Enum, auto
148c2ecf20Sopenharmony_cifrom functools import reduce
158c2ecf20Sopenharmony_cifrom typing import List, Optional, Tuple
168c2ecf20Sopenharmony_ci
178c2ecf20Sopenharmony_ciTestResult = namedtuple('TestResult', ['status','suites','log'])
188c2ecf20Sopenharmony_ci
198c2ecf20Sopenharmony_ciclass TestSuite(object):
208c2ecf20Sopenharmony_ci	def __init__(self):
218c2ecf20Sopenharmony_ci		self.status = None
228c2ecf20Sopenharmony_ci		self.name = None
238c2ecf20Sopenharmony_ci		self.cases = []
248c2ecf20Sopenharmony_ci
258c2ecf20Sopenharmony_ci	def __str__(self):
268c2ecf20Sopenharmony_ci		return 'TestSuite(' + self.status + ',' + self.name + ',' + str(self.cases) + ')'
278c2ecf20Sopenharmony_ci
288c2ecf20Sopenharmony_ci	def __repr__(self):
298c2ecf20Sopenharmony_ci		return str(self)
308c2ecf20Sopenharmony_ci
318c2ecf20Sopenharmony_ciclass TestCase(object):
328c2ecf20Sopenharmony_ci	def __init__(self):
338c2ecf20Sopenharmony_ci		self.status = None
348c2ecf20Sopenharmony_ci		self.name = ''
358c2ecf20Sopenharmony_ci		self.log = []
368c2ecf20Sopenharmony_ci
378c2ecf20Sopenharmony_ci	def __str__(self):
388c2ecf20Sopenharmony_ci		return 'TestCase(' + self.status + ',' + self.name + ',' + str(self.log) + ')'
398c2ecf20Sopenharmony_ci
408c2ecf20Sopenharmony_ci	def __repr__(self):
418c2ecf20Sopenharmony_ci		return str(self)
428c2ecf20Sopenharmony_ci
438c2ecf20Sopenharmony_ciclass TestStatus(Enum):
448c2ecf20Sopenharmony_ci	SUCCESS = auto()
458c2ecf20Sopenharmony_ci	FAILURE = auto()
468c2ecf20Sopenharmony_ci	TEST_CRASHED = auto()
478c2ecf20Sopenharmony_ci	NO_TESTS = auto()
488c2ecf20Sopenharmony_ci	FAILURE_TO_PARSE_TESTS = auto()
498c2ecf20Sopenharmony_ci
508c2ecf20Sopenharmony_cikunit_start_re = re.compile(r'TAP version [0-9]+$')
518c2ecf20Sopenharmony_cikunit_end_re = re.compile('(List of all partitions:|'
528c2ecf20Sopenharmony_ci			  'Kernel panic - not syncing: VFS:)')
538c2ecf20Sopenharmony_ci
548c2ecf20Sopenharmony_cidef isolate_kunit_output(kernel_output):
558c2ecf20Sopenharmony_ci	started = False
568c2ecf20Sopenharmony_ci	for line in kernel_output:
578c2ecf20Sopenharmony_ci		line = line.rstrip()  # line always has a trailing \n
588c2ecf20Sopenharmony_ci		if kunit_start_re.search(line):
598c2ecf20Sopenharmony_ci			prefix_len = len(line.split('TAP version')[0])
608c2ecf20Sopenharmony_ci			started = True
618c2ecf20Sopenharmony_ci			yield line[prefix_len:] if prefix_len > 0 else line
628c2ecf20Sopenharmony_ci		elif kunit_end_re.search(line):
638c2ecf20Sopenharmony_ci			break
648c2ecf20Sopenharmony_ci		elif started:
658c2ecf20Sopenharmony_ci			yield line[prefix_len:] if prefix_len > 0 else line
668c2ecf20Sopenharmony_ci
678c2ecf20Sopenharmony_cidef raw_output(kernel_output):
688c2ecf20Sopenharmony_ci	for line in kernel_output:
698c2ecf20Sopenharmony_ci		print(line.rstrip())
708c2ecf20Sopenharmony_ci
718c2ecf20Sopenharmony_ciDIVIDER = '=' * 60
728c2ecf20Sopenharmony_ci
738c2ecf20Sopenharmony_ciRESET = '\033[0;0m'
748c2ecf20Sopenharmony_ci
758c2ecf20Sopenharmony_cidef red(text):
768c2ecf20Sopenharmony_ci	return '\033[1;31m' + text + RESET
778c2ecf20Sopenharmony_ci
788c2ecf20Sopenharmony_cidef yellow(text):
798c2ecf20Sopenharmony_ci	return '\033[1;33m' + text + RESET
808c2ecf20Sopenharmony_ci
818c2ecf20Sopenharmony_cidef green(text):
828c2ecf20Sopenharmony_ci	return '\033[1;32m' + text + RESET
838c2ecf20Sopenharmony_ci
848c2ecf20Sopenharmony_cidef print_with_timestamp(message):
858c2ecf20Sopenharmony_ci	print('[%s] %s' % (datetime.now().strftime('%H:%M:%S'), message))
868c2ecf20Sopenharmony_ci
878c2ecf20Sopenharmony_cidef format_suite_divider(message):
888c2ecf20Sopenharmony_ci	return '======== ' + message + ' ========'
898c2ecf20Sopenharmony_ci
908c2ecf20Sopenharmony_cidef print_suite_divider(message):
918c2ecf20Sopenharmony_ci	print_with_timestamp(DIVIDER)
928c2ecf20Sopenharmony_ci	print_with_timestamp(format_suite_divider(message))
938c2ecf20Sopenharmony_ci
948c2ecf20Sopenharmony_cidef print_log(log):
958c2ecf20Sopenharmony_ci	for m in log:
968c2ecf20Sopenharmony_ci		print_with_timestamp(m)
978c2ecf20Sopenharmony_ci
988c2ecf20Sopenharmony_ciTAP_ENTRIES = re.compile(r'^(TAP|[\s]*ok|[\s]*not ok|[\s]*[0-9]+\.\.[0-9]+|[\s]*#).*$')
998c2ecf20Sopenharmony_ci
1008c2ecf20Sopenharmony_cidef consume_non_diagnositic(lines: List[str]) -> None:
1018c2ecf20Sopenharmony_ci	while lines and not TAP_ENTRIES.match(lines[0]):
1028c2ecf20Sopenharmony_ci		lines.pop(0)
1038c2ecf20Sopenharmony_ci
1048c2ecf20Sopenharmony_cidef save_non_diagnositic(lines: List[str], test_case: TestCase) -> None:
1058c2ecf20Sopenharmony_ci	while lines and not TAP_ENTRIES.match(lines[0]):
1068c2ecf20Sopenharmony_ci		test_case.log.append(lines[0])
1078c2ecf20Sopenharmony_ci		lines.pop(0)
1088c2ecf20Sopenharmony_ci
1098c2ecf20Sopenharmony_ciOkNotOkResult = namedtuple('OkNotOkResult', ['is_ok','description', 'text'])
1108c2ecf20Sopenharmony_ci
1118c2ecf20Sopenharmony_ciOK_NOT_OK_SUBTEST = re.compile(r'^[\s]+(ok|not ok) [0-9]+ - (.*)$')
1128c2ecf20Sopenharmony_ci
1138c2ecf20Sopenharmony_ciOK_NOT_OK_MODULE = re.compile(r'^(ok|not ok) ([0-9]+) - (.*)$')
1148c2ecf20Sopenharmony_ci
1158c2ecf20Sopenharmony_cidef parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool:
1168c2ecf20Sopenharmony_ci	save_non_diagnositic(lines, test_case)
1178c2ecf20Sopenharmony_ci	if not lines:
1188c2ecf20Sopenharmony_ci		test_case.status = TestStatus.TEST_CRASHED
1198c2ecf20Sopenharmony_ci		return True
1208c2ecf20Sopenharmony_ci	line = lines[0]
1218c2ecf20Sopenharmony_ci	match = OK_NOT_OK_SUBTEST.match(line)
1228c2ecf20Sopenharmony_ci	while not match and lines:
1238c2ecf20Sopenharmony_ci		line = lines.pop(0)
1248c2ecf20Sopenharmony_ci		match = OK_NOT_OK_SUBTEST.match(line)
1258c2ecf20Sopenharmony_ci	if match:
1268c2ecf20Sopenharmony_ci		test_case.log.append(lines.pop(0))
1278c2ecf20Sopenharmony_ci		test_case.name = match.group(2)
1288c2ecf20Sopenharmony_ci		if test_case.status == TestStatus.TEST_CRASHED:
1298c2ecf20Sopenharmony_ci			return True
1308c2ecf20Sopenharmony_ci		if match.group(1) == 'ok':
1318c2ecf20Sopenharmony_ci			test_case.status = TestStatus.SUCCESS
1328c2ecf20Sopenharmony_ci		else:
1338c2ecf20Sopenharmony_ci			test_case.status = TestStatus.FAILURE
1348c2ecf20Sopenharmony_ci		return True
1358c2ecf20Sopenharmony_ci	else:
1368c2ecf20Sopenharmony_ci		return False
1378c2ecf20Sopenharmony_ci
1388c2ecf20Sopenharmony_ciSUBTEST_DIAGNOSTIC = re.compile(r'^[\s]+# .*?: (.*)$')
1398c2ecf20Sopenharmony_ciDIAGNOSTIC_CRASH_MESSAGE = 'kunit test case crashed!'
1408c2ecf20Sopenharmony_ci
1418c2ecf20Sopenharmony_cidef parse_diagnostic(lines: List[str], test_case: TestCase) -> bool:
1428c2ecf20Sopenharmony_ci	save_non_diagnositic(lines, test_case)
1438c2ecf20Sopenharmony_ci	if not lines:
1448c2ecf20Sopenharmony_ci		return False
1458c2ecf20Sopenharmony_ci	line = lines[0]
1468c2ecf20Sopenharmony_ci	match = SUBTEST_DIAGNOSTIC.match(line)
1478c2ecf20Sopenharmony_ci	if match:
1488c2ecf20Sopenharmony_ci		test_case.log.append(lines.pop(0))
1498c2ecf20Sopenharmony_ci		if match.group(1) == DIAGNOSTIC_CRASH_MESSAGE:
1508c2ecf20Sopenharmony_ci			test_case.status = TestStatus.TEST_CRASHED
1518c2ecf20Sopenharmony_ci		return True
1528c2ecf20Sopenharmony_ci	else:
1538c2ecf20Sopenharmony_ci		return False
1548c2ecf20Sopenharmony_ci
1558c2ecf20Sopenharmony_cidef parse_test_case(lines: List[str]) -> Optional[TestCase]:
1568c2ecf20Sopenharmony_ci	test_case = TestCase()
1578c2ecf20Sopenharmony_ci	save_non_diagnositic(lines, test_case)
1588c2ecf20Sopenharmony_ci	while parse_diagnostic(lines, test_case):
1598c2ecf20Sopenharmony_ci		pass
1608c2ecf20Sopenharmony_ci	if parse_ok_not_ok_test_case(lines, test_case):
1618c2ecf20Sopenharmony_ci		return test_case
1628c2ecf20Sopenharmony_ci	else:
1638c2ecf20Sopenharmony_ci		return None
1648c2ecf20Sopenharmony_ci
1658c2ecf20Sopenharmony_ciSUBTEST_HEADER = re.compile(r'^[\s]+# Subtest: (.*)$')
1668c2ecf20Sopenharmony_ci
1678c2ecf20Sopenharmony_cidef parse_subtest_header(lines: List[str]) -> Optional[str]:
1688c2ecf20Sopenharmony_ci	consume_non_diagnositic(lines)
1698c2ecf20Sopenharmony_ci	if not lines:
1708c2ecf20Sopenharmony_ci		return None
1718c2ecf20Sopenharmony_ci	match = SUBTEST_HEADER.match(lines[0])
1728c2ecf20Sopenharmony_ci	if match:
1738c2ecf20Sopenharmony_ci		lines.pop(0)
1748c2ecf20Sopenharmony_ci		return match.group(1)
1758c2ecf20Sopenharmony_ci	else:
1768c2ecf20Sopenharmony_ci		return None
1778c2ecf20Sopenharmony_ci
1788c2ecf20Sopenharmony_ciSUBTEST_PLAN = re.compile(r'[\s]+[0-9]+\.\.([0-9]+)')
1798c2ecf20Sopenharmony_ci
1808c2ecf20Sopenharmony_cidef parse_subtest_plan(lines: List[str]) -> Optional[int]:
1818c2ecf20Sopenharmony_ci	consume_non_diagnositic(lines)
1828c2ecf20Sopenharmony_ci	match = SUBTEST_PLAN.match(lines[0])
1838c2ecf20Sopenharmony_ci	if match:
1848c2ecf20Sopenharmony_ci		lines.pop(0)
1858c2ecf20Sopenharmony_ci		return int(match.group(1))
1868c2ecf20Sopenharmony_ci	else:
1878c2ecf20Sopenharmony_ci		return None
1888c2ecf20Sopenharmony_ci
1898c2ecf20Sopenharmony_cidef max_status(left: TestStatus, right: TestStatus) -> TestStatus:
1908c2ecf20Sopenharmony_ci	if left == TestStatus.TEST_CRASHED or right == TestStatus.TEST_CRASHED:
1918c2ecf20Sopenharmony_ci		return TestStatus.TEST_CRASHED
1928c2ecf20Sopenharmony_ci	elif left == TestStatus.FAILURE or right == TestStatus.FAILURE:
1938c2ecf20Sopenharmony_ci		return TestStatus.FAILURE
1948c2ecf20Sopenharmony_ci	elif left != TestStatus.SUCCESS:
1958c2ecf20Sopenharmony_ci		return left
1968c2ecf20Sopenharmony_ci	elif right != TestStatus.SUCCESS:
1978c2ecf20Sopenharmony_ci		return right
1988c2ecf20Sopenharmony_ci	else:
1998c2ecf20Sopenharmony_ci		return TestStatus.SUCCESS
2008c2ecf20Sopenharmony_ci
2018c2ecf20Sopenharmony_cidef parse_ok_not_ok_test_suite(lines: List[str],
2028c2ecf20Sopenharmony_ci			       test_suite: TestSuite,
2038c2ecf20Sopenharmony_ci			       expected_suite_index: int) -> bool:
2048c2ecf20Sopenharmony_ci	consume_non_diagnositic(lines)
2058c2ecf20Sopenharmony_ci	if not lines:
2068c2ecf20Sopenharmony_ci		test_suite.status = TestStatus.TEST_CRASHED
2078c2ecf20Sopenharmony_ci		return False
2088c2ecf20Sopenharmony_ci	line = lines[0]
2098c2ecf20Sopenharmony_ci	match = OK_NOT_OK_MODULE.match(line)
2108c2ecf20Sopenharmony_ci	if match:
2118c2ecf20Sopenharmony_ci		lines.pop(0)
2128c2ecf20Sopenharmony_ci		if match.group(1) == 'ok':
2138c2ecf20Sopenharmony_ci			test_suite.status = TestStatus.SUCCESS
2148c2ecf20Sopenharmony_ci		else:
2158c2ecf20Sopenharmony_ci			test_suite.status = TestStatus.FAILURE
2168c2ecf20Sopenharmony_ci		suite_index = int(match.group(2))
2178c2ecf20Sopenharmony_ci		if suite_index != expected_suite_index:
2188c2ecf20Sopenharmony_ci			print_with_timestamp(
2198c2ecf20Sopenharmony_ci				red('[ERROR] ') + 'expected_suite_index ' +
2208c2ecf20Sopenharmony_ci				str(expected_suite_index) + ', but got ' +
2218c2ecf20Sopenharmony_ci				str(suite_index))
2228c2ecf20Sopenharmony_ci		return True
2238c2ecf20Sopenharmony_ci	else:
2248c2ecf20Sopenharmony_ci		return False
2258c2ecf20Sopenharmony_ci
2268c2ecf20Sopenharmony_cidef bubble_up_errors(to_status, status_container_list) -> TestStatus:
2278c2ecf20Sopenharmony_ci	status_list = map(to_status, status_container_list)
2288c2ecf20Sopenharmony_ci	return reduce(max_status, status_list, TestStatus.SUCCESS)
2298c2ecf20Sopenharmony_ci
2308c2ecf20Sopenharmony_cidef bubble_up_test_case_errors(test_suite: TestSuite) -> TestStatus:
2318c2ecf20Sopenharmony_ci	max_test_case_status = bubble_up_errors(lambda x: x.status, test_suite.cases)
2328c2ecf20Sopenharmony_ci	return max_status(max_test_case_status, test_suite.status)
2338c2ecf20Sopenharmony_ci
2348c2ecf20Sopenharmony_cidef parse_test_suite(lines: List[str], expected_suite_index: int) -> Optional[TestSuite]:
2358c2ecf20Sopenharmony_ci	if not lines:
2368c2ecf20Sopenharmony_ci		return None
2378c2ecf20Sopenharmony_ci	consume_non_diagnositic(lines)
2388c2ecf20Sopenharmony_ci	test_suite = TestSuite()
2398c2ecf20Sopenharmony_ci	test_suite.status = TestStatus.SUCCESS
2408c2ecf20Sopenharmony_ci	name = parse_subtest_header(lines)
2418c2ecf20Sopenharmony_ci	if not name:
2428c2ecf20Sopenharmony_ci		return None
2438c2ecf20Sopenharmony_ci	test_suite.name = name
2448c2ecf20Sopenharmony_ci	expected_test_case_num = parse_subtest_plan(lines)
2458c2ecf20Sopenharmony_ci	if expected_test_case_num is None:
2468c2ecf20Sopenharmony_ci		return None
2478c2ecf20Sopenharmony_ci	while expected_test_case_num > 0:
2488c2ecf20Sopenharmony_ci		test_case = parse_test_case(lines)
2498c2ecf20Sopenharmony_ci		if not test_case:
2508c2ecf20Sopenharmony_ci			break
2518c2ecf20Sopenharmony_ci		test_suite.cases.append(test_case)
2528c2ecf20Sopenharmony_ci		expected_test_case_num -= 1
2538c2ecf20Sopenharmony_ci	if parse_ok_not_ok_test_suite(lines, test_suite, expected_suite_index):
2548c2ecf20Sopenharmony_ci		test_suite.status = bubble_up_test_case_errors(test_suite)
2558c2ecf20Sopenharmony_ci		return test_suite
2568c2ecf20Sopenharmony_ci	elif not lines:
2578c2ecf20Sopenharmony_ci		print_with_timestamp(red('[ERROR] ') + 'ran out of lines before end token')
2588c2ecf20Sopenharmony_ci		return test_suite
2598c2ecf20Sopenharmony_ci	else:
2608c2ecf20Sopenharmony_ci		print('failed to parse end of suite' + lines[0])
2618c2ecf20Sopenharmony_ci		return None
2628c2ecf20Sopenharmony_ci
2638c2ecf20Sopenharmony_ciTAP_HEADER = re.compile(r'^TAP version 14$')
2648c2ecf20Sopenharmony_ci
2658c2ecf20Sopenharmony_cidef parse_tap_header(lines: List[str]) -> bool:
2668c2ecf20Sopenharmony_ci	consume_non_diagnositic(lines)
2678c2ecf20Sopenharmony_ci	if TAP_HEADER.match(lines[0]):
2688c2ecf20Sopenharmony_ci		lines.pop(0)
2698c2ecf20Sopenharmony_ci		return True
2708c2ecf20Sopenharmony_ci	else:
2718c2ecf20Sopenharmony_ci		return False
2728c2ecf20Sopenharmony_ci
2738c2ecf20Sopenharmony_ciTEST_PLAN = re.compile(r'[0-9]+\.\.([0-9]+)')
2748c2ecf20Sopenharmony_ci
2758c2ecf20Sopenharmony_cidef parse_test_plan(lines: List[str]) -> Optional[int]:
2768c2ecf20Sopenharmony_ci	consume_non_diagnositic(lines)
2778c2ecf20Sopenharmony_ci	match = TEST_PLAN.match(lines[0])
2788c2ecf20Sopenharmony_ci	if match:
2798c2ecf20Sopenharmony_ci		lines.pop(0)
2808c2ecf20Sopenharmony_ci		return int(match.group(1))
2818c2ecf20Sopenharmony_ci	else:
2828c2ecf20Sopenharmony_ci		return None
2838c2ecf20Sopenharmony_ci
2848c2ecf20Sopenharmony_cidef bubble_up_suite_errors(test_suite_list: List[TestSuite]) -> TestStatus:
2858c2ecf20Sopenharmony_ci	return bubble_up_errors(lambda x: x.status, test_suite_list)
2868c2ecf20Sopenharmony_ci
2878c2ecf20Sopenharmony_cidef parse_test_result(lines: List[str]) -> TestResult:
2888c2ecf20Sopenharmony_ci	consume_non_diagnositic(lines)
2898c2ecf20Sopenharmony_ci	if not lines or not parse_tap_header(lines):
2908c2ecf20Sopenharmony_ci		return TestResult(TestStatus.NO_TESTS, [], lines)
2918c2ecf20Sopenharmony_ci	expected_test_suite_num = parse_test_plan(lines)
2928c2ecf20Sopenharmony_ci	if not expected_test_suite_num:
2938c2ecf20Sopenharmony_ci		return TestResult(TestStatus.FAILURE_TO_PARSE_TESTS, [], lines)
2948c2ecf20Sopenharmony_ci	test_suites = []
2958c2ecf20Sopenharmony_ci	for i in range(1, expected_test_suite_num + 1):
2968c2ecf20Sopenharmony_ci		test_suite = parse_test_suite(lines, i)
2978c2ecf20Sopenharmony_ci		if test_suite:
2988c2ecf20Sopenharmony_ci			test_suites.append(test_suite)
2998c2ecf20Sopenharmony_ci		else:
3008c2ecf20Sopenharmony_ci			print_with_timestamp(
3018c2ecf20Sopenharmony_ci				red('[ERROR] ') + ' expected ' +
3028c2ecf20Sopenharmony_ci				str(expected_test_suite_num) +
3038c2ecf20Sopenharmony_ci				' test suites, but got ' + str(i - 2))
3048c2ecf20Sopenharmony_ci			break
3058c2ecf20Sopenharmony_ci	test_suite = parse_test_suite(lines, -1)
3068c2ecf20Sopenharmony_ci	if test_suite:
3078c2ecf20Sopenharmony_ci		print_with_timestamp(red('[ERROR] ') +
3088c2ecf20Sopenharmony_ci			'got unexpected test suite: ' + test_suite.name)
3098c2ecf20Sopenharmony_ci	if test_suites:
3108c2ecf20Sopenharmony_ci		return TestResult(bubble_up_suite_errors(test_suites), test_suites, lines)
3118c2ecf20Sopenharmony_ci	else:
3128c2ecf20Sopenharmony_ci		return TestResult(TestStatus.NO_TESTS, [], lines)
3138c2ecf20Sopenharmony_ci
3148c2ecf20Sopenharmony_cidef print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:
3158c2ecf20Sopenharmony_ci	total_tests = 0
3168c2ecf20Sopenharmony_ci	failed_tests = 0
3178c2ecf20Sopenharmony_ci	crashed_tests = 0
3188c2ecf20Sopenharmony_ci	for test_suite in test_result.suites:
3198c2ecf20Sopenharmony_ci		if test_suite.status == TestStatus.SUCCESS:
3208c2ecf20Sopenharmony_ci			print_suite_divider(green('[PASSED] ') + test_suite.name)
3218c2ecf20Sopenharmony_ci		elif test_suite.status == TestStatus.TEST_CRASHED:
3228c2ecf20Sopenharmony_ci			print_suite_divider(red('[CRASHED] ' + test_suite.name))
3238c2ecf20Sopenharmony_ci		else:
3248c2ecf20Sopenharmony_ci			print_suite_divider(red('[FAILED] ') + test_suite.name)
3258c2ecf20Sopenharmony_ci		for test_case in test_suite.cases:
3268c2ecf20Sopenharmony_ci			total_tests += 1
3278c2ecf20Sopenharmony_ci			if test_case.status == TestStatus.SUCCESS:
3288c2ecf20Sopenharmony_ci				print_with_timestamp(green('[PASSED] ') + test_case.name)
3298c2ecf20Sopenharmony_ci			elif test_case.status == TestStatus.TEST_CRASHED:
3308c2ecf20Sopenharmony_ci				crashed_tests += 1
3318c2ecf20Sopenharmony_ci				print_with_timestamp(red('[CRASHED] ' + test_case.name))
3328c2ecf20Sopenharmony_ci				print_log(map(yellow, test_case.log))
3338c2ecf20Sopenharmony_ci				print_with_timestamp('')
3348c2ecf20Sopenharmony_ci			else:
3358c2ecf20Sopenharmony_ci				failed_tests += 1
3368c2ecf20Sopenharmony_ci				print_with_timestamp(red('[FAILED] ') + test_case.name)
3378c2ecf20Sopenharmony_ci				print_log(map(yellow, test_case.log))
3388c2ecf20Sopenharmony_ci				print_with_timestamp('')
3398c2ecf20Sopenharmony_ci	return total_tests, failed_tests, crashed_tests
3408c2ecf20Sopenharmony_ci
3418c2ecf20Sopenharmony_cidef parse_run_tests(kernel_output) -> TestResult:
3428c2ecf20Sopenharmony_ci	total_tests = 0
3438c2ecf20Sopenharmony_ci	failed_tests = 0
3448c2ecf20Sopenharmony_ci	crashed_tests = 0
3458c2ecf20Sopenharmony_ci	test_result = parse_test_result(list(isolate_kunit_output(kernel_output)))
3468c2ecf20Sopenharmony_ci	if test_result.status == TestStatus.NO_TESTS:
3478c2ecf20Sopenharmony_ci		print(red('[ERROR] ') + yellow('no tests run!'))
3488c2ecf20Sopenharmony_ci	elif test_result.status == TestStatus.FAILURE_TO_PARSE_TESTS:
3498c2ecf20Sopenharmony_ci		print(red('[ERROR] ') + yellow('could not parse test results!'))
3508c2ecf20Sopenharmony_ci	else:
3518c2ecf20Sopenharmony_ci		(total_tests,
3528c2ecf20Sopenharmony_ci		 failed_tests,
3538c2ecf20Sopenharmony_ci		 crashed_tests) = print_and_count_results(test_result)
3548c2ecf20Sopenharmony_ci	print_with_timestamp(DIVIDER)
3558c2ecf20Sopenharmony_ci	fmt = green if test_result.status == TestStatus.SUCCESS else red
3568c2ecf20Sopenharmony_ci	print_with_timestamp(
3578c2ecf20Sopenharmony_ci		fmt('Testing complete. %d tests run. %d failed. %d crashed.' %
3588c2ecf20Sopenharmony_ci		    (total_tests, failed_tests, crashed_tests)))
3598c2ecf20Sopenharmony_ci	return test_result
360