run_kernel() always tees the kernel's console output to test.log in the build directory. If more than one kernel were to be run at a time they would all write to the same file, so allow the caller to choose where the log goes with a new optional outfile argument.
Also teach get_outfile_path() to generate per-kernel log names of the form test.<name>.log when given a name, for callers that need to keep the logs of several kernels apart. Characters that are awkward in file names, such as the '*' in a filter glob, are replaced with '_'. Add a unit test covering the new argument. Signed-off-by: Mark Brown <[email protected]> --- tools/testing/kunit/kunit_kernel.py | 28 ++++++++++++++++++++++++---- tools/testing/kunit/kunit_tool_test.py | 17 +++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/tools/testing/kunit/kunit_kernel.py b/tools/testing/kunit/kunit_kernel.py index 58557c47d85f..2641db9eddba 100644 --- a/tools/testing/kunit/kunit_kernel.py +++ b/tools/testing/kunit/kunit_kernel.py @@ -11,6 +11,7 @@ import importlib.util import logging import subprocess import os +import re import shlex import shutil import signal @@ -193,8 +194,19 @@ def get_parsed_kunitconfig(build_dir: str, merged.merge_in_entries(partial) return merged -def get_outfile_path(build_dir: str) -> str: - return os.path.join(build_dir, OUTFILE_PATH) +def get_outfile_path(build_dir: str, name: Optional[str]=None) -> str: + """Returns the path of the kernel console log. + + When several kernels are run each needs its own log, so `name` is + used to tell them apart, giving test.<name>.log. Characters that are + awkward in file names (anything other than letters, digits, '_', '-' + and '.') are replaced with '_'. + """ + if name is None: + return os.path.join(build_dir, OUTFILE_PATH) + base, ext = os.path.splitext(OUTFILE_PATH) + name = re.sub(r'[^\w.-]', '_', name) + return os.path.join(build_dir, f'{base}.{name}{ext}') def _default_qemu_config_path(arch: str) -> str: config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py') @@ -352,7 +364,12 @@ class LinuxSourceTree: return subprocess.call(['stty', 'sane']) - def run_kernel(self, args: Optional[List[str]]=None, build_dir: str='', filter_glob: str='', filter: str='', filter_action: Optional[str]=None, timeout: Optional[int]=None) -> Iterator[str]: + def run_kernel(self, args: Optional[List[str]]=None, build_dir: str='', filter_glob: str='', filter: str='', filter_action: Optional[str]=None, timeout: Optional[int]=None, outfile: Optional[str]=None) -> Iterator[str]: + """Boots the kernel and yields its console output line by line. + + The output is also written to `outfile`, which defaults to + test.log in the build directory. + """ # Copy to avoid mutating the caller-supplied list. exec_tests() reuses # the same args across repeated run_kernel() calls (e.g. --run_isolated), # so appending to the original would accumulate stale flags on each call. @@ -365,6 +382,9 @@ class LinuxSourceTree: args.append('kunit.filter_action=' + filter_action) args.append('kunit.enable=1') + if outfile is None: + outfile = get_outfile_path(build_dir) + self._process = self._ops.start(args, build_dir) assert self._process is not None # tell mypy it's set assert self._process.stdout is not None # tell mypy it's set @@ -382,7 +402,7 @@ class LinuxSourceTree: waiter = threading.Thread(target=_wait_proc) waiter.start() - output = open(get_outfile_path(build_dir), 'w') + output = open(outfile, 'w') try: # Tee the output to the file and to our caller in real time. for line in self._process.stdout: diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py index 17f926ef2820..4e8068dce918 100755 --- a/tools/testing/kunit/kunit_tool_test.py +++ b/tools/testing/kunit/kunit_tool_test.py @@ -522,6 +522,23 @@ class LinuxSourceTreeTest(unittest.TestCase): with open(kunit_kernel.get_outfile_path(build_dir), 'rt') as outfile: self.assertEqual(outfile.read(), 'hi\nbye\n', msg='Missing some output') + def test_run_kernel_outfile(self): + """Verify run_kernel() logs to the given outfile instead of test.log.""" + def fake_start(unused_args, unused_build_dir): + return subprocess.Popen(['printf', 'hi\n'], text=True, stdout=subprocess.PIPE) + + with tempfile.TemporaryDirectory('') as build_dir: + tree = kunit_kernel.LinuxSourceTree(build_dir, kunitconfig_paths=[os.devnull]) + mock.patch.object(tree._ops, 'start', side_effect=fake_start).start() + + # Awkward characters in the name are sanitised. + outfile = kunit_kernel.get_outfile_path(build_dir, 'my suite.test*') + self.assertEqual(outfile, os.path.join(build_dir, 'test.my_suite.test_.log')) + self.assertEqual(list(tree.run_kernel(build_dir=build_dir, outfile=outfile)), ['hi\n']) + with open(outfile, 'rt') as f: + self.assertEqual(f.read(), 'hi\n') + self.assertFalse(os.path.exists(kunit_kernel.get_outfile_path(build_dir))) + def test_run_kernel_args_not_mutated(self): """Verify run_kernel() copies args so callers can reuse them.""" start_calls = [] -- 2.47.3

