Files
linux/rust/macros/kunit.rs
Miguel Ojeda 42ec980024 rust: macros: simplify code using feature(extract_if)
`feature(extract_if)` [1] was stabilized in Rust 1.87.0 [2], and the last
significant change happened in Rust 1.85.0 [3] when the range parameter
was added.

That is, with our new minimum version, we can start using the feature.

Thus simplify the code using the feature and remove the TODO comment.

Suggested-by: Gary Guo <gary@garyguo.net>
Link: https://lore.kernel.org/rust-for-linux/DHHVSX66206Y.3E7I9QUNTCJ8I@garyguo.net/
Link: https://github.com/rust-lang/rust/issues/43244 [1]
Link: https://github.com/rust-lang/rust/pull/137109 [2]
Link: https://github.com/rust-lang/rust/pull/133265 [3]
Link: https://patch.msgid.link/20260405235309.418950-16-ojeda@kernel.org
Reviewed-by: Tamir Duberstein <tamird@kernel.org>
Signed-off-by: Miguel Ojeda <ojeda@kernel.org>
2026-04-07 10:00:24 +02:00

173 lines
5.4 KiB
Rust

// SPDX-License-Identifier: GPL-2.0
//! Procedural macro to run KUnit tests using a user-space like syntax.
//!
//! Copyright (c) 2023 José Expósito <jose.exposito89@gmail.com>
use std::ffi::CString;
use proc_macro2::TokenStream;
use quote::{
format_ident,
quote,
ToTokens, //
};
use syn::{
parse_quote,
Error,
Ident,
Item,
ItemMod,
LitCStr,
Result, //
};
pub(crate) fn kunit_tests(test_suite: Ident, mut module: ItemMod) -> Result<TokenStream> {
if test_suite.to_string().len() > 255 {
return Err(Error::new_spanned(
test_suite,
"test suite names cannot exceed the maximum length of 255 bytes",
));
}
// We cannot handle modules that defer to another file (e.g. `mod foo;`).
let Some((module_brace, module_items)) = module.content.take() else {
Err(Error::new_spanned(
module,
"`#[kunit_tests(test_name)]` attribute should only be applied to inline modules",
))?
};
// Make the entire module gated behind `CONFIG_KUNIT`.
module
.attrs
.insert(0, parse_quote!(#[cfg(CONFIG_KUNIT="y")]));
let mut processed_items = Vec::new();
let mut test_cases = Vec::new();
// Generate the test KUnit test suite and a test case for each `#[test]`.
//
// The code generated for the following test module:
//
// ```
// #[kunit_tests(kunit_test_suit_name)]
// mod tests {
// #[test]
// fn foo() {
// assert_eq!(1, 1);
// }
//
// #[test]
// fn bar() {
// assert_eq!(2, 2);
// }
// }
// ```
//
// Looks like:
//
// ```
// unsafe extern "C" fn kunit_rust_wrapper_foo(_test: *mut ::kernel::bindings::kunit) { foo(); }
// unsafe extern "C" fn kunit_rust_wrapper_bar(_test: *mut ::kernel::bindings::kunit) { bar(); }
//
// static mut TEST_CASES: [::kernel::bindings::kunit_case; 3] = [
// ::kernel::kunit::kunit_case(c"foo", kunit_rust_wrapper_foo),
// ::kernel::kunit::kunit_case(c"bar", kunit_rust_wrapper_bar),
// ::pin_init::zeroed(),
// ];
//
// ::kernel::kunit_unsafe_test_suite!(kunit_test_suit_name, TEST_CASES);
// ```
//
// Non-function items (e.g. imports) are preserved.
for item in module_items {
let Item::Fn(mut f) = item else {
processed_items.push(item);
continue;
};
if f.attrs
.extract_if(.., |attr| attr.path().is_ident("test"))
.count()
== 0
{
processed_items.push(Item::Fn(f));
continue;
}
let test = f.sig.ident.clone();
// Retrieve `#[cfg]` applied on the function which needs to be present on derived items too.
let cfg_attrs: Vec<_> = f
.attrs
.iter()
.filter(|attr| attr.path().is_ident("cfg"))
.cloned()
.collect();
// Before the test, override usual `assert!` and `assert_eq!` macros with ones that call
// KUnit instead.
let test_str = test.to_string();
let path = CString::new(crate::helpers::file()).expect("file path cannot contain NUL");
processed_items.push(parse_quote! {
#[allow(unused)]
macro_rules! assert {
($cond:expr $(,)?) => {{
kernel::kunit_assert!(#test_str, #path, 0, $cond);
}}
}
});
processed_items.push(parse_quote! {
#[allow(unused)]
macro_rules! assert_eq {
($left:expr, $right:expr $(,)?) => {{
kernel::kunit_assert_eq!(#test_str, #path, 0, $left, $right);
}}
}
});
// Add back the test item.
processed_items.push(Item::Fn(f));
let kunit_wrapper_fn_name = format_ident!("kunit_rust_wrapper_{test}");
let test_cstr = LitCStr::new(
&CString::new(test_str.as_str()).expect("identifier cannot contain NUL"),
test.span(),
);
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;
// Append any `cfg` attributes the user might have written on their tests so we
// don't attempt to call them when they are `cfg`'d out. An extra `use` is used
// here to reduce the length of the assert message.
#(#cfg_attrs)*
{
(*_test).status = ::kernel::bindings::kunit_status_KUNIT_SUCCESS;
use ::kernel::kunit::is_test_result_ok;
assert!(is_test_result_ok(#test()));
}
}
});
test_cases.push(quote!(
::kernel::kunit::kunit_case(#test_cstr, #kunit_wrapper_fn_name)
));
}
let num_tests_plus_1 = test_cases.len() + 1;
processed_items.push(parse_quote! {
static mut TEST_CASES: [::kernel::bindings::kunit_case; #num_tests_plus_1] = [
#(#test_cases,)*
::pin_init::zeroed(),
];
});
processed_items.push(parse_quote! {
::kernel::kunit_unsafe_test_suite!(#test_suite, TEST_CASES);
});
module.content = Some((module_brace, processed_items));
Ok(module.to_token_stream())
}