- ``--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"``.
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
class KunitParseRequest:
raw_output: Optional[str]
json: Optional[str]
+ junit: Optional[str]
summary: bool
failed: bool
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
# 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]:
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.',
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,
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,
# 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:
--- /dev/null
+# 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)
import kunit_parser
import kunit_kernel
import kunit_json
+import kunit_junit
import kunit
from kunit_printer import stdout
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')
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(
# 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),
# 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),