Merge tag 'linux_kselftest-kunit-7.2-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest

Pull kunit updates from Shuah Khan:
 "Fixes to tool and kunit core and new features to both to support JUnit
  XML (primitive) and backtrace suppression API:
   - Core support for suppressing warning backtraces
   - Parse and print the reason tests are skipped
   - Add (primitive) support for outputting JUnit XML
   - Don't write to stdout when it should be disabled
   - Add backtrace suppression self-tests
   - Suppress intentional warning backtraces in scaling unit tests
   - Add documentation for warning backtrace suppression API
   - Fix spelling mistakes in comments and messages
   - gen_compile_commands: Ignore libgcc.a
   - qemu_configs: Add or1k / openrisc configuration"

* tag 'linux_kselftest-kunit-7.2-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest:
  kunit:tool: Don't write to stdout when it should be disabled
  kunit: tool: Add (primitive) support for outputting JUnit XML
  kunit: tool: Parse and print the reason tests are skipped
  kunit: Add documentation for warning backtrace suppression API
  drm: Suppress intentional warning backtraces in scaling unit tests
  kunit: Add backtrace suppression self-tests
  bug/kunit: Core support for suppressing warning backtraces
  kunit: Fix spelling mistakes in comments and messages
  kunit: qemu_configs: Add or1k / openrisc configuration
  gen_compile_commands: Ignore libgcc.a
This commit is contained in:
Linus Torvalds
2026-06-16 16:33:57 +05:30
18 changed files with 730 additions and 27 deletions

View File

@@ -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"``.

View File

@@ -157,6 +157,50 @@ Alternatively, one can take full control over the error message by using
if (some_setup_function())
KUNIT_FAIL(test, "Failed to setup thing for testing");
Suppressing warning backtraces
------------------------------
Some unit tests trigger warning backtraces either intentionally or as a side
effect. Such backtraces are normally undesirable since they distract from
the actual test and may result in the impression that there is a problem.
Backtraces can be suppressed with **task-scoped suppression**: while
suppression is active on the current task, the backtrace and stack dump from
``WARN*()``, ``WARN_ON*()``, and related macros on that task are suppressed.
Two API forms are available.
- Scoped suppression is the simplest form. Wrap the code that triggers
warnings in a ``kunit_warning_suppress()`` block:
.. code-block:: c
static void some_test(struct kunit *test)
{
kunit_warning_suppress(test) {
trigger_backtrace();
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1);
}
}
.. note::
The warning count must be checked inside the block; the suppression handle
is not accessible after the block exits.
- Direct functions return an explicit handle pointer. Use them when the handle
needs to be retained or passed across helper functions:
.. code-block:: c
static void some_test(struct kunit *test)
{
struct kunit_suppressed_warning *w;
w = kunit_start_suppress_warning(test);
trigger_backtrace();
kunit_end_suppress_warning(test, w);
KUNIT_EXPECT_EQ(test, kunit_suppressed_warning_count(w), 1);
}
Test Suites
~~~~~~~~~~~
@@ -1211,4 +1255,4 @@ For example:
dev_managed_string = devm_kstrdup(fake_device, "Hello, World!");
// Everything is cleaned up automatically when the test ends.
}
}

View File

