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


Reply via email to