xref: /linux/tools/testing/kunit/kunit_parser.py (revision 84b9b44b)
1# SPDX-License-Identifier: GPL-2.0
2#
3# Parses KTAP test results from a kernel dmesg log and incrementally prints
4# results with reader-friendly format. Stores and returns test results in a
5# Test object.
6#
7# Copyright (C) 2019, Google LLC.
8# Author: Felix Guo <felixguoxiuping@gmail.com>
9# Author: Brendan Higgins <brendanhiggins@google.com>
10# Author: Rae Moar <rmoar@google.com>
11
12from __future__ import annotations
13from dataclasses import dataclass
14import re
15import textwrap
16
17from enum import Enum, auto
18from typing import Iterable, Iterator, List, Optional, Tuple
19
20from kunit_printer import stdout
21
22class Test:
23	"""
24	A class to represent a test parsed from KTAP results. All KTAP
25	results within a test log are stored in a main Test object as
26	subtests.
27
28	Attributes:
29	status : TestStatus - status of the test
30	name : str - name of the test
31	expected_count : int - expected number of subtests (0 if single
32		test case and None if unknown expected number of subtests)
33	subtests : List[Test] - list of subtests
34	log : List[str] - log of KTAP lines that correspond to the test
35	counts : TestCounts - counts of the test statuses and errors of
36		subtests or of the test itself if the test is a single
37		test case.
38	"""
39	def __init__(self) -> None:
40		"""Creates Test object with default attributes."""
41		self.status = TestStatus.TEST_CRASHED
42		self.name = ''
43		self.expected_count = 0  # type: Optional[int]
44		self.subtests = []  # type: List[Test]
45		self.log = []  # type: List[str]
46		self.counts = TestCounts()
47
48	def __str__(self) -> str:
49		"""Returns string representation of a Test class object."""
50		return (f'Test({self.status}, {self.name}, {self.expected_count}, '
51			f'{self.subtests}, {self.log}, {self.counts})')
52
53	def __repr__(self) -> str:
54		"""Returns string representation of a Test class object."""
55		return str(self)
56
57	def add_error(self, error_message: str) -> None:
58		"""Records an error that occurred while parsing this test."""
59		self.counts.errors += 1
60		stdout.print_with_timestamp(stdout.red('[ERROR]') + f' Test: {self.name}: {error_message}')
61
62	def ok_status(self) -> bool:
63		"""Returns true if the status was ok, i.e. passed or skipped."""
64		return self.status in (TestStatus.SUCCESS, TestStatus.SKIPPED)
65
66class TestStatus(Enum):
67	"""An enumeration class to represent the status of a test."""
68	SUCCESS = auto()
69	FAILURE = auto()
70	SKIPPED = auto()
71	TEST_CRASHED = auto()
72	NO_TESTS = auto()
73	FAILURE_TO_PARSE_TESTS = auto()
74
75@dataclass
76class TestCounts:
77	"""
78	Tracks the counts of statuses of all test cases and any errors within
79	a Test.
80	"""
81	passed: int = 0
82	failed: int = 0
83	crashed: int = 0
84	skipped: int = 0
85	errors: int = 0
86
87	def __str__(self) -> str:
88		"""Returns the string representation of a TestCounts object."""
89		statuses = [('passed', self.passed), ('failed', self.failed),
90			('crashed', self.crashed), ('skipped', self.skipped),
91			('errors', self.errors)]
92		return f'Ran {self.total()} tests: ' + \
93			', '.join(f'{s}: {n}' for s, n in statuses if n > 0)
94
95	def total(self) -> int:
96		"""Returns the total number of test cases within a test
97		object, where a test case is a test with no subtests.
98		"""
99		return (self.passed + self.failed + self.crashed +
100			self.skipped)
101
102	def add_subtest_counts(self, counts: TestCounts) -> None:
103		"""
104		Adds the counts of another TestCounts object to the current
105		TestCounts object. Used to add the counts of a subtest to the
106		parent test.
107
108		Parameters:
109		counts - a different TestCounts object whose counts
110			will be added to the counts of the TestCounts object
111		"""
112		self.passed += counts.passed
113		self.failed += counts.failed
114		self.crashed += counts.crashed
115		self.skipped += counts.skipped
116		self.errors += counts.errors
117
118	def get_status(self) -> TestStatus:
119		"""Returns the aggregated status of a Test using test
120		counts.
121		"""
122		if self.total() == 0:
123			return TestStatus.NO_TESTS
124		if self.crashed:
125			# Crashes should take priority.
126			return TestStatus.TEST_CRASHED
127		if self.failed:
128			return TestStatus.FAILURE
129		if self.passed:
130			# No failures or crashes, looks good!
131			return TestStatus.SUCCESS
132		# We have only skipped tests.
133		return TestStatus.SKIPPED
134
135	def add_status(self, status: TestStatus) -> None:
136		"""Increments the count for `status`."""
137		if status == TestStatus.SUCCESS:
138			self.passed += 1
139		elif status == TestStatus.FAILURE:
140			self.failed += 1
141		elif status == TestStatus.SKIPPED:
142			self.skipped += 1
143		elif status != TestStatus.NO_TESTS:
144			self.crashed += 1
145
146class LineStream:
147	"""
148	A class to represent the lines of kernel output.
149	Provides a lazy peek()/pop() interface over an iterator of
150	(line#, text).
151	"""
152	_lines: Iterator[Tuple[int, str]]
153	_next: Tuple[int, str]
154	_need_next: bool
155	_done: bool
156
157	def __init__(self, lines: Iterator[Tuple[int, str]]):
158		"""Creates a new LineStream that wraps the given iterator."""
159		self._lines = lines
160		self._done = False
161		self._need_next = True
162		self._next = (0, '')
163
164	def _get_next(self) -> None:
165		"""Advances the LineSteam to the next line, if necessary."""
166		if not self._need_next:
167			return
168		try:
169			self._next = next(self._lines)
170		except StopIteration:
171			self._done = True
172		finally:
173			self._need_next = False
174
175	def peek(self) -> str:
176		"""Returns the current line, without advancing the LineStream.
177		"""
178		self._get_next()
179		return self._next[1]
180
181	def pop(self) -> str:
182		"""Returns the current line and advances the LineStream to
183		the next line.
184		"""
185		s = self.peek()
186		if self._done:
187			raise ValueError(f'LineStream: going past EOF, last line was {s}')
188		self._need_next = True
189		return s
190
191	def __bool__(self) -> bool:
192		"""Returns True if stream has more lines."""
193		self._get_next()
194		return not self._done
195
196	# Only used by kunit_tool_test.py.
197	def __iter__(self) -> Iterator[str]:
198		"""Empties all lines stored in LineStream object into
199		Iterator object and returns the Iterator object.
200		"""
201		while bool(self):
202			yield self.pop()
203
204	def line_number(self) -> int:
205		"""Returns the line number of the current line."""
206		self._get_next()
207		return self._next[0]
208
209# Parsing helper methods:
210
211KTAP_START = re.compile(r'\s*KTAP version ([0-9]+)$')
212TAP_START = re.compile(r'\s*TAP version ([0-9]+)$')
213KTAP_END = re.compile(r'\s*(List of all partitions:|'
214	'Kernel panic - not syncing: VFS:|reboot: System halted)')
215
216def extract_tap_lines(kernel_output: Iterable[str]) -> LineStream:
217	"""Extracts KTAP lines from the kernel output."""
218	def isolate_ktap_output(kernel_output: Iterable[str]) \
219			-> Iterator[Tuple[int, str]]:
220		line_num = 0
221		started = False
222		for line in kernel_output:
223			line_num += 1
224			line = line.rstrip()  # remove trailing \n
225			if not started and KTAP_START.search(line):
226				# start extracting KTAP lines and set prefix
227				# to number of characters before version line
228				prefix_len = len(
229					line.split('KTAP version')[0])
230				started = True
231				yield line_num, line[prefix_len:]
232			elif not started and TAP_START.search(line):
233				# start extracting KTAP lines and set prefix
234				# to number of characters before version line
235				prefix_len = len(line.split('TAP version')[0])
236				started = True
237				yield line_num, line[prefix_len:]
238			elif started and KTAP_END.search(line):
239				# stop extracting KTAP lines
240				break
241			elif started:
242				# remove the prefix, if any.
243				line = line[prefix_len:]
244				yield line_num, line
245	return LineStream(lines=isolate_ktap_output(kernel_output))
246
247KTAP_VERSIONS = [1]
248TAP_VERSIONS = [13, 14]
249
250def check_version(version_num: int, accepted_versions: List[int],
251			version_type: str, test: Test) -> None:
252	"""
253	Adds error to test object if version number is too high or too
254	low.
255
256	Parameters:
257	version_num - The inputted version number from the parsed KTAP or TAP
258		header line
259	accepted_version - List of accepted KTAP or TAP versions
260	version_type - 'KTAP' or 'TAP' depending on the type of
261		version line.
262	test - Test object for current test being parsed
263	"""
264	if version_num < min(accepted_versions):
265		test.add_error(f'{version_type} version lower than expected!')
266	elif version_num > max(accepted_versions):
267		test.add_error(f'{version_type} version higer than expected!')
268
269def parse_ktap_header(lines: LineStream, test: Test) -> bool:
270	"""
271	Parses KTAP/TAP header line and checks version number.
272	Returns False if fails to parse KTAP/TAP header line.
273
274	Accepted formats:
275	- 'KTAP version [version number]'
276	- 'TAP version [version number]'
277
278	Parameters:
279	lines - LineStream of KTAP output to parse
280	test - Test object for current test being parsed
281
282	Return:
283	True if successfully parsed KTAP/TAP header line
284	"""
285	ktap_match = KTAP_START.match(lines.peek())
286	tap_match = TAP_START.match(lines.peek())
287	if ktap_match:
288		version_num = int(ktap_match.group(1))
289		check_version(version_num, KTAP_VERSIONS, 'KTAP', test)
290	elif tap_match:
291		version_num = int(tap_match.group(1))
292		check_version(version_num, TAP_VERSIONS, 'TAP', test)
293	else:
294		return False
295	lines.pop()
296	return True
297
298TEST_HEADER = re.compile(r'^\s*# Subtest: (.*)$')
299
300def parse_test_header(lines: LineStream, test: Test) -> bool:
301	"""
302	Parses test header and stores test name in test object.
303	Returns False if fails to parse test header line.
304
305	Accepted format:
306	- '# Subtest: [test name]'
307
308	Parameters:
309	lines - LineStream of KTAP output to parse
310	test - Test object for current test being parsed
311
312	Return:
313	True if successfully parsed test header line
314	"""
315	match = TEST_HEADER.match(lines.peek())
316	if not match:
317		return False
318	test.name = match.group(1)
319	lines.pop()
320	return True
321
322TEST_PLAN = re.compile(r'^\s*1\.\.([0-9]+)')
323
324def parse_test_plan(lines: LineStream, test: Test) -> bool:
325	"""
326	Parses test plan line and stores the expected number of subtests in
327	test object. Reports an error if expected count is 0.
328	Returns False and sets expected_count to None if there is no valid test
329	plan.
330
331	Accepted format:
332	- '1..[number of subtests]'
333
334	Parameters:
335	lines - LineStream of KTAP output to parse
336	test - Test object for current test being parsed
337
338	Return:
339	True if successfully parsed test plan line
340	"""
341	match = TEST_PLAN.match(lines.peek())
342	if not match:
343		test.expected_count = None
344		return False
345	expected_count = int(match.group(1))
346	test.expected_count = expected_count
347	lines.pop()
348	return True
349
350TEST_RESULT = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
351
352TEST_RESULT_SKIP = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?(.*) # SKIP(.*)$')
353
354def peek_test_name_match(lines: LineStream, test: Test) -> bool:
355	"""
356	Matches current line with the format of a test result line and checks
357	if the name matches the name of the current test.
358	Returns False if fails to match format or name.
359
360	Accepted format:
361	- '[ok|not ok] [test number] [-] [test name] [optional skip
362		directive]'
363
364	Parameters:
365	lines - LineStream of KTAP output to parse
366	test - Test object for current test being parsed
367
368	Return:
369	True if matched a test result line and the name matching the
370		expected test name
371	"""
372	line = lines.peek()
373	match = TEST_RESULT.match(line)
374	if not match:
375		return False
376	name = match.group(4)
377	return name == test.name
378
379def parse_test_result(lines: LineStream, test: Test,
380			expected_num: int) -> bool:
381	"""
382	Parses test result line and stores the status and name in the test
383	object. Reports an error if the test number does not match expected
384	test number.
385	Returns False if fails to parse test result line.
386
387	Note that the SKIP directive is the only direction that causes a
388	change in status.
389
390	Accepted format:
391	- '[ok|not ok] [test number] [-] [test name] [optional skip
392		directive]'
393
394	Parameters:
395	lines - LineStream of KTAP output to parse
396	test - Test object for current test being parsed
397	expected_num - expected test number for current test
398
399	Return:
400	True if successfully parsed a test result line.
401	"""
402	line = lines.peek()
403	match = TEST_RESULT.match(line)
404	skip_match = TEST_RESULT_SKIP.match(line)
405
406	# Check if line matches test result line format
407	if not match:
408		return False
409	lines.pop()
410
411	# Set name of test object
412	if skip_match:
413		test.name = skip_match.group(4)
414	else:
415		test.name = match.group(4)
416
417	# Check test num
418	num = int(match.group(2))
419	if num != expected_num:
420		test.add_error(f'Expected test number {expected_num} but found {num}')
421
422	# Set status of test object
423	status = match.group(1)
424	if skip_match:
425		test.status = TestStatus.SKIPPED
426	elif status == 'ok':
427		test.status = TestStatus.SUCCESS
428	else:
429		test.status = TestStatus.FAILURE
430	return True
431
432def parse_diagnostic(lines: LineStream) -> List[str]:
433	"""
434	Parse lines that do not match the format of a test result line or
435	test header line and returns them in list.
436
437	Line formats that are not parsed:
438	- '# Subtest: [test name]'
439	- '[ok|not ok] [test number] [-] [test name] [optional skip
440		directive]'
441	- 'KTAP version [version number]'
442
443	Parameters:
444	lines - LineStream of KTAP output to parse
445
446	Return:
447	Log of diagnostic lines
448	"""
449	log = []  # type: List[str]
450	non_diagnostic_lines = [TEST_RESULT, TEST_HEADER, KTAP_START]
451	while lines and not any(re.match(lines.peek())
452			for re in non_diagnostic_lines):
453		log.append(lines.pop())
454	return log
455
456
457# Printing helper methods:
458
459DIVIDER = '=' * 60
460
461def format_test_divider(message: str, len_message: int) -> str:
462	"""
463	Returns string with message centered in fixed width divider.
464
465	Example:
466	'===================== message example ====================='
467
468	Parameters:
469	message - message to be centered in divider line
470	len_message - length of the message to be printed such that
471		any characters of the color codes are not counted
472
473	Return:
474	String containing message centered in fixed width divider
475	"""
476	default_count = 3  # default number of dashes
477	len_1 = default_count
478	len_2 = default_count
479	difference = len(DIVIDER) - len_message - 2  # 2 spaces added
480	if difference > 0:
481		# calculate number of dashes for each side of the divider
482		len_1 = int(difference / 2)
483		len_2 = difference - len_1
484	return ('=' * len_1) + f' {message} ' + ('=' * len_2)
485
486def print_test_header(test: Test) -> None:
487	"""
488	Prints test header with test name and optionally the expected number
489	of subtests.
490
491	Example:
492	'=================== example (2 subtests) ==================='
493
494	Parameters:
495	test - Test object representing current test being printed
496	"""
497	message = test.name
498	if message != "":
499		# Add a leading space before the subtest counts only if a test name
500		# is provided using a "# Subtest" header line.
501		message += " "
502	if test.expected_count:
503		if test.expected_count == 1:
504			message += '(1 subtest)'
505		else:
506			message += f'({test.expected_count} subtests)'
507	stdout.print_with_timestamp(format_test_divider(message, len(message)))
508
509def print_log(log: Iterable[str]) -> None:
510	"""Prints all strings in saved log for test in yellow."""
511	formatted = textwrap.dedent('\n'.join(log))
512	for line in formatted.splitlines():
513		stdout.print_with_timestamp(stdout.yellow(line))
514
515def format_test_result(test: Test) -> str:
516	"""
517	Returns string with formatted test result with colored status and test
518	name.
519
520	Example:
521	'[PASSED] example'
522
523	Parameters:
524	test - Test object representing current test being printed
525
526	Return:
527	String containing formatted test result
528	"""
529	if test.status == TestStatus.SUCCESS:
530		return stdout.green('[PASSED] ') + test.name
531	if test.status == TestStatus.SKIPPED:
532		return stdout.yellow('[SKIPPED] ') + test.name
533	if test.status == TestStatus.NO_TESTS:
534		return stdout.yellow('[NO TESTS RUN] ') + test.name
535	if test.status == TestStatus.TEST_CRASHED:
536		print_log(test.log)
537		return stdout.red('[CRASHED] ') + test.name
538	print_log(test.log)
539	return stdout.red('[FAILED] ') + test.name
540
541def print_test_result(test: Test) -> None:
542	"""
543	Prints result line with status of test.
544
545	Example:
546	'[PASSED] example'
547
548	Parameters:
549	test - Test object representing current test being printed
550	"""
551	stdout.print_with_timestamp(format_test_result(test))
552
553def print_test_footer(test: Test) -> None:
554	"""
555	Prints test footer with status of test.
556
557	Example:
558	'===================== [PASSED] example ====================='
559
560	Parameters:
561	test - Test object representing current test being printed
562	"""
563	message = format_test_result(test)
564	stdout.print_with_timestamp(format_test_divider(message,
565		len(message) - stdout.color_len()))
566
567
568
569def _summarize_failed_tests(test: Test) -> str:
570	"""Tries to summarize all the failing subtests in `test`."""
571
572	def failed_names(test: Test, parent_name: str) -> List[str]:
573		# Note: we use 'main' internally for the top-level test.
574		if not parent_name or parent_name == 'main':
575			full_name = test.name
576		else:
577			full_name = parent_name + '.' + test.name
578
579		if not test.subtests:  # this is a leaf node
580			return [full_name]
581
582		# If all the children failed, just say this subtest failed.
583		# Don't summarize it down "the top-level test failed", though.
584		failed_subtests = [sub for sub in test.subtests if not sub.ok_status()]
585		if parent_name and len(failed_subtests) ==  len(test.subtests):
586			return [full_name]
587
588		all_failures = []  # type: List[str]
589		for t in failed_subtests:
590			all_failures.extend(failed_names(t, full_name))
591		return all_failures
592
593	failures = failed_names(test, '')
594	# If there are too many failures, printing them out will just be noisy.
595	if len(failures) > 10:  # this is an arbitrary limit
596		return ''
597
598	return 'Failures: ' + ', '.join(failures)
599
600
601def print_summary_line(test: Test) -> None:
602	"""
603	Prints summary line of test object. Color of line is dependent on
604	status of test. Color is green if test passes, yellow if test is
605	skipped, and red if the test fails or crashes. Summary line contains
606	counts of the statuses of the tests subtests or the test itself if it
607	has no subtests.
608
609	Example:
610	"Testing complete. Passed: 2, Failed: 0, Crashed: 0, Skipped: 0,
611	Errors: 0"
612
613	test - Test object representing current test being printed
614	"""
615	if test.status == TestStatus.SUCCESS:
616		color = stdout.green
617	elif test.status in (TestStatus.SKIPPED, TestStatus.NO_TESTS):
618		color = stdout.yellow
619	else:
620		color = stdout.red
621	stdout.print_with_timestamp(color(f'Testing complete. {test.counts}'))
622
623	# Summarize failures that might have gone off-screen since we had a lot
624	# of tests (arbitrarily defined as >=100 for now).
625	if test.ok_status() or test.counts.total() < 100:
626		return
627	summarized = _summarize_failed_tests(test)
628	if not summarized:
629		return
630	stdout.print_with_timestamp(color(summarized))
631
632# Other methods:
633
634def bubble_up_test_results(test: Test) -> None:
635	"""
636	If the test has subtests, add the test counts of the subtests to the
637	test and check if any of the tests crashed and if so set the test
638	status to crashed. Otherwise if the test has no subtests add the
639	status of the test to the test counts.
640
641	Parameters:
642	test - Test object for current test being parsed
643	"""
644	subtests = test.subtests
645	counts = test.counts
646	status = test.status
647	for t in subtests:
648		counts.add_subtest_counts(t.counts)
649	if counts.total() == 0:
650		counts.add_status(status)
651	elif test.counts.get_status() == TestStatus.TEST_CRASHED:
652		test.status = TestStatus.TEST_CRASHED
653
654def parse_test(lines: LineStream, expected_num: int, log: List[str], is_subtest: bool) -> Test:
655	"""
656	Finds next test to parse in LineStream, creates new Test object,
657	parses any subtests of the test, populates Test object with all
658	information (status, name) about the test and the Test objects for
659	any subtests, and then returns the Test object. The method accepts
660	three formats of tests:
661
662	Accepted test formats:
663
664	- Main KTAP/TAP header
665
666	Example:
667
668	KTAP version 1
669	1..4
670	[subtests]
671
672	- Subtest header (must include either the KTAP version line or
673	  "# Subtest" header line)
674
675	Example (preferred format with both KTAP version line and
676	"# Subtest" line):
677
678	KTAP version 1
679	# Subtest: name
680	1..3
681	[subtests]
682	ok 1 name
683
684	Example (only "# Subtest" line):
685
686	# Subtest: name
687	1..3
688	[subtests]
689	ok 1 name
690
691	Example (only KTAP version line, compliant with KTAP v1 spec):
692
693	KTAP version 1
694	1..3
695	[subtests]
696	ok 1 name
697
698	- Test result line
699
700	Example:
701
702	ok 1 - test
703
704	Parameters:
705	lines - LineStream of KTAP output to parse
706	expected_num - expected test number for test to be parsed
707	log - list of strings containing any preceding diagnostic lines
708		corresponding to the current test
709	is_subtest - boolean indicating whether test is a subtest
710
711	Return:
712	Test object populated with characteristics and any subtests
713	"""
714	test = Test()
715	test.log.extend(log)
716	if not is_subtest:
717		# If parsing the main/top-level test, parse KTAP version line and
718		# test plan
719		test.name = "main"
720		ktap_line = parse_ktap_header(lines, test)
721		parse_test_plan(lines, test)
722		parent_test = True
723	else:
724		# If not the main test, attempt to parse a test header containing
725		# the KTAP version line and/or subtest header line
726		ktap_line = parse_ktap_header(lines, test)
727		subtest_line = parse_test_header(lines, test)
728		parent_test = (ktap_line or subtest_line)
729		if parent_test:
730			# If KTAP version line and/or subtest header is found, attempt
731			# to parse test plan and print test header
732			parse_test_plan(lines, test)
733			print_test_header(test)
734	expected_count = test.expected_count
735	subtests = []
736	test_num = 1
737	while parent_test and (expected_count is None or test_num <= expected_count):
738		# Loop to parse any subtests.
739		# Break after parsing expected number of tests or
740		# if expected number of tests is unknown break when test
741		# result line with matching name to subtest header is found
742		# or no more lines in stream.
743		sub_log = parse_diagnostic(lines)
744		sub_test = Test()
745		if not lines or (peek_test_name_match(lines, test) and
746				is_subtest):
747			if expected_count and test_num <= expected_count:
748				# If parser reaches end of test before
749				# parsing expected number of subtests, print
750				# crashed subtest and record error
751				test.add_error('missing expected subtest!')
752				sub_test.log.extend(sub_log)
753				test.counts.add_status(
754					TestStatus.TEST_CRASHED)
755				print_test_result(sub_test)
756			else:
757				test.log.extend(sub_log)
758				break
759		else:
760			sub_test = parse_test(lines, test_num, sub_log, True)
761		subtests.append(sub_test)
762		test_num += 1
763	test.subtests = subtests
764	if is_subtest:
765		# If not main test, look for test result line
766		test.log.extend(parse_diagnostic(lines))
767		if test.name != "" and not peek_test_name_match(lines, test):
768			test.add_error('missing subtest result line!')
769		else:
770			parse_test_result(lines, test, expected_num)
771
772	# Check for there being no subtests within parent test
773	if parent_test and len(subtests) == 0:
774		# Don't override a bad status if this test had one reported.
775		# Assumption: no subtests means CRASHED is from Test.__init__()
776		if test.status in (TestStatus.TEST_CRASHED, TestStatus.SUCCESS):
777			test.status = TestStatus.NO_TESTS
778			test.add_error('0 tests run!')
779
780	# Add statuses to TestCounts attribute in Test object
781	bubble_up_test_results(test)
782	if parent_test and is_subtest:
783		# If test has subtests and is not the main test object, print
784		# footer.
785		print_test_footer(test)
786	elif is_subtest:
787		print_test_result(test)
788	return test
789
790def parse_run_tests(kernel_output: Iterable[str]) -> Test:
791	"""
792	Using kernel output, extract KTAP lines, parse the lines for test
793	results and print condensed test results and summary line.
794
795	Parameters:
796	kernel_output - Iterable object contains lines of kernel output
797
798	Return:
799	Test - the main test object with all subtests.
800	"""
801	stdout.print_with_timestamp(DIVIDER)
802	lines = extract_tap_lines(kernel_output)
803	test = Test()
804	if not lines:
805		test.name = '<missing>'
806		test.add_error('Could not find any KTAP output. Did any KUnit tests run?')
807		test.status = TestStatus.FAILURE_TO_PARSE_TESTS
808	else:
809		test = parse_test(lines, 0, [], False)
810		if test.status != TestStatus.NO_TESTS:
811			test.status = test.counts.get_status()
812	stdout.print_with_timestamp(DIVIDER)
813	print_summary_line(test)
814	return test
815