* [PATCH 0/7] kunit: tool: Support parallel test execution
@ 2026-10-13 15:43 Mark Brown
2026-10-13 15:43 ` [PATCH 1/7] kunit: tool: Preserve failures from kernels without test results Mark Brown
` (6 more replies)
0 siblings, 7 replies; 10+ messages in thread
From: Mark Brown @ 2026-10-13 15:43 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Rae Moar, Shuah Khan, Daniel Latypov,
Jonathan Corbet, Randy Dunlap
Cc: linux-kselftest, kunit-dev, linux-kernel, linux-doc, Mark Brown
Currently the test execution part of KUnit is very single threaded,
meaning that on larger systems with many CPUs the wall clock time for
running the full suite is longer than it could be. While we don't want
to run multiple tests in parallel on the same kernel for stability and
clarity reasons all the tests are being run in a virtual environment of
some kind so we can achieve better parallelism by splitting the tests
between multiple simultaneously running virtual environments. The KUnit
runner already has support for splitting tests up and running either
suites or individual tests in isolated guests, build on this to support
parallel execution.
While developing this some existing issues became more visible, some of
these are fixed with some standalone patches at the start of the series
but I split some issues with test naming out, these were sent as:
- lib/tests: util_macros: Drop the '.' from the KUnit suite name
https://lore.kernel.org/r/20261012-kunit-util-macros-name-v1-1-810b401baeb0@kernel.org
- kunit: Add KUNIT_CASE_PARAM_UNIQUE() and KUNIT_CASE_PARAM_WITH_INIT_UNIQUE()
https://lore.kernel.org/r/20261013-kunit-param-names-v1-0-4b22ec5b85b9@kernel.org
Performance results with --alltests, using the elapsed time reported by
the KUnit runner with an already built kernel on a Raspberry Pi 5 with 4
CPUs and a Graviton 4 with 192 CPUs:
default parallel per test parallel per test
Raspberry Pi 5 67.7s 49.4s 836.4s 326.5s
Graviton 4 64.1s 30.8s 445.9s 39.7s
Even with the overhead of having to boot more kernels we are seeing
substantial speedups, even on lower end systems with limited parallelism
available.
Signed-off-by: Mark Brown <broonie@kernel.org>
---
Mark Brown (7):
kunit: tool: Preserve failures from kernels without test results
kunit: tool: Combine reports from isolated kernels
kunit: tool: Boot with a larger log buffer when listing tests
kunit: tool: Add an outfile argument to run_kernel()
kunit: tool: Allow several run_kernel() calls to be in flight at once
kunit: tool: Serialise output written from multiple threads
kunit: tool: Add --parallel to run suites in concurrent kernels
Documentation/dev-tools/kunit/run_wrapper.rst | 11 ++
tools/testing/kunit/kunit.py | 151 +++++++++++----
tools/testing/kunit/kunit_junit.py | 3 +-
tools/testing/kunit/kunit_kernel.py | 78 +++++---
tools/testing/kunit/kunit_printer.py | 9 +-
tools/testing/kunit/kunit_tool_test.py | 253 +++++++++++++++++++++++++-
6 files changed, 438 insertions(+), 67 deletions(-)
---
base-commit: 24a19b953e2a3d0ea5a8c4951a0ab8f32720c907
base-tree: git://git.kernel.org/pub/scm/linux/kernel/git/rafael/linux-pm master
change-id: 20261012-kunit-parallel-a8a7e3c19a30
Best regards,
--
Mark Brown <broonie@kernel.org>
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 1/7] kunit: tool: Preserve failures from kernels without test results
2026-10-13 15:43 [PATCH 0/7] kunit: tool: Support parallel test execution Mark Brown
@ 2026-10-13 15:43 ` Mark Brown
2026-10-13 15:43 ` [PATCH 2/7] kunit: tool: Combine reports from isolated kernels Mark Brown
` (5 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Mark Brown @ 2026-10-13 15:43 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Rae Moar, Shuah Khan, Daniel Latypov,
Jonathan Corbet, Randy Dunlap
Cc: linux-kselftest, kunit-dev, linux-kernel, linux-doc, Mark Brown
When an isolated kernel produces no test results, the parser
reports failure without adding failed test counts. Aggregating only the
counts therefore lets a passing kernel mask the missing results and makes
the command exit successfully. A kernel reporting an empty test plan has
the same problem.
Preserve each kernel's failure status alongside the aggregate counts.
Keep treating skipped tests as successful and retain the failure when no
kernels run at all. Add regression coverage for missing and empty
results in either execution order and for passing and skipped kernels.
Fixes: d65d07cb5b01 ("kunit: tool: improve compatibility of kunit_parser with KTAP specification")
Signed-off-by: Mark Brown <broonie@kernel.org>
---
tools/testing/kunit/kunit.py | 6 ++++++
tools/testing/kunit/kunit_tool_test.py | 31 +++++++++++++++++++++++++++++++
2 files changed, 37 insertions(+)
diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index 91d234ac3b57..218ae294ad1b 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -191,6 +191,7 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -
metadata = kunit_json.Metadata(arch=linux.arch(), build_dir=request.build_dir, def_config='kunit_defconfig')
test_counts = kunit_parser.TestCounts()
+ test_failed = False
exec_time = 0.0
for i, filter_glob in enumerate(filter_globs):
stdout.print_with_timestamp('Starting KUnit Kernel ({}/{})...'.format(i+1, len(filter_globs)))
@@ -212,6 +213,9 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -
exec_time += test_end - test_start
test_counts.add_subtest_counts(test_result.counts)
+ # A kernel with no results has no failed test counts to aggregate.
+ if _map_to_overall_status(test_result.status) != KunitStatus.SUCCESS:
+ test_failed = True
if len(filter_globs) == 1 and test_counts.crashed > 0:
bd = request.build_dir
@@ -220,6 +224,8 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -
bd, bd, kunit_kernel.get_outfile_path(bd), bd, sys.argv[0]))
kunit_status = _map_to_overall_status(test_counts.get_status())
+ if test_failed:
+ kunit_status = KunitStatus.TEST_FAILURE
return KunitResult(status=kunit_status, elapsed_time=exec_time)
def _map_to_overall_status(test_status: kunit_parser.TestStatus) -> KunitStatus:
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index 85ae21754bdf..ca2dac6651aa 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -1018,6 +1018,37 @@ class KUnitMainTest(unittest.TestCase):
mock.call(args=None, build_dir='.kunit', filter_glob='suite2.test1', filter='', filter_action=None, timeout=300),
])
+ @mock.patch.object(kunit, '_list_tests')
+ def test_isolated_kernel_without_results_fails(self, mock_tests):
+ mock_tests.return_value = ['suite1.test1', 'suite2.test1']
+ for missing_output in ([], ['KTAP version 1', '1..0']):
+ for missing_suite in ('suite1', 'suite2'):
+ with self.subTest(output=missing_output,
+ suite=missing_suite):
+ def fake_run_kernel(filter_glob, **kwargs):
+ if filter_glob == missing_suite:
+ return missing_output
+ return ['KTAP version 1', '1..1', 'ok 1 - test1']
+ self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
+
+ with self.assertRaises(SystemExit) as e:
+ kunit.main(['exec', '--run_isolated=suite'])
+ self.assertEqual(e.exception.code, 1)
+
+ @mock.patch.object(kunit, '_list_tests')
+ def test_isolated_skipped_kernels_succeed(self, mock_tests):
+ mock_tests.return_value = ['suite1.test1', 'suite2.test1']
+ for all_skipped in (False, True):
+ with self.subTest(all_skipped=all_skipped):
+ def fake_run_kernel(filter_glob, **kwargs):
+ result = 'ok 1 - test1'
+ if all_skipped or filter_glob == 'suite1':
+ result += ' # SKIP unavailable'
+ return ['KTAP version 1', '1..1', result]
+ self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
+
+ kunit.main(['exec', '--run_isolated=suite'])
+
@mock.patch.object(kunit, '_list_tests')
@mock.patch.object(sys, 'stdout', new_callable=io.StringIO)
def test_list_suites(self, mock_stdout, mock_tests):
--
2.47.3
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 2/7] kunit: tool: Combine reports from isolated kernels
2026-10-13 15:43 [PATCH 0/7] kunit: tool: Support parallel test execution Mark Brown
2026-10-13 15:43 ` [PATCH 1/7] kunit: tool: Preserve failures from kernels without test results Mark Brown
@ 2026-10-13 15:43 ` Mark Brown
2026-10-13 15:43 ` [PATCH 3/7] kunit: tool: Boot with a larger log buffer when listing tests Mark Brown
` (4 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Mark Brown @ 2026-10-13 15:43 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Rae Moar, Shuah Khan, Daniel Latypov,
Jonathan Corbet, Randy Dunlap
Cc: linux-kselftest, kunit-dev, linux-kernel, linux-doc, Mark Brown
With --run_isolated, parse_tests() writes the same JSON or JUnit
destination after every kernel. Only the last kernel survives in a file,
so an earlier failing suite can disappear from the saved results.
Stdout similarly receives several independent report documents.
Separate report writing from parsing. Keep printing each kernel's results
as they are parsed, accumulate the top-level tests and counts, and write
one report after execution finishes. Preserve the parsed root for a
single kernel and continue suppressing reports with --raw_output.
Test JSON and JUnit files with an early failure and suite and test
isolation. Also check equivalence between single-kernel execution and
parsing.
Fixes: ff9e09a3762f ("kunit: tool: support running each suite/test separately")
Signed-off-by: Mark Brown <broonie@kernel.org>
---
tools/testing/kunit/kunit.py | 35 +++++++++++++++----
tools/testing/kunit/kunit_tool_test.py | 61 ++++++++++++++++++++++++++++++++++
2 files changed, 89 insertions(+), 7 deletions(-)
diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index 218ae294ad1b..aea67b6a72c4 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -16,7 +16,7 @@ import time
assert sys.version_info >= (3, 7), "Python version is too old"
-from dataclasses import dataclass
+from dataclasses import dataclass, replace
from enum import Enum, auto
from typing import Iterable, List, Optional, Sequence, Tuple
@@ -191,6 +191,10 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -
metadata = kunit_json.Metadata(arch=linux.arch(), build_dir=request.build_dir, def_config='kunit_defconfig')
test_counts = kunit_parser.TestCounts()
+ report = kunit_parser.Test()
+ report.name = 'main'
+ report.counts = test_counts
+ parse_request = replace(request, json=None, junit=None)
test_failed = False
exec_time = 0.0
for i, filter_glob in enumerate(filter_globs):
@@ -205,7 +209,7 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -
filter_action=request.filter_action,
build_dir=request.build_dir)
- _, test_result = parse_tests(request, metadata, run_result)
+ _, test_result = parse_tests(parse_request, metadata, run_result)
# run_kernel() doesn't block on the kernel exiting.
# That only happens after we get the last line of output from `run_result`.
# So exec_time here actually contains parsing + execution time, which is fine.
@@ -216,6 +220,19 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -
# A kernel with no results has no failed test counts to aggregate.
if _map_to_overall_status(test_result.status) != KunitStatus.SUCCESS:
test_failed = True
+ if request.json or request.junit:
+ if len(filter_globs) == 1:
+ report = test_result
+ else:
+ report.subtests.extend(test_result.subtests)
+ report.log.extend(test_result.log)
+ if len(filter_globs) != 1:
+ report.expected_count = len(report.subtests)
+ report.status = test_counts.get_status()
+ if test_failed and report.ok_status():
+ report.status = kunit_parser.TestStatus.FAILURE_TO_PARSE_TESTS
+ if not request.raw_output:
+ _write_test_results(request, metadata, report)
if len(filter_globs) == 1 and test_counts.crashed > 0:
bd = request.build_dir
@@ -264,6 +281,15 @@ def parse_tests(request: KunitParseRequest, metadata: kunit_json.Metadata, input
kunit_parser.print_test(test, request.failed, stdout)
kunit_parser.print_summary_line(test, stdout)
+ _write_test_results(request, metadata, test)
+
+ if test.status != kunit_parser.TestStatus.SUCCESS:
+ return KunitResult(KunitStatus.TEST_FAILURE, parse_time), test
+
+ return KunitResult(KunitStatus.SUCCESS, parse_time), test
+
+def _write_test_results(request: KunitParseRequest, metadata: kunit_json.Metadata,
+ test: kunit_parser.Test) -> None:
if request.json:
json_str = kunit_json.get_json_result(
test=test,
@@ -283,11 +309,6 @@ def parse_tests(request: KunitParseRequest, metadata: kunit_json.Metadata, input
kunit_junit.write_junit_result(test=test,filename=request.junit)
stdout.print_with_timestamp(f"Test results stored in {os.path.abspath(request.junit)}")
- if test.status != kunit_parser.TestStatus.SUCCESS:
- return KunitResult(KunitStatus.TEST_FAILURE, parse_time), test
-
- return KunitResult(KunitStatus.SUCCESS, parse_time), test
-
def run_tests(linux: kunit_kernel.LinuxSourceTree,
request: KunitRequest) -> KunitResult:
run_start = time.time()
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index ca2dac6651aa..4590108e4306 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -19,6 +19,7 @@ import signal
import subprocess
import sys
from typing import Iterable
+import xml.etree.ElementTree as ET
import kunit_config
import kunit_parser
@@ -738,6 +739,7 @@ class KUnitMainTest(unittest.TestCase):
self.mock_linux_init = mock.patch.object(kunit_kernel, 'LinuxSourceTree').start()
self.linux_source_mock = self.mock_linux_init.return_value
+ self.linux_source_mock.arch.return_value = 'um'
self.linux_source_mock.build_reconfig.return_value = True
self.linux_source_mock.build_kernel.return_value = True
self.linux_source_mock.run_kernel.return_value = all_passed_log
@@ -1049,6 +1051,65 @@ class KUnitMainTest(unittest.TestCase):
kunit.main(['exec', '--run_isolated=suite'])
+ @mock.patch.object(kunit, '_list_tests')
+ def test_isolated_reports_include_all_kernels(self, mock_tests):
+ def fake_run_kernel(filter_glob, **kwargs):
+ if '.' in filter_glob:
+ suite, case = filter_glob.split('.')
+ failed = case == 'test1'
+ else:
+ suite, case = filter_glob, 'test'
+ failed = suite == 'suite1'
+ status = 'not ok' if failed else 'ok'
+ return ['KTAP version 1', '1..1', ' # Subtest: ' + suite,
+ ' 1..1', f' {status} 1 - {case}', f'{status} 1 - {suite}']
+ self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
+
+ for isolation in ('suite', 'test'):
+ with self.subTest(isolation=isolation), \
+ tempfile.TemporaryDirectory() as output_dir:
+ if isolation == 'suite':
+ mock_tests.return_value = ['suite1.test', 'suite2.test']
+ expected = [('suite1', 'test', 'FAIL'), ('suite2', 'test', 'PASS')]
+ else:
+ mock_tests.return_value = ['suite.test1', 'suite.test2']
+ expected = [('suite', 'test1', 'FAIL'), ('suite', 'test2', 'PASS')]
+ json_path = os.path.join(output_dir, 'results.json')
+ xml_path = os.path.join(output_dir, 'results.xml')
+ with self.assertRaises(SystemExit) as e:
+ kunit.main(['exec', '--run_isolated=' + isolation,
+ '--json=' + json_path, '--junit=' + xml_path])
+ self.assertEqual(e.exception.code, 1)
+
+ with open(json_path) as f:
+ report = json.load(f)
+ self.assertEqual(report['misc']['tests'], 2)
+ self.assertEqual(report['misc']['failed'], 1)
+ self.assertEqual(report['arch'], 'um')
+ self.assertEqual([(suite['name'], case['name'], case['status'])
+ for suite in report['sub_groups'] for case in suite['test_cases']], expected)
+ root = ET.parse(xml_path).getroot()
+ self.assertEqual(root.get('tests'), '2')
+ self.assertEqual(root.get('failures'), '1')
+ self.assertEqual([(suite.get('name'), case.get('name'),
+ 'FAIL' if case.find('failure') is not None else 'PASS')
+ for suite in root for case in suite], expected)
+
+ def test_single_kernel_reports_match_parse(self):
+ with tempfile.TemporaryDirectory() as output_dir:
+ json_path = os.path.join(output_dir, 'results.json')
+ xml_path = os.path.join(output_dir, 'results.xml')
+ reports = []
+ for args in (['exec'], ['parse', _test_data_path('test_is_test_passed-all_passed.log')]):
+ kunit.main(args + ['--json=' + json_path, '--junit=' + xml_path])
+ with open(json_path) as f:
+ report = json.load(f)
+ # Execution adds build metadata, which parse cannot know.
+ reports.append((report['misc'],
+ [(s['name'], s['test_cases']) for s in report['sub_groups']],
+ ET.tostring(ET.parse(xml_path).getroot())))
+ self.assertEqual(reports[0], reports[1])
+
@mock.patch.object(kunit, '_list_tests')
@mock.patch.object(sys, 'stdout', new_callable=io.StringIO)
def test_list_suites(self, mock_stdout, mock_tests):
--
2.47.3
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 3/7] kunit: tool: Boot with a larger log buffer when listing tests
2026-10-13 15:43 [PATCH 0/7] kunit: tool: Support parallel test execution Mark Brown
2026-10-13 15:43 ` [PATCH 1/7] kunit: tool: Preserve failures from kernels without test results Mark Brown
2026-10-13 15:43 ` [PATCH 2/7] kunit: tool: Combine reports from isolated kernels Mark Brown
@ 2026-10-13 15:43 ` Mark Brown
2026-10-13 15:43 ` [PATCH 4/7] kunit: tool: Add an outfile argument to run_kernel() Mark Brown
` (3 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Mark Brown @ 2026-10-13 15:43 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Rae Moar, Shuah Khan, Daniel Latypov,
Jonathan Corbet, Randy Dunlap
Cc: linux-kselftest, kunit-dev, linux-kernel, linux-doc, Mark Brown
kunit.action=list prints the name of every suite and test in one burst
during boot, which on a large configuration such as --alltests comes to
several thousand lines. That is more than the default 128KiB printk
ring buffer (CONFIG_LOG_BUF_SHIFT=17, overridden by many defconfigs) can
hold, and since the console drains more slowly than the executor prints
the buffer wraps and lines can easily be dropped.
If the KTAP header is dropped _list_tests() fails with:
ValueError: LineStream: going past EOF, last line was
but otherwise the loss is silent, --list_tests prints an incomplete list
and --run_isolated=test will execute a timing dependent sent of tests.
Pass log_buf_len=4M on the kernel command line when booting to list
tests so that the whole list fits in the buffer, at least until we get
substantially more tests than we currently have.
Signed-off-by: Mark Brown <broonie@kernel.org>
---
tools/testing/kunit/kunit.py | 11 +++++++++--
tools/testing/kunit/kunit_tool_test.py | 2 +-
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index aea67b6a72c4..971e70b0fb1a 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -109,8 +109,15 @@ def config_and_build_tests(linux: kunit_kernel.LinuxSourceTree,
return build_tests(linux, request)
+# kunit.action=list prints every suite and test name in one burst at boot,
+# which can be thousands of lines on a large config. That easily overflows
+# the default 128KiB printk buffer before the console has drained it, so
+# lines (sometimes including the KTAP header) are dropped and the list
+# comes out incomplete. Give the kernel a large enough buffer to hold it.
+_LIST_KERNEL_ARGS = ['log_buf_len=4M']
+
def _list_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> List[str]:
- args = ['kunit.action=list']
+ args = ['kunit.action=list'] + _LIST_KERNEL_ARGS
if request.kernel_args:
args.extend(request.kernel_args)
@@ -129,7 +136,7 @@ def _list_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest)
return [l for l in lines if re.match(r'^[^\s.]+\.[^\s.]+$', l)]
def _list_tests_attr(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> Iterable[str]:
- args = ['kunit.action=list_attr']
+ args = ['kunit.action=list_attr'] + _LIST_KERNEL_ARGS
if request.kernel_args:
args.extend(request.kernel_args)
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index 4590108e4306..17f926ef2820 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -979,7 +979,7 @@ class KUnitMainTest(unittest.TestCase):
self.assertEqual(got, want)
# Should respect the user's filter glob when listing tests.
self.linux_source_mock.run_kernel.assert_called_once_with(
- args=['kunit.action=list'], build_dir='.kunit', filter_glob='suite*', filter='', filter_action=None, timeout=300)
+ args=['kunit.action=list', 'log_buf_len=4M'], build_dir='.kunit', filter_glob='suite*', filter='', filter_action=None, timeout=300)
def test_list_tests_with_prefix(self):
want = ['suite.test1', 'suite.test2', 'suite2.test1']
--
2.47.3
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 4/7] kunit: tool: Add an outfile argument to run_kernel()
2026-10-13 15:43 [PATCH 0/7] kunit: tool: Support parallel test execution Mark Brown
` (2 preceding siblings ...)
2026-10-13 15:43 ` [PATCH 3/7] kunit: tool: Boot with a larger log buffer when listing tests Mark Brown
@ 2026-10-13 15:43 ` Mark Brown
2026-10-13 15:43 ` [PATCH 5/7] kunit: tool: Allow several run_kernel() calls to be in flight at once Mark Brown
` (2 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Mark Brown @ 2026-10-13 15:43 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Rae Moar, Shuah Khan, Daniel Latypov,
Jonathan Corbet, Randy Dunlap
Cc: linux-kselftest, kunit-dev, linux-kernel, linux-doc, Mark Brown
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 <broonie@kernel.org>
---
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
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 5/7] kunit: tool: Allow several run_kernel() calls to be in flight at once
2026-10-13 15:43 [PATCH 0/7] kunit: tool: Support parallel test execution Mark Brown
` (3 preceding siblings ...)
2026-10-13 15:43 ` [PATCH 4/7] kunit: tool: Add an outfile argument to run_kernel() Mark Brown
@ 2026-10-13 15:43 ` Mark Brown
2026-10-13 15:43 ` [PATCH 6/7] kunit: tool: Serialise output written from multiple threads Mark Brown
2026-10-13 15:43 ` [PATCH 7/7] kunit: tool: Add --parallel to run suites in concurrent kernels Mark Brown
6 siblings, 0 replies; 10+ messages in thread
From: Mark Brown @ 2026-10-13 15:43 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Rae Moar, Shuah Khan, Daniel Latypov,
Jonathan Corbet, Randy Dunlap
Cc: linux-kselftest, kunit-dev, linux-kernel, linux-doc, Mark Brown
run_kernel() records the kernel it starts in a single self._process
attribute, which the timeout thread, the cleanup at the end of the
output and the SIGINT handler all read back. That only works while one
kernel is running at a time, which rules out running suites in
parallel guests from multiple threads.
Track running kernels in a list protected by a lock instead, with each
run_kernel() invocation only adding and removing its own entry. The
timeout thread and the cleanup code work on a local reference to their
own process rather than going via self, so one kernel timing out can no
longer terminate a different one. The SIGINT handler terminates every
kernel that is still running.
No functional change when a single kernel is run at a time.
Signed-off-by: Mark Brown <broonie@kernel.org>
---
tools/testing/kunit/kunit_kernel.py | 45 ++++++++++++++++++++--------------
tools/testing/kunit/kunit_tool_test.py | 27 ++++++++++++++++++++
2 files changed, 53 insertions(+), 19 deletions(-)
diff --git a/tools/testing/kunit/kunit_kernel.py b/tools/testing/kunit/kunit_kernel.py
index 2641db9eddba..1d3226f5a136 100644
--- a/tools/testing/kunit/kunit_kernel.py
+++ b/tools/testing/kunit/kunit_kernel.py
@@ -277,7 +277,11 @@ class LinuxSourceTree:
if kconfig_add:
kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
self._kconfig.merge_in_entries(kconfig)
- self._process : Optional[subprocess.Popen[Any]] = None
+ # Kernels currently running. There can be several when
+ # run_kernel() is called concurrently from multiple threads.
+ self._processes : List[subprocess.Popen[Any]] = []
+ # SIGINT can interrupt the main thread while it holds this lock.
+ self._processes_lock = threading.RLock()
def arch(self) -> str:
return self._arch
@@ -368,7 +372,9 @@ class LinuxSourceTree:
"""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.
+ test.log in the build directory. This may be called from
+ several threads at once to run kernels concurrently, in which
+ case each caller must supply a distinct `outfile`.
"""
# Copy to avoid mutating the caller-supplied list. exec_tests() reuses
# the same args across repeated run_kernel() calls (e.g. --run_isolated),
@@ -385,37 +391,36 @@ class LinuxSourceTree:
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
+ process = self._ops.start(args, build_dir)
+ assert process is not None # tell mypy it's set
+ assert process.stdout is not None # tell mypy it's set
+ with self._processes_lock:
+ self._processes.append(process)
# Enforce the timeout in a background thread.
def _wait_proc() -> None:
try:
- if self._process:
- self._process.wait(timeout=timeout)
+ process.wait(timeout=timeout)
except Exception as e:
print(e)
- if self._process:
- self._process.terminate()
- self._process.wait()
+ process.terminate()
+ process.wait()
waiter = threading.Thread(target=_wait_proc)
waiter.start()
output = open(outfile, 'w')
try:
# Tee the output to the file and to our caller in real time.
- for line in self._process.stdout:
+ for line in process.stdout:
output.write(line)
yield line
# This runs even if our caller doesn't consume every line.
finally:
# Flush any leftover output to the file
- if self._process:
- if self._process.stdout:
- output.write(self._process.stdout.read())
- self._process.stdout.close()
- self._process = None
+ output.write(process.stdout.read())
+ process.stdout.close()
+ with self._processes_lock:
+ self._processes.remove(process)
output.close()
waiter.join()
@@ -423,7 +428,9 @@ class LinuxSourceTree:
def signal_handler(self, unused_sig: int, unused_frame: Optional[FrameType]) -> None:
logging.error('Build interruption occurred. Cleaning console.')
- if self._process:
- self._process.terminate()
- self._process.wait()
+ with self._processes_lock:
+ processes = list(self._processes)
+ for process in processes:
+ process.terminate()
+ process.wait()
self._restore_terminal_if_tty()
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index 4e8068dce918..c4999273e491 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -18,6 +18,7 @@ import os
import signal
import subprocess
import sys
+import textwrap
from typing import Iterable
import xml.etree.ElementTree as ET
@@ -538,6 +539,8 @@ class LinuxSourceTreeTest(unittest.TestCase):
with open(outfile, 'rt') as f:
self.assertEqual(f.read(), 'hi\n')
self.assertFalse(os.path.exists(kunit_kernel.get_outfile_path(build_dir)))
+ # The kernel has exited so should no longer be tracked.
+ self.assertEqual(tree._processes, [])
def test_run_kernel_args_not_mutated(self):
"""Verify run_kernel() copies args so callers can reuse them."""
@@ -607,6 +610,30 @@ class LinuxSourceTreeTest(unittest.TestCase):
mock_error.assert_called_once()
mock_call.assert_called_once_with(['stty', 'sane'])
+ def test_signal_handler_during_process_update(self):
+ # Run in a subprocess so a deadlock fails with a timeout rather
+ # than hanging the test runner. Deliver a real signal while the
+ # main thread holds the lock used to update the process list.
+ code = textwrap.dedent('''\
+ import os
+ import signal
+ from unittest import mock
+ import kunit_kernel
+
+ tree = kunit_kernel.LinuxSourceTree('', kunitconfig_paths=[os.devnull])
+ process = mock.Mock()
+ with tree._processes_lock:
+ tree._processes.append(process)
+ os.kill(os.getpid(), signal.SIGINT)
+ process.terminate.assert_called_once_with()
+ process.wait.assert_called_once_with()
+ ''')
+ result = subprocess.run([sys.executable, '-c', code],
+ cwd=os.path.dirname(os.path.abspath(__file__)),
+ stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, text=True, timeout=5)
+ self.assertEqual(result.returncode, 0, msg=result.stdout)
+
def test_build_reconfig_no_config(self):
with tempfile.TemporaryDirectory('') as build_dir:
with open(kunit_kernel.get_kunitconfig_path(build_dir), 'w') as f:
--
2.47.3
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 6/7] kunit: tool: Serialise output written from multiple threads
2026-10-13 15:43 [PATCH 0/7] kunit: tool: Support parallel test execution Mark Brown
` (4 preceding siblings ...)
2026-10-13 15:43 ` [PATCH 5/7] kunit: tool: Allow several run_kernel() calls to be in flight at once Mark Brown
@ 2026-10-13 15:43 ` Mark Brown
2026-10-13 19:53 ` Daniel Latypov
2026-10-13 15:43 ` [PATCH 7/7] kunit: tool: Add --parallel to run suites in concurrent kernels Mark Brown
6 siblings, 1 reply; 10+ messages in thread
From: Mark Brown @ 2026-10-13 15:43 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Rae Moar, Shuah Khan, Daniel Latypov,
Jonathan Corbet, Randy Dunlap
Cc: linux-kselftest, kunit-dev, linux-kernel, linux-doc, Mark Brown
Python's text streams are not thread safe. If two threads write to
sys.stdout at the same time the stream's internal buffer can be
corrupted, resulting in lost output and runs of NUL bytes or heap
garbage being written in its place. This isn't an issue currently, but
will be when we implement support for parallel test execution.
Add a lock to kunit_printer which Printer.print() takes around each
write, and route the other prints that can run concurrently with the
test results through the printer: the kernel start banners and timeout
message in kunit_kernel, and the raw, JSON and JUnit output to stdout.
Prints that only happen before any kernel is started, such as during
configuration and build, are left alone.
No change in output when a single kernel is run at a time.
Signed-off-by: Mark Brown <broonie@kernel.org>
---
tools/testing/kunit/kunit.py | 4 ++--
tools/testing/kunit/kunit_junit.py | 3 ++-
tools/testing/kunit/kunit_kernel.py | 7 ++++---
tools/testing/kunit/kunit_printer.py | 9 ++++++++-
4 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index 971e70b0fb1a..0132687cdd43 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -272,7 +272,7 @@ def parse_tests(request: KunitParseRequest, metadata: kunit_json.Metadata, input
elif request.raw_output == 'kunit':
output = kunit_parser.extract_tap_lines(output)
for line in output:
- print(line.rstrip())
+ stdout.print(line.rstrip())
parse_time = time.time() - parse_start
return KunitResult(KunitStatus.SUCCESS, parse_time), fake_test
@@ -302,7 +302,7 @@ def _write_test_results(request: KunitParseRequest, metadata: kunit_json.Metadat
test=test,
metadata=metadata)
if request.json == 'stdout':
- print(json_str)
+ stdout.print(json_str)
else:
with open(request.json, 'w') as f:
f.write(json_str)
diff --git a/tools/testing/kunit/kunit_junit.py b/tools/testing/kunit/kunit_junit.py
index 3622070358e7..a15c6061003f 100644
--- a/tools/testing/kunit/kunit_junit.py
+++ b/tools/testing/kunit/kunit_junit.py
@@ -6,6 +6,7 @@
from xml.sax.saxutils import quoteattr, XMLGenerator
import xml.etree.ElementTree as ET
+import kunit_printer
from kunit_parser import Test, TestStatus
from typing import Optional
@@ -51,7 +52,7 @@ def get_junit_result(test: Test) -> str:
def print_junit_result(test: Test) -> None:
root_element = get_test_suite(test, None)
ET.indent(root_element)
- ET.dump(root_element)
+ kunit_printer.stdout.print(ET.tostring(root_element, encoding='unicode'))
# Write an entire XML file for the test structure starting at test
def write_junit_result(test: Test, filename: str) -> None:
diff --git a/tools/testing/kunit/kunit_kernel.py b/tools/testing/kunit/kunit_kernel.py
index 1d3226f5a136..508bd62eaa40 100644
--- a/tools/testing/kunit/kunit_kernel.py
+++ b/tools/testing/kunit/kunit_kernel.py
@@ -21,6 +21,7 @@ from typing import Iterator, List, Optional, Tuple, Any
from types import FrameType
import kunit_config
+import kunit_printer
import qemu_config
KCONFIG_PATH = '.config'
@@ -132,7 +133,7 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
'-accel', 'tcg',
'-serial', self._serial] + self._extra_qemu_params
# Note: shlex.join() does what we want, but requires python 3.8+.
- print('Running tests with:\n$', ' '.join(shlex.quote(arg) for arg in qemu_command))
+ kunit_printer.stdout.print('Running tests with:\n$ ' + ' '.join(shlex.quote(arg) for arg in qemu_command))
return subprocess.Popen(qemu_command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
@@ -154,7 +155,7 @@ class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
"""Runs the Linux UML binary. Must be named 'linux'."""
linux_bin = os.path.join(build_dir, 'linux')
params.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
- print('Running tests with:\n$', linux_bin, ' '.join(shlex.quote(arg) for arg in params))
+ kunit_printer.stdout.print('Running tests with:\n$ ' + linux_bin + ' ' + ' '.join(shlex.quote(arg) for arg in params))
return subprocess.Popen([linux_bin] + params,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
@@ -402,7 +403,7 @@ class LinuxSourceTree:
try:
process.wait(timeout=timeout)
except Exception as e:
- print(e)
+ kunit_printer.stdout.print(str(e))
process.terminate()
process.wait()
waiter = threading.Thread(target=_wait_proc)
diff --git a/tools/testing/kunit/kunit_printer.py b/tools/testing/kunit/kunit_printer.py
index ca119f61fe79..440aa7667ece 100644
--- a/tools/testing/kunit/kunit_printer.py
+++ b/tools/testing/kunit/kunit_printer.py
@@ -8,10 +8,16 @@
import datetime
import sys
+import threading
import typing
_RESET = '\033[0;0m'
+# Python's text streams aren't thread safe: concurrent writes to
+# sys.stdout from several threads can lose output or write garbage. All
+# output from the tool should go through this lock.
+print_lock = threading.Lock()
+
class Printer:
"""Wraps a file object, providing utilities for coloring output, etc."""
@@ -25,7 +31,8 @@ class Printer:
def print(self, message: str) -> None:
if self._print:
- print(message, file=self._output)
+ with print_lock:
+ print(message, file=self._output)
def print_with_timestamp(self, message: str) -> None:
ts = datetime.datetime.now().strftime('%H:%M:%S')
--
2.47.3
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH 7/7] kunit: tool: Add --parallel to run suites in concurrent kernels
2026-10-13 15:43 [PATCH 0/7] kunit: tool: Support parallel test execution Mark Brown
` (5 preceding siblings ...)
2026-10-13 15:43 ` [PATCH 6/7] kunit: tool: Serialise output written from multiple threads Mark Brown
@ 2026-10-13 15:43 ` Mark Brown
6 siblings, 0 replies; 10+ messages in thread
From: Mark Brown @ 2026-10-13 15:43 UTC (permalink / raw)
To: Brendan Higgins, David Gow, Rae Moar, Shuah Khan, Daniel Latypov,
Jonathan Corbet, Randy Dunlap
Cc: linux-kselftest, kunit-dev, linux-kernel, linux-doc, Mark Brown
kunit.py boots a single kernel to run all the selected tests, one after
another on a single CPU. This is good for robustness but means that the
overall time taken to complete testing could be improved. Since we are
already able to split up the set of tests run into multiple kernel boots
with the --run-isolated option let's take advantage of that and allow
execution in parallel.
Add a --parallel N option to the run and exec subcommands. When N is
greater than one the tests are enumerated with kunit.action=list and
split up by top level suite, as --run_isolated=suite does, or by
individual test if --run_isolated=test is also given. The resulting
kernels are run from a thread pool N wide, each logging its console to
test.<suite/test>.log in the build directory.
Each kernel's output is buffered by its worker and handed to the parser
in the order the suites were enumerated in, which is the order the
kernel would have run them, so the results are reported identically
regardless of which kernel happens to finish first. While developing
this I noticed some issues with the existing --run_isolated support due
to test naming, with those fixes in place results were identical with
and without parallelism.
Performance results with --alltests, using the elapsed time reported by
the KUnit runner with an already built kernel on a Raspberry Pi 5 with 4
CPUs and a Graviton 4 with 192 CPUs:
default parallel per test parallel per test
Raspberry Pi 5 67.7s 49.4s 836.4s 326.5s
Graviton 4 64.1s 30.8s 445.9s 39.7s
Even with the overhead of having to boot more kernels we are seeing
substantial speedups, for obvious reasons these are much better with
more CPUs available.
Signed-off-by: Mark Brown <broonie@kernel.org>
---
Documentation/dev-tools/kunit/run_wrapper.rst | 11 ++
tools/testing/kunit/kunit.py | 95 +++++++++++----
tools/testing/kunit/kunit_tool_test.py | 165 +++++++++++++++++++++-----
3 files changed, 218 insertions(+), 53 deletions(-)
diff --git a/Documentation/dev-tools/kunit/run_wrapper.rst b/Documentation/dev-tools/kunit/run_wrapper.rst
index cecc110a3399..af5176463966 100644
--- a/Documentation/dev-tools/kunit/run_wrapper.rst
+++ b/Documentation/dev-tools/kunit/run_wrapper.rst
@@ -315,6 +315,17 @@ command line arguments:
This is useful for debugging a non-hermetic test, one that
might pass/fail based on what ran before it.
+- ``--parallel``: Specifies the number of kernels to run at the same time.
+ The tests are split up by suite (or by test, with ``--run_isolated=test``)
+ and each is run in its own kernel, up to ``N`` at a time. The results are
+ reported in the same order as a sequential run, so the output is stable
+ regardless of which kernel finishes first. Each kernel's console output is
+ saved to its own log in the build directory, named after the suite or test
+ it ran, e.g. ``test.kunit_executor_test.log``. Note that each
+ kernel needs its own memory (1GiB for QEMU), so this is best combined with
+ ``--filter`` or a filter glob to avoid running more kernels than the host
+ can comfortably accommodate.
+
- ``--raw_output``: If set, generates unformatted output from kernel. Possible options are:
- ``all``: To view the full kernel output, use ``--raw_output=all``.
diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index 0132687cdd43..4c320ef207df 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -8,6 +8,7 @@
# Author: Brendan Higgins <brendanhiggins@google.com>
import argparse
+import concurrent.futures
import os
import re
import shlex
@@ -18,7 +19,7 @@ assert sys.version_info >= (3, 7), "Python version is too old"
from dataclasses import dataclass, replace
from enum import Enum, auto
-from typing import Iterable, List, Optional, Sequence, Tuple
+from typing import Iterable, Iterator, List, Optional, Sequence, Tuple
import kunit_json
import kunit_junit
@@ -66,6 +67,7 @@ class KunitExecRequest(KunitParseRequest):
list_tests: bool
list_tests_attr: bool
list_suites: bool
+ parallel: int
@dataclass
class KunitRequest(KunitExecRequest, KunitBuildRequest):
@@ -166,6 +168,52 @@ def _suites_from_test_list(tests: List[str]) -> List[str]:
suites.append(suite)
return suites
+def _run_kernels(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest,
+ filter_globs: List[str]) -> Iterator[Iterable[str]]:
+ """Boots a kernel for each filter glob and yields each kernel's output.
+
+ Outputs are always yielded in the order of filter_globs, so the
+ results are reported in the same order whether the kernels are run
+ one after another or (with --parallel) several at a time.
+ """
+ num_kernels = len(filter_globs)
+ workers = min(request.parallel, num_kernels)
+ if workers <= 1:
+ for i, filter_glob in enumerate(filter_globs):
+ stdout.print_with_timestamp('Starting KUnit Kernel ({}/{})...'.format(i+1, num_kernels))
+ yield linux.run_kernel(
+ args=request.kernel_args,
+ timeout=request.timeout,
+ filter_glob=filter_glob,
+ filter=request.filter,
+ filter_action=request.filter_action,
+ build_dir=request.build_dir)
+ return
+
+ # Each kernel's output is buffered by a worker thread and only
+ # handed back once all the kernels before it in filter_globs have
+ # been reported, so which kernel finishes first doesn't matter.
+ # The kernels write to separate log files named after the suite or
+ # test they ran, e.g. test.<suite>.log.
+ def run(filter_glob: str) -> List[str]:
+ return list(linux.run_kernel(
+ args=request.kernel_args,
+ timeout=request.timeout,
+ filter_glob=filter_glob,
+ filter=request.filter,
+ filter_action=request.filter_action,
+ build_dir=request.build_dir,
+ outfile=kunit_kernel.get_outfile_path(request.build_dir, filter_glob)))
+
+ stdout.print_with_timestamp('Starting {} KUnit Kernels, {} at a time...'.format(num_kernels, workers))
+ with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
+ futures = [pool.submit(run, filter_glob) for filter_glob in filter_globs]
+ for i, (filter_glob, future) in enumerate(zip(filter_globs, futures)):
+ output = future.result()
+ stdout.print_with_timestamp('Results from KUnit Kernel ({}/{}) for {}, console log in {}:'.format(
+ i+1, num_kernels, filter_glob, kunit_kernel.get_outfile_path(request.build_dir, filter_glob)))
+ yield output
+
def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> KunitResult:
filter_globs = [request.filter_glob]
if request.list_tests:
@@ -184,11 +232,14 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -
for line in output:
print(line.rstrip())
return KunitResult(status=KunitStatus.SUCCESS, elapsed_time=0.0)
- if request.run_isolated:
+ if request.run_isolated or request.parallel > 1:
tests = _list_tests(linux, request)
if request.run_isolated == 'test':
filter_globs = tests
- elif request.run_isolated == 'suite':
+ else:
+ # One kernel per suite: either --run_isolated=suite, or
+ # --parallel, which splits the tests up by suite so that
+ # each suite can be run in a separate kernel.
filter_globs = _suites_from_test_list(tests)
# Apply the test-part of the user's glob, if present.
if '.' in request.filter_glob:
@@ -203,26 +254,9 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -
report.counts = test_counts
parse_request = replace(request, json=None, junit=None)
test_failed = False
- exec_time = 0.0
- for i, filter_glob in enumerate(filter_globs):
- stdout.print_with_timestamp('Starting KUnit Kernel ({}/{})...'.format(i+1, len(filter_globs)))
-
- test_start = time.time()
- run_result = linux.run_kernel(
- args=request.kernel_args,
- timeout=request.timeout,
- filter_glob=filter_glob,
- filter=request.filter,
- filter_action=request.filter_action,
- build_dir=request.build_dir)
-
+ exec_start = time.time()
+ for run_result in _run_kernels(linux, request, filter_globs):
_, test_result = parse_tests(parse_request, metadata, run_result)
- # run_kernel() doesn't block on the kernel exiting.
- # That only happens after we get the last line of output from `run_result`.
- # So exec_time here actually contains parsing + execution time, which is fine.
- test_end = time.time()
- exec_time += test_end - test_start
-
test_counts.add_subtest_counts(test_result.counts)
# A kernel with no results has no failed test counts to aggregate.
if _map_to_overall_status(test_result.status) != KunitStatus.SUCCESS:
@@ -240,6 +274,10 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -
report.status = kunit_parser.TestStatus.FAILURE_TO_PARSE_TESTS
if not request.raw_output:
_write_test_results(request, metadata, report)
+ # run_kernel() doesn't block on the kernel exiting.
+ # That only happens after we get the last line of output from `run_result`.
+ # So exec_time here actually contains parsing + execution time, which is fine.
+ exec_time = time.time() - exec_start
if len(filter_globs) == 1 and test_counts.crashed > 0:
bd = request.build_dir
@@ -492,6 +530,13 @@ def add_exec_opts(parser: argparse.ArgumentParser) -> None:
parser.add_argument('--list_suites', help='If set, list all suites that will be '
'run.',
action='store_true')
+ parser.add_argument('--parallel',
+ help='Number of kernels to run at the same time. The tests '
+ 'are split up by suite (or by test with --run_isolated=test) '
+ 'and each suite is run in its own kernel, with the results '
+ 'reported in the same order as a sequential run. Defaults '
+ 'to 1, i.e. a single kernel running all the tests.',
+ type=int, default=1, metavar='N')
def add_parse_opts(parser: argparse.ArgumentParser) -> None:
parser.add_argument('--raw_output', help='If set don\'t parse output from kernel. '
@@ -562,7 +607,8 @@ def run_handler(cli_args: argparse.Namespace) -> None:
run_isolated=cli_args.run_isolated,
list_tests=cli_args.list_tests,
list_tests_attr=cli_args.list_tests_attr,
- list_suites=cli_args.list_suites)
+ list_suites=cli_args.list_suites,
+ parallel=cli_args.parallel)
result = run_tests(linux, request)
if result.status != KunitStatus.SUCCESS:
sys.exit(1)
@@ -613,7 +659,8 @@ def exec_handler(cli_args: argparse.Namespace) -> None:
run_isolated=cli_args.run_isolated,
list_tests=cli_args.list_tests,
list_tests_attr=cli_args.list_tests_attr,
- list_suites=cli_args.list_suites)
+ list_suites=cli_args.list_suites,
+ parallel=cli_args.parallel)
result = exec_tests(linux, exec_request)
stdout.print_with_timestamp((
'Elapsed time: %.3fs\n') % (result.elapsed_time))
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index c4999273e491..beb80c728004 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -19,6 +19,7 @@ import signal
import subprocess
import sys
import textwrap
+import time
from typing import Iterable
import xml.etree.ElementTree as ET
@@ -1019,7 +1020,7 @@ class KUnitMainTest(unittest.TestCase):
self.linux_source_mock.run_kernel.return_value = ['TAP version 14', 'init: random output'] + want
got = kunit._list_tests(self.linux_source_mock,
- kunit.KunitExecRequest(None, None, None, False, False, '.kunit', 300, 'suite*', '', None, None, 'suite', False, False, False))
+ kunit.KunitExecRequest(None, None, None, False, False, '.kunit', 300, 'suite*', '', None, None, 'suite', False, False, False, 1))
self.assertEqual(got, want)
# Should respect the user's filter glob when listing tests.
self.linux_source_mock.run_kernel.assert_called_once_with(
@@ -1034,7 +1035,7 @@ class KUnitMainTest(unittest.TestCase):
'[ 0.300000] suite2.test1']
got = kunit._list_tests(self.linux_source_mock,
- kunit.KunitExecRequest(None, None, None, False, False, '.kunit', 300, 'suite*', '', None, None, 'suite', False, False, False))
+ kunit.KunitExecRequest(None, None, None, False, False, '.kunit', 300, 'suite*', '', None, None, 'suite', False, False, False, 1))
self.assertEqual(got, want)
@mock.patch.object(kunit, '_list_tests')
@@ -1044,7 +1045,7 @@ class KUnitMainTest(unittest.TestCase):
# Should respect the user's filter glob when listing tests.
mock_tests.assert_called_once_with(mock.ANY,
- kunit.KunitExecRequest(None, None, None, False, False, '.kunit', 300, 'suite*.test*', '', None, None, 'suite', False, False, False))
+ kunit.KunitExecRequest(None, None, None, False, False, '.kunit', 300, 'suite*.test*', '', None, None, 'suite', False, False, False, 1))
self.linux_source_mock.run_kernel.assert_has_calls([
mock.call(args=None, build_dir='.kunit', filter_glob='suite.test*', filter='', filter_action=None, timeout=300),
mock.call(args=None, build_dir='.kunit', filter_glob='suite2.test*', filter='', filter_action=None, timeout=300),
@@ -1057,7 +1058,7 @@ class KUnitMainTest(unittest.TestCase):
# Should respect the user's filter glob when listing tests.
mock_tests.assert_called_once_with(mock.ANY,
- kunit.KunitExecRequest(None, None, None, False, False, '.kunit', 300, 'suite*', '', None, None, 'test', False, False, False))
+ kunit.KunitExecRequest(None, None, None, False, False, '.kunit', 300, 'suite*', '', None, None, 'test', False, False, False, 1))
self.linux_source_mock.run_kernel.assert_has_calls([
mock.call(args=None, build_dir='.kunit', filter_glob='suite.test1', filter='', filter_action=None, timeout=300),
mock.call(args=None, build_dir='.kunit', filter_glob='suite.test2', filter='', filter_action=None, timeout=300),
@@ -1065,35 +1066,115 @@ class KUnitMainTest(unittest.TestCase):
])
@mock.patch.object(kunit, '_list_tests')
- def test_isolated_kernel_without_results_fails(self, mock_tests):
+ def test_parallel_by_suite(self, mock_tests):
+ mock_tests.return_value = ['suite.test1', 'suite.test2', 'suite2.test1']
+ kunit.main(['exec', '--parallel=2', 'suite*.test*'])
+
+ # Should split by suite, respecting the user's filter glob.
+ mock_tests.assert_called_once_with(mock.ANY,
+ kunit.KunitExecRequest(None, None, None, False, False, '.kunit', 300, 'suite*.test*', '', None, None, None, False, False, False, 2))
+ # Each kernel gets its own log file, named after its suite.
+ # The kernels run concurrently, so don't depend on the order
+ # they start in.
+ self.assertEqual(self.linux_source_mock.run_kernel.call_count, 2)
+ self.linux_source_mock.run_kernel.assert_has_calls([
+ mock.call(args=None, build_dir='.kunit', filter_glob='suite.test*', filter='', filter_action=None, timeout=300, outfile='.kunit/test.suite.test_.log'),
+ mock.call(args=None, build_dir='.kunit', filter_glob='suite2.test*', filter='', filter_action=None, timeout=300, outfile='.kunit/test.suite2.test_.log'),
+ ], any_order=True)
+ self.print_mock.assert_any_call(StrContains('Starting 2 KUnit Kernels, 2 at a time...'))
+ self.print_mock.assert_any_call(StrContains('Results from KUnit Kernel (1/2) for suite.test*, console log in .kunit/test.suite.test_.log:'))
+ self.print_mock.assert_any_call(StrContains('Results from KUnit Kernel (2/2) for suite2.test*, console log in .kunit/test.suite2.test_.log:'))
+
+ @mock.patch.object(kunit, '_list_tests')
+ def test_parallel_by_test(self, mock_tests):
+ mock_tests.return_value = ['suite.test1', 'suite.test2', 'suite2.test1']
+ kunit.main(['exec', '--parallel=2', '--run_isolated=test', 'suite*'])
+
+ self.assertEqual(self.linux_source_mock.run_kernel.call_count, 3)
+ self.linux_source_mock.run_kernel.assert_has_calls([
+ mock.call(args=None, build_dir='.kunit', filter_glob='suite.test1', filter='', filter_action=None, timeout=300, outfile='.kunit/test.suite.test1.log'),
+ mock.call(args=None, build_dir='.kunit', filter_glob='suite.test2', filter='', filter_action=None, timeout=300, outfile='.kunit/test.suite.test2.log'),
+ mock.call(args=None, build_dir='.kunit', filter_glob='suite2.test1', filter='', filter_action=None, timeout=300, outfile='.kunit/test.suite2.test1.log'),
+ ], any_order=True)
+
+ @mock.patch.object(kunit, '_list_tests')
+ def test_parallel_single_suite_runs_one_kernel(self, mock_tests):
+ # With only one suite there's nothing to parallelise, so this
+ # should behave exactly like a sequential run.
+ mock_tests.return_value = ['suite.test1', 'suite.test2']
+ kunit.main(['exec', '--parallel=4', 'suite'])
+
+ self.linux_source_mock.run_kernel.assert_called_once_with(
+ args=None, build_dir='.kunit', filter_glob='suite', filter='', filter_action=None, timeout=300)
+ self.print_mock.assert_any_call(StrContains('Starting KUnit Kernel (1/1)...'))
+
+ @mock.patch.object(kunit, '_list_tests')
+ def test_parallel_stable_order(self, mock_tests):
+ mock_tests.return_value = ['suite1.test1', 'suite2.test1', 'suite3.test1']
+
+ # Make the kernels finish in the reverse of the order they're
+ # listed in: results must still come out in the listed order.
+ def fake_run_kernel(filter_glob, **kwargs):
+ delay = {'suite1': 0.2, 'suite2': 0.1, 'suite3': 0.0}[filter_glob]
+ time.sleep(delay)
+ return ['TAP version 14', '1..1', 'ok 1 - ' + filter_glob]
+ self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
+
+ kunit.main(['exec', '--parallel=3', '--raw_output=kunit'])
+
+ output = [call.args[0] for call in self.print_mock.call_args_list]
+ want = ['ok 1 - suite1', 'ok 1 - suite2', 'ok 1 - suite3']
+ self.assertEqual([l for l in output if l.startswith('ok')], want)
+
+ @mock.patch.object(kunit, '_list_tests')
+ def test_parallel_counts_all_kernels(self, mock_tests):
mock_tests.return_value = ['suite1.test1', 'suite2.test1']
- for missing_output in ([], ['KTAP version 1', '1..0']):
- for missing_suite in ('suite1', 'suite2'):
- with self.subTest(output=missing_output,
- suite=missing_suite):
- def fake_run_kernel(filter_glob, **kwargs):
- if filter_glob == missing_suite:
- return missing_output
- return ['KTAP version 1', '1..1', 'ok 1 - test1']
- self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
- with self.assertRaises(SystemExit) as e:
- kunit.main(['exec', '--run_isolated=suite'])
- self.assertEqual(e.exception.code, 1)
+ def fake_run_kernel(filter_glob, **kwargs):
+ if filter_glob == 'suite2':
+ return ['TAP version 14', '1..1', 'not ok 1 - suite2']
+ return ['TAP version 14', '1..1', 'ok 1 - suite1']
+ self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
+
+ # A failure in any of the kernels must fail the overall run.
+ with self.assertRaises(SystemExit) as e:
+ kunit.main(['exec', '--parallel=2'])
+ self.assertEqual(e.exception.code, 1)
+
+ @mock.patch.object(kunit, '_list_tests')
+ def test_isolated_kernel_without_results_fails(self, mock_tests):
+ mock_tests.return_value = ['suite1.test1', 'suite2.test1']
+ for parallel in (1, 2):
+ for missing_output in ([], ['KTAP version 1', '1..0']):
+ for missing_suite in ('suite1', 'suite2'):
+ with self.subTest(parallel=parallel, output=missing_output,
+ suite=missing_suite):
+ def fake_run_kernel(filter_glob, **kwargs):
+ if filter_glob == missing_suite:
+ return missing_output
+ return ['KTAP version 1', '1..1', 'ok 1 - test1']
+ self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
+
+ with self.assertRaises(SystemExit) as e:
+ kunit.main(['exec', '--run_isolated=suite',
+ '--parallel=' + str(parallel)])
+ self.assertEqual(e.exception.code, 1)
@mock.patch.object(kunit, '_list_tests')
def test_isolated_skipped_kernels_succeed(self, mock_tests):
mock_tests.return_value = ['suite1.test1', 'suite2.test1']
- for all_skipped in (False, True):
- with self.subTest(all_skipped=all_skipped):
- def fake_run_kernel(filter_glob, **kwargs):
- result = 'ok 1 - test1'
- if all_skipped or filter_glob == 'suite1':
- result += ' # SKIP unavailable'
- return ['KTAP version 1', '1..1', result]
- self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
+ for parallel in (1, 2):
+ for all_skipped in (False, True):
+ with self.subTest(parallel=parallel, all_skipped=all_skipped):
+ def fake_run_kernel(filter_glob, **kwargs):
+ result = 'ok 1 - test1'
+ if all_skipped or filter_glob == 'suite1':
+ result += ' # SKIP unavailable'
+ return ['KTAP version 1', '1..1', result]
+ self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
- kunit.main(['exec', '--run_isolated=suite'])
+ kunit.main(['exec', '--run_isolated=suite',
+ '--parallel=' + str(parallel)])
@mock.patch.object(kunit, '_list_tests')
def test_isolated_reports_include_all_kernels(self, mock_tests):
@@ -1109,8 +1190,8 @@ class KUnitMainTest(unittest.TestCase):
' 1..1', f' {status} 1 - {case}', f'{status} 1 - {suite}']
self.linux_source_mock.run_kernel.side_effect = fake_run_kernel
- for isolation in ('suite', 'test'):
- with self.subTest(isolation=isolation), \
+ for parallel, isolation in itertools.product((1, 2), ('suite', 'test')):
+ with self.subTest(parallel=parallel, isolation=isolation), \
tempfile.TemporaryDirectory() as output_dir:
if isolation == 'suite':
mock_tests.return_value = ['suite1.test', 'suite2.test']
@@ -1121,7 +1202,8 @@ class KUnitMainTest(unittest.TestCase):
json_path = os.path.join(output_dir, 'results.json')
xml_path = os.path.join(output_dir, 'results.xml')
with self.assertRaises(SystemExit) as e:
- kunit.main(['exec', '--run_isolated=' + isolation,
+ kunit.main(['exec', '--parallel=' + str(parallel),
+ '--run_isolated=' + isolation,
'--json=' + json_path, '--junit=' + xml_path])
self.assertEqual(e.exception.code, 1)
@@ -1139,6 +1221,31 @@ class KUnitMainTest(unittest.TestCase):
'FAIL' if case.find('failure') is not None else 'PASS')
for suite in root for case in suite], expected)
+ @mock.patch.object(kunit, '_list_tests')
+ def test_parallel_reports_to_stdout(self, mock_tests):
+ mock_tests.return_value = ['suite1.test', 'suite2.test']
+ self.linux_source_mock.run_kernel.side_effect = lambda filter_glob, **kwargs: [
+ 'KTAP version 1', '1..1', 'ok 1 - ' + filter_glob]
+ kunit.main(['exec', '--parallel=2', '--json', '--junit'])
+ output = [call.args[0] for call in self.print_mock.call_args_list]
+ json_reports = [json.loads(line) for line in output if line.startswith('{')]
+ xml_reports = [ET.fromstring(line) for line in output if line.startswith('<testsuite')]
+ self.assertEqual(len(json_reports), 1)
+ self.assertEqual(json_reports[0]['misc']['tests'], 2)
+ self.assertEqual(len(xml_reports), 1)
+ self.assertEqual(xml_reports[0].get('tests'), '2')
+
+ @mock.patch.object(kunit, '_list_tests')
+ def test_parallel_raw_output_does_not_write_reports(self, mock_tests):
+ mock_tests.return_value = ['suite1.test', 'suite2.test']
+ with tempfile.TemporaryDirectory() as output_dir:
+ json_path = os.path.join(output_dir, 'results.json')
+ xml_path = os.path.join(output_dir, 'results.xml')
+ kunit.main(['exec', '--parallel=2', '--raw_output=all',
+ '--json=' + json_path, '--junit=' + xml_path])
+ self.assertFalse(os.path.exists(json_path))
+ self.assertFalse(os.path.exists(xml_path))
+
def test_single_kernel_reports_match_parse(self):
with tempfile.TemporaryDirectory() as output_dir:
json_path = os.path.join(output_dir, 'results.json')
--
2.47.3
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH 6/7] kunit: tool: Serialise output written from multiple threads
2026-10-13 15:43 ` [PATCH 6/7] kunit: tool: Serialise output written from multiple threads Mark Brown
@ 2026-10-13 19:53 ` Daniel Latypov
2026-10-13 20:47 ` Mark Brown
0 siblings, 1 reply; 10+ messages in thread
From: Daniel Latypov @ 2026-10-13 19:53 UTC (permalink / raw)
To: Mark Brown
Cc: Brendan Higgins, David Gow, Rae Moar, Shuah Khan,
Jonathan Corbet, Randy Dunlap, linux-kselftest, kunit-dev,
linux-kernel, linux-doc
On Tue, Oct 13, 2026 at 8:52 AM Mark Brown <broonie@kernel.org> wrote:
>
> Python's text streams are not thread safe. If two threads write to
> sys.stdout at the same time the stream's internal buffer can be
> corrupted, resulting in lost output and runs of NUL bytes or heap
> garbage being written in its place. This isn't an issue currently, but
> will be when we implement support for parallel test execution.
>
> Add a lock to kunit_printer which Printer.print() takes around each
Firstly, thanks for taking on test parallelization!
In March 2022, I'd written a proof of concept that worked, it just
leaked some memory on the C side so I never shared it.
(Instead of using the existing test filter glob, I'd added the ability
to specify arbitrary test indices to run a slice of tests)
I'd have to reacquaint myself with the code to actually review the series.
But one thing that sticks out immediately is, my version had added
this new type to avoid the need for any locking:
+class BufferPrinter(Printer):
+ """Like Printer, but only prints to an in-memory buffer."""
+
+ def __init__(self, parent: Printer):
+ """Creates a new printer that inherits parent's
settings (use_color)."""
+ buf = io.StringIO()
+ super().__init__(buf)
+ self._use_color = parent._use_color
+
+ self._parent = parent
+ self._buf = buf
+
+ def close(self):
+ """Prints the contents to the parent printer and frees
memory."""
+ self._parent.print(self._buf.getvalue().rstrip('\n'))
+ self._buf.close()
+
It required passing along kunit_printer.Printer objects to more
functions, but I think it might be a cleaner end result.
By explicitly calling `close()`, we could control when exactly output
gets flushed so we can avoid weird interleaving.
It seems in a later patch, you write each kernel's output to a file to
get the same effect, but an object like this could be used in more
places.
Thoughts?
-Daniel
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH 6/7] kunit: tool: Serialise output written from multiple threads
2026-10-13 19:53 ` Daniel Latypov
@ 2026-10-13 20:47 ` Mark Brown
0 siblings, 0 replies; 10+ messages in thread
From: Mark Brown @ 2026-10-13 20:47 UTC (permalink / raw)
To: Daniel Latypov
Cc: Brendan Higgins, David Gow, Rae Moar, Shuah Khan,
Jonathan Corbet, Randy Dunlap, linux-kselftest, kunit-dev,
linux-kernel, linux-doc
[-- Attachment #1: Type: text/plain, Size: 1702 bytes --]
On Tue, Oct 13, 2026 at 12:53:08PM -0700, Daniel Latypov wrote:
> Firstly, thanks for taking on test parallelization!
> In March 2022, I'd written a proof of concept that worked, it just
> leaked some memory on the C side so I never shared it.
> (Instead of using the existing test filter glob, I'd added the ability
> to specify arbitrary test indices to run a slice of tests)
Perhaps that could be done by adding --run_isolated=N or A..B to split
into numbered blocks of tests? TBH mine is implemented this way so we
have to solve fewer problems but it does also feel sensible to be able
to compose the paralleism and how things are split up.
> I'd have to reacquaint myself with the code to actually review the series.
> But one thing that sticks out immediately is, my version had added
> this new type to avoid the need for any locking:
> It required passing along kunit_printer.Printer objects to more
> functions, but I think it might be a cleaner end result.
> By explicitly calling `close()`, we could control when exactly output
> gets flushed so we can avoid weird interleaving.
> It seems in a later patch, you write each kernel's output to a file to
> get the same effect, but an object like this could be used in more
> places.
The existing code does log the kernel output to test.log, this was
piggybacking on that since those logs are there anyway - we'd want the
separate logs regardless I think. That doesn't preclude doing in memory
buffering though, from a first pass other than invasiveness the only big
issue I can think of with buffering in memory is that if we faceplant
badly enough it might be harder to figure out what went wrong but I'm
not sure that's a real issue.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-10-13 20:48 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-13 15:43 [PATCH 0/7] kunit: tool: Support parallel test execution Mark Brown
2026-10-13 15:43 ` [PATCH 1/7] kunit: tool: Preserve failures from kernels without test results Mark Brown
2026-10-13 15:43 ` [PATCH 2/7] kunit: tool: Combine reports from isolated kernels Mark Brown
2026-10-13 15:43 ` [PATCH 3/7] kunit: tool: Boot with a larger log buffer when listing tests Mark Brown
2026-10-13 15:43 ` [PATCH 4/7] kunit: tool: Add an outfile argument to run_kernel() Mark Brown
2026-10-13 15:43 ` [PATCH 5/7] kunit: tool: Allow several run_kernel() calls to be in flight at once Mark Brown
2026-10-13 15:43 ` [PATCH 6/7] kunit: tool: Serialise output written from multiple threads Mark Brown
2026-10-13 19:53 ` Daniel Latypov
2026-10-13 20:47 ` Mark Brown
2026-10-13 15:43 ` [PATCH 7/7] kunit: tool: Add --parallel to run suites in concurrent kernels Mark Brown
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®