kunit: tool: print parsed test results fully incrementally
authorDaniel Latypov <dlatypov@google.com>
Thu, 7 Oct 2021 21:14:17 +0000 (14:14 -0700)
committerShuah Khan <skhan@linuxfoundation.org>
Mon, 13 Dec 2021 20:37:38 +0000 (13:37 -0700)
With the parser rework [1] and run_kernel() rework [2], this allows the
parser to print out test results incrementally.

Currently, that's held up by the fact that the LineStream eagerly
pre-fetches the next line when you call pop().
This blocks parse_test_result() from returning until the line *after*
the "ok 1 - test name" line is also printed.

One can see this with the following example:
$ (echo -e 'TAP version 14\n1..3\nok 1 - fake test'; sleep 2; echo -e 'ok 2 - fake test 2'; sleep 3; echo -e 'ok 3 - fake test 3') | ./tools/testing/kunit/kunit.py parse

Before this patch [1]: there's a pause before 'fake test' is printed.
After this patch: 'fake test' is printed out immediately.

This patch also adds
* a unit test to verify LineStream's behavior directly
* a test case to ensure that it's lazily calling the generator
* an explicit exception for when users go beyond EOF

[1] https://lore.kernel.org/linux-kselftest/20211006170049.106852-1-dlatypov@google.com/
[2] https://lore.kernel.org/linux-kselftest/20211005011340.2826268-1-dlatypov@google.com/

Signed-off-by: Daniel Latypov <dlatypov@google.com>
Reviewed-by: David Gow <davidgow@google.com>
Reviewed-by: Brendan Higgins <brendanhiggins@google.com>
Signed-off-by: Shuah Khan <skhan@linuxfoundation.org>
tools/testing/kunit/kunit_parser.py
tools/testing/kunit/kunit_tool_test.py

index 68c847e8ca589d1febb19c01079b70a280563c7d..8e42b6ef3fe3e71c5739c9435f4127fcb6290586 100644 (file)
@@ -168,42 +168,51 @@ class TestCounts:
 class LineStream:
        """
        A class to represent the lines of kernel output.
-       Provides a peek()/pop() interface over an iterator of
+       Provides a lazy peek()/pop() interface over an iterator of
        (line#, text).
        """
        _lines: Iterator[Tuple[int, str]]
        _next: Tuple[int, str]
+       _need_next: bool
        _done: bool
 
        def __init__(self, lines: Iterator[Tuple[int, str]]):
                """Creates a new LineStream that wraps the given iterator."""
                self._lines = lines
                self._done = False
+               self._need_next = True
                self._next = (0, '')
-               self._get_next()
 
        def _get_next(self) -> None:
-               """Advances the LineSteam to the next line."""
+               """Advances the LineSteam to the next line, if necessary."""
+               if not self._need_next:
+                       return
                try:
                        self._next = next(self._lines)
                except StopIteration:
                        self._done = True
+               finally:
+                       self._need_next = False
 
        def peek(self) -> str:
                """Returns the current line, without advancing the LineStream.
                """
+               self._get_next()
                return self._next[1]
 
        def pop(self) -> str:
                """Returns the current line and advances the LineStream to
                the next line.
                """
-               n = self._next
-               self._get_next()
-               return n[1]
+               s = self.peek()
+               if self._done:
+                       raise ValueError(f'LineStream: going past EOF, last line was {s}')
+               self._need_next = True
+               return s
 
        def __bool__(self) -> bool:
                """Returns True if stream has more lines."""
+               self._get_next()
                return not self._done
 
        # Only used by kunit_tool_test.py.
@@ -216,6 +225,7 @@ class LineStream:
 
        def line_number(self) -> int:
                """Returns the line number of the current line."""
+               self._get_next()
                return self._next[0]
 
 # Parsing helper methods:
index 1b93f1151fcf3c52347bc30bd02bf38b4507225b..2d4d9d2713f986df4b12542cf760c83a7857e828 100755 (executable)
@@ -13,9 +13,10 @@ import tempfile, shutil # Handling test_tmpdir
 
 import itertools
 import json
+import os
 import signal
 import subprocess
-import os
+from typing import Iterable
 
 import kunit_config
 import kunit_parser
@@ -331,6 +332,45 @@ class KUnitParserTest(unittest.TestCase):
                                result.status)
                        self.assertEqual('kunit-resource-test', result.test.subtests[0].name)
 
+def line_stream_from_strs(strs: Iterable[str]) -> kunit_parser.LineStream:
+       return kunit_parser.LineStream(enumerate(strs, start=1))
+
+class LineStreamTest(unittest.TestCase):
+
+       def test_basic(self):
+               stream = line_stream_from_strs(['hello', 'world'])
+
+               self.assertTrue(stream, msg='Should be more input')
+               self.assertEqual(stream.line_number(), 1)
+               self.assertEqual(stream.peek(), 'hello')
+               self.assertEqual(stream.pop(), 'hello')
+
+               self.assertTrue(stream, msg='Should be more input')
+               self.assertEqual(stream.line_number(), 2)
+               self.assertEqual(stream.peek(), 'world')
+               self.assertEqual(stream.pop(), 'world')
+
+               self.assertFalse(stream, msg='Should be no more input')
+               with self.assertRaisesRegex(ValueError, 'LineStream: going past EOF'):
+                       stream.pop()
+
+       def test_is_lazy(self):
+               called_times = 0
+               def generator():
+                       nonlocal called_times
+                       for i in range(1,5):
+                               called_times += 1
+                               yield called_times, str(called_times)
+
+               stream = kunit_parser.LineStream(generator())
+               self.assertEqual(called_times, 0)
+
+               self.assertEqual(stream.pop(), '1')
+               self.assertEqual(called_times, 1)
+
+               self.assertEqual(stream.pop(), '2')
+               self.assertEqual(called_times, 2)
+
 class LinuxSourceTreeTest(unittest.TestCase):
 
        def setUp(self):