@@ -10,6 +10,7 @@
#include <drm/drm_rect.h>
#include <drm/drm_mode.h>
#include <linux/limits.h>
#include <linux/string_helpers.h>
#include <linux/errno.h>
@@ -407,10 +408,27 @@ KUNIT_ARRAY_PARAM(drm_rect_scale, drm_rect_scale_cases, drm_rect_scale_case_desc
static void drm_test_rect_calc_hscale(struct kunit *test)
{
const struct drm_rect_scale_case *params = test->param_value;
int scaling_factor;
int expected_warnings = params->expected_scaling_factor == -EINVAL;
int scaling_factor = INT_MIN;
scaling_factor = drm_rect_calc_hscale(&params->src, &params->dst,
params->min_range, params->max_range);
/*
* Without CONFIG_BUG, WARN_ON() is a no-op and the suppressed warning
* count stays zero, failing the assertion.
*/
if (expected_warnings && !IS_ENABLED(CONFIG_BUG))
kunit_skip(test, "requires CONFIG_BUG");
/*
* drm_rect_calc_hscale() generates a warning backtrace whenever bad
* parameters are passed to it. This affects unit tests with -EINVAL
* error code in expected_scaling_factor.
*/
kunit_warning_suppress(test) {
scaling_factor = drm_rect_calc_hscale(&params->src, &params->dst,
params->min_range,
params->max_range);
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, expected_warnings);
}
KUNIT_EXPECT_EQ(test, scaling_factor, params->expected_scaling_factor);
}
@@ -418,10 +436,26 @@ static void drm_test_rect_calc_hscale(struct kunit *test)
static void drm_test_rect_calc_vscale(struct kunit *test)
{
const struct drm_rect_scale_case *params = test->param_value;
int scaling_factor;
int expected_warnings = params->expected_scaling_factor == -EINVAL;
int scaling_factor = INT_MIN;
scaling_factor = drm_rect_calc_vscale(&params->src, &params->dst,
params->min_range, params->max_range);
/*
* Without CONFIG_BUG, WARN_ON() is a no-op and the suppressed warning
* count stays zero, failing the assertion.
*/
if (expected_warnings && !IS_ENABLED(CONFIG_BUG))
kunit_skip(test, "requires CONFIG_BUG");
/*
* drm_rect_calc_vscale() generates a warning backtrace whenever bad
* parameters are passed to it. This affects unit tests with -EINVAL
* error code in expected_scaling_factor.
*/
kunit_warning_suppress(test) {
scaling_factor = drm_rect_calc_vscale(&params->src, &params->dst,
params->min_range, params->max_range);
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, expected_warnings);
}
KUNIT_EXPECT_EQ(test, scaling_factor, params->expected_scaling_factor);
}

View File

@@ -10,6 +10,7 @@
#define _KUNIT_TEST_BUG_H
#include <linux/stddef.h> /* for NULL */
#include <linux/types.h> /* for bool */
#if IS_ENABLED(CONFIG_KUNIT)
@@ -23,6 +24,7 @@ DECLARE_STATIC_KEY_FALSE(kunit_running);
extern struct kunit_hooks_table {
__printf(3, 4) void (*fail_current_test)(const char*, int, const char*, ...);
void *(*get_static_stub_address)(struct kunit *test, void *real_fn_addr);
bool (*is_suppressed_warning)(bool count);
} kunit_hooks;
/**
@@ -60,9 +62,33 @@ static inline struct kunit *kunit_get_current_test(void)
} \
} while (0)
/**
* kunit_is_suppressed_warning() - Check if warnings are being suppressed
* by the current KUnit test.
* @count: if true, increment the suppression counter on match.
*
* Returns true if the current task has active warning suppression.
* Uses the kunit_running static branch for zero overhead when no tests run.
*
* A single WARN*() may traverse multiple call sites in the warning path
* (e.g., __warn_printk() and __report_bug()). Pass @count = true at the
* primary suppression point to count each warning exactly once, and
* @count = false at secondary points to suppress output without
* inflating the count.
*/
static inline bool kunit_is_suppressed_warning(bool count)
{
if (!static_branch_unlikely(&kunit_running))
return false;
return kunit_hooks.is_suppressed_warning &&
kunit_hooks.is_suppressed_warning(count);
}
#else
static inline struct kunit *kunit_get_current_test(void) { return NULL; }
static inline bool kunit_is_suppressed_warning(bool count) { return false; }
#define kunit_fail_current_test(fmt, ...) do {} while (0)

View File

