kunit_parser.py (5937e0c04afc7d4b7b737fda93316ba4b74183c0) kunit_parser.py (c2bb92bc4ea13842fdd27819c0d5b48df2b86ea5)
1# SPDX-License-Identifier: GPL-2.0
2#
3# Parses KTAP test results from a kernel dmesg log and incrementally prints
4# results with reader-friendly format. Stores and returns test results in a
5# Test object.
6#
7# Copyright (C) 2019, Google LLC.
8# Author: Felix Guo <felixguoxiuping@gmail.com>
9# Author: Brendan Higgins <brendanhiggins@google.com>
10# Author: Rae Moar <rmoar@google.com>
11
12from __future__ import annotations
13from dataclasses import dataclass
14import re
15import sys
1# SPDX-License-Identifier: GPL-2.0
2#
3# Parses KTAP test results from a kernel dmesg log and incrementally prints
4# results with reader-friendly format. Stores and returns test results in a
5# Test object.
6#
7# Copyright (C) 2019, Google LLC.
8# Author: Felix Guo <felixguoxiuping@gmail.com>
9# Author: Brendan Higgins <brendanhiggins@google.com>
10# Author: Rae Moar <rmoar@google.com>
11
12from __future__ import annotations
13from dataclasses import dataclass
14import re
15import sys
16import textwrap
16
17from enum import Enum, auto
18from typing import Iterable, Iterator, List, Optional, Tuple
19
20from kunit_printer import stdout
21
22class Test:
23 """

--- 179 unchanged lines hidden (view full) ---

203
204 def line_number(self) -> int:
205 """Returns the line number of the current line."""
206 self._get_next()
207 return self._next[0]
208
209# Parsing helper methods:
210
17
18from enum import Enum, auto
19from typing import Iterable, Iterator, List, Optional, Tuple
20
21from kunit_printer import stdout
22
23class Test:
24 """

--- 179 unchanged lines hidden (view full) ---

204
205 def line_number(self) -> int:
206 """Returns the line number of the current line."""
207 self._get_next()
208 return self._next[0]
209
210# Parsing helper methods:
211
211KTAP_START = re.compile(r'KTAP version ([0-9]+)$')
212TAP_START = re.compile(r'TAP version ([0-9]+)$')
213KTAP_END = re.compile('(List of all partitions:|'
212KTAP_START = re.compile(r'\s*KTAP version ([0-9]+)$')
213TAP_START = re.compile(r'\s*TAP version ([0-9]+)$')
214KTAP_END = re.compile(r'\s*(List of all partitions:|'
214 'Kernel panic - not syncing: VFS:|reboot: System halted)')
215
215 'Kernel panic - not syncing: VFS:|reboot: System halted)')
216
216def extract_tap_lines(kernel_output: Iterable[str], lstrip=True) -> LineStream:
217def extract_tap_lines(kernel_output: Iterable[str]) -> LineStream:
217 """Extracts KTAP lines from the kernel output."""
218 def isolate_ktap_output(kernel_output: Iterable[str]) \
219 -> Iterator[Tuple[int, str]]:
220 line_num = 0
221 started = False
222 for line in kernel_output:
223 line_num += 1
224 line = line.rstrip() # remove trailing \n

--- 9 unchanged lines hidden (view full) ---

234 # to number of characters before version line
235 prefix_len = len(line.split('TAP version')[0])
236 started = True
237 yield line_num, line[prefix_len:]
238 elif started and KTAP_END.search(line):
239 # stop extracting KTAP lines
240 break
241 elif started:
218 """Extracts KTAP lines from the kernel output."""
219 def isolate_ktap_output(kernel_output: Iterable[str]) \
220 -> Iterator[Tuple[int, str]]:
221 line_num = 0
222 started = False
223 for line in kernel_output:
224 line_num += 1
225 line = line.rstrip() # remove trailing \n

--- 9 unchanged lines hidden (view full) ---

235 # to number of characters before version line
236 prefix_len = len(line.split('TAP version')[0])
237 started = True
238 yield line_num, line[prefix_len:]
239 elif started and KTAP_END.search(line):
240 # stop extracting KTAP lines
241 break
242 elif started:
242 # remove the prefix and optionally any leading
243 # whitespace. Our parsing logic relies on this.
243 # remove the prefix, if any.
244 line = line[prefix_len:]
244 line = line[prefix_len:]
245 if lstrip:
246 line = line.lstrip()
247 yield line_num, line
248 return LineStream(lines=isolate_ktap_output(kernel_output))
249
250KTAP_VERSIONS = [1]
251TAP_VERSIONS = [13, 14]
252
253def check_version(version_num: int, accepted_versions: List[int],
254 version_type: str, test: Test) -> None:

--- 38 unchanged lines hidden (view full) ---

293 elif tap_match:
294 version_num = int(tap_match.group(1))
295 check_version(version_num, TAP_VERSIONS, 'TAP', test)
296 else:
297 return False
298 lines.pop()
299 return True
300
245 yield line_num, line
246 return LineStream(lines=isolate_ktap_output(kernel_output))
247
248KTAP_VERSIONS = [1]
249TAP_VERSIONS = [13, 14]
250
251def check_version(version_num: int, accepted_versions: List[int],
252 version_type: str, test: Test) -> None:

