Re: [PATCH v2] kunit: tool: Add (primitive) support for outputting JUnit XML

From: Thomas Weißschuh

Date: Mon May 04 2026 - 02:25:00 EST


Hi David,

On Sat, May 02, 2026 at 10:49:17AM +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 injest it and it is already useful.

"ingest"?

> Signed-off-by: David Gow <david@xxxxxxxxxxxx>
> ---
>
> Finally got around to doing a new version of this. I'm running this
> locally with Jenkins, and it's giving nice summaries of test results.
>
> Changes since v1:
> https://lore.kernel.org/all/20260119073426.1952867-1-davidgow@xxxxxxxxxx/
> - Use python's provided XML quote escaping, rather than coding our own (Thanks Thomas)

What I tried to suggest was to do all of the XML generation through the Python
module instead of manual string concatenation. The XML escaping would go away
as a side effect.

A pseudo-code example:

out = xml.sax.saxutils.XMLGenerator(f, encoding='utf-8')
out.startDocument()

for subtest in test.subtests:
out.startElement('testcase', {'name': 'subtest.name' })

if subtest.status == TestStatus.FAILURE:
out.startElement('failure', {})
out.characters('Test failed')
out.endElement('failure')

out.endElement('testcase')

How I used it recently:
https://github.com/Linutronix/elbe/blob/master/website/ext/elbedocoverview.py#L133

> - 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 | 25 ++++++++++-
> tools/testing/kunit/kunit_junit.py | 43 +++++++++++++++++++
> tools/testing/kunit/kunit_tool_test.py | 38 ++++++++++++++--
> 4 files changed, 105 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..1a7ff594b791 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,17 @@ 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:
> + junit_str = kunit_junit.get_junit_result(
> + test=test)

Unnecessary linebreak?

> + if request.junit == 'stdout':
> + print(junit_str)
> + else:
> + with open(request.junit, 'w') as f:
> + f.write(junit_str)
> + stdout.print_with_timestamp("Test results stored in %s" %
> + os.path.abspath(request.junit))

f-string?

> +
> if test.status != kunit_parser.TestStatus.SUCCESS:
> return KunitResult(KunitStatus.TEST_FAILURE, parse_time), test
>
> @@ -309,6 +322,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',
> }

(...)

But all of it are just suggestions.


Thomas