@@ -1796,4 +1796,102 @@ do { \
// include resource.h themselves if they need it.
#include <kunit/resource.h>
/*
* Warning backtrace suppression API.
*
* Suppresses WARN*() backtraces on the current task while active. Two forms
* are provided:
*
* - Scoped: kunit_warning_suppress(test) { ... }
* Suppression is active for the duration of the block. On normal exit,
* the for-loop increment deactivates suppression. On early exit (break,
* return, goto), the __cleanup attribute fires. On kthread_exit() (e.g.,
* a failed KUnit assertion), kunit_add_action() cleans up at test
* teardown. The suppression handle is only accessible inside the block,
* so warning counts must be checked before the block exits.
*
* - Direct: kunit_start_suppress_warning() / kunit_end_suppress_warning()
* The underlying functions, returning an explicit handle pointer. Use
* when the handle needs to be retained (e.g., for post-suppression
* count checks) or passed across helper functions.
*/
struct kunit_suppressed_warning;
struct kunit_suppressed_warning *
kunit_start_suppress_warning(struct kunit *test);
void kunit_end_suppress_warning(struct kunit *test,
struct kunit_suppressed_warning *w);
int kunit_suppressed_warning_count(struct kunit_suppressed_warning *w);
void __kunit_suppress_auto_cleanup(struct kunit_suppressed_warning **wp);
bool kunit_has_active_suppress_warning(void);
/**
* kunit_warning_suppress() - Suppress WARN*() backtraces for the duration
* of a block.
* @test: The test context object.
*
* Scoped form of the suppression API. Suppression starts when the block is
* entered and ends automatically when the block exits through any path. See
* the section comment above for the cleanup guarantees on each exit path.
* Fails the test if suppression is already active; nesting is not supported.
*
* The warning count can be checked inside the block via
* KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(). The handle is not accessible
* after the block exits.
*
* Example::
*
* kunit_warning_suppress(test) {
* trigger_warning();
* KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1);
* }
*/
#define kunit_warning_suppress(test) \
for (struct kunit_suppressed_warning *__kunit_suppress \
__cleanup(__kunit_suppress_auto_cleanup) = \
kunit_start_suppress_warning(test); \
__kunit_suppress; \
kunit_end_suppress_warning(test, __kunit_suppress), \
__kunit_suppress = NULL)
/**
* KUNIT_SUPPRESSED_WARNING_COUNT() - Returns the suppressed warning count.
*
* Returns the number of WARN*() calls suppressed since the current
* suppression block started, or 0 if the handle is NULL. Usable inside a
* kunit_warning_suppress() block.
*/
#define KUNIT_SUPPRESSED_WARNING_COUNT() \
kunit_suppressed_warning_count(__kunit_suppress)
/**
* KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT() - Sets an expectation that the
* suppressed warning count equals
* @expected.
* @test: The test context object.
* @expected: an expression that evaluates to the expected warning count.
*
* Sets an expectation that the number of suppressed WARN*() calls equals
* @expected. This is semantically equivalent to
* KUNIT_EXPECT_EQ(@test, KUNIT_SUPPRESSED_WARNING_COUNT(), @expected).
* See KUNIT_EXPECT_EQ() for more information.
*/
#define KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, expected) \
KUNIT_EXPECT_EQ(test, KUNIT_SUPPRESSED_WARNING_COUNT(), expected)
/**
* KUNIT_ASSERT_SUPPRESSED_WARNING_COUNT() - Sets an assertion that the
* suppressed warning count equals
* @expected.
* @test: The test context object.
* @expected: an expression that evaluates to the expected warning count.
*
* Sets an assertion that the number of suppressed WARN*() calls equals
* @expected. This is the same as KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(),
* except it causes an assertion failure (see KUNIT_ASSERT_TRUE()) when the
* assertion is not met.
*/
#define KUNIT_ASSERT_SUPPRESSED_WARNING_COUNT(test, expected) \
KUNIT_ASSERT_EQ(test, KUNIT_SUPPRESSED_WARNING_COUNT(), expected)
#endif /* _KUNIT_TEST_H */

View File

@@ -39,6 +39,7 @@
#include <linux/sys_info.h>
#include <trace/events/error_report.h>
#include <asm/sections.h>
#include <kunit/test-bug.h>
#define PANIC_TIMER_STEP 100
#define PANIC_BLINK_SPD 18
@@ -1124,6 +1125,11 @@ void warn_slowpath_fmt(const char *file, int line, unsigned taint,
bool rcu = warn_rcu_enter();
struct warn_args args;
if (kunit_is_suppressed_warning(true)) {
warn_rcu_exit(rcu);
return;
}
pr_warn(CUT_HERE);
if (!fmt) {
@@ -1146,6 +1152,11 @@ void __warn_printk(const char *fmt, ...)
bool rcu = warn_rcu_enter();
va_list args;
if (kunit_is_suppressed_warning(false)) {
warn_rcu_exit(rcu);
return;
}
pr_warn(CUT_HERE);
va_start(args, fmt);

View File

@@ -48,6 +48,7 @@
#include <linux/rculist.h>
#include <linux/ftrace.h>
#include <linux/context_tracking.h>
#include <kunit/test-bug.h>
extern struct bug_entry __start___bug_table[], __stop___bug_table[];
@@ -209,8 +210,6 @@ static enum bug_trap_type __report_bug(struct bug_entry *bug, unsigned long buga
return BUG_TRAP_TYPE_NONE;
}
disable_trace_on_warning();
bug_get_file_line(bug, &file, &line);
fmt = bug_get_format(bug);
@@ -220,6 +219,17 @@ static enum bug_trap_type __report_bug(struct bug_entry *bug, unsigned long buga
no_cut = bug->flags & BUGFLAG_NO_CUT_HERE;
has_args = bug->flags & BUGFLAG_ARGS;
#ifdef CONFIG_KUNIT
/*
* Before the once logic so suppressed warnings do not consume
* the single-fire budget of WARN_ON_ONCE().
*/
if (warning && kunit_is_suppressed_warning(true))
return BUG_TRAP_TYPE_WARN;
#endif
disable_trace_on_warning();
if (warning && once) {
if (done)
return BUG_TRAP_TYPE_WARN;

View File

@@ -10,7 +10,8 @@ kunit-objs += test.o \
executor.o \
attributes.o \
device.o \
platform.o
platform.o \
bug.o
ifeq ($(CONFIG_KUNIT_DEBUGFS),y)
kunit-objs += debugfs.o
@@ -21,6 +22,7 @@ obj-$(if $(CONFIG_KUNIT),y) += hooks.o
obj-$(CONFIG_KUNIT_TEST) += kunit-test.o
obj-$(CONFIG_KUNIT_TEST) += platform-test.o
obj-$(CONFIG_KUNIT_TEST) += backtrace-suppression-test.o
# string-stream-test compiles built-in only.
ifeq ($(CONFIG_KUNIT_TEST),y)

View File

@@ -0,0 +1,198 @@
// SPDX-License-Identifier: GPL-2.0
/*
* KUnit test for suppressing warning tracebacks.
*
* Copyright (C) 2024, Guenter Roeck
* Author: Guenter Roeck <linux@roeck-us.net>
*/
#include <kunit/test.h>
#include <linux/bug.h>
#include <linux/completion.h>
#include <linux/kthread.h>
static void backtrace_suppression_test_warn_direct(struct kunit *test)
{
if (!IS_ENABLED(CONFIG_BUG))
kunit_skip(test, "requires CONFIG_BUG");
kunit_warning_suppress(test) {
WARN(1, "This backtrace should be suppressed");
/*
* Count must be checked inside the scope; the handle
* is not accessible after the block exits.
*/
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1);
}
KUNIT_EXPECT_FALSE(test, kunit_has_active_suppress_warning());
}
static noinline void trigger_backtrace_warn(void)
{
WARN(1, "This backtrace should be suppressed");
}
static void backtrace_suppression_test_warn_indirect(struct kunit *test)
{
if (!IS_ENABLED(CONFIG_BUG))
kunit_skip(test, "requires CONFIG_BUG");
kunit_warning_suppress(test) {
trigger_backtrace_warn();
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1);
}
}
static void backtrace_suppression_test_warn_multi(struct kunit *test)
{
if (!IS_ENABLED(CONFIG_BUG))
kunit_skip(test, "requires CONFIG_BUG");
kunit_warning_suppress(test) {
WARN(1, "This backtrace should be suppressed");
trigger_backtrace_warn();
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 2);
}
}
static void backtrace_suppression_test_warn_on_direct(struct kunit *test)
{
if (!IS_ENABLED(CONFIG_BUG))
kunit_skip(test, "requires CONFIG_BUG");
if (!IS_ENABLED(CONFIG_DEBUG_BUGVERBOSE) && !IS_ENABLED(CONFIG_KALLSYMS))
kunit_skip(test, "requires CONFIG_DEBUG_BUGVERBOSE or CONFIG_KALLSYMS");
kunit_warning_suppress(test) {
WARN_ON(1);
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1);
}
}
static noinline void trigger_backtrace_warn_on(void)
{
WARN_ON(1);
}
static void backtrace_suppression_test_warn_on_indirect(struct kunit *test)
{
if (!IS_ENABLED(CONFIG_BUG))
kunit_skip(test, "requires CONFIG_BUG");
if (!IS_ENABLED(CONFIG_DEBUG_BUGVERBOSE))
kunit_skip(test, "requires CONFIG_DEBUG_BUGVERBOSE");
kunit_warning_suppress(test) {
trigger_backtrace_warn_on();
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1);
}
}
static void backtrace_suppression_test_count(struct kunit *test)
{
if (!IS_ENABLED(CONFIG_BUG))
kunit_skip(test, "requires CONFIG_BUG");
kunit_warning_suppress(test) {
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 0);
WARN(1, "suppressed");
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1);
WARN(1, "suppressed again");
KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 2);
}
}
static void backtrace_suppression_test_active_state(struct kunit *test)
{
KUNIT_EXPECT_FALSE(test, kunit_has_active_suppress_warning());
kunit_warning_suppress(test) {
KUNIT_EXPECT_TRUE(test, kunit_has_active_suppress_warning());
}
KUNIT_EXPECT_FALSE(test, kunit_has_active_suppress_warning());
kunit_warning_suppress(test) {
KUNIT_EXPECT_TRUE(test, kunit_has_active_suppress_warning());
}
KUNIT_EXPECT_FALSE(test, kunit_has_active_suppress_warning());
}
static void backtrace_suppression_test_multi_scope(struct kunit *test)
{
struct kunit_suppressed_warning *sw1, *sw2;
if (!IS_ENABLED(CONFIG_BUG))
kunit_skip(test, "requires CONFIG_BUG");
if (!IS_ENABLED(CONFIG_DEBUG_BUGVERBOSE))
kunit_skip(test, "requires CONFIG_DEBUG_BUGVERBOSE");
sw1 = kunit_start_suppress_warning(test);
trigger_backtrace_warn_on();
WARN(1, "suppressed by sw1");
kunit_end_suppress_warning(test, sw1);
sw2 = kunit_start_suppress_warning(test);
WARN(1, "suppressed by sw2");
kunit_end_suppress_warning(test, sw2);
KUNIT_EXPECT_EQ(test, kunit_suppressed_warning_count(sw1), 2);
KUNIT_EXPECT_EQ(test, kunit_suppressed_warning_count(sw2), 1);
}
struct cross_kthread_data {
bool was_active;
struct completion done;
};
static int cross_kthread_fn(void *data)
{
struct cross_kthread_data *d = data;
d->was_active = kunit_has_active_suppress_warning();
complete(&d->done);
while (!kthread_should_stop())
schedule();
return 0;
}
static void backtrace_suppression_test_cross_kthread(struct kunit *test)
{
struct cross_kthread_data data;
struct task_struct *task;
data.was_active = false;
init_completion(&data.done);
kunit_warning_suppress(test) {
task = kthread_run(cross_kthread_fn, &data, "kunit-cross-test");
KUNIT_ASSERT_FALSE(test, IS_ERR(task));
wait_for_completion(&data.done);
kthread_stop(task);
}
KUNIT_EXPECT_FALSE(test, data.was_active);
}
static struct kunit_case backtrace_suppression_test_cases[] = {
KUNIT_CASE(backtrace_suppression_test_warn_direct),
KUNIT_CASE(backtrace_suppression_test_warn_indirect),
KUNIT_CASE(backtrace_suppression_test_warn_multi),
KUNIT_CASE(backtrace_suppression_test_warn_on_direct),
KUNIT_CASE(backtrace_suppression_test_warn_on_indirect),
KUNIT_CASE(backtrace_suppression_test_count),
KUNIT_CASE(backtrace_suppression_test_active_state),
KUNIT_CASE(backtrace_suppression_test_multi_scope),
KUNIT_CASE(backtrace_suppression_test_cross_kthread),
{}
};
static struct kunit_suite backtrace_suppression_test_suite = {
.name = "backtrace-suppression-test",
.test_cases = backtrace_suppression_test_cases,
};
kunit_test_suites(&backtrace_suppression_test_suite);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("KUnit test to verify warning backtrace suppression");

