提交 7d7c48df 编写于 作者: D Daniel Latypov 提交者: Shuah Khan

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: NDaniel Latypov <dlatypov@google.com>
Reviewed-by: NDavid Gow <davidgow@google.com>
Reviewed-by: NBrendan Higgins <brendanhiggins@google.com>
Signed-off-by: NShuah Khan <skhan@linuxfoundation.org>
上级 ff9e09a3
...@@ -12,7 +12,8 @@ import subprocess ...@@ -12,7 +12,8 @@ import subprocess
import os import os
import shutil import shutil
import signal import signal
from typing import Iterator, Optional, Tuple import threading
from typing import Iterator, List, Optional, Tuple
import kunit_config import kunit_config
import kunit_parser import kunit_parser
...@@ -99,8 +100,8 @@ class LinuxSourceTreeOperations(object): ...@@ -99,8 +100,8 @@ class LinuxSourceTreeOperations(object):
if stderr: # likely only due to build warnings if stderr: # likely only due to build warnings
print(stderr.decode()) print(stderr.decode())
def run(self, params, timeout, build_dir, outfile) -> None: def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
pass raise RuntimeError('not implemented!')
class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
...@@ -119,7 +120,7 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): ...@@ -119,7 +120,7 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
kconfig.parse_from_string(self._kconfig) kconfig.parse_from_string(self._kconfig)
base_kunitconfig.merge_in_entries(kconfig) base_kunitconfig.merge_in_entries(kconfig)
def run(self, params, timeout, build_dir, outfile): def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
kernel_path = os.path.join(build_dir, self._kernel_path) kernel_path = os.path.join(build_dir, self._kernel_path)
qemu_command = ['qemu-system-' + self._qemu_arch, qemu_command = ['qemu-system-' + self._qemu_arch,
'-nodefaults', '-nodefaults',
...@@ -130,18 +131,11 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): ...@@ -130,18 +131,11 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
'-nographic', '-nographic',
'-serial stdio'] + self._extra_qemu_params '-serial stdio'] + self._extra_qemu_params
print('Running tests with:\n$', ' '.join(qemu_command)) print('Running tests with:\n$', ' '.join(qemu_command))
with open(outfile, 'w') as output: return subprocess.Popen(' '.join(qemu_command),
process = subprocess.Popen(' '.join(qemu_command), stdin=subprocess.PIPE,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stdout=output, stderr=subprocess.STDOUT,
stderr=subprocess.STDOUT, text=True, shell=True)
text=True, shell=True)
try:
process.wait(timeout=timeout)
except Exception as e:
print(e)
process.terminate()
return process
class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations): class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
"""An abstraction over command line operations performed on a source tree.""" """An abstraction over command line operations performed on a source tree."""
...@@ -171,17 +165,14 @@ class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations): ...@@ -171,17 +165,14 @@ class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
kunit_parser.print_with_timestamp( kunit_parser.print_with_timestamp(
'Starting Kernel with all configs takes a few minutes...') 'Starting Kernel with all configs takes a few minutes...')
def run(self, params, timeout, build_dir, outfile): def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
"""Runs the Linux UML binary. Must be named 'linux'.""" """Runs the Linux UML binary. Must be named 'linux'."""
linux_bin = get_file_path(build_dir, 'linux') linux_bin = get_file_path(build_dir, 'linux')
outfile = get_outfile_path(build_dir) return subprocess.Popen([linux_bin] + params,
with open(outfile, 'w') as output: stdin=subprocess.PIPE,
process = subprocess.Popen([linux_bin] + params, stdout=subprocess.PIPE,
stdin=subprocess.PIPE, stderr=subprocess.STDOUT,
stdout=output, text=True)
stderr=subprocess.STDOUT,
text=True)
process.wait(timeout)
def get_kconfig_path(build_dir) -> str: def get_kconfig_path(build_dir) -> str:
return get_file_path(build_dir, KCONFIG_PATH) return get_file_path(build_dir, KCONFIG_PATH)
...@@ -327,12 +318,36 @@ class LinuxSourceTree(object): ...@@ -327,12 +318,36 @@ class LinuxSourceTree(object):
args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt']) args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
if filter_glob: if filter_glob:
args.append('kunit.filter_glob='+filter_glob) args.append('kunit.filter_glob='+filter_glob)
outfile = get_outfile_path(build_dir)
self._ops.run(args, timeout, build_dir, outfile) process = self._ops.start(args, build_dir)
subprocess.call(['stty', 'sane']) assert process.stdout is not None # tell mypy it's set
with open(outfile, 'r') as file:
for line in file: # Enforce the timeout in a background thread.
def _wait_proc():
try:
process.wait(timeout=timeout)
except Exception as e:
print(e)
process.terminate()
process.wait()
waiter = threading.Thread(target=_wait_proc)
waiter.start()
output = open(get_outfile_path(build_dir), 'w')
try:
# Tee the output to the file and to our caller in real time.
for line in process.stdout:
output.write(line)
yield line yield line
# This runs even if our caller doesn't consume every line.
finally:
# Flush any leftover output to the file
output.write(process.stdout.read())
output.close()
process.stdout.close()
waiter.join()
subprocess.call(['stty', 'sane'])
def signal_handler(self, sig, frame) -> None: def signal_handler(self, sig, frame) -> None:
logging.error('Build interruption occurred. Cleaning console.') logging.error('Build interruption occurred. Cleaning console.')
......
...@@ -14,6 +14,7 @@ import tempfile, shutil # Handling test_tmpdir ...@@ -14,6 +14,7 @@ import tempfile, shutil # Handling test_tmpdir
import itertools import itertools
import json import json
import signal import signal
import subprocess
import os import os
import kunit_config import kunit_config
...@@ -293,6 +294,22 @@ class LinuxSourceTreeTest(unittest.TestCase): ...@@ -293,6 +294,22 @@ class LinuxSourceTreeTest(unittest.TestCase):
with self.assertRaisesRegex(kunit_kernel.ConfigError, 'not a valid arch, options are.*x86_64'): with self.assertRaisesRegex(kunit_kernel.ConfigError, 'not a valid arch, options are.*x86_64'):
kunit_kernel.LinuxSourceTree('', arch='invalid') kunit_kernel.LinuxSourceTree('', arch='invalid')
def test_run_kernel_hits_exception(self):
def fake_start(unused_args, unused_build_dir):
return subprocess.Popen(['echo "hi\nbye"'], shell=True, text=True, stdout=subprocess.PIPE)
with tempfile.TemporaryDirectory('') as build_dir:
tree = kunit_kernel.LinuxSourceTree(build_dir, load_config=False)
mock.patch.object(tree._ops, 'start', side_effect=fake_start).start()
with self.assertRaises(ValueError):
for line in tree.run_kernel(build_dir=build_dir):
self.assertEqual(line, 'hi\n')
raise ValueError('uh oh, did not read all output')
with open(kunit_kernel.get_outfile_path(build_dir), 'rt') as outfile:
self.assertEqual(outfile.read(), 'hi\nbye\n', msg='Missing some output')
# TODO: add more test cases. # TODO: add more test cases.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册