162306a36Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0
262306a36Sopenharmony_ci#
362306a36Sopenharmony_ci# Parses KTAP test results from a kernel dmesg log and incrementally prints
462306a36Sopenharmony_ci# results with reader-friendly format. Stores and returns test results in a
562306a36Sopenharmony_ci# Test object.
662306a36Sopenharmony_ci#
762306a36Sopenharmony_ci# Copyright (C) 2019, Google LLC.
862306a36Sopenharmony_ci# Author: Felix Guo <felixguoxiuping@gmail.com>
962306a36Sopenharmony_ci# Author: Brendan Higgins <brendanhiggins@google.com>
1062306a36Sopenharmony_ci# Author: Rae Moar <rmoar@google.com>
1162306a36Sopenharmony_ci
1262306a36Sopenharmony_cifrom __future__ import annotations
1362306a36Sopenharmony_cifrom dataclasses import dataclass
1462306a36Sopenharmony_ciimport re
1562306a36Sopenharmony_ciimport textwrap
1662306a36Sopenharmony_ci
1762306a36Sopenharmony_cifrom enum import Enum, auto
1862306a36Sopenharmony_cifrom typing import Iterable, Iterator, List, Optional, Tuple
1962306a36Sopenharmony_ci
2062306a36Sopenharmony_cifrom kunit_printer import stdout
2162306a36Sopenharmony_ci
2262306a36Sopenharmony_ciclass Test:
2362306a36Sopenharmony_ci	"""
2462306a36Sopenharmony_ci	A class to represent a test parsed from KTAP results. All KTAP
2562306a36Sopenharmony_ci	results within a test log are stored in a main Test object as
2662306a36Sopenharmony_ci	subtests.
2762306a36Sopenharmony_ci
2862306a36Sopenharmony_ci	Attributes:
2962306a36Sopenharmony_ci	status : TestStatus - status of the test
3062306a36Sopenharmony_ci	name : str - name of the test
3162306a36Sopenharmony_ci	expected_count : int - expected number of subtests (0 if single
3262306a36Sopenharmony_ci		test case and None if unknown expected number of subtests)
3362306a36Sopenharmony_ci	subtests : List[Test] - list of subtests
3462306a36Sopenharmony_ci	log : List[str] - log of KTAP lines that correspond to the test
3562306a36Sopenharmony_ci	counts : TestCounts - counts of the test statuses and errors of
3662306a36Sopenharmony_ci		subtests or of the test itself if the test is a single
3762306a36Sopenharmony_ci		test case.
3862306a36Sopenharmony_ci	"""
3962306a36Sopenharmony_ci	def __init__(self) -> None:
4062306a36Sopenharmony_ci		"""Creates Test object with default attributes."""
4162306a36Sopenharmony_ci		self.status = TestStatus.TEST_CRASHED
4262306a36Sopenharmony_ci		self.name = ''
4362306a36Sopenharmony_ci		self.expected_count = 0  # type: Optional[int]
4462306a36Sopenharmony_ci		self.subtests = []  # type: List[Test]
4562306a36Sopenharmony_ci		self.log = []  # type: List[str]
4662306a36Sopenharmony_ci		self.counts = TestCounts()
4762306a36Sopenharmony_ci
4862306a36Sopenharmony_ci	def __str__(self) -> str:
4962306a36Sopenharmony_ci		"""Returns string representation of a Test class object."""
5062306a36Sopenharmony_ci		return (f'Test({self.status}, {self.name}, {self.expected_count}, '
5162306a36Sopenharmony_ci			f'{self.subtests}, {self.log}, {self.counts})')
5262306a36Sopenharmony_ci
5362306a36Sopenharmony_ci	def __repr__(self) -> str:
5462306a36Sopenharmony_ci		"""Returns string representation of a Test class object."""
5562306a36Sopenharmony_ci		return str(self)
5662306a36Sopenharmony_ci
5762306a36Sopenharmony_ci	def add_error(self, error_message: str) -> None:
5862306a36Sopenharmony_ci		"""Records an error that occurred while parsing this test."""
5962306a36Sopenharmony_ci		self.counts.errors += 1
6062306a36Sopenharmony_ci		stdout.print_with_timestamp(stdout.red('[ERROR]') + f' Test: {self.name}: {error_message}')
6162306a36Sopenharmony_ci
6262306a36Sopenharmony_ci	def ok_status(self) -> bool:
6362306a36Sopenharmony_ci		"""Returns true if the status was ok, i.e. passed or skipped."""
6462306a36Sopenharmony_ci		return self.status in (TestStatus.SUCCESS, TestStatus.SKIPPED)
6562306a36Sopenharmony_ci
6662306a36Sopenharmony_ciclass TestStatus(Enum):
6762306a36Sopenharmony_ci	"""An enumeration class to represent the status of a test."""
6862306a36Sopenharmony_ci	SUCCESS = auto()
6962306a36Sopenharmony_ci	FAILURE = auto()
7062306a36Sopenharmony_ci	SKIPPED = auto()
7162306a36Sopenharmony_ci	TEST_CRASHED = auto()
7262306a36Sopenharmony_ci	NO_TESTS = auto()
7362306a36Sopenharmony_ci	FAILURE_TO_PARSE_TESTS = auto()
7462306a36Sopenharmony_ci
7562306a36Sopenharmony_ci@dataclass
7662306a36Sopenharmony_ciclass TestCounts:
7762306a36Sopenharmony_ci	"""
7862306a36Sopenharmony_ci	Tracks the counts of statuses of all test cases and any errors within
7962306a36Sopenharmony_ci	a Test.
8062306a36Sopenharmony_ci	"""
8162306a36Sopenharmony_ci	passed: int = 0
8262306a36Sopenharmony_ci	failed: int = 0
8362306a36Sopenharmony_ci	crashed: int = 0
8462306a36Sopenharmony_ci	skipped: int = 0
8562306a36Sopenharmony_ci	errors: int = 0
8662306a36Sopenharmony_ci
8762306a36Sopenharmony_ci	def __str__(self) -> str:
8862306a36Sopenharmony_ci		"""Returns the string representation of a TestCounts object."""
8962306a36Sopenharmony_ci		statuses = [('passed', self.passed), ('failed', self.failed),
9062306a36Sopenharmony_ci			('crashed', self.crashed), ('skipped', self.skipped),
9162306a36Sopenharmony_ci			('errors', self.errors)]
9262306a36Sopenharmony_ci		return f'Ran {self.total()} tests: ' + \
9362306a36Sopenharmony_ci			', '.join(f'{s}: {n}' for s, n in statuses if n > 0)
9462306a36Sopenharmony_ci
9562306a36Sopenharmony_ci	def total(self) -> int:
9662306a36Sopenharmony_ci		"""Returns the total number of test cases within a test
9762306a36Sopenharmony_ci		object, where a test case is a test with no subtests.
9862306a36Sopenharmony_ci		"""
9962306a36Sopenharmony_ci		return (self.passed + self.failed + self.crashed +
10062306a36Sopenharmony_ci			self.skipped)
10162306a36Sopenharmony_ci
10262306a36Sopenharmony_ci	def add_subtest_counts(self, counts: TestCounts) -> None:
10362306a36Sopenharmony_ci		"""
10462306a36Sopenharmony_ci		Adds the counts of another TestCounts object to the current
10562306a36Sopenharmony_ci		TestCounts object. Used to add the counts of a subtest to the
10662306a36Sopenharmony_ci		parent test.
10762306a36Sopenharmony_ci
10862306a36Sopenharmony_ci		Parameters:
10962306a36Sopenharmony_ci		counts - a different TestCounts object whose counts
11062306a36Sopenharmony_ci			will be added to the counts of the TestCounts object
11162306a36Sopenharmony_ci		"""
11262306a36Sopenharmony_ci		self.passed += counts.passed
11362306a36Sopenharmony_ci		self.failed += counts.failed
11462306a36Sopenharmony_ci		self.crashed += counts.crashed
11562306a36Sopenharmony_ci		self.skipped += counts.skipped
11662306a36Sopenharmony_ci		self.errors += counts.errors
11762306a36Sopenharmony_ci
11862306a36Sopenharmony_ci	def get_status(self) -> TestStatus:
11962306a36Sopenharmony_ci		"""Returns the aggregated status of a Test using test
12062306a36Sopenharmony_ci		counts.
12162306a36Sopenharmony_ci		"""
12262306a36Sopenharmony_ci		if self.total() == 0:
12362306a36Sopenharmony_ci			return TestStatus.NO_TESTS
12462306a36Sopenharmony_ci		if self.crashed:
12562306a36Sopenharmony_ci			# Crashes should take priority.
12662306a36Sopenharmony_ci			return TestStatus.TEST_CRASHED
12762306a36Sopenharmony_ci		if self.failed:
12862306a36Sopenharmony_ci			return TestStatus.FAILURE
12962306a36Sopenharmony_ci		if self.passed:
13062306a36Sopenharmony_ci			# No failures or crashes, looks good!
13162306a36Sopenharmony_ci			return TestStatus.SUCCESS
13262306a36Sopenharmony_ci		# We have only skipped tests.
13362306a36Sopenharmony_ci		return TestStatus.SKIPPED
13462306a36Sopenharmony_ci
13562306a36Sopenharmony_ci	def add_status(self, status: TestStatus) -> None:
13662306a36Sopenharmony_ci		"""Increments the count for `status`."""
13762306a36Sopenharmony_ci		if status == TestStatus.SUCCESS:
13862306a36Sopenharmony_ci			self.passed += 1
13962306a36Sopenharmony_ci		elif status == TestStatus.FAILURE:
14062306a36Sopenharmony_ci			self.failed += 1
14162306a36Sopenharmony_ci		elif status == TestStatus.SKIPPED:
14262306a36Sopenharmony_ci			self.skipped += 1
14362306a36Sopenharmony_ci		elif status != TestStatus.NO_TESTS:
14462306a36Sopenharmony_ci			self.crashed += 1
14562306a36Sopenharmony_ci
14662306a36Sopenharmony_ciclass LineStream:
14762306a36Sopenharmony_ci	"""
14862306a36Sopenharmony_ci	A class to represent the lines of kernel output.
14962306a36Sopenharmony_ci	Provides a lazy peek()/pop() interface over an iterator of
15062306a36Sopenharmony_ci	(line#, text).
15162306a36Sopenharmony_ci	"""
15262306a36Sopenharmony_ci	_lines: Iterator[Tuple[int, str]]
15362306a36Sopenharmony_ci	_next: Tuple[int, str]
15462306a36Sopenharmony_ci	_need_next: bool
15562306a36Sopenharmony_ci	_done: bool
15662306a36Sopenharmony_ci
15762306a36Sopenharmony_ci	def __init__(self, lines: Iterator[Tuple[int, str]]):
15862306a36Sopenharmony_ci		"""Creates a new LineStream that wraps the given iterator."""
15962306a36Sopenharmony_ci		self._lines = lines
16062306a36Sopenharmony_ci		self._done = False
16162306a36Sopenharmony_ci		self._need_next = True
16262306a36Sopenharmony_ci		self._next = (0, '')
16362306a36Sopenharmony_ci
16462306a36Sopenharmony_ci	def _get_next(self) -> None:
16562306a36Sopenharmony_ci		"""Advances the LineSteam to the next line, if necessary."""
16662306a36Sopenharmony_ci		if not self._need_next:
16762306a36Sopenharmony_ci			return
16862306a36Sopenharmony_ci		try:
16962306a36Sopenharmony_ci			self._next = next(self._lines)
17062306a36Sopenharmony_ci		except StopIteration:
17162306a36Sopenharmony_ci			self._done = True
17262306a36Sopenharmony_ci		finally:
17362306a36Sopenharmony_ci			self._need_next = False
17462306a36Sopenharmony_ci
17562306a36Sopenharmony_ci	def peek(self) -> str:
17662306a36Sopenharmony_ci		"""Returns the current line, without advancing the LineStream.
17762306a36Sopenharmony_ci		"""
17862306a36Sopenharmony_ci		self._get_next()
17962306a36Sopenharmony_ci		return self._next[1]
18062306a36Sopenharmony_ci
18162306a36Sopenharmony_ci	def pop(self) -> str:
18262306a36Sopenharmony_ci		"""Returns the current line and advances the LineStream to
18362306a36Sopenharmony_ci		the next line.
18462306a36Sopenharmony_ci		"""
18562306a36Sopenharmony_ci		s = self.peek()
18662306a36Sopenharmony_ci		if self._done:
18762306a36Sopenharmony_ci			raise ValueError(f'LineStream: going past EOF, last line was {s}')
18862306a36Sopenharmony_ci		self._need_next = True
18962306a36Sopenharmony_ci		return s
19062306a36Sopenharmony_ci
19162306a36Sopenharmony_ci	def __bool__(self) -> bool:
19262306a36Sopenharmony_ci		"""Returns True if stream has more lines."""
19362306a36Sopenharmony_ci		self._get_next()
19462306a36Sopenharmony_ci		return not self._done
19562306a36Sopenharmony_ci
19662306a36Sopenharmony_ci	# Only used by kunit_tool_test.py.
19762306a36Sopenharmony_ci	def __iter__(self) -> Iterator[str]:
19862306a36Sopenharmony_ci		"""Empties all lines stored in LineStream object into
19962306a36Sopenharmony_ci		Iterator object and returns the Iterator object.
20062306a36Sopenharmony_ci		"""
20162306a36Sopenharmony_ci		while bool(self):
20262306a36Sopenharmony_ci			yield self.pop()
20362306a36Sopenharmony_ci
20462306a36Sopenharmony_ci	def line_number(self) -> int:
20562306a36Sopenharmony_ci		"""Returns the line number of the current line."""
20662306a36Sopenharmony_ci		self._get_next()
20762306a36Sopenharmony_ci		return self._next[0]
20862306a36Sopenharmony_ci
20962306a36Sopenharmony_ci# Parsing helper methods:
21062306a36Sopenharmony_ci
21162306a36Sopenharmony_ciKTAP_START = re.compile(r'\s*KTAP version ([0-9]+)$')
21262306a36Sopenharmony_ciTAP_START = re.compile(r'\s*TAP version ([0-9]+)$')
21362306a36Sopenharmony_ciKTAP_END = re.compile(r'\s*(List of all partitions:|'
21462306a36Sopenharmony_ci	'Kernel panic - not syncing: VFS:|reboot: System halted)')
21562306a36Sopenharmony_ciEXECUTOR_ERROR = re.compile(r'\s*kunit executor: (.*)$')
21662306a36Sopenharmony_ci
21762306a36Sopenharmony_cidef extract_tap_lines(kernel_output: Iterable[str]) -> LineStream:
21862306a36Sopenharmony_ci	"""Extracts KTAP lines from the kernel output."""
21962306a36Sopenharmony_ci	def isolate_ktap_output(kernel_output: Iterable[str]) \
22062306a36Sopenharmony_ci			-> Iterator[Tuple[int, str]]:
22162306a36Sopenharmony_ci		line_num = 0
22262306a36Sopenharmony_ci		started = False
22362306a36Sopenharmony_ci		for line in kernel_output:
22462306a36Sopenharmony_ci			line_num += 1
22562306a36Sopenharmony_ci			line = line.rstrip()  # remove trailing \n
22662306a36Sopenharmony_ci			if not started and KTAP_START.search(line):
22762306a36Sopenharmony_ci				# start extracting KTAP lines and set prefix
22862306a36Sopenharmony_ci				# to number of characters before version line
22962306a36Sopenharmony_ci				prefix_len = len(
23062306a36Sopenharmony_ci					line.split('KTAP version')[0])
23162306a36Sopenharmony_ci				started = True
23262306a36Sopenharmony_ci				yield line_num, line[prefix_len:]
23362306a36Sopenharmony_ci			elif not started and TAP_START.search(line):
23462306a36Sopenharmony_ci				# start extracting KTAP lines and set prefix
23562306a36Sopenharmony_ci				# to number of characters before version line
23662306a36Sopenharmony_ci				prefix_len = len(line.split('TAP version')[0])
23762306a36Sopenharmony_ci				started = True
23862306a36Sopenharmony_ci				yield line_num, line[prefix_len:]
23962306a36Sopenharmony_ci			elif started and KTAP_END.search(line):
24062306a36Sopenharmony_ci				# stop extracting KTAP lines
24162306a36Sopenharmony_ci				break
24262306a36Sopenharmony_ci			elif started:
24362306a36Sopenharmony_ci				# remove the prefix, if any.
24462306a36Sopenharmony_ci				line = line[prefix_len:]
24562306a36Sopenharmony_ci				yield line_num, line
24662306a36Sopenharmony_ci			elif EXECUTOR_ERROR.search(line):
24762306a36Sopenharmony_ci				yield line_num, line
24862306a36Sopenharmony_ci	return LineStream(lines=isolate_ktap_output(kernel_output))
24962306a36Sopenharmony_ci
25062306a36Sopenharmony_ciKTAP_VERSIONS = [1]
25162306a36Sopenharmony_ciTAP_VERSIONS = [13, 14]
25262306a36Sopenharmony_ci
25362306a36Sopenharmony_cidef check_version(version_num: int, accepted_versions: List[int],
25462306a36Sopenharmony_ci			version_type: str, test: Test) -> None:
25562306a36Sopenharmony_ci	"""
25662306a36Sopenharmony_ci	Adds error to test object if version number is too high or too
25762306a36Sopenharmony_ci	low.
25862306a36Sopenharmony_ci
25962306a36Sopenharmony_ci	Parameters:
26062306a36Sopenharmony_ci	version_num - The inputted version number from the parsed KTAP or TAP
26162306a36Sopenharmony_ci		header line
26262306a36Sopenharmony_ci	accepted_version - List of accepted KTAP or TAP versions
26362306a36Sopenharmony_ci	version_type - 'KTAP' or 'TAP' depending on the type of
26462306a36Sopenharmony_ci		version line.
26562306a36Sopenharmony_ci	test - Test object for current test being parsed
26662306a36Sopenharmony_ci	"""
26762306a36Sopenharmony_ci	if version_num < min(accepted_versions):
26862306a36Sopenharmony_ci		test.add_error(f'{version_type} version lower than expected!')
26962306a36Sopenharmony_ci	elif version_num > max(accepted_versions):
27062306a36Sopenharmony_ci		test.add_error(f'{version_type} version higer than expected!')
27162306a36Sopenharmony_ci
27262306a36Sopenharmony_cidef parse_ktap_header(lines: LineStream, test: Test) -> bool:
27362306a36Sopenharmony_ci	"""
27462306a36Sopenharmony_ci	Parses KTAP/TAP header line and checks version number.
27562306a36Sopenharmony_ci	Returns False if fails to parse KTAP/TAP header line.
27662306a36Sopenharmony_ci
27762306a36Sopenharmony_ci	Accepted formats:
27862306a36Sopenharmony_ci	- 'KTAP version [version number]'
27962306a36Sopenharmony_ci	- 'TAP version [version number]'
28062306a36Sopenharmony_ci
28162306a36Sopenharmony_ci	Parameters:
28262306a36Sopenharmony_ci	lines - LineStream of KTAP output to parse
28362306a36Sopenharmony_ci	test - Test object for current test being parsed
28462306a36Sopenharmony_ci
28562306a36Sopenharmony_ci	Return:
28662306a36Sopenharmony_ci	True if successfully parsed KTAP/TAP header line
28762306a36Sopenharmony_ci	"""
28862306a36Sopenharmony_ci	ktap_match = KTAP_START.match(lines.peek())
28962306a36Sopenharmony_ci	tap_match = TAP_START.match(lines.peek())
29062306a36Sopenharmony_ci	if ktap_match:
29162306a36Sopenharmony_ci		version_num = int(ktap_match.group(1))
29262306a36Sopenharmony_ci		check_version(version_num, KTAP_VERSIONS, 'KTAP', test)
29362306a36Sopenharmony_ci	elif tap_match:
29462306a36Sopenharmony_ci		version_num = int(tap_match.group(1))
29562306a36Sopenharmony_ci		check_version(version_num, TAP_VERSIONS, 'TAP', test)
29662306a36Sopenharmony_ci	else:
29762306a36Sopenharmony_ci		return False
29862306a36Sopenharmony_ci	lines.pop()
29962306a36Sopenharmony_ci	return True
30062306a36Sopenharmony_ci
30162306a36Sopenharmony_ciTEST_HEADER = re.compile(r'^\s*# Subtest: (.*)$')
30262306a36Sopenharmony_ci
30362306a36Sopenharmony_cidef parse_test_header(lines: LineStream, test: Test) -> bool:
30462306a36Sopenharmony_ci	"""
30562306a36Sopenharmony_ci	Parses test header and stores test name in test object.
30662306a36Sopenharmony_ci	Returns False if fails to parse test header line.
30762306a36Sopenharmony_ci
30862306a36Sopenharmony_ci	Accepted format:
30962306a36Sopenharmony_ci	- '# Subtest: [test name]'
31062306a36Sopenharmony_ci
31162306a36Sopenharmony_ci	Parameters:
31262306a36Sopenharmony_ci	lines - LineStream of KTAP output to parse
31362306a36Sopenharmony_ci	test - Test object for current test being parsed
31462306a36Sopenharmony_ci
31562306a36Sopenharmony_ci	Return:
31662306a36Sopenharmony_ci	True if successfully parsed test header line
31762306a36Sopenharmony_ci	"""
31862306a36Sopenharmony_ci	match = TEST_HEADER.match(lines.peek())
31962306a36Sopenharmony_ci	if not match:
32062306a36Sopenharmony_ci		return False
32162306a36Sopenharmony_ci	test.name = match.group(1)
32262306a36Sopenharmony_ci	lines.pop()
32362306a36Sopenharmony_ci	return True
32462306a36Sopenharmony_ci
32562306a36Sopenharmony_ciTEST_PLAN = re.compile(r'^\s*1\.\.([0-9]+)')
32662306a36Sopenharmony_ci
32762306a36Sopenharmony_cidef parse_test_plan(lines: LineStream, test: Test) -> bool:
32862306a36Sopenharmony_ci	"""
32962306a36Sopenharmony_ci	Parses test plan line and stores the expected number of subtests in
33062306a36Sopenharmony_ci	test object. Reports an error if expected count is 0.
33162306a36Sopenharmony_ci	Returns False and sets expected_count to None if there is no valid test
33262306a36Sopenharmony_ci	plan.
33362306a36Sopenharmony_ci
33462306a36Sopenharmony_ci	Accepted format:
33562306a36Sopenharmony_ci	- '1..[number of subtests]'
33662306a36Sopenharmony_ci
33762306a36Sopenharmony_ci	Parameters:
33862306a36Sopenharmony_ci	lines - LineStream of KTAP output to parse
33962306a36Sopenharmony_ci	test - Test object for current test being parsed
34062306a36Sopenharmony_ci
34162306a36Sopenharmony_ci	Return:
34262306a36Sopenharmony_ci	True if successfully parsed test plan line
34362306a36Sopenharmony_ci	"""
34462306a36Sopenharmony_ci	match = TEST_PLAN.match(lines.peek())
34562306a36Sopenharmony_ci	if not match:
34662306a36Sopenharmony_ci		test.expected_count = None
34762306a36Sopenharmony_ci		return False
34862306a36Sopenharmony_ci	expected_count = int(match.group(1))
34962306a36Sopenharmony_ci	test.expected_count = expected_count
35062306a36Sopenharmony_ci	lines.pop()
35162306a36Sopenharmony_ci	return True
35262306a36Sopenharmony_ci
35362306a36Sopenharmony_ciTEST_RESULT = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
35462306a36Sopenharmony_ci
35562306a36Sopenharmony_ciTEST_RESULT_SKIP = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?(.*) # SKIP(.*)$')
35662306a36Sopenharmony_ci
35762306a36Sopenharmony_cidef peek_test_name_match(lines: LineStream, test: Test) -> bool:
35862306a36Sopenharmony_ci	"""
35962306a36Sopenharmony_ci	Matches current line with the format of a test result line and checks
36062306a36Sopenharmony_ci	if the name matches the name of the current test.
36162306a36Sopenharmony_ci	Returns False if fails to match format or name.
36262306a36Sopenharmony_ci
36362306a36Sopenharmony_ci	Accepted format:
36462306a36Sopenharmony_ci	- '[ok|not ok] [test number] [-] [test name] [optional skip
36562306a36Sopenharmony_ci		directive]'
36662306a36Sopenharmony_ci
36762306a36Sopenharmony_ci	Parameters:
36862306a36Sopenharmony_ci	lines - LineStream of KTAP output to parse
36962306a36Sopenharmony_ci	test - Test object for current test being parsed
37062306a36Sopenharmony_ci
37162306a36Sopenharmony_ci	Return:
37262306a36Sopenharmony_ci	True if matched a test result line and the name matching the
37362306a36Sopenharmony_ci		expected test name
37462306a36Sopenharmony_ci	"""
37562306a36Sopenharmony_ci	line = lines.peek()
37662306a36Sopenharmony_ci	match = TEST_RESULT.match(line)
37762306a36Sopenharmony_ci	if not match:
37862306a36Sopenharmony_ci		return False
37962306a36Sopenharmony_ci	name = match.group(4)
38062306a36Sopenharmony_ci	return name == test.name
38162306a36Sopenharmony_ci
38262306a36Sopenharmony_cidef parse_test_result(lines: LineStream, test: Test,
38362306a36Sopenharmony_ci			expected_num: int) -> bool:
38462306a36Sopenharmony_ci	"""
38562306a36Sopenharmony_ci	Parses test result line and stores the status and name in the test
38662306a36Sopenharmony_ci	object. Reports an error if the test number does not match expected
38762306a36Sopenharmony_ci	test number.
38862306a36Sopenharmony_ci	Returns False if fails to parse test result line.
38962306a36Sopenharmony_ci
39062306a36Sopenharmony_ci	Note that the SKIP directive is the only direction that causes a
39162306a36Sopenharmony_ci	change in status.
39262306a36Sopenharmony_ci
39362306a36Sopenharmony_ci	Accepted format:
39462306a36Sopenharmony_ci	- '[ok|not ok] [test number] [-] [test name] [optional skip
39562306a36Sopenharmony_ci		directive]'
39662306a36Sopenharmony_ci
39762306a36Sopenharmony_ci	Parameters:
39862306a36Sopenharmony_ci	lines - LineStream of KTAP output to parse
39962306a36Sopenharmony_ci	test - Test object for current test being parsed
40062306a36Sopenharmony_ci	expected_num - expected test number for current test
40162306a36Sopenharmony_ci
40262306a36Sopenharmony_ci	Return:
40362306a36Sopenharmony_ci	True if successfully parsed a test result line.
40462306a36Sopenharmony_ci	"""
40562306a36Sopenharmony_ci	line = lines.peek()
40662306a36Sopenharmony_ci	match = TEST_RESULT.match(line)
40762306a36Sopenharmony_ci	skip_match = TEST_RESULT_SKIP.match(line)
40862306a36Sopenharmony_ci
40962306a36Sopenharmony_ci	# Check if line matches test result line format
41062306a36Sopenharmony_ci	if not match:
41162306a36Sopenharmony_ci		return False
41262306a36Sopenharmony_ci	lines.pop()
41362306a36Sopenharmony_ci
41462306a36Sopenharmony_ci	# Set name of test object
41562306a36Sopenharmony_ci	if skip_match:
41662306a36Sopenharmony_ci		test.name = skip_match.group(4)
41762306a36Sopenharmony_ci	else:
41862306a36Sopenharmony_ci		test.name = match.group(4)
41962306a36Sopenharmony_ci
42062306a36Sopenharmony_ci	# Check test num
42162306a36Sopenharmony_ci	num = int(match.group(2))
42262306a36Sopenharmony_ci	if num != expected_num:
42362306a36Sopenharmony_ci		test.add_error(f'Expected test number {expected_num} but found {num}')
42462306a36Sopenharmony_ci
42562306a36Sopenharmony_ci	# Set status of test object
42662306a36Sopenharmony_ci	status = match.group(1)
42762306a36Sopenharmony_ci	if skip_match:
42862306a36Sopenharmony_ci		test.status = TestStatus.SKIPPED
42962306a36Sopenharmony_ci	elif status == 'ok':
43062306a36Sopenharmony_ci		test.status = TestStatus.SUCCESS
43162306a36Sopenharmony_ci	else:
43262306a36Sopenharmony_ci		test.status = TestStatus.FAILURE
43362306a36Sopenharmony_ci	return True
43462306a36Sopenharmony_ci
43562306a36Sopenharmony_cidef parse_diagnostic(lines: LineStream) -> List[str]:
43662306a36Sopenharmony_ci	"""
43762306a36Sopenharmony_ci	Parse lines that do not match the format of a test result line or
43862306a36Sopenharmony_ci	test header line and returns them in list.
43962306a36Sopenharmony_ci
44062306a36Sopenharmony_ci	Line formats that are not parsed:
44162306a36Sopenharmony_ci	- '# Subtest: [test name]'
44262306a36Sopenharmony_ci	- '[ok|not ok] [test number] [-] [test name] [optional skip
44362306a36Sopenharmony_ci		directive]'
44462306a36Sopenharmony_ci	- 'KTAP version [version number]'
44562306a36Sopenharmony_ci
44662306a36Sopenharmony_ci	Parameters:
44762306a36Sopenharmony_ci	lines - LineStream of KTAP output to parse
44862306a36Sopenharmony_ci
44962306a36Sopenharmony_ci	Return:
45062306a36Sopenharmony_ci	Log of diagnostic lines
45162306a36Sopenharmony_ci	"""
45262306a36Sopenharmony_ci	log = []  # type: List[str]
45362306a36Sopenharmony_ci	non_diagnostic_lines = [TEST_RESULT, TEST_HEADER, KTAP_START, TAP_START, TEST_PLAN]
45462306a36Sopenharmony_ci	while lines and not any(re.match(lines.peek())
45562306a36Sopenharmony_ci			for re in non_diagnostic_lines):
45662306a36Sopenharmony_ci		log.append(lines.pop())
45762306a36Sopenharmony_ci	return log
45862306a36Sopenharmony_ci
45962306a36Sopenharmony_ci
46062306a36Sopenharmony_ci# Printing helper methods:
46162306a36Sopenharmony_ci
46262306a36Sopenharmony_ciDIVIDER = '=' * 60
46362306a36Sopenharmony_ci
46462306a36Sopenharmony_cidef format_test_divider(message: str, len_message: int) -> str:
46562306a36Sopenharmony_ci	"""
46662306a36Sopenharmony_ci	Returns string with message centered in fixed width divider.
46762306a36Sopenharmony_ci
46862306a36Sopenharmony_ci	Example:
46962306a36Sopenharmony_ci	'===================== message example ====================='
47062306a36Sopenharmony_ci
47162306a36Sopenharmony_ci	Parameters:
47262306a36Sopenharmony_ci	message - message to be centered in divider line
47362306a36Sopenharmony_ci	len_message - length of the message to be printed such that
47462306a36Sopenharmony_ci		any characters of the color codes are not counted
47562306a36Sopenharmony_ci
47662306a36Sopenharmony_ci	Return:
47762306a36Sopenharmony_ci	String containing message centered in fixed width divider
47862306a36Sopenharmony_ci	"""
47962306a36Sopenharmony_ci	default_count = 3  # default number of dashes
48062306a36Sopenharmony_ci	len_1 = default_count
48162306a36Sopenharmony_ci	len_2 = default_count
48262306a36Sopenharmony_ci	difference = len(DIVIDER) - len_message - 2  # 2 spaces added
48362306a36Sopenharmony_ci	if difference > 0:
48462306a36Sopenharmony_ci		# calculate number of dashes for each side of the divider
48562306a36Sopenharmony_ci		len_1 = int(difference / 2)
48662306a36Sopenharmony_ci		len_2 = difference - len_1
48762306a36Sopenharmony_ci	return ('=' * len_1) + f' {message} ' + ('=' * len_2)
48862306a36Sopenharmony_ci
48962306a36Sopenharmony_cidef print_test_header(test: Test) -> None:
49062306a36Sopenharmony_ci	"""
49162306a36Sopenharmony_ci	Prints test header with test name and optionally the expected number
49262306a36Sopenharmony_ci	of subtests.
49362306a36Sopenharmony_ci
49462306a36Sopenharmony_ci	Example:
49562306a36Sopenharmony_ci	'=================== example (2 subtests) ==================='
49662306a36Sopenharmony_ci
49762306a36Sopenharmony_ci	Parameters:
49862306a36Sopenharmony_ci	test - Test object representing current test being printed
49962306a36Sopenharmony_ci	"""
50062306a36Sopenharmony_ci	message = test.name
50162306a36Sopenharmony_ci	if message != "":
50262306a36Sopenharmony_ci		# Add a leading space before the subtest counts only if a test name
50362306a36Sopenharmony_ci		# is provided using a "# Subtest" header line.
50462306a36Sopenharmony_ci		message += " "
50562306a36Sopenharmony_ci	if test.expected_count:
50662306a36Sopenharmony_ci		if test.expected_count == 1:
50762306a36Sopenharmony_ci			message += '(1 subtest)'
50862306a36Sopenharmony_ci		else:
50962306a36Sopenharmony_ci			message += f'({test.expected_count} subtests)'
51062306a36Sopenharmony_ci	stdout.print_with_timestamp(format_test_divider(message, len(message)))
51162306a36Sopenharmony_ci
51262306a36Sopenharmony_cidef print_log(log: Iterable[str]) -> None:
51362306a36Sopenharmony_ci	"""Prints all strings in saved log for test in yellow."""
51462306a36Sopenharmony_ci	formatted = textwrap.dedent('\n'.join(log))
51562306a36Sopenharmony_ci	for line in formatted.splitlines():
51662306a36Sopenharmony_ci		stdout.print_with_timestamp(stdout.yellow(line))
51762306a36Sopenharmony_ci
51862306a36Sopenharmony_cidef format_test_result(test: Test) -> str:
51962306a36Sopenharmony_ci	"""
52062306a36Sopenharmony_ci	Returns string with formatted test result with colored status and test
52162306a36Sopenharmony_ci	name.
52262306a36Sopenharmony_ci
52362306a36Sopenharmony_ci	Example:
52462306a36Sopenharmony_ci	'[PASSED] example'
52562306a36Sopenharmony_ci
52662306a36Sopenharmony_ci	Parameters:
52762306a36Sopenharmony_ci	test - Test object representing current test being printed
52862306a36Sopenharmony_ci
52962306a36Sopenharmony_ci	Return:
53062306a36Sopenharmony_ci	String containing formatted test result
53162306a36Sopenharmony_ci	"""
53262306a36Sopenharmony_ci	if test.status == TestStatus.SUCCESS:
53362306a36Sopenharmony_ci		return stdout.green('[PASSED] ') + test.name
53462306a36Sopenharmony_ci	if test.status == TestStatus.SKIPPED:
53562306a36Sopenharmony_ci		return stdout.yellow('[SKIPPED] ') + test.name
53662306a36Sopenharmony_ci	if test.status == TestStatus.NO_TESTS:
53762306a36Sopenharmony_ci		return stdout.yellow('[NO TESTS RUN] ') + test.name
53862306a36Sopenharmony_ci	if test.status == TestStatus.TEST_CRASHED:
53962306a36Sopenharmony_ci		print_log(test.log)
54062306a36Sopenharmony_ci		return stdout.red('[CRASHED] ') + test.name
54162306a36Sopenharmony_ci	print_log(test.log)
54262306a36Sopenharmony_ci	return stdout.red('[FAILED] ') + test.name
54362306a36Sopenharmony_ci
54462306a36Sopenharmony_cidef print_test_result(test: Test) -> None:
54562306a36Sopenharmony_ci	"""
54662306a36Sopenharmony_ci	Prints result line with status of test.
54762306a36Sopenharmony_ci
54862306a36Sopenharmony_ci	Example:
54962306a36Sopenharmony_ci	'[PASSED] example'
55062306a36Sopenharmony_ci
55162306a36Sopenharmony_ci	Parameters:
55262306a36Sopenharmony_ci	test - Test object representing current test being printed
55362306a36Sopenharmony_ci	"""
55462306a36Sopenharmony_ci	stdout.print_with_timestamp(format_test_result(test))
55562306a36Sopenharmony_ci
55662306a36Sopenharmony_cidef print_test_footer(test: Test) -> None:
55762306a36Sopenharmony_ci	"""
55862306a36Sopenharmony_ci	Prints test footer with status of test.
55962306a36Sopenharmony_ci
56062306a36Sopenharmony_ci	Example:
56162306a36Sopenharmony_ci	'===================== [PASSED] example ====================='
56262306a36Sopenharmony_ci
56362306a36Sopenharmony_ci	Parameters:
56462306a36Sopenharmony_ci	test - Test object representing current test being printed
56562306a36Sopenharmony_ci	"""
56662306a36Sopenharmony_ci	message = format_test_result(test)
56762306a36Sopenharmony_ci	stdout.print_with_timestamp(format_test_divider(message,
56862306a36Sopenharmony_ci		len(message) - stdout.color_len()))
56962306a36Sopenharmony_ci
57062306a36Sopenharmony_ci
57162306a36Sopenharmony_ci
57262306a36Sopenharmony_cidef _summarize_failed_tests(test: Test) -> str:
57362306a36Sopenharmony_ci	"""Tries to summarize all the failing subtests in `test`."""
57462306a36Sopenharmony_ci
57562306a36Sopenharmony_ci	def failed_names(test: Test, parent_name: str) -> List[str]:
57662306a36Sopenharmony_ci		# Note: we use 'main' internally for the top-level test.
57762306a36Sopenharmony_ci		if not parent_name or parent_name == 'main':
57862306a36Sopenharmony_ci			full_name = test.name
57962306a36Sopenharmony_ci		else:
58062306a36Sopenharmony_ci			full_name = parent_name + '.' + test.name
58162306a36Sopenharmony_ci
58262306a36Sopenharmony_ci		if not test.subtests:  # this is a leaf node
58362306a36Sopenharmony_ci			return [full_name]
58462306a36Sopenharmony_ci
58562306a36Sopenharmony_ci		# If all the children failed, just say this subtest failed.
58662306a36Sopenharmony_ci		# Don't summarize it down "the top-level test failed", though.
58762306a36Sopenharmony_ci		failed_subtests = [sub for sub in test.subtests if not sub.ok_status()]
58862306a36Sopenharmony_ci		if parent_name and len(failed_subtests) ==  len(test.subtests):
58962306a36Sopenharmony_ci			return [full_name]
59062306a36Sopenharmony_ci
59162306a36Sopenharmony_ci		all_failures = []  # type: List[str]
59262306a36Sopenharmony_ci		for t in failed_subtests:
59362306a36Sopenharmony_ci			all_failures.extend(failed_names(t, full_name))
59462306a36Sopenharmony_ci		return all_failures
59562306a36Sopenharmony_ci
59662306a36Sopenharmony_ci	failures = failed_names(test, '')
59762306a36Sopenharmony_ci	# If there are too many failures, printing them out will just be noisy.
59862306a36Sopenharmony_ci	if len(failures) > 10:  # this is an arbitrary limit
59962306a36Sopenharmony_ci		return ''
60062306a36Sopenharmony_ci
60162306a36Sopenharmony_ci	return 'Failures: ' + ', '.join(failures)
60262306a36Sopenharmony_ci
60362306a36Sopenharmony_ci
60462306a36Sopenharmony_cidef print_summary_line(test: Test) -> None:
60562306a36Sopenharmony_ci	"""
60662306a36Sopenharmony_ci	Prints summary line of test object. Color of line is dependent on
60762306a36Sopenharmony_ci	status of test. Color is green if test passes, yellow if test is
60862306a36Sopenharmony_ci	skipped, and red if the test fails or crashes. Summary line contains
60962306a36Sopenharmony_ci	counts of the statuses of the tests subtests or the test itself if it
61062306a36Sopenharmony_ci	has no subtests.
61162306a36Sopenharmony_ci
61262306a36Sopenharmony_ci	Example:
61362306a36Sopenharmony_ci	"Testing complete. Passed: 2, Failed: 0, Crashed: 0, Skipped: 0,
61462306a36Sopenharmony_ci	Errors: 0"
61562306a36Sopenharmony_ci
61662306a36Sopenharmony_ci	test - Test object representing current test being printed
61762306a36Sopenharmony_ci	"""
61862306a36Sopenharmony_ci	if test.status == TestStatus.SUCCESS:
61962306a36Sopenharmony_ci		color = stdout.green
62062306a36Sopenharmony_ci	elif test.status in (TestStatus.SKIPPED, TestStatus.NO_TESTS):
62162306a36Sopenharmony_ci		color = stdout.yellow
62262306a36Sopenharmony_ci	else:
62362306a36Sopenharmony_ci		color = stdout.red
62462306a36Sopenharmony_ci	stdout.print_with_timestamp(color(f'Testing complete. {test.counts}'))
62562306a36Sopenharmony_ci
62662306a36Sopenharmony_ci	# Summarize failures that might have gone off-screen since we had a lot
62762306a36Sopenharmony_ci	# of tests (arbitrarily defined as >=100 for now).
62862306a36Sopenharmony_ci	if test.ok_status() or test.counts.total() < 100:
62962306a36Sopenharmony_ci		return
63062306a36Sopenharmony_ci	summarized = _summarize_failed_tests(test)
63162306a36Sopenharmony_ci	if not summarized:
63262306a36Sopenharmony_ci		return
63362306a36Sopenharmony_ci	stdout.print_with_timestamp(color(summarized))
63462306a36Sopenharmony_ci
63562306a36Sopenharmony_ci# Other methods:
63662306a36Sopenharmony_ci
63762306a36Sopenharmony_cidef bubble_up_test_results(test: Test) -> None:
63862306a36Sopenharmony_ci	"""
63962306a36Sopenharmony_ci	If the test has subtests, add the test counts of the subtests to the
64062306a36Sopenharmony_ci	test and check if any of the tests crashed and if so set the test
64162306a36Sopenharmony_ci	status to crashed. Otherwise if the test has no subtests add the
64262306a36Sopenharmony_ci	status of the test to the test counts.
64362306a36Sopenharmony_ci
64462306a36Sopenharmony_ci	Parameters:
64562306a36Sopenharmony_ci	test - Test object for current test being parsed
64662306a36Sopenharmony_ci	"""
64762306a36Sopenharmony_ci	subtests = test.subtests
64862306a36Sopenharmony_ci	counts = test.counts
64962306a36Sopenharmony_ci	status = test.status
65062306a36Sopenharmony_ci	for t in subtests:
65162306a36Sopenharmony_ci		counts.add_subtest_counts(t.counts)
65262306a36Sopenharmony_ci	if counts.total() == 0:
65362306a36Sopenharmony_ci		counts.add_status(status)
65462306a36Sopenharmony_ci	elif test.counts.get_status() == TestStatus.TEST_CRASHED:
65562306a36Sopenharmony_ci		test.status = TestStatus.TEST_CRASHED
65662306a36Sopenharmony_ci
65762306a36Sopenharmony_cidef parse_test(lines: LineStream, expected_num: int, log: List[str], is_subtest: bool) -> Test:
65862306a36Sopenharmony_ci	"""
65962306a36Sopenharmony_ci	Finds next test to parse in LineStream, creates new Test object,
66062306a36Sopenharmony_ci	parses any subtests of the test, populates Test object with all
66162306a36Sopenharmony_ci	information (status, name) about the test and the Test objects for
66262306a36Sopenharmony_ci	any subtests, and then returns the Test object. The method accepts
66362306a36Sopenharmony_ci	three formats of tests:
66462306a36Sopenharmony_ci
66562306a36Sopenharmony_ci	Accepted test formats:
66662306a36Sopenharmony_ci
66762306a36Sopenharmony_ci	- Main KTAP/TAP header
66862306a36Sopenharmony_ci
66962306a36Sopenharmony_ci	Example:
67062306a36Sopenharmony_ci
67162306a36Sopenharmony_ci	KTAP version 1
67262306a36Sopenharmony_ci	1..4
67362306a36Sopenharmony_ci	[subtests]
67462306a36Sopenharmony_ci
67562306a36Sopenharmony_ci	- Subtest header (must include either the KTAP version line or
67662306a36Sopenharmony_ci	  "# Subtest" header line)
67762306a36Sopenharmony_ci
67862306a36Sopenharmony_ci	Example (preferred format with both KTAP version line and
67962306a36Sopenharmony_ci	"# Subtest" line):
68062306a36Sopenharmony_ci
68162306a36Sopenharmony_ci	KTAP version 1
68262306a36Sopenharmony_ci	# Subtest: name
68362306a36Sopenharmony_ci	1..3
68462306a36Sopenharmony_ci	[subtests]
68562306a36Sopenharmony_ci	ok 1 name
68662306a36Sopenharmony_ci
68762306a36Sopenharmony_ci	Example (only "# Subtest" line):
68862306a36Sopenharmony_ci
68962306a36Sopenharmony_ci	# Subtest: name
69062306a36Sopenharmony_ci	1..3
69162306a36Sopenharmony_ci	[subtests]
69262306a36Sopenharmony_ci	ok 1 name
69362306a36Sopenharmony_ci
69462306a36Sopenharmony_ci	Example (only KTAP version line, compliant with KTAP v1 spec):
69562306a36Sopenharmony_ci
69662306a36Sopenharmony_ci	KTAP version 1
69762306a36Sopenharmony_ci	1..3
69862306a36Sopenharmony_ci	[subtests]
69962306a36Sopenharmony_ci	ok 1 name
70062306a36Sopenharmony_ci
70162306a36Sopenharmony_ci	- Test result line
70262306a36Sopenharmony_ci
70362306a36Sopenharmony_ci	Example:
70462306a36Sopenharmony_ci
70562306a36Sopenharmony_ci	ok 1 - test
70662306a36Sopenharmony_ci
70762306a36Sopenharmony_ci	Parameters:
70862306a36Sopenharmony_ci	lines - LineStream of KTAP output to parse
70962306a36Sopenharmony_ci	expected_num - expected test number for test to be parsed
71062306a36Sopenharmony_ci	log - list of strings containing any preceding diagnostic lines
71162306a36Sopenharmony_ci		corresponding to the current test
71262306a36Sopenharmony_ci	is_subtest - boolean indicating whether test is a subtest
71362306a36Sopenharmony_ci
71462306a36Sopenharmony_ci	Return:
71562306a36Sopenharmony_ci	Test object populated with characteristics and any subtests
71662306a36Sopenharmony_ci	"""
71762306a36Sopenharmony_ci	test = Test()
71862306a36Sopenharmony_ci	test.log.extend(log)
71962306a36Sopenharmony_ci
72062306a36Sopenharmony_ci	# Parse any errors prior to parsing tests
72162306a36Sopenharmony_ci	err_log = parse_diagnostic(lines)
72262306a36Sopenharmony_ci	test.log.extend(err_log)
72362306a36Sopenharmony_ci
72462306a36Sopenharmony_ci	if not is_subtest:
72562306a36Sopenharmony_ci		# If parsing the main/top-level test, parse KTAP version line and
72662306a36Sopenharmony_ci		# test plan
72762306a36Sopenharmony_ci		test.name = "main"
72862306a36Sopenharmony_ci		ktap_line = parse_ktap_header(lines, test)
72962306a36Sopenharmony_ci		test.log.extend(parse_diagnostic(lines))
73062306a36Sopenharmony_ci		parse_test_plan(lines, test)
73162306a36Sopenharmony_ci		parent_test = True
73262306a36Sopenharmony_ci	else:
73362306a36Sopenharmony_ci		# If not the main test, attempt to parse a test header containing
73462306a36Sopenharmony_ci		# the KTAP version line and/or subtest header line
73562306a36Sopenharmony_ci		ktap_line = parse_ktap_header(lines, test)
73662306a36Sopenharmony_ci		subtest_line = parse_test_header(lines, test)
73762306a36Sopenharmony_ci		parent_test = (ktap_line or subtest_line)
73862306a36Sopenharmony_ci		if parent_test:
73962306a36Sopenharmony_ci			# If KTAP version line and/or subtest header is found, attempt
74062306a36Sopenharmony_ci			# to parse test plan and print test header
74162306a36Sopenharmony_ci			test.log.extend(parse_diagnostic(lines))
74262306a36Sopenharmony_ci			parse_test_plan(lines, test)
74362306a36Sopenharmony_ci			print_test_header(test)
74462306a36Sopenharmony_ci	expected_count = test.expected_count
74562306a36Sopenharmony_ci	subtests = []
74662306a36Sopenharmony_ci	test_num = 1
74762306a36Sopenharmony_ci	while parent_test and (expected_count is None or test_num <= expected_count):
74862306a36Sopenharmony_ci		# Loop to parse any subtests.
74962306a36Sopenharmony_ci		# Break after parsing expected number of tests or
75062306a36Sopenharmony_ci		# if expected number of tests is unknown break when test
75162306a36Sopenharmony_ci		# result line with matching name to subtest header is found
75262306a36Sopenharmony_ci		# or no more lines in stream.
75362306a36Sopenharmony_ci		sub_log = parse_diagnostic(lines)
75462306a36Sopenharmony_ci		sub_test = Test()
75562306a36Sopenharmony_ci		if not lines or (peek_test_name_match(lines, test) and
75662306a36Sopenharmony_ci				is_subtest):
75762306a36Sopenharmony_ci			if expected_count and test_num <= expected_count:
75862306a36Sopenharmony_ci				# If parser reaches end of test before
75962306a36Sopenharmony_ci				# parsing expected number of subtests, print
76062306a36Sopenharmony_ci				# crashed subtest and record error
76162306a36Sopenharmony_ci				test.add_error('missing expected subtest!')
76262306a36Sopenharmony_ci				sub_test.log.extend(sub_log)
76362306a36Sopenharmony_ci				test.counts.add_status(
76462306a36Sopenharmony_ci					TestStatus.TEST_CRASHED)
76562306a36Sopenharmony_ci				print_test_result(sub_test)
76662306a36Sopenharmony_ci			else:
76762306a36Sopenharmony_ci				test.log.extend(sub_log)
76862306a36Sopenharmony_ci				break
76962306a36Sopenharmony_ci		else:
77062306a36Sopenharmony_ci			sub_test = parse_test(lines, test_num, sub_log, True)
77162306a36Sopenharmony_ci		subtests.append(sub_test)
77262306a36Sopenharmony_ci		test_num += 1
77362306a36Sopenharmony_ci	test.subtests = subtests
77462306a36Sopenharmony_ci	if is_subtest:
77562306a36Sopenharmony_ci		# If not main test, look for test result line
77662306a36Sopenharmony_ci		test.log.extend(parse_diagnostic(lines))
77762306a36Sopenharmony_ci		if test.name != "" and not peek_test_name_match(lines, test):
77862306a36Sopenharmony_ci			test.add_error('missing subtest result line!')
77962306a36Sopenharmony_ci		else:
78062306a36Sopenharmony_ci			parse_test_result(lines, test, expected_num)
78162306a36Sopenharmony_ci
78262306a36Sopenharmony_ci	# Check for there being no subtests within parent test
78362306a36Sopenharmony_ci	if parent_test and len(subtests) == 0:
78462306a36Sopenharmony_ci		# Don't override a bad status if this test had one reported.
78562306a36Sopenharmony_ci		# Assumption: no subtests means CRASHED is from Test.__init__()
78662306a36Sopenharmony_ci		if test.status in (TestStatus.TEST_CRASHED, TestStatus.SUCCESS):
78762306a36Sopenharmony_ci			print_log(test.log)
78862306a36Sopenharmony_ci			test.status = TestStatus.NO_TESTS
78962306a36Sopenharmony_ci			test.add_error('0 tests run!')
79062306a36Sopenharmony_ci
79162306a36Sopenharmony_ci	# Add statuses to TestCounts attribute in Test object
79262306a36Sopenharmony_ci	bubble_up_test_results(test)
79362306a36Sopenharmony_ci	if parent_test and is_subtest:
79462306a36Sopenharmony_ci		# If test has subtests and is not the main test object, print
79562306a36Sopenharmony_ci		# footer.
79662306a36Sopenharmony_ci		print_test_footer(test)
79762306a36Sopenharmony_ci	elif is_subtest:
79862306a36Sopenharmony_ci		print_test_result(test)
79962306a36Sopenharmony_ci	return test
80062306a36Sopenharmony_ci
80162306a36Sopenharmony_cidef parse_run_tests(kernel_output: Iterable[str]) -> Test:
80262306a36Sopenharmony_ci	"""
80362306a36Sopenharmony_ci	Using kernel output, extract KTAP lines, parse the lines for test
80462306a36Sopenharmony_ci	results and print condensed test results and summary line.
80562306a36Sopenharmony_ci
80662306a36Sopenharmony_ci	Parameters:
80762306a36Sopenharmony_ci	kernel_output - Iterable object contains lines of kernel output
80862306a36Sopenharmony_ci
80962306a36Sopenharmony_ci	Return:
81062306a36Sopenharmony_ci	Test - the main test object with all subtests.
81162306a36Sopenharmony_ci	"""
81262306a36Sopenharmony_ci	stdout.print_with_timestamp(DIVIDER)
81362306a36Sopenharmony_ci	lines = extract_tap_lines(kernel_output)
81462306a36Sopenharmony_ci	test = Test()
81562306a36Sopenharmony_ci	if not lines:
81662306a36Sopenharmony_ci		test.name = '<missing>'
81762306a36Sopenharmony_ci		test.add_error('Could not find any KTAP output. Did any KUnit tests run?')
81862306a36Sopenharmony_ci		test.status = TestStatus.FAILURE_TO_PARSE_TESTS
81962306a36Sopenharmony_ci	else:
82062306a36Sopenharmony_ci		test = parse_test(lines, 0, [], False)
82162306a36Sopenharmony_ci		if test.status != TestStatus.NO_TESTS:
82262306a36Sopenharmony_ci			test.status = test.counts.get_status()
82362306a36Sopenharmony_ci	stdout.print_with_timestamp(DIVIDER)
82462306a36Sopenharmony_ci	print_summary_line(test)
82562306a36Sopenharmony_ci	return test
826