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