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