Skip to content

Commit 987c136

Browse files
committed
rust: macros: add macro to easily run KUnit tests
Add a new procedural macro (`#[kunit_tests(kunit_test_suit_name)]`) to run KUnit tests using a user-space like syntax. The macro, that should be used on modules, transforms every `#[test]` in a `kunit_case!` and adds a `kunit_unsafe_test_suite!` registering all of them. The only difference with user-space tests is that instead of using `#[cfg(test)]`, `#[kunit_tests(kunit_test_suit_name)]` is used. Note that `#[cfg(CONFIG_KUNIT)]` is added so the test module is not compiled when `CONFIG_KUNIT` is set to `n`. Reviewed-by: David Gow <[email protected]> Signed-off-by: José Expósito <[email protected]>
1 parent f16a86a commit 987c136

File tree

4 files changed

+191
-0
lines changed

4 files changed

+191
-0
lines changed

rust/kernel/kunit.rs

+11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
//!
77
//! Reference: <https://www.kernel.org/doc/html/latest/dev-tools/kunit/index.html>
88
9+
use macros::kunit_tests;
10+
911
/// Asserts that a boolean expression is `true` at runtime.
1012
///
1113
/// Public but hidden since it should only be used from generated tests.
@@ -180,3 +182,12 @@ macro_rules! kunit_unsafe_test_suite {
180182
};
181183
};
182184
}
185+
186+
#[kunit_tests(rust_kernel_kunit)]
187+
mod tests {
188+
#[test]
189+
fn rust_test_kunit_kunit_tests() {
190+
let running = true;
191+
assert_eq!(running, true);
192+
}
193+
}

rust/kernel/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
#[cfg(not(CONFIG_RUST))]
3232
compile_error!("Missing kernel configuration for conditional compilation");
3333

34+
extern crate self as kernel;
35+
3436
#[cfg(not(test))]
3537
#[cfg(not(testlib))]
3638
mod allocator;

