xref: /OK3568_Linux_fs/kernel/tools/testing/kunit/kunit_parser.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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