Skip to content

Commit 49b40c8

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_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`. Signed-off-by: José Expósito <jose.exposito89@gmail.com>
1 parent 2dc7ae0 commit 49b40c8

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

rust/macros/kunit.rs

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 <jose.exposito89@gmail.com>
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_stub_foo(_test: *mut crate::bindings::kunit) {
82+
// foo();
83+
// }
84+
// static mut KUNIT_CASE_FOO: crate::bindings::kunit_case = crate::kunit_case!(foo, kunit_stub_foo);
85+
//
86+
// unsafe extern "C" fn kunit_stub_bar(_test: * mut crate::bindings::kunit) {
87+
// bar();
88+
// }
89+
// static mut KUNIT_CASE_BAR: crate::bindings::kunit_case = crate::kunit_case!(bar, kunit_stub_bar);
90+
//
91+
// static mut KUNIT_CASE_NULL: crate::bindings::kunit_case = crate::kunit_case!();
92+
//
93+
// static mut TEST_CASES : &mut[crate::bindings::kunit_case] = unsafe {
94+
// &mut [KUNIT_CASE_FOO, KUNIT_CASE_BAR, KUNIT_CASE_NULL]
95+
// };
96+
//
97+
// crate::kunit_test_suite!(kunit_test_suit_name, TEST_CASES);
98+
// ```
99+
let mut kunit_macros = "".to_owned();
100+
let mut test_cases = "".to_owned();
101+
for test in tests {
102+
let kunit_stub_fn_name = format!("kunit_stub_{}", test);
103+
let kunit_case_name = format!("KUNIT_CASE_{}", test.to_uppercase());
104+
let kunit_stub = format!(
105+
"unsafe extern \"C\" fn {}(_test: *mut crate::bindings::kunit) {{ {}(); }}",
106+
kunit_stub_fn_name, test
107+
);
108+
let kunit_case = format!(
109+
"static mut {}: crate::bindings::kunit_case = crate::kunit_case!({}, {});",
110+
kunit_case_name, test, kunit_stub_fn_name
111+
);
112+
writeln!(kunit_macros, "{kunit_stub}").unwrap();
113+
writeln!(kunit_macros, "{kunit_case}").unwrap();
114+
writeln!(test_cases, "{kunit_case_name},").unwrap();
115+
}
116+
117+
writeln!(
118+
kunit_macros,
119+
"static mut KUNIT_CASE_NULL: crate::bindings::kunit_case = crate::kunit_case!();"
120+
)
121+
.unwrap();
122+
123+
writeln!(
124+
kunit_macros,
125+
"static mut TEST_CASES : &mut[crate::bindings::kunit_case] = unsafe {{ &mut[{test_cases} KUNIT_CASE_NULL] }};"
126+
)
127+
.unwrap();
128+
129+
writeln!(
130+
kunit_macros,
131+
"crate::kunit_test_suite!({attr}, TEST_CASES);"
132+
)
133+
.unwrap();
134+
135+
let new_body: TokenStream = vec![body.stream(), kunit_macros.parse().unwrap()]
136+
.into_iter()
137+
.collect();
138+
139+
// Remove the `#[test]` macros.
140+
let new_body = new_body.to_string().replace("#[test]", "");
141+
tokens.push(TokenTree::Group(Group::new(
142+
Delimiter::Brace,
143+
new_body.parse().unwrap(),
144+
)));
145+
146+
tokens.into_iter().collect()
147+
}

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)