rust/macros/kunit.rs

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// SPDX-License-Identifier: GPL-2.0
2+
3+
//! Procedural macro to run KUnit tests using a user-space like syntax.
4+
//!
5+
//! Copyright (c) 2023 José Expósito <[email protected]>
6+
7+
use proc_macro::{Delimiter, Group, TokenStream, TokenTree};
8+
use std::fmt::Write;
9+
10+
pub(crate) fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
11+
if attr.to_string().is_empty() {
12+
panic!("Missing test name in #[kunit_tests(test_name)] macro")
13+
}
14+
15+
let mut tokens: Vec<_> = ts.into_iter().collect();
16+
17+
// Scan for the "mod" keyword.
18+
tokens
19+
.iter()
20+
.find_map(|token| match token {
21+
TokenTree::Ident(ident) => match ident.to_string().as_str() {
22+
"mod" => Some(true),
23+
_ => None,
24+
},
25+
_ => None,
26+
})
27+
.expect("#[kunit_tests(test_name)] attribute should only be applied to modules");
28+
29+
// Retrieve the main body. The main body should be the last token tree.
30+
let body = match tokens.pop() {
31+
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => group,
32+
_ => panic!("cannot locate main body of module"),
33+
};
34+
35+
// Get the functions set as tests. Search for `[test]` -> `fn`.
36+
let mut body_it = body.stream().into_iter();
37+
let mut tests = Vec::new();
38+
while let Some(token) = body_it.next() {
39+
match token {
40+
TokenTree::Group(ident) if ident.to_string() == "[test]" => match body_it.next() {
41+
Some(TokenTree::Ident(ident)) if ident.to_string() == "fn" => {
42+
let test_name = match body_it.next() {
43+
Some(TokenTree::Ident(ident)) => ident.to_string(),
44+
_ => continue,
45+
};
46+
tests.push(test_name);
47+
}
48+
_ => continue,
49+
},
50+
_ => (),
51+
}
52+
}
53+
54+
// Add `#[cfg(CONFIG_KUNIT)]` before the module declaration.
55+
let config_kunit = "#[cfg(CONFIG_KUNIT)]".to_owned().parse().unwrap();
56+
tokens.insert(
57+
0,
58+
TokenTree::Group(Group::new(Delimiter::None, config_kunit)),
59+
);
60+
61+
// Generate the test KUnit test suite and a test case for each `#[test]`.
62+
// The code generated for the following test module:
63+
//
64+
// ```
65+
// #[kunit_tests(kunit_test_suit_name)]
66+
// mod tests {
67+
// #[test]
68+
// fn foo() {
69+
// assert_eq!(1, 1);
70+
// }
71+
//
72+
// #[test]
73+
// fn bar() {
74+
// assert_eq!(2, 2);
75+
// }
76+
// ```
77+
//
78+
// Looks like:
79+
//
80+
// ```
81+
// unsafe extern "C" fn kunit_rust_wrapper_foo(_test: *mut kernel::bindings::kunit) {
82+
// foo();
83+
// }
84+
// static mut KUNIT_CASE_FOO: kernel::bindings::kunit_case =
85+
// kernel::kunit_case!(foo, kunit_rust_wrapper_foo);
86+
//
87+
// unsafe extern "C" fn kunit_rust_wrapper_bar(_test: * mut kernel::bindings::kunit) {
88+
// bar();
89+
// }
90+
// static mut KUNIT_CASE_BAR: kernel::bindings::kunit_case =
91+
// kernel::kunit_case!(bar, kunit_rust_wrapper_bar);
92+
//
93+
// static mut KUNIT_CASE_NULL: kernel::bindings::kunit_case = kernel::kunit_case!();
94+
//
95+
// static mut TEST_CASES : &mut[kernel::bindings::kunit_case] = unsafe {
96+
// &mut [KUNIT_CASE_FOO, KUNIT_CASE_BAR, KUNIT_CASE_NULL]
97+
// };
98+
//
99+
// kernel::kunit_unsafe_test_suite!(kunit_test_suit_name, TEST_CASES);
100+
// ```
101+
let mut kunit_macros = "".to_owned();
102+
let mut test_cases = "".to_owned();
103+
for test in tests {
104+
let kunit_wrapper_fn_name = format!("kunit_rust_wrapper_{}", test);
105+
let kunit_case_name = format!("KUNIT_CASE_{}", test.to_uppercase());
106+
let kunit_wrapper = format!(
107+
"unsafe extern \"C\" fn {}(_test: *mut kernel::bindings::kunit) {{ {}(); }}",
108+
kunit_wrapper_fn_name, test
109+
);
110+
let kunit_case = format!(
111+
"static mut {}: kernel::bindings::kunit_case = kernel::kunit_case!({}, {});",
112+
kunit_case_name, test, kunit_wrapper_fn_name
113+
);
114+
writeln!(kunit_macros, "{kunit_wrapper}").unwrap();
115+
writeln!(kunit_macros, "{kunit_case}").unwrap();
116+
writeln!(test_cases, "{kunit_case_name},").unwrap();
117+
}
118+
119+
writeln!(
120+
kunit_macros,
121+
"static mut KUNIT_CASE_NULL: kernel::bindings::kunit_case = kernel::kunit_case!();"
122+
)
123+
.unwrap();
124+
125+
writeln!(
126+
kunit_macros,
127+
"static mut TEST_CASES : &mut[kernel::bindings::kunit_case] = unsafe {{ &mut[{test_cases} KUNIT_CASE_NULL] }};"
128+
)
129+
.unwrap();
130+
131+
writeln!(
132+
kunit_macros,
133+
"kernel::kunit_unsafe_test_suite!({attr}, TEST_CASES);"
134+
)
135+
.unwrap();
136+
137+
let new_body: TokenStream = vec![body.stream(), kunit_macros.parse().unwrap()]
138+
.into_iter()
139+
.collect();
140+
141+
// Remove the `#[test]` macros.
142+
let new_body = new_body.to_string().replace("#[test]", "");
143+
tokens.push(TokenTree::Group(Group::new(
144+
Delimiter::Brace,
145+
new_body.parse().unwrap(),
146+
)));
147+
148+
tokens.into_iter().collect()
149+
}

rust/macros/lib.rs

+29
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
mod concat_idents;
66
mod helpers;
7+
mod kunit;
78
mod module;
89
mod vtable;
910

@@ -189,3 +190,31 @@ pub fn vtable(attr: TokenStream, ts: TokenStream) -> TokenStream {
189190
pub fn concat_idents(ts: TokenStream) -> TokenStream {
190191
concat_idents::concat_idents(ts)
191192
}
193+
194+
/// Registers a KUnit test suite and its test cases using a user-space like syntax.
195+
///
196+
/// This macro should be used on modules. If `CONFIG_KUNIT` (in `.config`) is `n`, the target module
197+
/// is ignored.
198+
///
199+
/// # Examples
200+
///
201+
/// ```ignore
202+
/// # use macros::kunit_tests;
203+
///
204+
/// #[kunit_tests(kunit_test_suit_name)]
205+
/// mod tests {
206+
/// #[test]
207+
/// fn foo() {
208+
/// assert_eq!(1, 1);
209+
/// }
210+
///
211+
/// #[test]
212+
/// fn bar() {
213+
/// assert_eq!(2, 2);
214+
/// }
215+
/// }
216+
/// ```
217+
#[proc_macro_attribute]
218+
pub fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
219+
kunit::kunit_tests(attr, ts)
220+
}

0 commit comments

Comments
 (0)