120
lib/kunit/bug.c Normal file
View File

@@ -0,0 +1,120 @@
// SPDX-License-Identifier: GPL-2.0
/*
* KUnit helpers for backtrace suppression
*
* Copyright (C) 2025 Alessandro Carminati <acarmina@redhat.com>
* Copyright (C) 2024 Guenter Roeck <linux@roeck-us.net>
*/
#include <kunit/resource.h>
#include <linux/export.h>
#include <linux/rculist.h>
#include <linux/sched.h>
#include <linux/sched/task.h>
#include <linux/spinlock.h>
#include "hooks-impl.h"
struct kunit_suppressed_warning {
struct list_head node;
struct task_struct *task;
struct kunit *test;
atomic_t counter;
};
static LIST_HEAD(suppressed_warnings);
static DEFINE_SPINLOCK(suppressed_warnings_lock);
static void kunit_suppress_warning_remove(struct kunit_suppressed_warning *w)
{
unsigned long flags;
spin_lock_irqsave(&suppressed_warnings_lock, flags);
list_del_rcu(&w->node);
spin_unlock_irqrestore(&suppressed_warnings_lock, flags);
put_task_struct(w->task);
}
KUNIT_DEFINE_ACTION_WRAPPER(kunit_suppress_warning_cleanup,
kunit_suppress_warning_remove,
struct kunit_suppressed_warning *);
bool kunit_has_active_suppress_warning(void)
{
return __kunit_is_suppressed_warning_impl(false);
}
EXPORT_SYMBOL_GPL(kunit_has_active_suppress_warning);
struct kunit_suppressed_warning *
kunit_start_suppress_warning(struct kunit *test)
{
struct kunit_suppressed_warning *w;
unsigned long flags;
int ret;
if (kunit_has_active_suppress_warning()) {
KUNIT_FAIL(test, "Another suppression block is already active");
return NULL;
}
w = kunit_kzalloc(test, sizeof(*w), GFP_KERNEL);
if (!w) {
KUNIT_FAIL(test, "Failed to allocate suppression handle.");
return NULL;
}
w->task = get_task_struct(current);
w->test = test;
spin_lock_irqsave(&suppressed_warnings_lock, flags);
list_add_rcu(&w->node, &suppressed_warnings);
spin_unlock_irqrestore(&suppressed_warnings_lock, flags);
ret = kunit_add_action_or_reset(test,
kunit_suppress_warning_cleanup, w);
if (ret) {
KUNIT_FAIL(test, "Failed to add suppression cleanup action.");
return NULL;
}
return w;
}
EXPORT_SYMBOL_GPL(kunit_start_suppress_warning);
void kunit_end_suppress_warning(struct kunit *test,
struct kunit_suppressed_warning *w)
{
if (!w)
return;
kunit_release_action(test, kunit_suppress_warning_cleanup, w);
}
EXPORT_SYMBOL_GPL(kunit_end_suppress_warning);
void __kunit_suppress_auto_cleanup(struct kunit_suppressed_warning **wp)
{
if (*wp)
kunit_end_suppress_warning((*wp)->test, *wp);
}
EXPORT_SYMBOL_GPL(__kunit_suppress_auto_cleanup);
int kunit_suppressed_warning_count(struct kunit_suppressed_warning *w)
{
return w ? atomic_read(&w->counter) : 0;
}
EXPORT_SYMBOL_GPL(kunit_suppressed_warning_count);
bool __kunit_is_suppressed_warning_impl(bool count)
{
struct kunit_suppressed_warning *w;
guard(rcu)();
list_for_each_entry_rcu(w, &suppressed_warnings, node) {
if (w->task == current) {
if (count)
atomic_inc(&w->counter);
return true;
}
}
return false;
}