--- 38 unchanged lines hidden (view full) ---

291 elif tap_match:
292 version_num = int(tap_match.group(1))
293 check_version(version_num, TAP_VERSIONS, 'TAP', test)
294 else:
295 return False
296 lines.pop()
297 return True
298
301TEST_HEADER = re.compile(r'^# Subtest: (.*)$')
299TEST_HEADER = re.compile(r'^\s*# Subtest: (.*)$')
302
303def parse_test_header(lines: LineStream, test: Test) -> bool:
304 """
305 Parses test header and stores test name in test object.
306 Returns False if fails to parse test header line.
307
308 Accepted format:
309 - '# Subtest: [test name]'

--- 7 unchanged lines hidden (view full) ---

317 """
318 match = TEST_HEADER.match(lines.peek())
319 if not match:
320 return False
321 test.name = match.group(1)
322 lines.pop()
323 return True
324
300
301def parse_test_header(lines: LineStream, test: Test) -> bool:
302 """
303 Parses test header and stores test name in test object.
304 Returns False if fails to parse test header line.
305
306 Accepted format:
307 - '# Subtest: [test name]'

--- 7 unchanged lines hidden (view full) ---

315 """
316 match = TEST_HEADER.match(lines.peek())
317 if not match:
318 return False
319 test.name = match.group(1)
320 lines.pop()
321 return True
322
325TEST_PLAN = re.compile(r'1\.\.([0-9]+)')
323TEST_PLAN = re.compile(r'^\s*1\.\.([0-9]+)')
326
327def parse_test_plan(lines: LineStream, test: Test) -> bool:
328 """
329 Parses test plan line and stores the expected number of subtests in
330 test object. Reports an error if expected count is 0.
331 Returns False and sets expected_count to None if there is no valid test
332 plan.
333

--- 11 unchanged lines hidden (view full) ---

345 if not match:
346 test.expected_count = None
347 return False
348 expected_count = int(match.group(1))
349 test.expected_count = expected_count
350 lines.pop()
351 return True
352
324
325def parse_test_plan(lines: LineStream, test: Test) -> bool:
326 """
327 Parses test plan line and stores the expected number of subtests in
328 test object. Reports an error if expected count is 0.
329 Returns False and sets expected_count to None if there is no valid test
330 plan.
331

--- 11 unchanged lines hidden (view full) ---

343 if not match:
344 test.expected_count = None
345 return False
346 expected_count = int(match.group(1))
347 test.expected_count = expected_count
348 lines.pop()
349 return True
350
353TEST_RESULT = re.compile(r'^(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
351TEST_RESULT = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
354
352
355TEST_RESULT_SKIP = re.compile(r'^(ok|not ok) ([0-9]+) (- )?(.*) # SKIP(.*)$')
353TEST_RESULT_SKIP = re.compile(r'^\s*(ok|not ok) ([0-9]+) (- )?(.*) # SKIP(.*)$')
356
357def peek_test_name_match(lines: LineStream, test: Test) -> bool:
358 """
359 Matches current line with the format of a test result line and checks
360 if the name matches the name of the current test.
361 Returns False if fails to match format or name.
362
363 Accepted format:

--- 142 unchanged lines hidden (view full) ---

506 if test.expected_count == 1:
507 message += '(1 subtest)'
508 else:
509 message += f'({test.expected_count} subtests)'
510 stdout.print_with_timestamp(format_test_divider(message, len(message)))
511
512def print_log(log: Iterable[str]) -> None:
513 """Prints all strings in saved log for test in yellow."""
354
355def peek_test_name_match(lines: LineStream, test: Test) -> bool:
356 """
357 Matches current line with the format of a test result line and checks
358 if the name matches the name of the current test.
359 Returns False if fails to match format or name.
360
361 Accepted format:

--- 142 unchanged lines hidden (view full) ---

504 if test.expected_count == 1:
505 message += '(1 subtest)'
506 else:
507 message += f'({test.expected_count} subtests)'
508 stdout.print_with_timestamp(format_test_divider(message, len(message)))
509
510def print_log(log: Iterable[str]) -> None:
511 """Prints all strings in saved log for test in yellow."""
514 for m in log:
515 stdout.print_with_timestamp(stdout.yellow(m))
512 formatted = textwrap.dedent('\n'.join(log))
513 for line in formatted.splitlines():
514 stdout.print_with_timestamp(stdout.yellow(line))
516
517def format_test_result(test: Test) -> str:
518 """
519 Returns string with formatted test result with colored status and test
520 name.
521
522 Example:
523 '[PASSED] example'

--- 293 unchanged lines hidden ---
515
516def format_test_result(test: Test) -> str:
517 """
518 Returns string with formatted test result with colored status and test
519 name.
520
521 Example:
522 '[PASSED] example'

--- 293 unchanged lines hidden ---