kunit_parser.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  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 <[email protected]>
  9. # Author: Brendan Higgins <[email protected]>
  10. # Author: Rae Moar <[email protected]>
  11. from __future__ import annotations
  12. import re
  13. import sys
  14. from enum import Enum, auto
  15. from typing import Iterable, Iterator, List, Optional, Tuple
  16. from kunit_printer import stdout
  17. class Test:
  18. """
  19. A class to represent a test parsed from KTAP results. All KTAP
  20. results within a test log are stored in a main Test object as
  21. subtests.
  22. Attributes:
  23. status : TestStatus - status of the test
  24. name : str - name of the test
  25. expected_count : int - expected number of subtests (0 if single
  26. test case and None if unknown expected number of subtests)
  27. subtests : List[Test] - list of subtests
  28. log : List[str] - log of KTAP lines that correspond to the test
  29. counts : TestCounts - counts of the test statuses and errors of
  30. subtests or of the test itself if the test is a single
  31. test case.
  32. """
  33. def __init__(self) -> None:
  34. """Creates Test object with default attributes."""
  35. self.status = TestStatus.TEST_CRASHED
  36. self.name = ''
  37. self.expected_count = 0 # type: Optional[int]
  38. self.subtests = [] # type: List[Test]
  39. self.log = [] # type: List[str]
  40. self.counts = TestCounts()
  41. def __str__(self) -> str:
  42. """Returns string representation of a Test class object."""
  43. return (f'Test({self.status}, {self.name}, {self.expected_count}, '
  44. f'{self.subtests}, {self.log}, {self.counts})')
  45. def __repr__(self) -> str:
  46. """Returns string representation of a Test class object."""
  47. return str(self)
  48. def add_error(self, error_message: str) -> None:
  49. """Records an error that occurred while parsing this test."""
  50. self.counts.errors += 1
  51. stdout.print_with_timestamp(stdout.red('[ERROR]') + f' Test: {self.name}: {error_message}')
  52. class TestStatus(Enum):
  53. """An enumeration class to represent the status of a test."""
  54. SUCCESS = auto()
  55. FAILURE = auto()
  56. SKIPPED = auto()
  57. TEST_CRASHED = auto()
  58. NO_TESTS = auto()
  59. FAILURE_TO_PARSE_TESTS = auto()
  60. class TestCounts:
  61. """
  62. Tracks the counts of statuses of all test cases and any errors within
  63. a Test.
  64. Attributes:
  65. passed : int - the number of tests that have passed
  66. failed : int - the number of tests that have failed
  67. crashed : int - the number of tests that have crashed
  68. skipped : int - the number of tests that have skipped
  69. errors : int - the number of errors in the test and subtests
  70. """
  71. def __init__(self):
  72. """Creates TestCounts object with counts of all test
  73. statuses and test errors set to 0.
  74. """
  75. self.passed = 0
  76. self.failed = 0
  77. self.crashed = 0
  78. self.skipped = 0
  79. self.errors = 0
  80. def __str__(self) -> str:
  81. """Returns the string representation of a TestCounts object."""
  82. statuses = [('passed', self.passed), ('failed', self.failed),
  83. ('crashed', self.crashed), ('skipped', self.skipped),
  84. ('errors', self.errors)]
  85. return f'Ran {self.total()} tests: ' + \
  86. ', '.join(f'{s}: {n}' for s, n in statuses if n > 0)
  87. def total(self) -> int:
  88. """Returns the total number of test cases within a test
  89. object, where a test case is a test with no subtests.
  90. """
  91. return (self.passed + self.failed + self.crashed +
  92. self.skipped)
  93. def add_subtest_counts(self, counts: TestCounts) -> None:
  94. """
  95. Adds the counts of another TestCounts object to the current
  96. TestCounts object. Used to add the counts of a subtest to the
  97. parent test.
  98. Parameters:
  99. counts - a different TestCounts object whose counts
  100. will be added to the counts of the TestCounts object
  101. """
  102. self.passed += counts.passed
  103. self.failed += counts.failed
  104. self.crashed += counts.crashed
  105. self.skipped += counts.skipped
  106. self.errors += counts.errors
  107. def get_status(self) -> TestStatus:
  108. """Returns the aggregated status of a Test using test
  109. counts.
  110. """
  111. if self.total() == 0:
  112. return TestStatus.NO_TESTS
  113. if self.crashed:
  114. # Crashes should take priority.
  115. return TestStatus.TEST_CRASHED
  116. if self.failed:
  117. return TestStatus.FAILURE
  118. if self.passed:
  119. # No failures or crashes, looks good!
  120. return TestStatus.SUCCESS
  121. # We have only skipped tests.
  122. return TestStatus.SKIPPED
  123. def add_status(self, status: TestStatus) -> None:
  124. """Increments the count for `status`."""
  125. if status == TestStatus.SUCCESS:
  126. self.passed += 1
  127. elif status == TestStatus.FAILURE:
  128. self.failed += 1
  129. elif status == TestStatus.SKIPPED:
  130. self.skipped += 1
  131. elif status != TestStatus.NO_TESTS:
  132. self.crashed += 1
  133. class LineStream:
  134. """
  135. A class to represent the lines of kernel output.
  136. Provides a lazy peek()/pop() interface over an iterator of
  137. (line#, text).
  138. """
  139. _lines: Iterator[Tuple[int, str]]
  140. _next: Tuple[int, str]
  141. _need_next: bool
  142. _done: bool
  143. def __init__(self, lines: Iterator[Tuple[int, str]]):
  144. """Creates a new LineStream that wraps the given iterator."""
  145. self._lines = lines
  146. self._done = False
  147. self._need_next = True
  148. self._next = (0, '')
  149. def _get_next(self) -> None:
  150. """Advances the LineSteam to the next line, if necessary."""
  151. if not self._need_next:
  152. return
  153. try:
  154. self._next = next(self._lines)
  155. except StopIteration:
  156. self._done = True
  157. finally:
  158. self._need_next = False
  159. def peek(self) -> str:
  160. """Returns the current line, without advancing the LineStream.
  161. """
  162. self._get_next()
  163. return self._next[1]
  164. def pop(self) -> str:
  165. """Returns the current line and advances the LineStream to
  166. the next line.
  167. """
  168. s = self.peek()
  169. if self._done:
  170. raise ValueError(f'LineStream: going past EOF, last line was {s}')
  171. self._need_next = True
  172. return s
  173. def __bool__(self) -> bool:
  174. """Returns True if stream has more lines."""
  175. self._get_next()
  176. return not self._done
  177. # Only used by kunit_tool_test.py.
  178. def __iter__(self) -> Iterator[str]:
  179. """Empties all lines stored in LineStream object into
  180. Iterator object and returns the Iterator object.
  181. """
  182. while bool(self):
  183. yield self.pop()
  184. def line_number(self) -> int:
  185. """Returns the line number of the current line."""
  186. self._get_next()
  187. return self._next[0]
  188. # Parsing helper methods:
  189. KTAP_START = re.compile(r'KTAP version ([0-9]+)$')
  190. TAP_START = re.compile(r'TAP version ([0-9]+)$')
  191. KTAP_END = re.compile('(List of all partitions:|'
  192. 'Kernel panic - not syncing: VFS:|reboot: System halted)')
  193. def extract_tap_lines(kernel_output: Iterable[str], lstrip=True) -> LineStream:
  194. """Extracts KTAP lines from the kernel output."""
  195. def isolate_ktap_output(kernel_output: Iterable[str]) \
  196. -> Iterator[Tuple[int, str]]:
  197. line_num = 0
  198. started = False
  199. for line in kernel_output:
  200. line_num += 1
  201. line = line.rstrip() # remove trailing \n
  202. if not started and KTAP_START.search(line):
  203. # start extracting KTAP lines and set prefix
  204. # to number of characters before version line
  205. prefix_len = len(
  206. line.split('KTAP version')[0])
  207. started = True
  208. yield line_num, line[prefix_len:]
  209. elif not started and TAP_START.search(line):
  210. # start extracting KTAP lines and set prefix
  211. # to number of characters before version line
  212. prefix_len = len(line.split('TAP version')[0])
  213. started = True
  214. yield line_num, line[prefix_len:]
  215. elif started and KTAP_END.search(line):
  216. # stop extracting KTAP lines
  217. break
  218. elif started:
  219. # remove the prefix and optionally any leading
  220. # whitespace. Our parsing logic relies on this.
  221. line = line[prefix_len:]
  222. if lstrip:
  223. line = line.lstrip()
  224. yield line_num, line
  225. return LineStream(lines=isolate_ktap_output(kernel_output))
  226. KTAP_VERSIONS = [1]
  227. TAP_VERSIONS = [13, 14]
  228. def check_version(version_num: int, accepted_versions: List[int],
  229. version_type: str, test: Test) -> None:
  230. """
  231. Adds error to test object if version number is too high or too
  232. low.
  233. Parameters:
  234. version_num - The inputted version number from the parsed KTAP or TAP
  235. header line
  236. accepted_version - List of accepted KTAP or TAP versions
  237. version_type - 'KTAP' or 'TAP' depending on the type of
  238. version line.
  239. test - Test object for current test being parsed
  240. """
  241. if version_num < min(accepted_versions):
  242. test.add_error(f'{version_type} version lower than expected!')
  243. elif version_num > max(accepted_versions):
  244. test.add_error(f'{version_type} version higer than expected!')
  245. def parse_ktap_header(lines: LineStream, test: Test) -> bool:
  246. """
  247. Parses KTAP/TAP header line and checks version number.
  248. Returns False if fails to parse KTAP/TAP header line.
  249. Accepted formats:
  250. - 'KTAP version [version number]'
  251. - 'TAP version [version number]'
  252. Parameters:
  253. lines - LineStream of KTAP output to parse
  254. test - Test object for current test being parsed
  255. Return:
  256. True if successfully parsed KTAP/TAP header line
  257. """
  258. ktap_match = KTAP_START.match(lines.peek())
  259. tap_match = TAP_START.match(lines.peek())
  260. if ktap_match:
  261. version_num = int(ktap_match.group(1))
  262. check_version(version_num, KTAP_VERSIONS, 'KTAP', test)
  263. elif tap_match:
  264. version_num = int(tap_match.group(1))
  265. check_version(version_num, TAP_VERSIONS, 'TAP', test)
  266. else:
  267. return False
  268. test.log.append(lines.pop())
  269. return True
  270. TEST_HEADER = re.compile(r'^# Subtest: (.*)$')
  271. def parse_test_header(lines: LineStream, test: Test) -> bool:
  272. """
  273. Parses test header and stores test name in test object.
  274. Returns False if fails to parse test header line.
  275. Accepted format:
  276. - '# Subtest: [test name]'
  277. Parameters:
  278. lines - LineStream of KTAP output to parse
  279. test - Test object for current test being parsed
  280. Return:
  281. True if successfully parsed test header line
  282. """
  283. match = TEST_HEADER.match(lines.peek())
  284. if not match:
  285. return False
  286. test.log.append(lines.pop())
  287. test.name = match.group(1)
  288. return True
  289. TEST_PLAN = re.compile(r'1\.\.([0-9]+)')
  290. def parse_test_plan(lines: LineStream, test: Test) -> bool:
  291. """
  292. Parses test plan line and stores the expected number of subtests in
  293. test object. Reports an error if expected count is 0.
  294. Returns False and sets expected_count to None if there is no valid test
  295. plan.
  296. Accepted format:
  297. - '1..[number of subtests]'
  298. Parameters:
  299. lines - LineStream of KTAP output to parse
  300. test - Test object for current test being parsed
  301. Return:
  302. True if successfully parsed test plan line
  303. """
  304. match = TEST_PLAN.match(lines.peek())
  305. if not match:
  306. test.expected_count = None
  307. return False
  308. test.log.append(lines.pop())
  309. expected_count = int(match.group(1))
  310. test.expected_count = expected_count
  311. return True
  312. TEST_RESULT = re.compile(r'^(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
  313. TEST_RESULT_SKIP = re.compile(r'^(ok|not ok) ([0-9]+) (- )?(.*) # SKIP(.*)$')
  314. def peek_test_name_match(lines: LineStream, test: Test) -> bool:
  315. """
  316. Matches current line with the format of a test result line and checks
  317. if the name matches the name of the current test.
  318. Returns False if fails to match format or name.
  319. Accepted format:
  320. - '[ok|not ok] [test number] [-] [test name] [optional skip
  321. directive]'
  322. Parameters:
  323. lines - LineStream of KTAP output to parse
  324. test - Test object for current test being parsed
  325. Return:
  326. True if matched a test result line and the name matching the
  327. expected test name
  328. """
  329. line = lines.peek()
  330. match = TEST_RESULT.match(line)
  331. if not match:
  332. return False
  333. name = match.group(4)
  334. return name == test.name
  335. def parse_test_result(lines: LineStream, test: Test,
  336. expected_num: int) -> bool:
  337. """
  338. Parses test result line and stores the status and name in the test
  339. object. Reports an error if the test number does not match expected
  340. test number.
  341. Returns False if fails to parse test result line.
  342. Note that the SKIP directive is the only direction that causes a
  343. change in status.
  344. Accepted format:
  345. - '[ok|not ok] [test number] [-] [test name] [optional skip
  346. directive]'
  347. Parameters:
  348. lines - LineStream of KTAP output to parse
  349. test - Test object for current test being parsed
  350. expected_num - expected test number for current test
  351. Return:
  352. True if successfully parsed a test result line.
  353. """
  354. line = lines.peek()
  355. match = TEST_RESULT.match(line)
  356. skip_match = TEST_RESULT_SKIP.match(line)
  357. # Check if line matches test result line format
  358. if not match:
  359. return False
  360. test.log.append(lines.pop())
  361. # Set name of test object
  362. if skip_match:
  363. test.name = skip_match.group(4)
  364. else:
  365. test.name = match.group(4)
  366. # Check test num
  367. num = int(match.group(2))
  368. if num != expected_num:
  369. test.add_error(f'Expected test number {expected_num} but found {num}')
  370. # Set status of test object
  371. status = match.group(1)
  372. if skip_match:
  373. test.status = TestStatus.SKIPPED
  374. elif status == 'ok':
  375. test.status = TestStatus.SUCCESS
  376. else:
  377. test.status = TestStatus.FAILURE
  378. return True
  379. def parse_diagnostic(lines: LineStream) -> List[str]:
  380. """
  381. Parse lines that do not match the format of a test result line or
  382. test header line and returns them in list.
  383. Line formats that are not parsed:
  384. - '# Subtest: [test name]'
  385. - '[ok|not ok] [test number] [-] [test name] [optional skip
  386. directive]'
  387. Parameters:
  388. lines - LineStream of KTAP output to parse
  389. Return:
  390. Log of diagnostic lines
  391. """
  392. log = [] # type: List[str]
  393. while lines and not TEST_RESULT.match(lines.peek()) and not \
  394. TEST_HEADER.match(lines.peek()):
  395. log.append(lines.pop())
  396. return log
  397. # Printing helper methods:
  398. DIVIDER = '=' * 60
  399. def format_test_divider(message: str, len_message: int) -> str:
  400. """
  401. Returns string with message centered in fixed width divider.
  402. Example:
  403. '===================== message example ====================='
  404. Parameters:
  405. message - message to be centered in divider line
  406. len_message - length of the message to be printed such that
  407. any characters of the color codes are not counted
  408. Return:
  409. String containing message centered in fixed width divider
  410. """
  411. default_count = 3 # default number of dashes
  412. len_1 = default_count
  413. len_2 = default_count
  414. difference = len(DIVIDER) - len_message - 2 # 2 spaces added
  415. if difference > 0:
  416. # calculate number of dashes for each side of the divider
  417. len_1 = int(difference / 2)
  418. len_2 = difference - len_1
  419. return ('=' * len_1) + f' {message} ' + ('=' * len_2)
  420. def print_test_header(test: Test) -> None:
  421. """
  422. Prints test header with test name and optionally the expected number
  423. of subtests.
  424. Example:
  425. '=================== example (2 subtests) ==================='
  426. Parameters:
  427. test - Test object representing current test being printed
  428. """
  429. message = test.name
  430. if test.expected_count:
  431. if test.expected_count == 1:
  432. message += ' (1 subtest)'
  433. else:
  434. message += f' ({test.expected_count} subtests)'
  435. stdout.print_with_timestamp(format_test_divider(message, len(message)))
  436. def print_log(log: Iterable[str]) -> None:
  437. """Prints all strings in saved log for test in yellow."""
  438. for m in log:
  439. stdout.print_with_timestamp(stdout.yellow(m))
  440. def format_test_result(test: Test) -> str:
  441. """
  442. Returns string with formatted test result with colored status and test
  443. name.
  444. Example:
  445. '[PASSED] example'
  446. Parameters:
  447. test - Test object representing current test being printed
  448. Return:
  449. String containing formatted test result
  450. """
  451. if test.status == TestStatus.SUCCESS:
  452. return stdout.green('[PASSED] ') + test.name
  453. if test.status == TestStatus.SKIPPED:
  454. return stdout.yellow('[SKIPPED] ') + test.name
  455. if test.status == TestStatus.NO_TESTS:
  456. return stdout.yellow('[NO TESTS RUN] ') + test.name
  457. if test.status == TestStatus.TEST_CRASHED:
  458. print_log(test.log)
  459. return stdout.red('[CRASHED] ') + test.name
  460. print_log(test.log)
  461. return stdout.red('[FAILED] ') + test.name
  462. def print_test_result(test: Test) -> None:
  463. """
  464. Prints result line with status of test.
  465. Example:
  466. '[PASSED] example'
  467. Parameters:
  468. test - Test object representing current test being printed
  469. """
  470. stdout.print_with_timestamp(format_test_result(test))
  471. def print_test_footer(test: Test) -> None:
  472. """
  473. Prints test footer with status of test.
  474. Example:
  475. '===================== [PASSED] example ====================='
  476. Parameters:
  477. test - Test object representing current test being printed
  478. """
  479. message = format_test_result(test)
  480. stdout.print_with_timestamp(format_test_divider(message,
  481. len(message) - stdout.color_len()))
  482. def print_summary_line(test: Test) -> None:
  483. """
  484. Prints summary line of test object. Color of line is dependent on
  485. status of test. Color is green if test passes, yellow if test is
  486. skipped, and red if the test fails or crashes. Summary line contains
  487. counts of the statuses of the tests subtests or the test itself if it
  488. has no subtests.
  489. Example:
  490. "Testing complete. Passed: 2, Failed: 0, Crashed: 0, Skipped: 0,
  491. Errors: 0"
  492. test - Test object representing current test being printed
  493. """
  494. if test.status == TestStatus.SUCCESS:
  495. color = stdout.green
  496. elif test.status in (TestStatus.SKIPPED, TestStatus.NO_TESTS):
  497. color = stdout.yellow
  498. else:
  499. color = stdout.red
  500. stdout.print_with_timestamp(color(f'Testing complete. {test.counts}'))
  501. # Other methods:
  502. def bubble_up_test_results(test: Test) -> None:
  503. """
  504. If the test has subtests, add the test counts of the subtests to the
  505. test and check if any of the tests crashed and if so set the test
  506. status to crashed. Otherwise if the test has no subtests add the
  507. status of the test to the test counts.
  508. Parameters:
  509. test - Test object for current test being parsed
  510. """
  511. subtests = test.subtests
  512. counts = test.counts
  513. status = test.status
  514. for t in subtests:
  515. counts.add_subtest_counts(t.counts)
  516. if counts.total() == 0:
  517. counts.add_status(status)
  518. elif test.counts.get_status() == TestStatus.TEST_CRASHED:
  519. test.status = TestStatus.TEST_CRASHED
  520. def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test:
  521. """
  522. Finds next test to parse in LineStream, creates new Test object,
  523. parses any subtests of the test, populates Test object with all
  524. information (status, name) about the test and the Test objects for
  525. any subtests, and then returns the Test object. The method accepts
  526. three formats of tests:
  527. Accepted test formats:
  528. - Main KTAP/TAP header
  529. Example:
  530. KTAP version 1
  531. 1..4
  532. [subtests]
  533. - Subtest header line
  534. Example:
  535. # Subtest: name
  536. 1..3
  537. [subtests]
  538. ok 1 name
  539. - Test result line
  540. Example:
  541. ok 1 - test
  542. Parameters:
  543. lines - LineStream of KTAP output to parse
  544. expected_num - expected test number for test to be parsed
  545. log - list of strings containing any preceding diagnostic lines
  546. corresponding to the current test
  547. Return:
  548. Test object populated with characteristics and any subtests
  549. """
  550. test = Test()
  551. test.log.extend(log)
  552. parent_test = False
  553. main = parse_ktap_header(lines, test)
  554. if main:
  555. # If KTAP/TAP header is found, attempt to parse
  556. # test plan
  557. test.name = "main"
  558. parse_test_plan(lines, test)
  559. parent_test = True
  560. else:
  561. # If KTAP/TAP header is not found, test must be subtest
  562. # header or test result line so parse attempt to parser
  563. # subtest header
  564. parent_test = parse_test_header(lines, test)
  565. if parent_test:
  566. # If subtest header is found, attempt to parse
  567. # test plan and print header
  568. parse_test_plan(lines, test)
  569. print_test_header(test)
  570. expected_count = test.expected_count
  571. subtests = []
  572. test_num = 1
  573. while parent_test and (expected_count is None or test_num <= expected_count):
  574. # Loop to parse any subtests.
  575. # Break after parsing expected number of tests or
  576. # if expected number of tests is unknown break when test
  577. # result line with matching name to subtest header is found
  578. # or no more lines in stream.
  579. sub_log = parse_diagnostic(lines)
  580. sub_test = Test()
  581. if not lines or (peek_test_name_match(lines, test) and
  582. not main):
  583. if expected_count and test_num <= expected_count:
  584. # If parser reaches end of test before
  585. # parsing expected number of subtests, print
  586. # crashed subtest and record error
  587. test.add_error('missing expected subtest!')
  588. sub_test.log.extend(sub_log)
  589. test.counts.add_status(
  590. TestStatus.TEST_CRASHED)
  591. print_test_result(sub_test)
  592. else:
  593. test.log.extend(sub_log)
  594. break
  595. else:
  596. sub_test = parse_test(lines, test_num, sub_log)
  597. subtests.append(sub_test)
  598. test_num += 1
  599. test.subtests = subtests
  600. if not main:
  601. # If not main test, look for test result line
  602. test.log.extend(parse_diagnostic(lines))
  603. if (parent_test and peek_test_name_match(lines, test)) or \
  604. not parent_test:
  605. parse_test_result(lines, test, expected_num)
  606. else:
  607. test.add_error('missing subtest result line!')
  608. # Check for there being no tests
  609. if parent_test and len(subtests) == 0:
  610. # Don't override a bad status if this test had one reported.
  611. # Assumption: no subtests means CRASHED is from Test.__init__()
  612. if test.status in (TestStatus.TEST_CRASHED, TestStatus.SUCCESS):
  613. test.status = TestStatus.NO_TESTS
  614. test.add_error('0 tests run!')
  615. # Add statuses to TestCounts attribute in Test object
  616. bubble_up_test_results(test)
  617. if parent_test and not main:
  618. # If test has subtests and is not the main test object, print
  619. # footer.
  620. print_test_footer(test)
  621. elif not main:
  622. print_test_result(test)
  623. return test
  624. def parse_run_tests(kernel_output: Iterable[str]) -> Test:
  625. """
  626. Using kernel output, extract KTAP lines, parse the lines for test
  627. results and print condensed test results and summary line.
  628. Parameters:
  629. kernel_output - Iterable object contains lines of kernel output
  630. Return:
  631. Test - the main test object with all subtests.
  632. """
  633. stdout.print_with_timestamp(DIVIDER)
  634. lines = extract_tap_lines(kernel_output)
  635. test = Test()
  636. if not lines:
  637. test.name = '<missing>'
  638. test.add_error('could not find any KTAP output!')
  639. test.status = TestStatus.FAILURE_TO_PARSE_TESTS
  640. else:
  641. test = parse_test(lines, 0, [])
  642. if test.status != TestStatus.NO_TESTS:
  643. test.status = test.counts.get_status()
  644. stdout.print_with_timestamp(DIVIDER)
  645. print_summary_line(test)
  646. return test