View File

@@ -19,6 +19,7 @@ void __printf(3, 4) __kunit_fail_current_test_impl(const char *file,
int line,
const char *fmt, ...);
void *__kunit_get_static_stub_address_impl(struct kunit *test, void *real_fn_addr);
bool __kunit_is_suppressed_warning_impl(bool count);
/* Code to set all of the function pointers. */
static inline void kunit_install_hooks(void)
@@ -26,6 +27,7 @@ static inline void kunit_install_hooks(void)
/* Install the KUnit hook functions. */
kunit_hooks.fail_current_test = __kunit_fail_current_test_impl;
kunit_hooks.get_static_stub_address = __kunit_get_static_stub_address_impl;
kunit_hooks.is_suppressed_warning = __kunit_is_suppressed_warning_impl;
}
#endif /* _KUNIT_HOOKS_IMPL_H */

View File

@@ -201,6 +201,8 @@ def main():
# Modules are listed in modules.order.
if os.path.isdir(path):
cmdfiles = cmdfiles_in_dir(path)
elif os.path.basename(path) == 'libgcc.a':
cmdfiles = []
elif path.endswith('.a'):
cmdfiles = cmdfiles_for_a(path, ar)
elif path.endswith('modules.order'):

View File

@@ -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:

View File

@@ -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)

