[PATCH v3 1/2] kunit: tool: Parse and print the reason tests are skipped

0 views
Skip to first unread message

David Gow

unread,
Jun 4, 2026, 8:32:29 AM (10 days ago) Jun 4
to Brendan Higgins, Rae Moar, Shuah Khan, Thomas Weißschuh, David Gow, kuni...@googlegroups.com, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, work...@vger.kernel.org
When a KUnit test (or other KTAP test) is skipped, a "skip reason" can be
provided. kunit.py has never done anything with this, ignoring anything
included in the KTAP output after the 'SKIP' directive.

Since we have it, and it's used, print it in a nice friendly yellow in
parentheses after a skipped test's name.

(And, by parsing it, it can be included in the JUnit results as well.)

Signed-off-by: David Gow <da...@davidgow.net>
---

There are a few bits of KTAP that kunit.py has never actually parsed, and
this is one of them. It's also nice to have a good way of quickly seeing
why a test has been skipped, given most tests do provide good reason
strings.

Happy to hear comments about the style: yellow in parentheses looks pretty
good here, but could be a bit confusing if KUnit do

This is the first version of this patch, as it's a new dependency of v3 of
patch 2, the JUnit support, which also includes the skip reason.

--
tools/testing/kunit/kunit_parser.py | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/tools/testing/kunit/kunit_parser.py b/tools/testing/kunit/kunit_parser.py
index 0e1d2f4985eb..22b8464c6383 100644
--- a/tools/testing/kunit/kunit_parser.py
+++ b/tools/testing/kunit/kunit_parser.py
@@ -44,11 +44,12 @@ class Test:
self.subtests = [] # type: List[Test]
self.log = [] # type: List[str]
self.counts = TestCounts()
+ self.skip_reason = ''

def __str__(self) -> str:
"""Returns string representation of a Test class object."""
return (f'Test({self.status}, {self.name}, {self.expected_count}, '
- f'{self.subtests}, {self.log}, {self.counts})')
+ f'{self.subtests}, {self.log}, {self.counts}, {self.skip_reason})')

def __repr__(self) -> str:
"""Returns string representation of a Test class object."""
@@ -352,9 +353,9 @@ def parse_test_plan(lines: LineStream, test: Test) -> bool:
lines.pop()
return True

-TEST_RESULT = re.compile(r'^\s*(ok|not ok) ([0-9]+) ?(- )?([^#]*)( # .*)?$')
+TEST_RESULT = re.compile(r'^\s*(ok|not ok) ([0-9]+) ?(:?- )?([^#]*)( # .*)?$')

-TEST_RESULT_SKIP = re.compile(r'^\s*(ok|not ok) ([0-9]+) ?(- )?(.*) # SKIP ?(.*)$')
+TEST_RESULT_SKIP = re.compile(r'^\s*(ok|not ok) ([0-9]+) ?(:?- )?(.*) # SKIP ?(.*)$')

