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 <[email protected]>
96ebf5866SFelix Guo# Author: Brendan Higgins <[email protected]>
10d65d07cbSRae Moar# Author: Rae Moar <[email protected]>
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
20062a9dd9SDavid Gowfrom kunit_printer import Printer, 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
57062a9dd9SDavid Gow	def add_error(self, printer: Printer, error_message: str) -> None:
58d65d07cbSRae Moar		"""Records an error that occurred while parsing this test."""
59d65d07cbSRae Moar		self.counts.errors += 1
60062a9dd9SDavid Gow		printer.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],
254062a9dd9SDavid Gow			version_type: str, test: Test, printer: Printer) -> 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
266062a9dd9SDavid Gow	printer - Printer object to output error
267d65d07cbSRae Moar	"""
268d65d07cbSRae Moar	if version_num < min(accepted_versions):
269062a9dd9SDavid Gow		test.add_error(printer, f'{version_type} version lower than expected!')
270d65d07cbSRae Moar	elif version_num > max(accepted_versions):
271062a9dd9SDavid Gow		test.add_error(printer, f'{version_type} version higer than expected!')
272d65d07cbSRae Moar
273062a9dd9SDavid Gowdef parse_ktap_header(lines: LineStream, test: Test, printer: Printer) -> bool:
274d65d07cbSRae Moar	"""
275d65d07cbSRae Moar	Parses KTAP/TAP header line and checks version number.
276d65d07cbSRae Moar	Returns False if fails to parse KTAP/TAP header line.
277d65d07cbSRae Moar
278d65d07cbSRae Moar	Accepted formats:
279d65d07cbSRae Moar	- 'KTAP version [version number]'
280d65d07cbSRae Moar	- 'TAP version [version number]'
281d65d07cbSRae Moar
282d65d07cbSRae Moar	Parameters:
283d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
284d65d07cbSRae Moar	test - Test object for current test being parsed
285062a9dd9SDavid Gow	printer - Printer object to output results
286d65d07cbSRae Moar
287d65d07cbSRae Moar	Return:
288d65d07cbSRae Moar	True if successfully parsed KTAP/TAP header line
289d65d07cbSRae Moar	"""
290d65d07cbSRae Moar	ktap_match = KTAP_START.match(lines.peek())
291d65d07cbSRae Moar	tap_match = TAP_START.match(lines.peek())
292d65d07cbSRae Moar	if ktap_match:
293d65d07cbSRae Moar		version_num = int(ktap_match.group(1))
294062a9dd9SDavid Gow		check_version(version_num, KTAP_VERSIONS, 'KTAP', test, printer)
295d65d07cbSRae Moar	elif tap_match:
296d65d07cbSRae Moar		version_num = int(tap_match.group(1))
297062a9dd9SDavid Gow		check_version(version_num, TAP_VERSIONS, 'TAP', test, printer)
298d65d07cbSRae Moar	else:
299d65d07cbSRae Moar		return False
3005937e0c0SDaniel Latypov	lines.pop()
301d65d07cbSRae Moar	return True
302d65d07cbSRae Moar
303c2bb92bcSDaniel LatypovTEST_HEADER = re.compile(r'^\s*# Subtest: (.*)$')
304d65d07cbSRae Moar
305d65d07cbSRae Moardef parse_test_header(lines: LineStream, test: Test) -> bool:
306d65d07cbSRae Moar	"""
307d65d07cbSRae Moar	Parses test header and stores test name in test object.
308d65d07cbSRae Moar	Returns False if fails to parse test header line.
309d65d07cbSRae Moar
310d65d07cbSRae Moar	Accepted format:
311d65d07cbSRae Moar	- '# Subtest: [test name]'
312d65d07cbSRae Moar
313d65d07cbSRae Moar	Parameters:
314d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
315d65d07cbSRae Moar	test - Test object for current test being parsed
316d65d07cbSRae Moar
317d65d07cbSRae Moar	Return:
318d65d07cbSRae Moar	True if successfully parsed test header line
319d65d07cbSRae Moar	"""
320d65d07cbSRae Moar	match = TEST_HEADER.match(lines.peek())
321d65d07cbSRae Moar	if not match:
322d65d07cbSRae Moar		return False
323d65d07cbSRae Moar	test.name = match.group(1)
3245937e0c0SDaniel Latypov	lines.pop()
325d65d07cbSRae Moar	return True
326d65d07cbSRae Moar
327c2bb92bcSDaniel LatypovTEST_PLAN = re.compile(r'^\s*1\.\.([0-9]+)')
328d65d07cbSRae Moar
329d65d07cbSRae Moardef parse_test_plan(lines: LineStream, test: Test) -> bool:
330d65d07cbSRae Moar	"""
331d65d07cbSRae Moar	Parses test plan line and stores the expected number of subtests in
332d65d07cbSRae Moar	test object. Reports an error if expected count is 0.
333c68077b1SDavid Gow	Returns False and sets expected_count to None if there is no valid test
334c68077b1SDavid Gow	plan.
335d65d07cbSRae Moar
336d65d07cbSRae Moar	Accepted format:
337d65d07cbSRae Moar	- '1..[number of subtests]'
338d65d07cbSRae Moar
339d65d07cbSRae Moar	Parameters:
340d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
341d65d07cbSRae Moar	test - Test object for current test being parsed
342d65d07cbSRae Moar
343d65d07cbSRae Moar	Return:
344d65d07cbSRae Moar	True if successfully parsed test plan line
345d65d07cbSRae Moar	"""
346d65d07cbSRae Moar	match = TEST_PLAN.match(lines.peek())
347d65d07cbSRae Moar	if not match:
348d65d07cbSRae Moar		test.expected_count = None
349d65d07cbSRae Moar		return False
350d65d07cbSRae Moar	expected_count = int(match.group(1))
351d65d07cbSRae Moar	test.expected_count = expected_count
3525937e0c0SDaniel Latypov	lines.pop()
353d65d07cbSRae Moar	return True
354d65d07cbSRae Moar
355c2bb92bcSDaniel LatypovTEST_RESULT = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
356d65d07cbSRae Moar
357c2bb92bcSDaniel LatypovTEST_RESULT_SKIP = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?(.*) # SKIP(.*)$')
358d65d07cbSRae Moar
359d65d07cbSRae Moardef peek_test_name_match(lines: LineStream, test: Test) -> bool:
360d65d07cbSRae Moar	"""
361d65d07cbSRae Moar	Matches current line with the format of a test result line and checks
362d65d07cbSRae Moar	if the name matches the name of the current test.
363d65d07cbSRae Moar	Returns False if fails to match format or name.
364d65d07cbSRae Moar
365d65d07cbSRae Moar	Accepted format:
366d65d07cbSRae Moar	- '[ok|not ok] [test number] [-] [test name] [optional skip
367d65d07cbSRae Moar		directive]'
368d65d07cbSRae Moar
369d65d07cbSRae Moar	Parameters:
370d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
371d65d07cbSRae Moar	test - Test object for current test being parsed
372d65d07cbSRae Moar
373d65d07cbSRae Moar	Return:
374d65d07cbSRae Moar	True if matched a test result line and the name matching the
375d65d07cbSRae Moar		expected test name
376d65d07cbSRae Moar	"""
377d65d07cbSRae Moar	line = lines.peek()
378d65d07cbSRae Moar	match = TEST_RESULT.match(line)
379d65d07cbSRae Moar	if not match:
380d65d07cbSRae Moar		return False
381d65d07cbSRae Moar	name = match.group(4)
3820453f984SDaniel Latypov	return name == test.name
383d65d07cbSRae Moar
384d65d07cbSRae Moardef parse_test_result(lines: LineStream, test: Test,
385062a9dd9SDavid Gow			expected_num: int, printer: Printer) -> bool:
386d65d07cbSRae Moar	"""
387d65d07cbSRae Moar	Parses test result line and stores the status and name in the test
388d65d07cbSRae Moar	object. Reports an error if the test number does not match expected
389d65d07cbSRae Moar	test number.
390d65d07cbSRae Moar	Returns False if fails to parse test result line.
391d65d07cbSRae Moar
392d65d07cbSRae Moar	Note that the SKIP directive is the only direction that causes a
393d65d07cbSRae Moar	change in status.
394d65d07cbSRae Moar
395d65d07cbSRae Moar	Accepted format:
396d65d07cbSRae Moar	- '[ok|not ok] [test number] [-] [test name] [optional skip
397d65d07cbSRae Moar		directive]'
398d65d07cbSRae Moar
399d65d07cbSRae Moar	Parameters:
400d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
401d65d07cbSRae Moar	test - Test object for current test being parsed
402d65d07cbSRae Moar	expected_num - expected test number for current test
403062a9dd9SDavid Gow	printer - Printer object to output results
404d65d07cbSRae Moar
405d65d07cbSRae Moar	Return:
406d65d07cbSRae Moar	True if successfully parsed a test result line.
407d65d07cbSRae Moar	"""
408d65d07cbSRae Moar	line = lines.peek()
409d65d07cbSRae Moar	match = TEST_RESULT.match(line)
410d65d07cbSRae Moar	skip_match = TEST_RESULT_SKIP.match(line)
411d65d07cbSRae Moar
412d65d07cbSRae Moar	# Check if line matches test result line format
413d65d07cbSRae Moar	if not match:
414d65d07cbSRae Moar		return False
4155937e0c0SDaniel Latypov	lines.pop()
416d65d07cbSRae Moar
417d65d07cbSRae Moar	# Set name of test object
418d65d07cbSRae Moar	if skip_match:
419d65d07cbSRae Moar		test.name = skip_match.group(4)
420d65d07cbSRae Moar	else:
421d65d07cbSRae Moar		test.name = match.group(4)
422d65d07cbSRae Moar
423d65d07cbSRae Moar	# Check test num
424d65d07cbSRae Moar	num = int(match.group(2))
425d65d07cbSRae Moar	if num != expected_num:
426062a9dd9SDavid Gow		test.add_error(printer, f'Expected test number {expected_num} but found {num}')
427d65d07cbSRae Moar
428d65d07cbSRae Moar	# Set status of test object
429d65d07cbSRae Moar	status = match.group(1)
430d65d07cbSRae Moar	if skip_match:
431d65d07cbSRae Moar		test.status = TestStatus.SKIPPED
432d65d07cbSRae Moar	elif status == 'ok':
433d65d07cbSRae Moar		test.status = TestStatus.SUCCESS
434d65d07cbSRae Moar	else:
435d65d07cbSRae Moar		test.status = TestStatus.FAILURE
436d65d07cbSRae Moar	return True
437d65d07cbSRae Moar
438d65d07cbSRae Moardef parse_diagnostic(lines: LineStream) -> List[str]:
439d65d07cbSRae Moar	"""
440d65d07cbSRae Moar	Parse lines that do not match the format of a test result line or
441d65d07cbSRae Moar	test header line and returns them in list.
442d65d07cbSRae Moar
443d65d07cbSRae Moar	Line formats that are not parsed:
444d65d07cbSRae Moar	- '# Subtest: [test name]'
445d65d07cbSRae Moar	- '[ok|not ok] [test number] [-] [test name] [optional skip
446d65d07cbSRae Moar		directive]'
447434498a6SRae Moar	- 'KTAP version [version number]'
448d65d07cbSRae Moar
449d65d07cbSRae Moar	Parameters:
450d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
451d65d07cbSRae Moar
452d65d07cbSRae Moar	Return:
453d65d07cbSRae Moar	Log of diagnostic lines
454d65d07cbSRae Moar	"""
455d65d07cbSRae Moar	log = []  # type: List[str]
4568ae27bc7SRae Moar	non_diagnostic_lines = [TEST_RESULT, TEST_HEADER, KTAP_START, TAP_START, TEST_PLAN]
457434498a6SRae Moar	while lines and not any(re.match(lines.peek())
458434498a6SRae Moar			for re in non_diagnostic_lines):
459d65d07cbSRae Moar		log.append(lines.pop())
460d65d07cbSRae Moar	return log
461d65d07cbSRae Moar
462d65d07cbSRae Moar
463d65d07cbSRae Moar# Printing helper methods:
4646ebf5866SFelix Guo
4656ebf5866SFelix GuoDIVIDER = '=' * 60
4666ebf5866SFelix Guo
467d65d07cbSRae Moardef format_test_divider(message: str, len_message: int) -> str:
468d65d07cbSRae Moar	"""
469d65d07cbSRae Moar	Returns string with message centered in fixed width divider.
4706ebf5866SFelix Guo
471d65d07cbSRae Moar	Example:
472d65d07cbSRae Moar	'===================== message example ====================='
4736ebf5866SFelix Guo
474d65d07cbSRae Moar	Parameters:
475d65d07cbSRae Moar	message - message to be centered in divider line
476d65d07cbSRae Moar	len_message - length of the message to be printed such that
477d65d07cbSRae Moar		any characters of the color codes are not counted
478d65d07cbSRae Moar
479d65d07cbSRae Moar	Return:
480d65d07cbSRae Moar	String containing message centered in fixed width divider
481d65d07cbSRae Moar	"""
482d65d07cbSRae Moar	default_count = 3  # default number of dashes
483d65d07cbSRae Moar	len_1 = default_count
484d65d07cbSRae Moar	len_2 = default_count
485d65d07cbSRae Moar	difference = len(DIVIDER) - len_message - 2  # 2 spaces added
486d65d07cbSRae Moar	if difference > 0:
487d65d07cbSRae Moar		# calculate number of dashes for each side of the divider
488d65d07cbSRae Moar		len_1 = int(difference / 2)
489d65d07cbSRae Moar		len_2 = difference - len_1
49094507ee3SDaniel Latypov	return ('=' * len_1) + f' {message} ' + ('=' * len_2)
491d65d07cbSRae Moar
492062a9dd9SDavid Gowdef print_test_header(test: Test, printer: Printer) -> None:
493d65d07cbSRae Moar	"""
494d65d07cbSRae Moar	Prints test header with test name and optionally the expected number
495d65d07cbSRae Moar	of subtests.
496d65d07cbSRae Moar
497d65d07cbSRae Moar	Example:
498d65d07cbSRae Moar	'=================== example (2 subtests) ==================='
499d65d07cbSRae Moar
500d65d07cbSRae Moar	Parameters:
501d65d07cbSRae Moar	test - Test object representing current test being printed
502062a9dd9SDavid Gow	printer - Printer object to output results
503d65d07cbSRae Moar	"""
504d65d07cbSRae Moar	message = test.name
505434498a6SRae Moar	if message != "":
506434498a6SRae Moar		# Add a leading space before the subtest counts only if a test name
507434498a6SRae Moar		# is provided using a "# Subtest" header line.
508434498a6SRae Moar		message += " "
509d65d07cbSRae Moar	if test.expected_count:
510d65d07cbSRae Moar		if test.expected_count == 1:
51194507ee3SDaniel Latypov			message += '(1 subtest)'
512d65d07cbSRae Moar		else:
51394507ee3SDaniel Latypov			message += f'({test.expected_count} subtests)'
514062a9dd9SDavid Gow	printer.print_with_timestamp(format_test_divider(message, len(message)))
515d65d07cbSRae Moar
516062a9dd9SDavid Gowdef print_log(log: Iterable[str], printer: Printer) -> None:
51794507ee3SDaniel Latypov	"""Prints all strings in saved log for test in yellow."""
518c2bb92bcSDaniel Latypov	formatted = textwrap.dedent('\n'.join(log))
519c2bb92bcSDaniel Latypov	for line in formatted.splitlines():
520062a9dd9SDavid Gow		printer.print_with_timestamp(printer.yellow(line))
5216ebf5866SFelix Guo
522062a9dd9SDavid Gowdef format_test_result(test: Test, printer: Printer) -> str:
523d65d07cbSRae Moar	"""
524d65d07cbSRae Moar	Returns string with formatted test result with colored status and test
525d65d07cbSRae Moar	name.
5266ebf5866SFelix Guo
527d65d07cbSRae Moar	Example:
528d65d07cbSRae Moar	'[PASSED] example'
5296ebf5866SFelix Guo
530d65d07cbSRae Moar	Parameters:
531d65d07cbSRae Moar	test - Test object representing current test being printed
532062a9dd9SDavid Gow	printer - Printer object to output results
5336ebf5866SFelix Guo
534d65d07cbSRae Moar	Return:
535d65d07cbSRae Moar	String containing formatted test result
536d65d07cbSRae Moar	"""
537d65d07cbSRae Moar	if test.status == TestStatus.SUCCESS:
538062a9dd9SDavid Gow		return printer.green('[PASSED] ') + test.name
5390453f984SDaniel Latypov	if test.status == TestStatus.SKIPPED:
540062a9dd9SDavid Gow		return printer.yellow('[SKIPPED] ') + test.name
5410453f984SDaniel Latypov	if test.status == TestStatus.NO_TESTS:
542062a9dd9SDavid Gow		return printer.yellow('[NO TESTS RUN] ') + test.name
5430453f984SDaniel Latypov	if test.status == TestStatus.TEST_CRASHED:
544062a9dd9SDavid Gow		print_log(test.log, printer)
545e756dbebSDaniel Latypov		return stdout.red('[CRASHED] ') + test.name
546062a9dd9SDavid Gow	print_log(test.log, printer)
547062a9dd9SDavid Gow	return printer.red('[FAILED] ') + test.name
548d65d07cbSRae Moar
549062a9dd9SDavid Gowdef print_test_result(test: Test, printer: Printer) -> None:
550d65d07cbSRae Moar	"""
551d65d07cbSRae Moar	Prints result line with status of test.
552d65d07cbSRae Moar
553d65d07cbSRae Moar	Example:
554d65d07cbSRae Moar	'[PASSED] example'
555d65d07cbSRae Moar
556d65d07cbSRae Moar	Parameters:
557d65d07cbSRae Moar	test - Test object representing current test being printed
558062a9dd9SDavid Gow	printer - Printer object
559d65d07cbSRae Moar	"""
560062a9dd9SDavid Gow	printer.print_with_timestamp(format_test_result(test, printer))
561d65d07cbSRae Moar
562062a9dd9SDavid Gowdef print_test_footer(test: Test, printer: Printer) -> None:
563d65d07cbSRae Moar	"""
564d65d07cbSRae Moar	Prints test footer with status of test.
565d65d07cbSRae Moar
566d65d07cbSRae Moar	Example:
567d65d07cbSRae Moar	'===================== [PASSED] example ====================='
568d65d07cbSRae Moar
569d65d07cbSRae Moar	Parameters:
570d65d07cbSRae Moar	test - Test object representing current test being printed
571062a9dd9SDavid Gow	printer - Printer object to output results
572d65d07cbSRae Moar	"""
573062a9dd9SDavid Gow	message = format_test_result(test, printer)
574062a9dd9SDavid Gow	printer.print_with_timestamp(format_test_divider(message,
575062a9dd9SDavid Gow		len(message) - printer.color_len()))
576d65d07cbSRae Moar
5773c67a2c0SRae Moardef print_test(test: Test, failed_only: bool, printer: Printer) -> None:
5783c67a2c0SRae Moar	"""
5793c67a2c0SRae Moar	Prints Test object to given printer. For a child test, the result line is
5803c67a2c0SRae Moar	printed. For a parent test, the test header, all child test results, and
5813c67a2c0SRae Moar	the test footer are all printed. If failed_only is true, only failed/crashed
5823c67a2c0SRae Moar	tests will be printed.
583f19dd011SDaniel Latypov
5843c67a2c0SRae Moar	Parameters:
5853c67a2c0SRae Moar	test - Test object to print
5863c67a2c0SRae Moar	failed_only - True if only failed/crashed tests should be printed.
5873c67a2c0SRae Moar	printer - Printer object to output results
5883c67a2c0SRae Moar	"""
5893c67a2c0SRae Moar	if test.name == "main":
5903c67a2c0SRae Moar		printer.print_with_timestamp(DIVIDER)
5913c67a2c0SRae Moar		for subtest in test.subtests:
5923c67a2c0SRae Moar			print_test(subtest, failed_only, printer)
5933c67a2c0SRae Moar		printer.print_with_timestamp(DIVIDER)
5943c67a2c0SRae Moar	elif test.subtests != []:
5953c67a2c0SRae Moar		if not failed_only or not test.ok_status():
5963c67a2c0SRae Moar			print_test_header(test, printer)
5973c67a2c0SRae Moar			for subtest in test.subtests:
5983c67a2c0SRae Moar				print_test(subtest, failed_only, printer)
5993c67a2c0SRae Moar			print_test_footer(test, printer)
6003c67a2c0SRae Moar	else:
6013c67a2c0SRae Moar		if not failed_only or not test.ok_status():
6023c67a2c0SRae Moar			print_test_result(test, printer)
603f19dd011SDaniel Latypov
604f19dd011SDaniel Latypovdef _summarize_failed_tests(test: Test) -> str:
605f19dd011SDaniel Latypov	"""Tries to summarize all the failing subtests in `test`."""
606f19dd011SDaniel Latypov
607f19dd011SDaniel Latypov	def failed_names(test: Test, parent_name: str) -> List[str]:
608f19dd011SDaniel Latypov		# Note: we use 'main' internally for the top-level test.
609f19dd011SDaniel Latypov		if not parent_name or parent_name == 'main':
610f19dd011SDaniel Latypov			full_name = test.name
611f19dd011SDaniel Latypov		else:
612f19dd011SDaniel Latypov			full_name = parent_name + '.' + test.name
613f19dd011SDaniel Latypov
614f19dd011SDaniel Latypov		if not test.subtests:  # this is a leaf node
615f19dd011SDaniel Latypov			return [full_name]
616f19dd011SDaniel Latypov
617f19dd011SDaniel Latypov		# If all the children failed, just say this subtest failed.
618f19dd011SDaniel Latypov		# Don't summarize it down "the top-level test failed", though.
619f19dd011SDaniel Latypov		failed_subtests = [sub for sub in test.subtests if not sub.ok_status()]
620f19dd011SDaniel Latypov		if parent_name and len(failed_subtests) ==  len(test.subtests):
621f19dd011SDaniel Latypov			return [full_name]
622f19dd011SDaniel Latypov
623f19dd011SDaniel Latypov		all_failures = []  # type: List[str]
624f19dd011SDaniel Latypov		for t in failed_subtests:
625f19dd011SDaniel Latypov			all_failures.extend(failed_names(t, full_name))
626f19dd011SDaniel Latypov		return all_failures
627f19dd011SDaniel Latypov
628f19dd011SDaniel Latypov	failures = failed_names(test, '')
629f19dd011SDaniel Latypov	# If there are too many failures, printing them out will just be noisy.
630f19dd011SDaniel Latypov	if len(failures) > 10:  # this is an arbitrary limit
631f19dd011SDaniel Latypov		return ''
632f19dd011SDaniel Latypov
633f19dd011SDaniel Latypov	return 'Failures: ' + ', '.join(failures)
634f19dd011SDaniel Latypov
635f19dd011SDaniel Latypov
636062a9dd9SDavid Gowdef print_summary_line(test: Test, printer: Printer) -> None:
637d65d07cbSRae Moar	"""
638d65d07cbSRae Moar	Prints summary line of test object. Color of line is dependent on
639d65d07cbSRae Moar	status of test. Color is green if test passes, yellow if test is
640d65d07cbSRae Moar	skipped, and red if the test fails or crashes. Summary line contains
641d65d07cbSRae Moar	counts of the statuses of the tests subtests or the test itself if it
642d65d07cbSRae Moar	has no subtests.
643d65d07cbSRae Moar
644d65d07cbSRae Moar	Example:
645d65d07cbSRae Moar	"Testing complete. Passed: 2, Failed: 0, Crashed: 0, Skipped: 0,
646d65d07cbSRae Moar	Errors: 0"
647d65d07cbSRae Moar
648d65d07cbSRae Moar	test - Test object representing current test being printed
649062a9dd9SDavid Gow	printer - Printer object to output results
650d65d07cbSRae Moar	"""
651d65d07cbSRae Moar	if test.status == TestStatus.SUCCESS:
652e756dbebSDaniel Latypov		color = stdout.green
6530453f984SDaniel Latypov	elif test.status in (TestStatus.SKIPPED, TestStatus.NO_TESTS):
654e756dbebSDaniel Latypov		color = stdout.yellow
6556ebf5866SFelix Guo	else:
656e756dbebSDaniel Latypov		color = stdout.red
657062a9dd9SDavid Gow	printer.print_with_timestamp(color(f'Testing complete. {test.counts}'))
658d65d07cbSRae Moar
659f19dd011SDaniel Latypov	# Summarize failures that might have gone off-screen since we had a lot
660f19dd011SDaniel Latypov	# of tests (arbitrarily defined as >=100 for now).
661f19dd011SDaniel Latypov	if test.ok_status() or test.counts.total() < 100:
662f19dd011SDaniel Latypov		return
663f19dd011SDaniel Latypov	summarized = _summarize_failed_tests(test)
664f19dd011SDaniel Latypov	if not summarized:
665f19dd011SDaniel Latypov		return
666062a9dd9SDavid Gow	printer.print_with_timestamp(color(summarized))
667f19dd011SDaniel Latypov
668d65d07cbSRae Moar# Other methods:
669d65d07cbSRae Moar
670d65d07cbSRae Moardef bubble_up_test_results(test: Test) -> None:
671d65d07cbSRae Moar	"""
672d65d07cbSRae Moar	If the test has subtests, add the test counts of the subtests to the
673d65d07cbSRae Moar	test and check if any of the tests crashed and if so set the test
674d65d07cbSRae Moar	status to crashed. Otherwise if the test has no subtests add the
675d65d07cbSRae Moar	status of the test to the test counts.
676d65d07cbSRae Moar
677d65d07cbSRae Moar	Parameters:
678d65d07cbSRae Moar	test - Test object for current test being parsed
679d65d07cbSRae Moar	"""
680d65d07cbSRae Moar	subtests = test.subtests
681d65d07cbSRae Moar	counts = test.counts
682d65d07cbSRae Moar	status = test.status
683d65d07cbSRae Moar	for t in subtests:
684d65d07cbSRae Moar		counts.add_subtest_counts(t.counts)
685d65d07cbSRae Moar	if counts.total() == 0:
686d65d07cbSRae Moar		counts.add_status(status)
687d65d07cbSRae Moar	elif test.counts.get_status() == TestStatus.TEST_CRASHED:
688d65d07cbSRae Moar		test.status = TestStatus.TEST_CRASHED
689d65d07cbSRae Moar
690062a9dd9SDavid Gowdef parse_test(lines: LineStream, expected_num: int, log: List[str], is_subtest: bool, printer: Printer) -> Test:
691d65d07cbSRae Moar	"""
692d65d07cbSRae Moar	Finds next test to parse in LineStream, creates new Test object,
693d65d07cbSRae Moar	parses any subtests of the test, populates Test object with all
694d65d07cbSRae Moar	information (status, name) about the test and the Test objects for
695d65d07cbSRae Moar	any subtests, and then returns the Test object. The method accepts
696d65d07cbSRae Moar	three formats of tests:
697d65d07cbSRae Moar
698d65d07cbSRae Moar	Accepted test formats:
699d65d07cbSRae Moar
700d65d07cbSRae Moar	- Main KTAP/TAP header
701d65d07cbSRae Moar
702d65d07cbSRae Moar	Example:
703d65d07cbSRae Moar
704d65d07cbSRae Moar	KTAP version 1
705d65d07cbSRae Moar	1..4
706d65d07cbSRae Moar	[subtests]
707d65d07cbSRae Moar
708434498a6SRae Moar	- Subtest header (must include either the KTAP version line or
709434498a6SRae Moar	  "# Subtest" header line)
710d65d07cbSRae Moar
711434498a6SRae Moar	Example (preferred format with both KTAP version line and
712434498a6SRae Moar	"# Subtest" line):
713434498a6SRae Moar
714434498a6SRae Moar	KTAP version 1
715434498a6SRae Moar	# Subtest: name
716434498a6SRae Moar	1..3
717434498a6SRae Moar	[subtests]
718434498a6SRae Moar	ok 1 name
719434498a6SRae Moar
720434498a6SRae Moar	Example (only "# Subtest" line):
721d65d07cbSRae Moar
722d65d07cbSRae Moar	# Subtest: name
723d65d07cbSRae Moar	1..3
724d65d07cbSRae Moar	[subtests]
725d65d07cbSRae Moar	ok 1 name
726d65d07cbSRae Moar
727434498a6SRae Moar	Example (only KTAP version line, compliant with KTAP v1 spec):
728434498a6SRae Moar
729434498a6SRae Moar	KTAP version 1
730434498a6SRae Moar	1..3
731434498a6SRae Moar	[subtests]
732434498a6SRae Moar	ok 1 name
733434498a6SRae Moar
734d65d07cbSRae Moar	- Test result line
735d65d07cbSRae Moar
736d65d07cbSRae Moar	Example:
737d65d07cbSRae Moar
738d65d07cbSRae Moar	ok 1 - test
739d65d07cbSRae Moar
740d65d07cbSRae Moar	Parameters:
741d65d07cbSRae Moar	lines - LineStream of KTAP output to parse
742d65d07cbSRae Moar	expected_num - expected test number for test to be parsed
743d65d07cbSRae Moar	log - list of strings containing any preceding diagnostic lines
744d65d07cbSRae Moar		corresponding to the current test
745434498a6SRae Moar	is_subtest - boolean indicating whether test is a subtest
746062a9dd9SDavid Gow	printer - Printer object to output results
747d65d07cbSRae Moar
748d65d07cbSRae Moar	Return:
749d65d07cbSRae Moar	Test object populated with characteristics and any subtests
750d65d07cbSRae Moar	"""
751d65d07cbSRae Moar	test = Test()
752d65d07cbSRae Moar	test.log.extend(log)
753723c8258SRae Moar
754723c8258SRae Moar	# Parse any errors prior to parsing tests
755723c8258SRae Moar	err_log = parse_diagnostic(lines)
756723c8258SRae Moar	test.log.extend(err_log)
757723c8258SRae Moar
758434498a6SRae Moar	if not is_subtest:
759434498a6SRae Moar		# If parsing the main/top-level test, parse KTAP version line and
760d65d07cbSRae Moar		# test plan
761d65d07cbSRae Moar		test.name = "main"
7621d4c06d5SRae Moar		parse_ktap_header(lines, test, printer)
7638ae27bc7SRae Moar		test.log.extend(parse_diagnostic(lines))
764d65d07cbSRae Moar		parse_test_plan(lines, test)
765e56e4828SDavid Gow		parent_test = True
7666ebf5866SFelix Guo	else:
767434498a6SRae Moar		# If not the main test, attempt to parse a test header containing
768434498a6SRae Moar		# the KTAP version line and/or subtest header line
769062a9dd9SDavid Gow		ktap_line = parse_ktap_header(lines, test, printer)
770434498a6SRae Moar		subtest_line = parse_test_header(lines, test)
7718ae27bc7SRae Moar		test.log.extend(parse_diagnostic(lines))
772d65d07cbSRae Moar		parse_test_plan(lines, test)
7731d4c06d5SRae Moar		parent_test = (ktap_line or subtest_line)
7741d4c06d5SRae Moar		if parent_test:
775062a9dd9SDavid Gow			print_test_header(test, printer)
7761d4c06d5SRae Moar
777d65d07cbSRae Moar	expected_count = test.expected_count
778d65d07cbSRae Moar	subtests = []
779d65d07cbSRae Moar	test_num = 1
780e56e4828SDavid Gow	while parent_test and (expected_count is None or test_num <= expected_count):
781d65d07cbSRae Moar		# Loop to parse any subtests.
782d65d07cbSRae Moar		# Break after parsing expected number of tests or
783d65d07cbSRae Moar		# if expected number of tests is unknown break when test
784d65d07cbSRae Moar		# result line with matching name to subtest header is found
785d65d07cbSRae Moar		# or no more lines in stream.
786d65d07cbSRae Moar		sub_log = parse_diagnostic(lines)
787d65d07cbSRae Moar		sub_test = Test()
788d65d07cbSRae Moar		if not lines or (peek_test_name_match(lines, test) and
789434498a6SRae Moar				is_subtest):
790d65d07cbSRae Moar			if expected_count and test_num <= expected_count:
791d65d07cbSRae Moar				# If parser reaches end of test before
792d65d07cbSRae Moar				# parsing expected number of subtests, print
793d65d07cbSRae Moar				# crashed subtest and record error
794062a9dd9SDavid Gow				test.add_error(printer, 'missing expected subtest!')
795d65d07cbSRae Moar				sub_test.log.extend(sub_log)
796d65d07cbSRae Moar				test.counts.add_status(
797d65d07cbSRae Moar					TestStatus.TEST_CRASHED)
798062a9dd9SDavid Gow				print_test_result(sub_test, printer)
7996ebf5866SFelix Guo			else:
800d65d07cbSRae Moar				test.log.extend(sub_log)
801afc63da6SHeidi Fahim				break
8026ebf5866SFelix Guo		else:
803062a9dd9SDavid Gow			sub_test = parse_test(lines, test_num, sub_log, True, printer)
804d65d07cbSRae Moar		subtests.append(sub_test)
805d65d07cbSRae Moar		test_num += 1
806d65d07cbSRae Moar	test.subtests = subtests
807434498a6SRae Moar	if is_subtest:
808d65d07cbSRae Moar		# If not main test, look for test result line
809d65d07cbSRae Moar		test.log.extend(parse_diagnostic(lines))
810434498a6SRae Moar		if test.name != "" and not peek_test_name_match(lines, test):
811062a9dd9SDavid Gow			test.add_error(printer, 'missing subtest result line!')
812*14e594a1SRae Moar		elif not lines:
813*14e594a1SRae Moar			print_log(test.log, printer)
814*14e594a1SRae Moar			test.status = TestStatus.NO_TESTS
815*14e594a1SRae Moar			test.add_error(printer, 'No more test results!')
816434498a6SRae Moar		else:
817062a9dd9SDavid Gow			parse_test_result(lines, test, expected_num, printer)
818e56e4828SDavid Gow
819434498a6SRae Moar	# Check for there being no subtests within parent test
820e56e4828SDavid Gow	if parent_test and len(subtests) == 0:
821dbf0b0d5SDaniel Latypov		# Don't override a bad status if this test had one reported.
822dbf0b0d5SDaniel Latypov		# Assumption: no subtests means CRASHED is from Test.__init__()
823dbf0b0d5SDaniel Latypov		if test.status in (TestStatus.TEST_CRASHED, TestStatus.SUCCESS):
824062a9dd9SDavid Gow			print_log(test.log, printer)
825e56e4828SDavid Gow			test.status = TestStatus.NO_TESTS
826062a9dd9SDavid Gow			test.add_error(printer, '0 tests run!')
827e56e4828SDavid Gow
828d65d07cbSRae Moar	# Add statuses to TestCounts attribute in Test object
829d65d07cbSRae Moar	bubble_up_test_results(test)
830434498a6SRae Moar	if parent_test and is_subtest:
831d65d07cbSRae Moar		# If test has subtests and is not the main test object, print
832d65d07cbSRae Moar		# footer.
833062a9dd9SDavid Gow		print_test_footer(test, printer)
834434498a6SRae Moar	elif is_subtest:
835062a9dd9SDavid Gow		print_test_result(test, printer)
836d65d07cbSRae Moar	return test
83745dcbb6fSBrendan Higgins
838062a9dd9SDavid Gowdef parse_run_tests(kernel_output: Iterable[str], printer: Printer) -> Test:
839d65d07cbSRae Moar	"""
840d65d07cbSRae Moar	Using kernel output, extract KTAP lines, parse the lines for test
841d65d07cbSRae Moar	results and print condensed test results and summary line.
842d65d07cbSRae Moar
843d65d07cbSRae Moar	Parameters:
844d65d07cbSRae Moar	kernel_output - Iterable object contains lines of kernel output
845062a9dd9SDavid Gow	printer - Printer object to output results
846d65d07cbSRae Moar
847d65d07cbSRae Moar	Return:
848e0cc8c05SDaniel Latypov	Test - the main test object with all subtests.
849d65d07cbSRae Moar	"""
850062a9dd9SDavid Gow	printer.print_with_timestamp(DIVIDER)
851d65d07cbSRae Moar	lines = extract_tap_lines(kernel_output)
852d65d07cbSRae Moar	test = Test()
853d65d07cbSRae Moar	if not lines:
8549660209dSDaniel Latypov		test.name = '<missing>'
855062a9dd9SDavid Gow		test.add_error(printer, 'Could not find any KTAP output. Did any KUnit tests run?')
856d65d07cbSRae Moar		test.status = TestStatus.FAILURE_TO_PARSE_TESTS
8575acaf603SDavid Gow	else:
858062a9dd9SDavid Gow		test = parse_test(lines, 0, [], False, printer)
859d65d07cbSRae Moar		if test.status != TestStatus.NO_TESTS:
860d65d07cbSRae Moar			test.status = test.counts.get_status()
861062a9dd9SDavid Gow	printer.print_with_timestamp(DIVIDER)
862e0cc8c05SDaniel Latypov	return test
863