[PATCH RFC 1/3] rust: kunit: add #[should_panic] support
From: Nicolás Antinori
Date: Tue Sep 15 2026 - 15:37:53 EST
KUnit tests in Rust are written using user-space like syntax. This patch
adds support for the `#[should_panic]` attribute, enabling the user to test
conditions that are expected to cause a panic and report the test as
successful.
Signed-off-by: Nicolás Antinori <nico.antinori.7@xxxxxxxxx>
---
include/kunit/test.h | 1 +
include/kunit/try-catch.h | 1 +
lib/kunit/test.c | 14 ++++++++++++
lib/kunit/try-catch.c | 7 ++++++
rust/kernel/kunit.rs | 9 ++++++++
rust/kernel/lib.rs | 46 ++++++++++++++++++++++++++++++++++++---
rust/macros/kunit.rs | 21 ++++++++++++++++--
7 files changed, 94 insertions(+), 5 deletions(-)
diff --git a/include/kunit/test.h b/include/kunit/test.h
index da5312e0dfa5..8b42f431e1c1 100644
--- a/include/kunit/test.h
+++ b/include/kunit/test.h
@@ -723,6 +723,7 @@ void __printf(2, 3) kunit_log_append(struct string_stream *log, const char *fmt,
#define KUNIT_SUCCEED(test) _KUNIT_SAVE_LOC(test)
void __noreturn __kunit_abort(struct kunit *test);
+void __noreturn __kunit_abort_expecting_error(struct kunit *test);
void __printf(6, 7) __kunit_do_failed_assertion(struct kunit *test,
const struct kunit_loc *loc,
diff --git a/include/kunit/try-catch.h b/include/kunit/try-catch.h
index d4e1a5b98ed6..a47b1cfbcf93 100644
--- a/include/kunit/try-catch.h
+++ b/include/kunit/try-catch.h
@@ -54,6 +54,7 @@ struct kunit_try_catch {
void kunit_try_catch_run(struct kunit_try_catch *try_catch, void *context);
void __noreturn kunit_try_catch_throw(struct kunit_try_catch *try_catch);
+void __noreturn kunit_try_catch_throw_expecting_error(struct kunit_try_catch *try_catch);
static inline int kunit_try_catch_get_result(struct kunit_try_catch *try_catch)
{
diff --git a/lib/kunit/test.c b/lib/kunit/test.c
index 09e3dabfac0c..32e5419a2d52 100644
--- a/lib/kunit/test.c
+++ b/lib/kunit/test.c
@@ -323,6 +323,20 @@ void __noreturn __kunit_abort(struct kunit *test)
}
EXPORT_SYMBOL_GPL(__kunit_abort);
+void __noreturn __kunit_abort_expecting_error(struct kunit *test)
+{
+ kunit_try_catch_throw_expecting_error(&test->try_catch); /* Does not return. */
+
+ /*
+ * Throw could not abort from test.
+ *
+ * XXX: we should never reach this line! As kunit_try_catch_throw_expecting_error
+ * is marked __noreturn.
+ */
+ WARN_ONCE(true, "Throw could not abort from test!\n");
+}
+EXPORT_SYMBOL_GPL(__kunit_abort_expecting_error);
+
void __kunit_do_failed_assertion(struct kunit *test,
const struct kunit_loc *loc,
enum kunit_assert_type type,
diff --git a/lib/kunit/try-catch.c b/lib/kunit/try-catch.c
index d84a879f0a78..123e1f86a5b3 100644
--- a/lib/kunit/try-catch.c
+++ b/lib/kunit/try-catch.c
@@ -22,6 +22,13 @@ void __noreturn kunit_try_catch_throw(struct kunit_try_catch *try_catch)
}
EXPORT_SYMBOL_GPL(kunit_try_catch_throw);
+void __noreturn kunit_try_catch_throw_expecting_error(struct kunit_try_catch *try_catch)
+{
+ try_catch->try_result = 0;
+ kthread_exit(0);
+}
+EXPORT_SYMBOL_GPL(kunit_try_catch_throw_expecting_error);
+
static int kunit_generic_run_threadfn_adapter(void *data)
{
struct kunit_try_catch *try_catch = data;
diff --git a/rust/kernel/kunit.rs b/rust/kernel/kunit.rs
index 91eaff8c186a..65a1040ee2b0 100644
--- a/rust/kernel/kunit.rs
+++ b/rust/kernel/kunit.rs
@@ -9,6 +9,9 @@
use crate::fmt;
use crate::prelude::*;
+#[doc(hidden)]
+pub static KUNIT_SHOULD_PANIC: u32 = 0xDEAD7357;
+
/// Prints a KUnit error-level message.
///
/// Public but hidden since it should only be used from KUnit generated code.
@@ -345,6 +348,12 @@ fn rust_test_kunit_in_kunit_test() {
assert!(in_kunit_test());
}
+ #[test]
+ #[should_panic]
+ fn rust_test_kunit_panic_in_kunit_test() {
+ panic!("This test should panic and pass");
+ }
+
#[test]
#[cfg(not(all()))]
fn rust_test_kunit_always_disabled_test() {
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index 4d5c96ddc49c..0f6c3c00ddd5 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -174,14 +174,54 @@ impl ModuleMetadata for LocalModule {
};
}
-#[cfg(not(testlib))]
-#[panic_handler]
-fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
+#[inline]
+fn bug_on_panic(info: &core::panic::PanicInfo<'_>) -> ! {
pr_emerg!("{}\n", info);
// SAFETY: FFI call.
unsafe { bindings::BUG() };
}
+#[cfg(all(not(testlib), not(CONFIG_KUNIT)))]
+#[panic_handler]
+fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
+ bug_on_panic(info);
+}
+
+#[cfg(all(not(testlib), CONFIG_KUNIT))]
+#[panic_handler]
+fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
+ // SAFETY: This function is safe to call even if CONFIG_KUNIT=n. If a null pointer is returned,
+ // the panic is handled same as if CONFIG_KUNIT=n.
+ let kunit_test = unsafe { ::bindings::kunit_get_current_test() };
+ if kunit_test.is_null() {
+ bug_on_panic(info);
+ } else {
+ // SAFETY: We are in the else branch of kunit_test.is_null() condition, meaning that
+ // `::bindings::kunit_get_current_test()` returned a kunit struct successfully.
+ let should_panic_ptr: *const u32 = unsafe { (*kunit_test).priv_ as *const u32 };
+ let should_panic_code: u32 = if should_panic_ptr.is_null() {
+ bug_on_panic(info);
+ } else {
+ // SAFETY: Already tested that the should_panic_ptr pointer is not null, casting it to
+ // its value should be safe since kunit_test is not null and KUnit Rust tests are
+ // initialized by assigning either null or a u32 value to the priv_ field.
+ unsafe { *should_panic_ptr }
+ };
+
+ if should_panic_code == crate::kunit::KUNIT_SHOULD_PANIC {
+ // SAFETY: We are in the else branch of kunit_test.is_null() condition, meaning that
+ // `::bindings::kunit_get_current_test()` returned a kunit struct successfully.
+ unsafe {
+ (*kunit_test).status = ::kernel::bindings::kunit_status_KUNIT_SUCCESS;
+ bindings::__kunit_abort_expecting_error(kunit_test);
+ };
+ } else {
+ pr_emerg!("Invalid KUnit priv_ code 0x{:x}\n", should_panic_code);
+ bug_on_panic(info);
+ }
+ }
+}
+
/// Produces a pointer to an object from a pointer to one of its fields.
///
/// If you encounter a type mismatch due to the [`Opaque`] type, then use [`Opaque::cast_into`] or
diff --git a/rust/macros/kunit.rs b/rust/macros/kunit.rs
index ae20ed6768f1..2c6405cebc0a 100644
--- a/rust/macros/kunit.rs
+++ b/rust/macros/kunit.rs
@@ -106,6 +106,11 @@ pub(crate) fn kunit_tests(test_suite: Ident, mut module: ItemMod) -> Result<Toke
.cloned()
.collect();
+ let should_panic = f
+ .attrs
+ .iter()
+ .any(|attr| attr.path().is_ident("should_panic"));
+
// Before the test, override usual `assert!` and `assert_eq!` macros with ones that call
// KUnit instead.
let test_str = test.to_string();
@@ -135,6 +140,19 @@ macro_rules! assert_eq {
&CString::new(test_str.as_str()).expect("identifier cannot contain NUL"),
test.span(),
);
+ let assertion = if should_panic {
+ quote!(
+ (*_test).priv_ = &raw const crate::kunit::KUNIT_SHOULD_PANIC as *mut ffi::c_void;
+ let _ = #test();
+ (*_test).status = ::kernel::bindings::kunit_status_KUNIT_FAILURE;
+ )
+ } else {
+ quote!(
+ (*_test).priv_ = core::ptr::null_mut();
+ use ::kernel::kunit::is_test_result_ok;
+ assert!(is_test_result_ok(#test()));
+ )
+ };
processed_items.push(parse_quote! {
unsafe extern "C" fn #kunit_wrapper_fn_name(_test: *mut ::kernel::bindings::kunit) {
(*_test).status = ::kernel::bindings::kunit_status_KUNIT_SKIPPED;
@@ -145,8 +163,7 @@ macro_rules! assert_eq {
#(#cfg_attrs)*
{
(*_test).status = ::kernel::bindings::kunit_status_KUNIT_SUCCESS;
- use ::kernel::kunit::is_test_result_ok;
- assert!(is_test_result_ok(#test()));
+ #assertion
}
}
});
--
2.47.3