def peek_test_name_match(lines: LineStream, test: Test) -> bool:
"""
@@ -418,7 +419,7 @@ def parse_test_result(lines: LineStream, test: Test,

# Set name of test object
if skip_match:
- test.name = skip_match.group(4) or skip_match.group(5)
+ test.name = skip_match.group(4)
else:
test.name = match.group(4)

@@ -431,6 +432,7 @@ def parse_test_result(lines: LineStream, test: Test,
status = match.group(1)
if skip_match:
test.status = TestStatus.SKIPPED
+ test.skip_reason = skip_match.group(5) or ''
elif status == 'ok':
test.status = TestStatus.SUCCESS
else:
@@ -539,7 +541,7 @@ def format_test_result(test: Test, printer: Printer) -> str:
if test.status == TestStatus.SUCCESS:
return printer.green('[PASSED] ') + test.name
if test.status == TestStatus.SKIPPED:
- return printer.yellow('[SKIPPED] ') + test.name
+ return printer.yellow('[SKIPPED] ') + test.name + ' (' + printer.yellow(test.skip_reason) + ')'
if test.status == TestStatus.NO_TESTS:
return printer.yellow('[NO TESTS RUN] ') + test.name
if test.status == TestStatus.TEST_CRASHED:
--
2.54.0

David Gow

unread,
Jun 4, 2026, 8:32:32 AM (10 days ago) Jun 4
to Brendan Higgins, Rae Moar, Shuah Khan, Thomas Weißschuh, David Gow, kuni...@googlegroups.com, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, work...@vger.kernel.org
This is used by things like Jenkins and other CI systems, which can
pretty-print the test output and potentially provide test-level comparisons
between runs.

The implementation here is pretty basic: it only provides the raw results,
split into tests and test suites, and doesn't provide any overall metadata.
However, CI systems like Jenkins can ingest it and it is already useful.

Signed-off-by: David Gow <da...@davidgow.net>
---
Here's version 3 of the JUnit support patch. This one uses python's ElementTree
API to generate the XML, rather than writing strings directly. The only real
difference in the output is that this escapes any log lines, rather than using
CDATA sections. (There are both advantages and disadvantages to this, but
they're all pretty minor. Equally, the option of using the
xml.sax.saxutils.XMLGenerator API instead is a possibility. ElementTree ended
up being slightly more aligned with the way the JSON version worked.)

The other big change is that skipped tests now include their skip reason, rather
than hardcoding "Test skipped", hence the introduction of patch 1.

Changes since v2:
https://lore.kernel.org/all/20260502024918....@davidgow.net/
- Rework to use Python's ElementTree XML writing API (Thanks Thomas)
- Output the reason for a test to be skipped.

Changes since v1:
https://lore.kernel.org/all/20260119073426.1...@google.com/
- Use python's provided XML quote escaping, rather than coding our own (Thanks Thomas)
- Output proper <skipped> tags for skipped tests
- Report crashed tests as <error>
- Don't output <system-out> tags if there are no lines of log data

Documentation/dev-tools/kunit/run_wrapper.rst | 3 +
tools/testing/kunit/kunit.py | 21 ++++++-
tools/testing/kunit/kunit_junit.py | 61 +++++++++++++++++++
tools/testing/kunit/kunit_tool_test.py | 39 +++++++++++-
4 files changed, 120 insertions(+), 4 deletions(-)
create mode 100644 tools/testing/kunit/kunit_junit.py

diff --git a/Documentation/dev-tools/kunit/run_wrapper.rst b/Documentation/dev-tools/kunit/run_wrapper.rst
index 770bb09a475a..cecc110a3399 100644
--- a/Documentation/dev-tools/kunit/run_wrapper.rst
+++ b/Documentation/dev-tools/kunit/run_wrapper.rst
@@ -324,6 +324,9 @@ command line arguments:
- ``--json``: If set, stores the test results in a JSON format and prints to `stdout` or
saves to a file if a filename is specified.

+- ``--junit``: If set, stores the test results in JUnit XML format and prints to `stdout` or
+ saves to a file if a filename is specified.
+
- ``--filter``: Specifies filters on test attributes, for example, ``speed!=slow``.
Multiple filters can be used by wrapping input in quotes and separating filters
by commas. Example: ``--filter "speed>slow, module=example"``.
diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index 742f5c555666..ac3f7159e67f 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -21,6 +21,7 @@ from enum import Enum, auto
from typing import Iterable, List, Optional, Sequence, Tuple

import kunit_json
+import kunit_junit
import kunit_kernel
import kunit_parser
from kunit_printer import stdout, null_printer
@@ -49,6 +50,7 @@ class KunitBuildRequest(KunitConfigRequest):
class KunitParseRequest:
raw_output: Optional[str]
json: Optional[str]
+ junit: Optional[str]
summary: bool
failed: bool

@@ -268,6 +270,13 @@ def parse_tests(request: KunitParseRequest, metadata: kunit_json.Metadata, input
stdout.print_with_timestamp("Test results stored in %s" %
os.path.abspath(request.json))

+ if request.junit:
+ if request.junit == 'stdout':
+ kunit_junit.print_junit_result(test=test)
+ else:
+ 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

@@ -309,6 +318,7 @@ def run_tests(linux: kunit_kernel.LinuxSourceTree,
# So we hackily automatically rewrite --json => --json=stdout
pseudo_bool_flag_defaults = {
'--json': 'stdout',
+ '--junit': 'stdout',
'--raw_output': 'kunit',
}
def massage_argv(argv: Sequence[str]) -> Sequence[str]:
@@ -459,6 +469,11 @@ def add_parse_opts(parser: argparse.ArgumentParser) -> None:
help='Prints parsed test results as JSON to stdout or a file if '
'a filename is specified. Does nothing if --raw_output is set.',
type=str, const='stdout', default=None, metavar='FILE')
+ parser.add_argument('--junit',
+ nargs='?',
+ help='Prints parsed test results as JUnit XML to stdout or a file if '
+ 'a filename is specified. Does nothing if --raw_output is set.',
+ type=str, const='stdout', default=None, metavar='FILE')
parser.add_argument('--summary',
help='Prints only the summary line for parsed test results.'
'Does nothing if --raw_output is set.',
@@ -502,6 +517,7 @@ def run_handler(cli_args: argparse.Namespace) -> None:
jobs=cli_args.jobs,
raw_output=cli_args.raw_output,
json=cli_args.json,
+ junit=cli_args.junit,
summary=cli_args.summary,
failed=cli_args.failed,
timeout=cli_args.timeout,
@@ -552,6 +568,7 @@ def exec_handler(cli_args: argparse.Namespace) -> None:
exec_request = KunitExecRequest(raw_output=cli_args.raw_output,
build_dir=cli_args.build_dir,
json=cli_args.json,
+ junit=cli_args.junit,
summary=cli_args.summary,
failed=cli_args.failed,
timeout=cli_args.timeout,
@@ -580,7 +597,9 @@ def parse_handler(cli_args: argparse.Namespace) -> None:
# We know nothing about how the result was created!
metadata = kunit_json.Metadata()
request = KunitParseRequest(raw_output=cli_args.raw_output,
- json=cli_args.json, summary=cli_args.summary,
+ json=cli_args.json,
+ junit=cli_args.junit,
+ summary=cli_args.summary,
failed=cli_args.failed)
result, _ = parse_tests(request, metadata, kunit_output)
if result.status != KunitStatus.SUCCESS:
diff --git a/tools/testing/kunit/kunit_junit.py b/tools/testing/kunit/kunit_junit.py
new file mode 100644
index 000000000000..3622070358e7
--- /dev/null
+++ b/tools/testing/kunit/kunit_junit.py
@@ -0,0 +1,61 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Generates JUnit XML files from KUnit test results
+#
+# Copyright (C) 2026, Google LLC and David Gow.
+
+from xml.sax.saxutils import quoteattr, XMLGenerator
+import xml.etree.ElementTree as ET
+from kunit_parser import Test, TestStatus
+from typing import Optional
+
+# Get a string representing a tes suite (including subtests) in JUnit XML
+def get_test_suite(test: Test, parent: Optional[ET.Element]) -> ET.Element:
+ suite_attrs = {
+ 'name': test.name,
+ 'tests': str(test.counts.total()),
+ 'failures': str(test.counts.failed),
+ 'skipped': str(test.counts.skipped),
+ 'errors': str(test.counts.crashed + test.counts.errors),
+ }
+
+ if parent is not None:
+ test_suite_element = ET.SubElement(parent, 'testsuite', suite_attrs)
+ else:
+ test_suite_element = ET.Element('testsuite', suite_attrs)
+
+ for subtest in test.subtests:
+ if subtest.subtests:
+ get_test_suite(subtest, test_suite_element)
+ continue
+ test_case_element = ET.SubElement(test_suite_element, 'testcase', {'name': subtest.name})
+ if subtest.status == TestStatus.FAILURE:
+ ET.SubElement(test_case_element, 'failure', {}).text = 'Test Failed'
+ elif subtest.status == TestStatus.SKIPPED:
+ ET.SubElement(test_case_element, 'skipped', {}).text = subtest.skip_reason
+ elif subtest.status == TestStatus.TEST_CRASHED:
+ ET.SubElement(test_case_element, 'error', {}).text = 'Test Crashed'
+
+ if subtest.log:
+ ET.SubElement(test_case_element, 'system-out', {}).text = "\n".join(subtest.log)
+
+ return test_suite_element
+
+# Get a string for an entire XML file for the test structure starting at test
+def get_junit_result(test: Test) -> str:
+ root_element = get_test_suite(test, None)
+ ET.indent(root_element)
+ return ET.tostring(root_element, encoding="unicode", xml_declaration=True)
+
+# Print a JUnit result to stdout.
+def print_junit_result(test: Test) -> None:
+ root_element = get_test_suite(test, None)
+ ET.indent(root_element)
+ ET.dump(root_element)
+
+# Write an entire XML file for the test structure starting at test
+def write_junit_result(test: Test, filename: str) -> None:
+ root_element = get_test_suite(test, None)
+ ET.indent(root_element)
+ root_et = ET.ElementTree(root_element)
+ root_et.write(filename, encoding='utf-8', xml_declaration=True)
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index 267c33cecf87..9797c26d981f 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -24,6 +24,7 @@ import kunit_config
import kunit_parser
import kunit_kernel
import kunit_json
+import kunit_junit
import kunit
from kunit_printer import stdout

@@ -676,6 +677,38 @@ class StrContains(str):
def __eq__(self, other):
return self in other

+class KUnitJUnitTest(unittest.TestCase):
+ def setUp(self):
+ self.print_mock = mock.patch('kunit_printer.Printer.print').start()
+ self.addCleanup(mock.patch.stopall)
+
+ def _junit_string(self, log_file):
+ with open(_test_data_path(log_file)) as file:
+ test_result = kunit_parser.parse_run_tests(file, stdout)
+ junit_string = kunit_junit.get_junit_result(
+ test=test_result)
+ print(junit_string)
+ return junit_string
+
+ def test_failed_test_junit(self):
+ result = self._junit_string('test_is_test_passed-failure.log')
+ self.assertTrue("<failure>" in result)
+
+ def test_skipped_test_junit(self):
+ result = self._junit_string('test_skip_tests.log')
+ self.assertTrue("<skipped>" in result)
+ self.assertTrue("skipped=\"1\"" in result)
+
+ def test_crashed_test_junit(self):
+ result = self._junit_string('test_kernel_panic_interrupt.log')
+ self.assertTrue("<error>" in result);
+
+ def test_no_tests_junit(self):
+ result = self._junit_string('test_is_test_passed-no_tests_run_with_header.log')
+ self.assertTrue("tests=\"0\"" in result)
+ self.assertFalse("testcase" in result)
+
+
class KUnitMainTest(unittest.TestCase):
def setUp(self):
path = _test_data_path('test_is_test_passed-all_passed.log')
@@ -923,7 +956,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, 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))
self.assertEqual(got, want)
# Should respect the user's filter glob when listing tests.
self.linux_source_mock.run_kernel.assert_called_once_with(
@@ -936,7 +969,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, 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))
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),
@@ -949,7 +982,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, 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))
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),
--
2.54.0

Thomas Weißschuh

unread,
Jun 5, 2026, 2:58:51 AM (9 days ago) Jun 5
to David Gow, Brendan Higgins, Rae Moar, Shuah Khan, kuni...@googlegroups.com, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, work...@vger.kernel.org
On Thu, Jun 04, 2026 at 08:32:04PM +0800, David Gow wrote:
> When a KUnit test (or other KTAP test) is skipped, a "skip reason" can be
> provided. kunit.py has never done anything with this, ignoring anything
> included in the KTAP output after the 'SKIP' directive.
>
> Since we have it, and it's used, print it in a nice friendly yellow in
> parentheses after a skipped test's name.
>
> (And, by parsing it, it can be included in the JUnit results as well.)

Some tests would be nice.

> Signed-off-by: David Gow <da...@davidgow.net>
> ---
>
> There are a few bits of KTAP that kunit.py has never actually parsed, and
> this is one of them. It's also nice to have a good way of quickly seeing
> why a test has been skipped, given most tests do provide good reason
> strings.
>
> Happy to hear comments about the style: yellow in parentheses looks pretty
> good here, but could be a bit confusing if KUnit do

Something is missing here?

For [SKIPPED] the brackets are colored, too.

> This is the first version of this patch, as it's a new dependency of v3 of
> patch 2, the JUnit support, which also includes the skip reason.
>
> --
> tools/testing/kunit/kunit_parser.py | 12 +++++++-----
> 1 file changed, 7 insertions(+), 5 deletions(-)

(...)

Thomas Weißschuh

unread,
Jun 5, 2026, 3:11:17 AM (9 days ago) Jun 5
to David Gow, Brendan Higgins, Rae Moar, Shuah Khan, kuni...@googlegroups.com, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, work...@vger.kernel.org
On Thu, Jun 04, 2026 at 08:32:05PM +0800, David Gow wrote:
> This is used by things like Jenkins and other CI systems, which can
> pretty-print the test output and potentially provide test-level comparisons
> between runs.
>
> The implementation here is pretty basic: it only provides the raw results,
> split into tests and test suites, and doesn't provide any overall metadata.
> However, CI systems like Jenkins can ingest it and it is already useful.
>
> Signed-off-by: David Gow <da...@davidgow.net>

Reviewed-by: Thomas Weißschuh <thomas.w...@linutronix.de>

> ---
> Here's version 3 of the JUnit support patch. This one uses python's ElementTree
> API to generate the XML, rather than writing strings directly. The only real
> difference in the output is that this escapes any log lines, rather than using
> CDATA sections. (There are both advantages and disadvantages to this, but
> they're all pretty minor. Equally, the option of using the
> xml.sax.saxutils.XMLGenerator API instead is a possibility. ElementTree ended
> up being slightly more aligned with the way the JSON version worked.)

I still somewhat prefer an event-based formatting.
(There is also xml.etree.ElementTree.TreeBuilder)
But if the JSON formatter works the same, its fine for me.

(...)

David Gow

unread,
Jun 5, 2026, 9:38:55 PM (9 days ago) Jun 5
to Brendan Higgins, Rae Moar, Shuah Khan, Thomas Weißschuh, David Gow, kuni...@googlegroups.com, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, work...@vger.kernel.org
When a KUnit test (or other KTAP test) is skipped, a "skip reason" can be
provided. kunit.py has never done anything with this, ignoring anything
included in the KTAP output after the 'SKIP' directive.

Since we have it, and it's used, print it in a nice friendly yellow in
parentheses after a skipped test's name.

(And, by parsing it, it can be included in the JUnit results as well.)

Signed-off-by: David Gow <da...@davidgow.net>
---

There are a few bits of KTAP that kunit.py has never actually parsed, and
this is one of them. It's also nice to have a good way of quickly seeing
why a test has been skipped, given most tests do provide good reason
strings.

Happy to hear comments about the style: yellow in parentheses looks pretty
good here, but could be a bit confusing if the test name includes parentheses
and there's no colour support.

Changes since v3:
https://lore.kernel.org/all/20260604123207....@davidgow.net/
- Make the parentheses yellow as well (Thanks, Thomas)
- Add some tests for the actual parsing of the SKIP reason. (Thanks, Thomas)

This patch was new in v2.

---
tools/testing/kunit/kunit_parser.py | 15 ++++++++++-----
tools/testing/kunit/kunit_tool_test.py | 17 +++++++++++++++++
2 files changed, 27 insertions(+), 5 deletions(-)

diff --git a/tools/testing/kunit/kunit_parser.py b/tools/testing/kunit/kunit_parser.py
index 0e1d2f4985eb..7a021517f58b 100644
@@ -539,7 +541,10 @@ def format_test_result(test: Test, printer: Printer) -> str:
if test.status == TestStatus.SUCCESS:
return printer.green('[PASSED] ') + test.name
if test.status == TestStatus.SKIPPED:
- return printer.yellow('[SKIPPED] ') + test.name
+ skip_message = printer.yellow('[SKIPPED] ') + test.name
+ if test.skip_reason != '':
+ skip_message += printer.yellow(' (' + test.skip_reason + ')')
+ return skip_message
if test.status == TestStatus.NO_TESTS:
return printer.yellow('[NO TESTS RUN] ') + test.name
if test.status == TestStatus.TEST_CRASHED:
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index 267c33cecf87..5ebd551b5072 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -235,10 +235,27 @@ class KUnitParserTest(unittest.TestCase):
with open(skipped_log) as file:
result = kunit_parser.parse_run_tests(file.readlines(), stdout)

+ # The test result is skipped, and the skip reason is valid
+ self.assertEqual(kunit_parser.TestStatus.SKIPPED, result.subtests[1].subtests[1].status)
+ self.assertEqual("this test should be skipped", result.subtests[1].subtests[1].skip_reason)
+
# A skipped test does not fail the whole suite.
self.assertEqual(kunit_parser.TestStatus.SUCCESS, result.status)
self.assertEqual(result.counts, kunit_parser.TestCounts(passed=4, skipped=1))

+ def test_skipped_reason_parse(self):
+ skipped_log = _test_data_path('test_skip_all_tests.log')
+ with open(skipped_log) as file:
+ result = kunit_parser.parse_run_tests(file.readlines(), stdout)
+
+ # The first test is skipped, with the correct reaons
+ self.assertEqual(kunit_parser.TestStatus.SKIPPED, result.subtests[0].subtests[0].status)
+ self.assertEqual("all tests skipped", result.subtests[0].subtests[0].skip_reason)
+
+ # The first suite is skipped, with no reason
+ self.assertEqual(kunit_parser.TestStatus.SKIPPED, result.subtests[0].status)
+ self.assertEqual("", result.subtests[0].skip_reason)
+
def test_skipped_all_tests(self):
skipped_log = _test_data_path('test_skip_all_tests.log')
with open(skipped_log) as file:
--
2.54.0

David Gow

unread,
Jun 5, 2026, 9:38:57 PM (9 days ago) Jun 5
to Brendan Higgins, Rae Moar, Shuah Khan, Thomas Weißschuh, David Gow, kuni...@googlegroups.com, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, work...@vger.kernel.org
This is used by things like Jenkins and other CI systems, which can
pretty-print the test output and potentially provide test-level comparisons
between runs.

The implementation here is pretty basic: it only provides the raw results,
split into tests and test suites, and doesn't provide any overall metadata.
However, CI systems like Jenkins can ingest it and it is already useful.

Reviewed-by: Thomas Weißschuh <thomas.w...@linutronix.de>
Signed-off-by: David Gow <da...@davidgow.net>
---

Only patch 1 changed, so v4 is the same as v3. I'm reserving the right to
switch to a more event-based XML writer in a follow-up, but that'd
probably happen as a part of a more broad refactoring of the various output
formats.

No changes since v3:
https://lore.kernel.org/all/20260604123207....@davidgow.net/

Changes since v2:
https://lore.kernel.org/all/20260502024918....@davidgow.net/
- Rework to use Python's ElementTree XML writing API (Thanks Thomas)
- Output the reason for a test to be skipped.

Changes since v1:
https://lore.kernel.org/all/20260119073426.1...@google.com/
- Use python's provided XML quote escaping, rather than coding our own (Thanks Thomas)
- Output proper <skipped> tags for skipped tests
- Report crashed tests as <error>
- Don't output <system-out> tags if there are no lines of log data

---
diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
index 5ebd551b5072..da88c3a1651d 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -24,6 +24,7 @@ import kunit_config
import kunit_parser
import kunit_kernel
import kunit_json
+import kunit_junit
import kunit
from kunit_printer import stdout

@@ -693,6 +694,38 @@ class StrContains(str):
@@ -940,7 +973,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, 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))
self.assertEqual(got, want)
# Should respect the user's filter glob when listing tests.
self.linux_source_mock.run_kernel.assert_called_once_with(
@@ -953,7 +986,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, 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))
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),
@@ -966,7 +999,7 @@ class KUnitMainTest(unittest.TestCase):

Kees Cook

unread,
Jun 10, 2026, 4:27:10 PM (4 days ago) Jun 10
to David Gow, Brendan Higgins, Rae Moar, Shuah Khan, Thomas Weißschuh, kuni...@googlegroups.com, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, work...@vger.kernel.org
On Sat, Jun 06, 2026 at 09:38:17AM +0800, David Gow wrote:
> When a KUnit test (or other KTAP test) is skipped, a "skip reason" can be
> provided. kunit.py has never done anything with this, ignoring anything
> included in the KTAP output after the 'SKIP' directive.
>
> Since we have it, and it's used, print it in a nice friendly yellow in
> parentheses after a skipped test's name.
>
> (And, by parsing it, it can be included in the JUnit results as well.)
>
> Signed-off-by: David Gow <da...@davidgow.net>

Thank you; this makes my life easier so I don't have to use --raw_output
in my fortify testing. :)

Reviewed-by: Kees Cook <ke...@kernel.org>

--
Kees Cook
Reply all
Reply to author
Forward
0 new messages