xref: /openbmc/linux/tools/testing/kunit/kunit_parser.py (revision 87832e937c808a7ebc41254b408362e3255c87c9)
16ebf5866SFelix Guo# SPDX-License-Identifier: GPL-2.0
26ebf5866SFelix Guo#
3d65d07cbSRae Moar# Parses KTAP test results from a kernel dmesg log and incrementally prints
4d65d07cbSRae Moar# results with reader-friendly format. Stores and returns test results in a
5d65d07cbSRae Moar# Test object.
66ebf5866SFelix Guo#
76ebf5866SFelix Guo# Copyright (C) 2019, Google LLC.
86ebf5866SFelix Guo# Author: Felix Guo <felixguoxiuping@gmail.com>
96ebf5866SFelix Guo# Author: Brendan Higgins <brendanhiggins@google.com>
10d65d07cbSRae Moar# Author: Rae Moar <rmoar@google.com>
116ebf5866SFelix Guo
12d65d07cbSRae Moarfrom __future__ import annotations
13f473dd94SDaniel Latypovfrom dataclasses import dataclass
146ebf5866SFelix Guoimport re
15c2bb92bcSDaniel Latypovimport textwrap
166ebf5866SFelix Guo
176ebf5866SFelix Guofrom enum import Enum, auto
1881c60306SDaniel Latypovfrom typing import Iterable, Iterator, List, Optional, Tuple
196ebf5866SFelix Guo
20e756dbebSDaniel Latypovfrom kunit_printer import stdout
21e756dbebSDaniel Latypov
220453f984SDaniel Latypovclass Test:
23d65d07cbSRae Moar	"""
24d65d07cbSRae Moar	A class to represent a test parsed from KTAP results. All KTAP
25d65d07cbSRae Moar	results within a test log are stored in a main Test object as
26d65d07cbSRae Moar	subtests.
27d65d07cbSRae Moar
28d65d07cbSRae Moar	Attributes:
29d65d07cbSRae Moar	status : TestStatus - status of the test
30d65d07cbSRae Moar	name : str - name of the test
31d65d07cbSRae Moar	expected_count : int - expected number of subtests (0 if single
32d65d07cbSRae Moar		test case and None if unknown expected number of subtests)
33d65d07cbSRae Moar	subtests : List[Test] - list of subtests
34d65d07cbSRae Moar	log : List[str] - log of KTAP lines that correspond to the test
35d65d07cbSRae Moar	counts : TestCounts - counts of the test statuses and errors of
36d65d07cbSRae Moar		subtests or of the test itself if the test is a single
37d65d07cbSRae Moar		test case.
38d65d07cbSRae Moar	"""
3909641f7cSDaniel Latypov	def __init__(self) -> None:
40d65d07cbSRae Moar		"""Creates Test object with default attributes."""
41d65d07cbSRae Moar		self.status = TestStatus.TEST_CRASHED
4209641f7cSDaniel Latypov		self.name = ''
43d65d07cbSRae Moar		self.expected_count = 0  # type: Optional[int]
44d65d07cbSRae Moar		self.subtests = []  # type: List[Test]
4509641f7cSDaniel Latypov		self.log = []  # type: List[str]
46d65d07cbSRae Moar		self.counts = TestCounts()
476ebf5866SFelix Guo
4809641f7cSDaniel Latypov	def __str__(self) -> str:
49d65d07cbSRae Moar		"""Returns string representation of a Test class object."""
5094507ee3SDaniel Latypov		return (f'Test({self.status}, {self.name}, {self.expected_count}, '
5194507ee3SDaniel Latypov			f'{self.subtests}, {self.log}, {self.counts})')
526ebf5866SFelix Guo
5309641f7cSDaniel Latypov	def __repr__(self) -> str:
54d65d07cbSRae Moar		"""Returns string representation of a Test class object."""
556ebf5866SFelix Guo		return str(self)
566ebf5866SFelix Guo
57d65d07cbSRae Moar	def add_error(self, error_message: str) -> None:
58d65d07cbSRae Moar		"""Records an error that occurred while parsing this test."""
59d65d07cbSRae Moar		self.counts.errors += 1
60e756dbebSDaniel Latypov		stdout.print_with_timestamp(stdout.red('[ERROR]') + f' Test: {self.name}: {error_message}')
61d65d07cbSRae Moar
62f19dd011SDaniel Latypov	def ok_status(self) -> bool:
63f19dd011SDaniel Latypov		"""Returns true if the status was ok, i.e. passed or skipped."""
64f19dd011SDaniel Latypov		return self.status in (TestStatus.SUCCESS, TestStatus.SKIPPED)
65f19dd011SDaniel Latypov
666ebf5866SFelix Guoclass TestStatus(Enum):
67d65d07cbSRae Moar	"""An enumeration class to represent the status of a test."""
686ebf5866SFelix Guo	SUCCESS = auto()
696ebf5866SFelix Guo	FAILURE = auto()
705acaf603SDavid Gow	SKIPPED = auto()
716ebf5866SFelix Guo	TEST_CRASHED = auto()
726ebf5866SFelix Guo	NO_TESTS = auto()
7345dcbb6fSBrendan Higgins	FAILURE_TO_PARSE_TESTS = auto()
746ebf5866SFelix Guo
75f473dd94SDaniel Latypov@dataclass
76d65d07cbSRae Moarclass TestCounts:
77d65d07cbSRae Moar	"""
78d65d07cbSRae Moar	Tracks the counts of statuses of all test cases and any errors within
79d65d07cbSRae Moar	a Test.
80d65d07cbSRae Moar	"""
81f473dd94SDaniel Latypov	passed: int = 0
82f473dd94SDaniel Latypov	failed: int = 0
83f473dd94SDaniel Latypov	crashed: int = 0
84f473dd94SDaniel Latypov	skipped: int = 0
85f473dd94SDaniel Latypov	errors: int = 0
86d65d07cbSRae Moar
87d65d07cbSRae Moar	def __str__(self) -> str:
8894507ee3SDaniel Latypov		"""Returns the string representation of a TestCounts object."""
89c2497643SDaniel Latypov		statuses = [('passed', self.passed), ('failed', self.failed),
90c2497643SDaniel Latypov			('crashed', self.crashed), ('skipped', self.skipped),
91c2497643SDaniel Latypov			('errors', self.errors)]
92c2497643SDaniel Latypov		return f'Ran {self.total()} tests: ' + \
93c2497643SDaniel Latypov			', '.join(f'{s}: {n}' for s, n in statuses if n > 0)
94d65d07cbSRae Moar
95d65d07cbSRae Moar	def total(self) -> int:
96d65d07cbSRae Moar		"""Returns the total number of test cases within a test
97d65d07cbSRae Moar		object, where a test case is a test with no subtests.
98d65d07cbSRae Moar		"""
99d65d07cbSRae Moar		return (self.passed + self.failed + self.crashed +
100d65d07cbSRae Moar			self.skipped)
101d65d07cbSRae Moar
102d65d07cbSRae Moar	def add_subtest_counts(self, counts: TestCounts) -> None:
103d65d07cbSRae Moar		"""
104d65d07cbSRae Moar		Adds the counts of another TestCounts object to the current
105d65d07cbSRae Moar		TestCounts object. Used to add the counts of a subtest to the
106d65d07cbSRae Moar		parent test.
107d65d07cbSRae Moar
108d65d07cbSRae Moar		Parameters:
109d65d07cbSRae Moar		counts - a different TestCounts object whose counts
110d65d07cbSRae Moar			will be added to the counts of the TestCounts object
111d65d07cbSRae Moar		"""
112d65d07cbSRae Moar		self.passed += counts.passed
113d65d07cbSRae Moar		self.failed += counts.failed
114d65d07cbSRae Moar		self.crashed += counts.crashed
115d65d07cbSRae Moar		self.skipped += counts.skipped
116d65d07cbSRae Moar		self.errors += counts.errors
117d65d07cbSRae Moar
118d65d07cbSRae Moar	def get_status(self) -> TestStatus:
119d65d07cbSRae Moar		"""Returns the aggregated status of a Test using test
120d65d07cbSRae Moar		counts.
121d65d07cbSRae Moar		"""
122d65d07cbSRae Moar		if self.total() == 0:
123d65d07cbSRae Moar			return TestStatus.NO_TESTS
1240453f984SDaniel Latypov		if self.crashed:
12594507ee3SDaniel Latypov			# Crashes should take priority.
126d65d07cbSRae Moar			return TestStatus.TEST_CRASHED
1270453f984SDaniel Latypov		if self.failed:
128d65d07cbSRae Moar			return TestStatus.FAILURE
1290453f984SDaniel Latypov		if self.passed:
13094507ee3SDaniel Latypov			# No failures or crashes, looks good!
131d65d07cbSRae Moar			return TestStatus.SUCCESS
13294507ee3SDaniel Latypov		# We have only skipped tests.
133d65d07cbSRae Moar		return TestStatus.SKIPPED
134d65d07cbSRae Moar
135d65d07cbSRae Moar	def add_status(self, status: TestStatus) -> None:
13694507ee3SDaniel Latypov		"""Increments the count for `status`."""
137d65d07cbSRae Moar		if status == TestStatus.SUCCESS:
138d65d07cbSRae Moar			self.passed += 1
139d65d07cbSRae Moar		elif status == TestStatus.FAILURE:
140d65d07cbSRae Moar			self.failed += 1
141d65d07cbSRae Moar		elif status == TestStatus.SKIPPED:
142d65d07cbSRae Moar			self.skipped += 1
143d65d07cbSRae Moar		elif status != TestStatus.NO_TESTS:
144d65d07cbSRae Moar			self.crashed += 1
145d65d07cbSRae Moar
146b29b14f1SDaniel Latypovclass LineStream:
147d65d07cbSRae Moar	"""
148d65d07cbSRae Moar	A class to represent the lines of kernel output.
149142189f0SDaniel Latypov	Provides a lazy peek()/pop() interface over an iterator of
150d65d07cbSRae Moar	(line#, text).
151d65d07cbSRae Moar	"""
152b29b14f1SDaniel Latypov	_lines: Iterator[Tuple[int, str]]
153b29b14f1SDaniel Latypov	_next: Tuple[int, str]
154142189f0SDaniel Latypov	_need_next: bool
155b29b14f1SDaniel Latypov	_done: bool
156b29b14f1SDaniel Latypov
157b29b14f1SDaniel Latypov	def __init__(self, lines: Iterator[Tuple[int, str]]):
158d65d07cbSRae Moar		"""Creates a new LineStream that wraps the given iterator."""
159b29b14f1SDaniel Latypov		self._lines = lines
160b29b14f1SDaniel Latypov		self._done = False
161142189f0SDaniel Latypov		self._need_next = True
162b29b14f1SDaniel Latypov		self._next = (0, '')
163b29b14f1SDaniel Latypov
164b29b14f1SDaniel Latypov	def _get_next(self) -> None:
165142189f0SDaniel Latypov		"""Advances the LineSteam to the next line, if necessary."""
166142189f0SDaniel Latypov		if not self._need_next:
167142189f0SDaniel Latypov			return
168b29b14f1SDaniel Latypov		try:
169b29b14f1SDaniel Latypov			self._next = next(self._lines)
170b29b14f1SDaniel Latypov		except StopIteration:
171b29b14f1SDaniel Latypov			self._done = True
172142189f0SDaniel Latypov		finally:
173142189f0SDaniel Latypov			self._need_next = False
174b29b14f1SDaniel Latypov
175b29b14f1SDaniel Latypov	def peek(self) -> str:
176d65d07cbSRae Moar		"""Returns the current line, without advancing the LineStream.
177d65d07cbSRae Moar		"""
178142189f0SDaniel Latypov		self._get_next()
179b29b14f1SDaniel Latypov		return self._next[1]
180b29b14f1SDaniel Latypov
181b29b14f1SDaniel Latypov	def pop(self) -> str:
182d65d07cbSRae Moar		"""Returns the current line and advances the LineStream to
183d65d07cbSRae Moar		the next line.
184d65d07cbSRae Moar		"""
185142189f0SDaniel Latypov		s = self.peek()
186142189f0SDaniel Latypov		if self._done:
187142189f0SDaniel Latypov			raise ValueError(f'LineStream: going past EOF, last line was {s}')
188142189f0SDaniel Latypov		self._need_next = True
189142189f0SDaniel Latypov		return s
190b29b14f1SDaniel Latypov
191b29b14f1SDaniel Latypov	def __bool__(self) -> bool:
192d65d07cbSRae Moar		"""Returns True if stream has more lines."""
193142189f0SDaniel Latypov		self._get_next()
194b29b14f1SDaniel Latypov		return not self._done
195b29b14f1SDaniel Latypov
196b29b14f1SDaniel Latypov	# Only used by kunit_tool_test.py.
197b29b14f1SDaniel Latypov	def __iter__(self) -> Iterator[str]:
198d65d07cbSRae Moar		"""Empties all lines stored in LineStream object into
199d65d07cbSRae Moar		Iterator object and returns the Iterator object.
200d65d07cbSRae Moar		"""
201b29b14f1SDaniel Latypov		while bool(self):
202b29b14f1SDaniel Latypov			yield self.pop()
203b29b14f1SDaniel Latypov
204b29b14f1SDaniel Latypov	def line_number(self) -> int:
205d65d07cbSRae Moar		"""Returns the line number of the current line."""
206142189f0SDaniel Latypov		self._get_next()
207b29b14f1SDaniel Latypov		return self._next[0]
208b29b14f1SDaniel Latypov
209d65d07cbSRae Moar# Parsing helper methods:
210d65d07cbSRae Moar
211c2bb92bcSDaniel LatypovKTAP_START = re.compile(r'\s*KTAP version ([0-9]+)$')
212c2bb92bcSDaniel LatypovTAP_START = re.compile(r'\s*TAP version ([0-9]+)$')
213c2bb92bcSDaniel LatypovKTAP_END = re.compile(r'\s*(List of all partitions:|'
214b6d5799bSDavid Gow	'Kernel panic - not syncing: VFS:|reboot: System halted)')
215723c8258SRae MoarEXECUTOR_ERROR = re.compile(r'\s*kunit executor: (.*)$')
2166ebf5866SFelix Guo
217c2bb92bcSDaniel Latypovdef extract_tap_lines(kernel_output: Iterable[str]) -> LineStream:
218d65d07cbSRae Moar	"""Extracts KTAP lines from the kernel output."""
219d65d07cbSRae Moar	def isolate_ktap_output(kernel_output: Iterable[str]) \
220d65d07cbSRae Moar			-> Iterator[Tuple[int, str]]:
221b29b14f1SDaniel Latypov		line_num = 0
2226ebf5866SFelix Guo		started = False
2236ebf5866SFelix Guo		for line in kernel_output:
224b29b14f1SDaniel Latypov			line_num += 1
225d65d07cbSRae Moar			line = line.rstrip()  # remove trailing \n
226d65d07cbSRae Moar			if not started and KTAP_START.search(line):
227d65d07cbSRae Moar				# start extracting KTAP lines and set prefix
228d65d07cbSRae Moar				# to number of characters before version line
229d65d07cbSRae Moar				prefix_len = len(
230d65d07cbSRae Moar					line.split('KTAP version')[0])
231d65d07cbSRae Moar				started = True
232d65d07cbSRae Moar				yield line_num, line[prefix_len:]
233d65d07cbSRae Moar			elif not started and TAP_START.search(line):
234d65d07cbSRae Moar				# start extracting KTAP lines and set prefix
235d65d07cbSRae Moar				# to number of characters before version line
236afc63da6SHeidi Fahim				prefix_len = len(line.split('TAP version')[0])
2376ebf5866SFelix Guo				started = True
238b29b14f1SDaniel Latypov				yield line_num, line[prefix_len:]
239d65d07cbSRae Moar			elif started and KTAP_END.search(line):
240d65d07cbSRae Moar				# stop extracting KTAP lines
2416ebf5866SFelix Guo				break
2426ebf5866SFelix Guo			elif started:
243c2bb92bcSDaniel Latypov				# remove the prefix, if any.
244a15cfa39SDaniel Latypov				line = line[prefix_len:]
245d65d07cbSRae Moar				yield line_num, line
246723c8258SRae Moar			elif EXECUTOR_ERROR.search(line):
247723c8258SRae Moar				yield line_num, line
248d65d07cbSRae Moar	return LineStream(lines=isolate_ktap_output(kernel_output))
249d65d07cbSRae Moar
250d65d07cbSRae MoarKTAP_VERSIONS = [1]
251d65d07cbSRae MoarTAP_VERSIONS = [13, 14]
252d65d07cbSRae Moar
253d65d07cbSRae Moardef check_version(version_num: int, accepted_versions: List[int],
254d65d07cbSRae Moar			version_type: str, test: Test) -> None:
255d65d07cbSRae Moar	"""
256d65d07cbSRae Moar	Adds error to test object if version number is too high or too
257d65d07cbSRae Moar	low.
258d65d07cbSRae Moar
259d65d07cbSRae Moar	Parameters:
260d65d07cbSRae Moar	version_num - The inputted version number from the parsed KTAP or TAP
261d65d07cbSRae Moar		header line
262d65d07cbSRae Moar	accepted_version - List of accepted KTAP or TAP versions
263d65d07cbSRae Moar	version_type - 'KTAP' or 'TAP' depending on the type of
264d65d07cbSRae Moar		version line.
265d65d07cbSRae Moar	test - Test object for current test being parsed
266d65d07cbSRae Moar	"""
267d65d07cbSRae Moar	if version_num < min(accepted_versions):
26894507ee3SDaniel Latypov		test.add_error(f'{version_type} version lower than expected!')
269d65d07cbSRae Moar	elif version_num > max(accepted_versions):
27094507ee3SDaniel Latypov		test.add_error(f'{version_type} version higer than expected!')
271d65d07cbSRae Moar
272d65d07cbSRae Moardef parse_ktap_header(lines: LineStream, test: Test) -> bool:
273d65d07cbSRae Moar	"""
274d65d07cbSRae Moar	Parses KTAP/TAP header line and checks version number.
275d65d07cbSRae Moar	Returns False if fails to parse KTAP/TAP header line.
276d65d07cbSRae Moar
277d65d07cbSRae Moar	Accepted formats:
278d65d07cbSRae Moar	- 'KTAP version [version number]'
279d65d07cbSRae Moar	- 'TAP version [version number]'
280d65d07cbSRae Moar
281d65d07cbSRae Moar	Parameters:
282d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
283d65d07cbSRae Moar	test - Test object for current test being parsed
284d65d07cbSRae Moar
285d65d07cbSRae Moar	Return:
286d65d07cbSRae Moar	True if successfully parsed KTAP/TAP header line
287d65d07cbSRae Moar	"""
288d65d07cbSRae Moar	ktap_match = KTAP_START.match(lines.peek())
289d65d07cbSRae Moar	tap_match = TAP_START.match(lines.peek())
290d65d07cbSRae Moar	if ktap_match:
291d65d07cbSRae Moar		version_num = int(ktap_match.group(1))
292d65d07cbSRae Moar		check_version(version_num, KTAP_VERSIONS, 'KTAP', test)
293d65d07cbSRae Moar	elif tap_match:
294d65d07cbSRae Moar		version_num = int(tap_match.group(1))
295d65d07cbSRae Moar		check_version(version_num, TAP_VERSIONS, 'TAP', test)
296d65d07cbSRae Moar	else:
297d65d07cbSRae Moar		return False
2985937e0c0SDaniel Latypov	lines.pop()
299d65d07cbSRae Moar	return True
300d65d07cbSRae Moar
301c2bb92bcSDaniel LatypovTEST_HEADER = re.compile(r'^\s*# Subtest: (.*)$')
302d65d07cbSRae Moar
303d65d07cbSRae Moardef parse_test_header(lines: LineStream, test: Test) -> bool:
304d65d07cbSRae Moar	"""
305d65d07cbSRae Moar	Parses test header and stores test name in test object.
306d65d07cbSRae Moar	Returns False if fails to parse test header line.
307d65d07cbSRae Moar
308d65d07cbSRae Moar	Accepted format:
309d65d07cbSRae Moar	- '# Subtest: [test name]'
310d65d07cbSRae Moar
311d65d07cbSRae Moar	Parameters:
312d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
313d65d07cbSRae Moar	test - Test object for current test being parsed
314d65d07cbSRae Moar
315d65d07cbSRae Moar	Return:
316d65d07cbSRae Moar	True if successfully parsed test header line
317d65d07cbSRae Moar	"""
318d65d07cbSRae Moar	match = TEST_HEADER.match(lines.peek())
319d65d07cbSRae Moar	if not match:
320d65d07cbSRae Moar		return False
321d65d07cbSRae Moar	test.name = match.group(1)
3225937e0c0SDaniel Latypov	lines.pop()
323d65d07cbSRae Moar	return True
324d65d07cbSRae Moar
325c2bb92bcSDaniel LatypovTEST_PLAN = re.compile(r'^\s*1\.\.([0-9]+)')
326d65d07cbSRae Moar
327d65d07cbSRae Moardef parse_test_plan(lines: LineStream, test: Test) -> bool:
328d65d07cbSRae Moar	"""
329d65d07cbSRae Moar	Parses test plan line and stores the expected number of subtests in
330d65d07cbSRae Moar	test object. Reports an error if expected count is 0.
331c68077b1SDavid Gow	Returns False and sets expected_count to None if there is no valid test
332c68077b1SDavid Gow	plan.
333d65d07cbSRae Moar
334d65d07cbSRae Moar	Accepted format:
335d65d07cbSRae Moar	- '1..[number of subtests]'
336d65d07cbSRae Moar
337d65d07cbSRae Moar	Parameters:
338d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
339d65d07cbSRae Moar	test - Test object for current test being parsed
340d65d07cbSRae Moar
341d65d07cbSRae Moar	Return:
342d65d07cbSRae Moar	True if successfully parsed test plan line
343d65d07cbSRae Moar	"""
344d65d07cbSRae Moar	match = TEST_PLAN.match(lines.peek())
345d65d07cbSRae Moar	if not match:
346d65d07cbSRae Moar		test.expected_count = None
347d65d07cbSRae Moar		return False
348d65d07cbSRae Moar	expected_count = int(match.group(1))
349d65d07cbSRae Moar	test.expected_count = expected_count
3505937e0c0SDaniel Latypov	lines.pop()
351d65d07cbSRae Moar	return True
352d65d07cbSRae Moar
353c2bb92bcSDaniel LatypovTEST_RESULT = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
354d65d07cbSRae Moar
355c2bb92bcSDaniel LatypovTEST_RESULT_SKIP = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?(.*) # SKIP(.*)$')
356d65d07cbSRae Moar
357d65d07cbSRae Moardef peek_test_name_match(lines: LineStream, test: Test) -> bool:
358d65d07cbSRae Moar	"""
359d65d07cbSRae Moar	Matches current line with the format of a test result line and checks
360d65d07cbSRae Moar	if the name matches the name of the current test.
361d65d07cbSRae Moar	Returns False if fails to match format or name.
362d65d07cbSRae Moar
363d65d07cbSRae Moar	Accepted format:
364d65d07cbSRae Moar	- '[ok|not ok] [test number] [-] [test name] [optional skip
365d65d07cbSRae Moar		directive]'
366d65d07cbSRae Moar
367d65d07cbSRae Moar	Parameters:
368d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
369d65d07cbSRae Moar	test - Test object for current test being parsed
370d65d07cbSRae Moar
371d65d07cbSRae Moar	Return:
372d65d07cbSRae Moar	True if matched a test result line and the name matching the
373d65d07cbSRae Moar		expected test name
374d65d07cbSRae Moar	"""
375d65d07cbSRae Moar	line = lines.peek()
376d65d07cbSRae Moar	match = TEST_RESULT.match(line)
377d65d07cbSRae Moar	if not match:
378d65d07cbSRae Moar		return False
379d65d07cbSRae Moar	name = match.group(4)
3800453f984SDaniel Latypov	return name == test.name
381d65d07cbSRae Moar
382d65d07cbSRae Moardef parse_test_result(lines: LineStream, test: Test,
383d65d07cbSRae Moar			expected_num: int) -> bool:
384d65d07cbSRae Moar	"""
385d65d07cbSRae Moar	Parses test result line and stores the status and name in the test
386d65d07cbSRae Moar	object. Reports an error if the test number does not match expected
387d65d07cbSRae Moar	test number.
388d65d07cbSRae Moar	Returns False if fails to parse test result line.
389d65d07cbSRae Moar
390d65d07cbSRae Moar	Note that the SKIP directive is the only direction that causes a
391d65d07cbSRae Moar	change in status.
392d65d07cbSRae Moar
393d65d07cbSRae Moar	Accepted format:
394d65d07cbSRae Moar	- '[ok|not ok] [test number] [-] [test name] [optional skip
395d65d07cbSRae Moar		directive]'
396d65d07cbSRae Moar
397d65d07cbSRae Moar	Parameters:
398d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
399d65d07cbSRae Moar	test - Test object for current test being parsed
400d65d07cbSRae Moar	expected_num - expected test number for current test
401d65d07cbSRae Moar
402d65d07cbSRae Moar	Return:
403d65d07cbSRae Moar	True if successfully parsed a test result line.
404d65d07cbSRae Moar	"""
405d65d07cbSRae Moar	line = lines.peek()
406d65d07cbSRae Moar	match = TEST_RESULT.match(line)
407d65d07cbSRae Moar	skip_match = TEST_RESULT_SKIP.match(line)
408d65d07cbSRae Moar
409d65d07cbSRae Moar	# Check if line matches test result line format
410d65d07cbSRae Moar	if not match:
411d65d07cbSRae Moar		return False
4125937e0c0SDaniel Latypov	lines.pop()
413d65d07cbSRae Moar
414d65d07cbSRae Moar	# Set name of test object
415d65d07cbSRae Moar	if skip_match:
416d65d07cbSRae Moar		test.name = skip_match.group(4)
417d65d07cbSRae Moar	else:
418d65d07cbSRae Moar		test.name = match.group(4)
419d65d07cbSRae Moar
420d65d07cbSRae Moar	# Check test num
421d65d07cbSRae Moar	num = int(match.group(2))
422d65d07cbSRae Moar	if num != expected_num:
42394507ee3SDaniel Latypov		test.add_error(f'Expected test number {expected_num} but found {num}')
424d65d07cbSRae Moar
425d65d07cbSRae Moar	# Set status of test object
426d65d07cbSRae Moar	status = match.group(1)
427d65d07cbSRae Moar	if skip_match:
428d65d07cbSRae Moar		test.status = TestStatus.SKIPPED
429d65d07cbSRae Moar	elif status == 'ok':
430d65d07cbSRae Moar		test.status = TestStatus.SUCCESS
431d65d07cbSRae Moar	else:
432d65d07cbSRae Moar		test.status = TestStatus.FAILURE
433d65d07cbSRae Moar	return True
434d65d07cbSRae Moar
435d65d07cbSRae Moardef parse_diagnostic(lines: LineStream) -> List[str]:
436d65d07cbSRae Moar	"""
437d65d07cbSRae Moar	Parse lines that do not match the format of a test result line or
438d65d07cbSRae Moar	test header line and returns them in list.
439d65d07cbSRae Moar
440d65d07cbSRae Moar	Line formats that are not parsed:
441d65d07cbSRae Moar	- '# Subtest: [test name]'
442d65d07cbSRae Moar	- '[ok|not ok] [test number] [-] [test name] [optional skip
443d65d07cbSRae Moar		directive]'
444434498a6SRae Moar	- 'KTAP version [version number]'
445d65d07cbSRae Moar
446d65d07cbSRae Moar	Parameters:
447d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
448d65d07cbSRae Moar
449d65d07cbSRae Moar	Return:
450d65d07cbSRae Moar	Log of diagnostic lines
451d65d07cbSRae Moar	"""
452d65d07cbSRae Moar	log = []  # type: List[str]
453*29482da8SRae Moar	non_diagnostic_lines = [TEST_RESULT, TEST_HEADER, KTAP_START, TAP_START, TEST_PLAN]
454434498a6SRae Moar	while lines and not any(re.match(lines.peek())
455434498a6SRae Moar			for re in non_diagnostic_lines):
456d65d07cbSRae Moar		log.append(lines.pop())
457d65d07cbSRae Moar	return log
458d65d07cbSRae Moar
459d65d07cbSRae Moar
460d65d07cbSRae Moar# Printing helper methods:
4616ebf5866SFelix Guo
4626ebf5866SFelix GuoDIVIDER = '=' * 60
4636ebf5866SFelix Guo
464d65d07cbSRae Moardef format_test_divider(message: str, len_message: int) -> str:
465d65d07cbSRae Moar	"""
466d65d07cbSRae Moar	Returns string with message centered in fixed width divider.
4676ebf5866SFelix Guo
468d65d07cbSRae Moar	Example:
469d65d07cbSRae Moar	'===================== message example ====================='
4706ebf5866SFelix Guo
471d65d07cbSRae Moar	Parameters:
472d65d07cbSRae Moar	message - message to be centered in divider line
473d65d07cbSRae Moar	len_message - length of the message to be printed such that
474d65d07cbSRae Moar		any characters of the color codes are not counted
475d65d07cbSRae Moar
476d65d07cbSRae Moar	Return:
477d65d07cbSRae Moar	String containing message centered in fixed width divider
478d65d07cbSRae Moar	"""
479d65d07cbSRae Moar	default_count = 3  # default number of dashes
480d65d07cbSRae Moar	len_1 = default_count
481d65d07cbSRae Moar	len_2 = default_count
482d65d07cbSRae Moar	difference = len(DIVIDER) - len_message - 2  # 2 spaces added
483d65d07cbSRae Moar	if difference > 0:
484d65d07cbSRae Moar		# calculate number of dashes for each side of the divider
485d65d07cbSRae Moar		len_1 = int(difference / 2)
486d65d07cbSRae Moar		len_2 = difference - len_1
48794507ee3SDaniel Latypov	return ('=' * len_1) + f' {message} ' + ('=' * len_2)
488d65d07cbSRae Moar
489d65d07cbSRae Moardef print_test_header(test: Test) -> None:
490d65d07cbSRae Moar	"""
491d65d07cbSRae Moar	Prints test header with test name and optionally the expected number
492d65d07cbSRae Moar	of subtests.
493d65d07cbSRae Moar
494d65d07cbSRae Moar	Example:
495d65d07cbSRae Moar	'=================== example (2 subtests) ==================='
496d65d07cbSRae Moar
497d65d07cbSRae Moar	Parameters:
498d65d07cbSRae Moar	test - Test object representing current test being printed
499d65d07cbSRae Moar	"""
500d65d07cbSRae Moar	message = test.name
501434498a6SRae Moar	if message != "":
502434498a6SRae Moar		# Add a leading space before the subtest counts only if a test name
503434498a6SRae Moar		# is provided using a "# Subtest" header line.
504434498a6SRae Moar		message += " "
505d65d07cbSRae Moar	if test.expected_count:
506d65d07cbSRae Moar		if test.expected_count == 1:
50794507ee3SDaniel Latypov			message += '(1 subtest)'
508d65d07cbSRae Moar		else:
50994507ee3SDaniel Latypov			message += f'({test.expected_count} subtests)'
510e756dbebSDaniel Latypov	stdout.print_with_timestamp(format_test_divider(message, len(message)))
511d65d07cbSRae Moar
512d65d07cbSRae Moardef print_log(log: Iterable[str]) -> None:
51394507ee3SDaniel Latypov	"""Prints all strings in saved log for test in yellow."""
514c2bb92bcSDaniel Latypov	formatted = textwrap.dedent('\n'.join(log))
515c2bb92bcSDaniel Latypov	for line in formatted.splitlines():
516c2bb92bcSDaniel Latypov		stdout.print_with_timestamp(stdout.yellow(line))
5176ebf5866SFelix Guo
518d65d07cbSRae Moardef format_test_result(test: Test) -> str:
519d65d07cbSRae Moar	"""
520d65d07cbSRae Moar	Returns string with formatted test result with colored status and test
521d65d07cbSRae Moar	name.
5226ebf5866SFelix Guo
523d65d07cbSRae Moar	Example:
524d65d07cbSRae Moar	'[PASSED] example'
5256ebf5866SFelix Guo
526d65d07cbSRae Moar	Parameters:
527d65d07cbSRae Moar	test - Test object representing current test being printed
5286ebf5866SFelix Guo
529d65d07cbSRae Moar	Return:
530d65d07cbSRae Moar	String containing formatted test result
531d65d07cbSRae Moar	"""
532d65d07cbSRae Moar	if test.status == TestStatus.SUCCESS:
533e756dbebSDaniel Latypov		return stdout.green('[PASSED] ') + test.name
5340453f984SDaniel Latypov	if test.status == TestStatus.SKIPPED:
535e756dbebSDaniel Latypov		return stdout.yellow('[SKIPPED] ') + test.name
5360453f984SDaniel Latypov	if test.status == TestStatus.NO_TESTS:
537e756dbebSDaniel Latypov		return stdout.yellow('[NO TESTS RUN] ') + test.name
5380453f984SDaniel Latypov	if test.status == TestStatus.TEST_CRASHED:
539d65d07cbSRae Moar		print_log(test.log)
540e756dbebSDaniel Latypov		return stdout.red('[CRASHED] ') + test.name
541d65d07cbSRae Moar	print_log(test.log)
542e756dbebSDaniel Latypov	return stdout.red('[FAILED] ') + test.name
543d65d07cbSRae Moar
544d65d07cbSRae Moardef print_test_result(test: Test) -> None:
545d65d07cbSRae Moar	"""
546d65d07cbSRae Moar	Prints result line with status of test.
547d65d07cbSRae Moar
548d65d07cbSRae Moar	Example:
549d65d07cbSRae Moar	'[PASSED] example'
550d65d07cbSRae Moar
551d65d07cbSRae Moar	Parameters:
552d65d07cbSRae Moar	test - Test object representing current test being printed
553d65d07cbSRae Moar	"""
554e756dbebSDaniel Latypov	stdout.print_with_timestamp(format_test_result(test))
555d65d07cbSRae Moar
556d65d07cbSRae Moardef print_test_footer(test: Test) -> None:
557d65d07cbSRae Moar	"""
558d65d07cbSRae Moar	Prints test footer with status of test.
559d65d07cbSRae Moar
560d65d07cbSRae Moar	Example:
561d65d07cbSRae Moar	'===================== [PASSED] example ====================='
562d65d07cbSRae Moar
563d65d07cbSRae Moar	Parameters:
564d65d07cbSRae Moar	test - Test object representing current test being printed
565d65d07cbSRae Moar	"""
566d65d07cbSRae Moar	message = format_test_result(test)
567e756dbebSDaniel Latypov	stdout.print_with_timestamp(format_test_divider(message,
568e756dbebSDaniel Latypov		len(message) - stdout.color_len()))
569d65d07cbSRae Moar
570f19dd011SDaniel Latypov
571f19dd011SDaniel Latypov
572f19dd011SDaniel Latypovdef _summarize_failed_tests(test: Test) -> str:
573f19dd011SDaniel Latypov	"""Tries to summarize all the failing subtests in `test`."""
574f19dd011SDaniel Latypov
575f19dd011SDaniel Latypov	def failed_names(test: Test, parent_name: str) -> List[str]:
576f19dd011SDaniel Latypov		# Note: we use 'main' internally for the top-level test.
577f19dd011SDaniel Latypov		if not parent_name or parent_name == 'main':
578f19dd011SDaniel Latypov			full_name = test.name
579f19dd011SDaniel Latypov		else:
580f19dd011SDaniel Latypov			full_name = parent_name + '.' + test.name
581f19dd011SDaniel Latypov
582f19dd011SDaniel Latypov		if not test.subtests:  # this is a leaf node
583f19dd011SDaniel Latypov			return [full_name]
584f19dd011SDaniel Latypov
585f19dd011SDaniel Latypov		# If all the children failed, just say this subtest failed.
586f19dd011SDaniel Latypov		# Don't summarize it down "the top-level test failed", though.
587f19dd011SDaniel Latypov		failed_subtests = [sub for sub in test.subtests if not sub.ok_status()]
588f19dd011SDaniel Latypov		if parent_name and len(failed_subtests) ==  len(test.subtests):
589f19dd011SDaniel Latypov			return [full_name]
590f19dd011SDaniel Latypov
591f19dd011SDaniel Latypov		all_failures = []  # type: List[str]
592f19dd011SDaniel Latypov		for t in failed_subtests:
593f19dd011SDaniel Latypov			all_failures.extend(failed_names(t, full_name))
594f19dd011SDaniel Latypov		return all_failures
595f19dd011SDaniel Latypov
596f19dd011SDaniel Latypov	failures = failed_names(test, '')
597f19dd011SDaniel Latypov	# If there are too many failures, printing them out will just be noisy.
598f19dd011SDaniel Latypov	if len(failures) > 10:  # this is an arbitrary limit
599f19dd011SDaniel Latypov		return ''
600f19dd011SDaniel Latypov
601f19dd011SDaniel Latypov	return 'Failures: ' + ', '.join(failures)
602f19dd011SDaniel Latypov
603f19dd011SDaniel Latypov
604d65d07cbSRae Moardef print_summary_line(test: Test) -> None:
605d65d07cbSRae Moar	"""
606d65d07cbSRae Moar	Prints summary line of test object. Color of line is dependent on
607d65d07cbSRae Moar	status of test. Color is green if test passes, yellow if test is
608d65d07cbSRae Moar	skipped, and red if the test fails or crashes. Summary line contains
609d65d07cbSRae Moar	counts of the statuses of the tests subtests or the test itself if it
610d65d07cbSRae Moar	has no subtests.
611d65d07cbSRae Moar
612d65d07cbSRae Moar	Example:
613d65d07cbSRae Moar	"Testing complete. Passed: 2, Failed: 0, Crashed: 0, Skipped: 0,
614d65d07cbSRae Moar	Errors: 0"
615d65d07cbSRae Moar
616d65d07cbSRae Moar	test - Test object representing current test being printed
617d65d07cbSRae Moar	"""
618d65d07cbSRae Moar	if test.status == TestStatus.SUCCESS:
619e756dbebSDaniel Latypov		color = stdout.green
6200453f984SDaniel Latypov	elif test.status in (TestStatus.SKIPPED, TestStatus.NO_TESTS):
621e756dbebSDaniel Latypov		color = stdout.yellow
6226ebf5866SFelix Guo	else:
623e756dbebSDaniel Latypov		color = stdout.red
624e756dbebSDaniel Latypov	stdout.print_with_timestamp(color(f'Testing complete. {test.counts}'))
625d65d07cbSRae Moar
626f19dd011SDaniel Latypov	# Summarize failures that might have gone off-screen since we had a lot
627f19dd011SDaniel Latypov	# of tests (arbitrarily defined as >=100 for now).
628f19dd011SDaniel Latypov	if test.ok_status() or test.counts.total() < 100:
629f19dd011SDaniel Latypov		return
630f19dd011SDaniel Latypov	summarized = _summarize_failed_tests(test)
631f19dd011SDaniel Latypov	if not summarized:
632f19dd011SDaniel Latypov		return
633f19dd011SDaniel Latypov	stdout.print_with_timestamp(color(summarized))
634f19dd011SDaniel Latypov
635d65d07cbSRae Moar# Other methods:
636d65d07cbSRae Moar
637d65d07cbSRae Moardef bubble_up_test_results(test: Test) -> None:
638d65d07cbSRae Moar	"""
639d65d07cbSRae Moar	If the test has subtests, add the test counts of the subtests to the
640d65d07cbSRae Moar	test and check if any of the tests crashed and if so set the test
641d65d07cbSRae Moar	status to crashed. Otherwise if the test has no subtests add the
642d65d07cbSRae Moar	status of the test to the test counts.
643d65d07cbSRae Moar
644d65d07cbSRae Moar	Parameters:
645d65d07cbSRae Moar	test - Test object for current test being parsed
646d65d07cbSRae Moar	"""
647d65d07cbSRae Moar	subtests = test.subtests
648d65d07cbSRae Moar	counts = test.counts
649d65d07cbSRae Moar	status = test.status
650d65d07cbSRae Moar	for t in subtests:
651d65d07cbSRae Moar		counts.add_subtest_counts(t.counts)
652d65d07cbSRae Moar	if counts.total() == 0:
653d65d07cbSRae Moar		counts.add_status(status)
654d65d07cbSRae Moar	elif test.counts.get_status() == TestStatus.TEST_CRASHED:
655d65d07cbSRae Moar		test.status = TestStatus.TEST_CRASHED
656d65d07cbSRae Moar
657434498a6SRae Moardef parse_test(lines: LineStream, expected_num: int, log: List[str], is_subtest: bool) -> Test:
658d65d07cbSRae Moar	"""
659d65d07cbSRae Moar	Finds next test to parse in LineStream, creates new Test object,
660d65d07cbSRae Moar	parses any subtests of the test, populates Test object with all
661d65d07cbSRae Moar	information (status, name) about the test and the Test objects for
662d65d07cbSRae Moar	any subtests, and then returns the Test object. The method accepts
663d65d07cbSRae Moar	three formats of tests:
664d65d07cbSRae Moar
665d65d07cbSRae Moar	Accepted test formats:
666d65d07cbSRae Moar
667d65d07cbSRae Moar	- Main KTAP/TAP header
668d65d07cbSRae Moar
669d65d07cbSRae Moar	Example:
670d65d07cbSRae Moar
671d65d07cbSRae Moar	KTAP version 1
672d65d07cbSRae Moar	1..4
673d65d07cbSRae Moar	[subtests]
674d65d07cbSRae Moar
675434498a6SRae Moar	- Subtest header (must include either the KTAP version line or
676434498a6SRae Moar	  "# Subtest" header line)
677d65d07cbSRae Moar
678434498a6SRae Moar	Example (preferred format with both KTAP version line and
679434498a6SRae Moar	"# Subtest" line):
680434498a6SRae Moar
681434498a6SRae Moar	KTAP version 1
682434498a6SRae Moar	# Subtest: name
683434498a6SRae Moar	1..3
684434498a6SRae Moar	[subtests]
685434498a6SRae Moar	ok 1 name
686434498a6SRae Moar
687434498a6SRae Moar	Example (only "# Subtest" line):
688d65d07cbSRae Moar
689d65d07cbSRae Moar	# Subtest: name
690d65d07cbSRae Moar	1..3
691d65d07cbSRae Moar	[subtests]
692d65d07cbSRae Moar	ok 1 name
693d65d07cbSRae Moar
694434498a6SRae Moar	Example (only KTAP version line, compliant with KTAP v1 spec):
695434498a6SRae Moar
696434498a6SRae Moar	KTAP version 1
697434498a6SRae Moar	1..3
698434498a6SRae Moar	[subtests]
699434498a6SRae Moar	ok 1 name
700434498a6SRae Moar
701d65d07cbSRae Moar	- Test result line
702d65d07cbSRae Moar
703d65d07cbSRae Moar	Example:
704d65d07cbSRae Moar
705d65d07cbSRae Moar	ok 1 - test
706d65d07cbSRae Moar
707d65d07cbSRae Moar	Parameters:
708d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
709d65d07cbSRae Moar	expected_num - expected test number for test to be parsed
710d65d07cbSRae Moar	log - list of strings containing any preceding diagnostic lines
711d65d07cbSRae Moar		corresponding to the current test
712434498a6SRae Moar	is_subtest - boolean indicating whether test is a subtest
713d65d07cbSRae Moar
714d65d07cbSRae Moar	Return:
715d65d07cbSRae Moar	Test object populated with characteristics and any subtests
716d65d07cbSRae Moar	"""
717d65d07cbSRae Moar	test = Test()
718d65d07cbSRae Moar	test.log.extend(log)
719723c8258SRae Moar
720723c8258SRae Moar	# Parse any errors prior to parsing tests
721723c8258SRae Moar	err_log = parse_diagnostic(lines)
722723c8258SRae Moar	test.log.extend(err_log)
723723c8258SRae Moar
724434498a6SRae Moar	if not is_subtest:
725434498a6SRae Moar		# If parsing the main/top-level test, parse KTAP version line and
726d65d07cbSRae Moar		# test plan
727d65d07cbSRae Moar		test.name = "main"
728434498a6SRae Moar		ktap_line = parse_ktap_header(lines, test)
729*29482da8SRae Moar		test.log.extend(parse_diagnostic(lines))
730d65d07cbSRae Moar		parse_test_plan(lines, test)
731e56e4828SDavid Gow		parent_test = True
7326ebf5866SFelix Guo	else:
733434498a6SRae Moar		# If not the main test, attempt to parse a test header containing
734434498a6SRae Moar		# the KTAP version line and/or subtest header line
735434498a6SRae Moar		ktap_line = parse_ktap_header(lines, test)
736434498a6SRae Moar		subtest_line = parse_test_header(lines, test)
737434498a6SRae Moar		parent_test = (ktap_line or subtest_line)
738d65d07cbSRae Moar		if parent_test:
739434498a6SRae Moar			# If KTAP version line and/or subtest header is found, attempt
740434498a6SRae Moar			# to parse test plan and print test header
741*29482da8SRae Moar			test.log.extend(parse_diagnostic(lines))
742d65d07cbSRae Moar			parse_test_plan(lines, test)
743d65d07cbSRae Moar			print_test_header(test)
744d65d07cbSRae Moar	expected_count = test.expected_count
745d65d07cbSRae Moar	subtests = []
746d65d07cbSRae Moar	test_num = 1
747e56e4828SDavid Gow	while parent_test and (expected_count is None or test_num <= expected_count):
748d65d07cbSRae Moar		# Loop to parse any subtests.
749d65d07cbSRae Moar		# Break after parsing expected number of tests or
750d65d07cbSRae Moar		# if expected number of tests is unknown break when test
751d65d07cbSRae Moar		# result line with matching name to subtest header is found
752d65d07cbSRae Moar		# or no more lines in stream.
753d65d07cbSRae Moar		sub_log = parse_diagnostic(lines)
754d65d07cbSRae Moar		sub_test = Test()
755d65d07cbSRae Moar		if not lines or (peek_test_name_match(lines, test) and
756434498a6SRae Moar				is_subtest):
757d65d07cbSRae Moar			if expected_count and test_num <= expected_count:
758d65d07cbSRae Moar				# If parser reaches end of test before
759d65d07cbSRae Moar				# parsing expected number of subtests, print
760d65d07cbSRae Moar				# crashed subtest and record error
761d65d07cbSRae Moar				test.add_error('missing expected subtest!')
762d65d07cbSRae Moar				sub_test.log.extend(sub_log)
763d65d07cbSRae Moar				test.counts.add_status(
764d65d07cbSRae Moar					TestStatus.TEST_CRASHED)
765d65d07cbSRae Moar				print_test_result(sub_test)
7666ebf5866SFelix Guo			else:
767d65d07cbSRae Moar				test.log.extend(sub_log)
768afc63da6SHeidi Fahim				break
7696ebf5866SFelix Guo		else:
770434498a6SRae Moar			sub_test = parse_test(lines, test_num, sub_log, True)
771d65d07cbSRae Moar		subtests.append(sub_test)
772d65d07cbSRae Moar		test_num += 1
773d65d07cbSRae Moar	test.subtests = subtests
774434498a6SRae Moar	if is_subtest:
775d65d07cbSRae Moar		# If not main test, look for test result line
776d65d07cbSRae Moar		test.log.extend(parse_diagnostic(lines))
777434498a6SRae Moar		if test.name != "" and not peek_test_name_match(lines, test):
778d65d07cbSRae Moar			test.add_error('missing subtest result line!')
779434498a6SRae Moar		else:
780434498a6SRae Moar			parse_test_result(lines, test, expected_num)
781e56e4828SDavid Gow
782434498a6SRae Moar	# Check for there being no subtests within parent test
783e56e4828SDavid Gow	if parent_test and len(subtests) == 0:
784dbf0b0d5SDaniel Latypov		# Don't override a bad status if this test had one reported.
785dbf0b0d5SDaniel Latypov		# Assumption: no subtests means CRASHED is from Test.__init__()
786dbf0b0d5SDaniel Latypov		if test.status in (TestStatus.TEST_CRASHED, TestStatus.SUCCESS):
787723c8258SRae Moar			print_log(test.log)
788e56e4828SDavid Gow			test.status = TestStatus.NO_TESTS
789e56e4828SDavid Gow			test.add_error('0 tests run!')
790e56e4828SDavid Gow
791d65d07cbSRae Moar	# Add statuses to TestCounts attribute in Test object
792d65d07cbSRae Moar	bubble_up_test_results(test)
793434498a6SRae Moar	if parent_test and is_subtest:
794d65d07cbSRae Moar		# If test has subtests and is not the main test object, print
795d65d07cbSRae Moar		# footer.
796d65d07cbSRae Moar		print_test_footer(test)
797434498a6SRae Moar	elif is_subtest:
798d65d07cbSRae Moar		print_test_result(test)
799d65d07cbSRae Moar	return test
80045dcbb6fSBrendan Higgins
801e0cc8c05SDaniel Latypovdef parse_run_tests(kernel_output: Iterable[str]) -> Test:
802d65d07cbSRae Moar	"""
803d65d07cbSRae Moar	Using kernel output, extract KTAP lines, parse the lines for test
804d65d07cbSRae Moar	results and print condensed test results and summary line.
805d65d07cbSRae Moar
806d65d07cbSRae Moar	Parameters:
807d65d07cbSRae Moar	kernel_output - Iterable object contains lines of kernel output
808d65d07cbSRae Moar
809d65d07cbSRae Moar	Return:
810e0cc8c05SDaniel Latypov	Test - the main test object with all subtests.
811d65d07cbSRae Moar	"""
812e756dbebSDaniel Latypov	stdout.print_with_timestamp(DIVIDER)
813d65d07cbSRae Moar	lines = extract_tap_lines(kernel_output)
814d65d07cbSRae Moar	test = Test()
815d65d07cbSRae Moar	if not lines:
8169660209dSDaniel Latypov		test.name = '<missing>'
8170a7d5c30SDaniel Latypov		test.add_error('Could not find any KTAP output. Did any KUnit tests run?')
818d65d07cbSRae Moar		test.status = TestStatus.FAILURE_TO_PARSE_TESTS
8195acaf603SDavid Gow	else:
820434498a6SRae Moar		test = parse_test(lines, 0, [], False)
821d65d07cbSRae Moar		if test.status != TestStatus.NO_TESTS:
822d65d07cbSRae Moar			test.status = test.counts.get_status()
823e756dbebSDaniel Latypov	stdout.print_with_timestamp(DIVIDER)
824d65d07cbSRae Moar	print_summary_line(test)
825e0cc8c05SDaniel Latypov	return test
826