Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

kunit: tool: yield output from run_kernel in real time

Currently, `run_kernel()` dumps all the kernel output to a file
(.kunit/test.log) and then opens the file and yields it to callers.
This made it easier to respect the requested timeout, if any.

But it means that we can't yield the results in real time, either to the
parser or to stdout (if --raw_output is set).

This change spins up a background thread to enforce the timeout, which
allows us to yield the kernel output in real time, while also copying it
to the .kunit/test.log file.
It's also careful to ensure that the .kunit/test.log file is complete,
even in the kunit_parser throws an exception/otherwise doesn't consume
every line, see the new `finally` block and unit test.

For example:

$ ./tools/testing/kunit/kunit.py run --arch=x86_64 --raw_output
<configure + build steps>
...
<can now see output from QEMU in real time>

This does not currently have a visible effect when --raw_output is not
passed, as kunit_parser.py currently only outputs everything at the end.
But that could change, and this patch is a necessary step towards
showing parsed test results in real time.

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>

authored by

Daniel Latypov and committed by
Shuah Khan
7d7c48df ff9e09a3

+62 -30
+45 -30
tools/testing/kunit/kunit_kernel.py
··· 12 12 import os 13 13 import shutil 14 14 import signal 15 - from typing import Iterator, Optional, Tuple 15 + import threading 16 + from typing import Iterator, List, Optional, Tuple 16 17 17 18 import kunit_config 18 19 import kunit_parser ··· 100 99 if stderr: # likely only due to build warnings 101 100 print(stderr.decode()) 102 101 103 - def run(self, params, timeout, build_dir, outfile) -> None: 104 - pass 102 + def start(self, params: List[str], build_dir: str) -> subprocess.Popen: 103 + raise RuntimeError('not implemented!') 105 104 106 105 107 106 class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): ··· 120 119 kconfig.parse_from_string(self._kconfig) 121 120 base_kunitconfig.merge_in_entries(kconfig) 122 121 123 - def run(self, params, timeout, build_dir, outfile): 122 + def start(self, params: List[str], build_dir: str) -> subprocess.Popen: 124 123 kernel_path = os.path.join(build_dir, self._kernel_path) 125 124 qemu_command = ['qemu-system-' + self._qemu_arch, 126 125 '-nodefaults', ··· 131 130 '-nographic', 132 131 '-serial stdio'] + self._extra_qemu_params 133 132 print('Running tests with:\n$', ' '.join(qemu_command)) 134 - with open(outfile, 'w') as output: 135 - process = subprocess.Popen(' '.join(qemu_command), 136 - stdin=subprocess.PIPE, 137 - stdout=output, 138 - stderr=subprocess.STDOUT, 139 - text=True, shell=True) 140 - try: 141 - process.wait(timeout=timeout) 142 - except Exception as e: 143 - print(e) 144 - process.terminate() 145 - return process 133 + return subprocess.Popen(' '.join(qemu_command), 134 + stdin=subprocess.PIPE, 135 + stdout=subprocess.PIPE, 136 + stderr=subprocess.STDOUT, 137 + text=True, shell=True) 146 138 147 139 class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations): 148 140 """An abstraction over command line operations performed on a source tree.""" ··· 165 171 kunit_parser.print_with_timestamp( 166 172 'Starting Kernel with all configs takes a few minutes...') 167 173 168 - def run(self, params, timeout, build_dir, outfile): 174 + def start(self, params: List[str], build_dir: str) -> subprocess.Popen: 169 175 """Runs the Linux UML binary. Must be named 'linux'.""" 170 176 linux_bin = get_file_path(build_dir, 'linux') 171 - outfile = get_outfile_path(build_dir) 172 - with open(outfile, 'w') as output: 173 - process = subprocess.Popen([linux_bin] + params, 174 - stdin=subprocess.PIPE, 175 - stdout=output, 176 - stderr=subprocess.STDOUT, 177 - text=True) 178 - process.wait(timeout) 177 + return subprocess.Popen([linux_bin] + params, 178 + stdin=subprocess.PIPE, 179 + stdout=subprocess.PIPE, 180 + stderr=subprocess.STDOUT, 181 + text=True) 179 182 180 183 def get_kconfig_path(build_dir) -> str: 181 184 return get_file_path(build_dir, KCONFIG_PATH) ··· 318 327 args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt']) 319 328 if filter_glob: 320 329 args.append('kunit.filter_glob='+filter_glob) 321 - outfile = get_outfile_path(build_dir) 322 - self._ops.run(args, timeout, build_dir, outfile) 323 - subprocess.call(['stty', 'sane']) 324 - with open(outfile, 'r') as file: 325 - for line in file: 330 + 331 + process = self._ops.start(args, build_dir) 332 + assert process.stdout is not None # tell mypy it's set 333 + 334 + # Enforce the timeout in a background thread. 335 + def _wait_proc(): 336 + try: 337 + process.wait(timeout=timeout) 338 + except Exception as e: 339 + print(e) 340 + process.terminate() 341 + process.wait() 342 + waiter = threading.Thread(target=_wait_proc) 343 + waiter.start() 344 + 345 + output = open(get_outfile_path(build_dir), 'w') 346 + try: 347 + # Tee the output to the file and to our caller in real time. 348 + for line in process.stdout: 349 + output.write(line) 326 350 yield line 351 + # This runs even if our caller doesn't consume every line. 352 + finally: 353 + # Flush any leftover output to the file 354 + output.write(process.stdout.read()) 355 + output.close() 356 + process.stdout.close() 357 + 358 + waiter.join() 359 + subprocess.call(['stty', 'sane']) 327 360 328 361 def signal_handler(self, sig, frame) -> None: 329 362 logging.error('Build interruption occurred. Cleaning console.')
+17
tools/testing/kunit/kunit_tool_test.py
··· 14 14 import itertools 15 15 import json 16 16 import signal 17 + import subprocess 17 18 import os 18 19 19 20 import kunit_config ··· 293 292 def test_invalid_arch(self): 294 293 with self.assertRaisesRegex(kunit_kernel.ConfigError, 'not a valid arch, options are.*x86_64'): 295 294 kunit_kernel.LinuxSourceTree('', arch='invalid') 295 + 296 + def test_run_kernel_hits_exception(self): 297 + def fake_start(unused_args, unused_build_dir): 298 + return subprocess.Popen(['echo "hi\nbye"'], shell=True, text=True, stdout=subprocess.PIPE) 299 + 300 + with tempfile.TemporaryDirectory('') as build_dir: 301 + tree = kunit_kernel.LinuxSourceTree(build_dir, load_config=False) 302 + mock.patch.object(tree._ops, 'start', side_effect=fake_start).start() 303 + 304 + with self.assertRaises(ValueError): 305 + for line in tree.run_kernel(build_dir=build_dir): 306 + self.assertEqual(line, 'hi\n') 307 + raise ValueError('uh oh, did not read all output') 308 + 309 + with open(kunit_kernel.get_outfile_path(build_dir), 'rt') as outfile: 310 + self.assertEqual(outfile.read(), 'hi\nbye\n', msg='Missing some output') 296 311 297 312 # TODO: add more test cases. 298 313