View File

@@ -218,7 +218,7 @@ def _get_qemu_ops(config_path: str,
# exists (I learned this through experimentation and could not find it
# anywhere in the Python documentation).
#
# Bascially, we completely ignore the actual file location of the config
# Basically, we completely ignore the actual file location of the config
# we are loading and just tell Python that the module lives in the
# QEMU_CONFIGS_DIR for import purposes regardless of where it actually
# exists as a file.

View File

@@ -17,7 +17,7 @@ import textwrap
from enum import Enum, auto
from typing import Iterable, Iterator, List, Optional, Tuple
from kunit_printer import Printer, stdout
from kunit_printer import Printer
class Test:
"""
@@ -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."""
@@ -57,7 +58,7 @@ class Test:
def add_error(self, printer: Printer, error_message: str) -> None:
"""Records an error that occurred while parsing this test."""
self.counts.errors += 1
printer.print_with_timestamp(stdout.red('[ERROR]') + f' Test: {self.name}: {error_message}')
printer.print_with_timestamp(printer.red('[ERROR]') + f' Test: {self.name}: {error_message}')
def ok_status(self) -> bool:
"""Returns true if the status was ok, i.e. passed or skipped."""
@@ -268,7 +269,7 @@ def check_version(version_num: int, accepted_versions: List[int],
if version_num < min(accepted_versions):
test.add_error(printer, f'{version_type} version lower than expected!')
elif version_num > max(accepted_versions):
test.add_error(printer, f'{version_type} version higer than expected!')
test.add_error(printer, f'{version_type} version higher than expected!')
def parse_ktap_header(lines: LineStream, test: Test, printer: Printer) -> bool:
"""
@@ -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,12 +541,15 @@ 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:
print_log(test.log, printer)
return stdout.red('[CRASHED] ') + test.name
return printer.red('[CRASHED] ') + test.name
print_log(test.log, printer)
return printer.red('[FAILED] ') + test.name
@@ -651,11 +656,11 @@ def print_summary_line(test: Test, printer: Printer) -> None:
printer - Printer object to output results
"""
if test.status == TestStatus.SUCCESS:
color = stdout.green
color = printer.green
elif test.status in (TestStatus.SKIPPED, TestStatus.NO_TESTS):
color = stdout.yellow
color = printer.yellow
else:
color = stdout.red
color = printer.red
printer.print_with_timestamp(color(f'Testing complete. {test.counts}'))
# Summarize failures that might have gone off-screen since we had a lot

View File

@@ -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
@@ -235,10 +236,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:
@@ -676,6 +694,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 +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(
@@ -936,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),
@@ -949,7 +999,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),

View File

@@ -0,0 +1,18 @@
# SPDX-License-Identifier: GPL-2.0-only
from ..qemu_config import QemuArchParams
QEMU_ARCH = QemuArchParams(linux_arch='openrisc',
kconfig='''
CONFIG_SERIAL_8250=y
CONFIG_SERIAL_8250_CONSOLE=y
CONFIG_SERIAL_OF_PLATFORM=y
CONFIG_POWER_RESET=y
CONFIG_POWER_RESET_SYSCON=y
''',
qemu_arch='or1k',
kernel_path='vmlinux',
kernel_command_line='console=ttyS0',
extra_qemu_params=[
'-machine', 'virt',
'-m', '512',
])