-
Notifications
You must be signed in to change notification settings - Fork 435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow to run unit tests using KUnit with a user-space like syntax #950
base: rust
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,10 @@ | |
//! | ||
//! Reference: <https://www.kernel.org/doc/html/latest/dev-tools/kunit/index.html> | ||
|
||
use crate::task::Task; | ||
use core::ops::Deref; | ||
use macros::kunit_tests; | ||
|
||
/// Asserts that a boolean expression is `true` at runtime. | ||
/// | ||
/// Public but hidden since it should only be used from generated tests. | ||
|
@@ -88,3 +92,180 @@ macro_rules! kunit_assert_eq { | |
$crate::kunit_assert!($test, $left == $right); | ||
}}; | ||
} | ||
|
||
/// Represents an individual test case. | ||
/// | ||
/// The test case should have the signature | ||
/// `unsafe extern "C" fn test_case(test: *mut crate::bindings::kunit)`. | ||
/// | ||
/// The `kunit_unsafe_test_suite!` macro expects a NULL-terminated list of test cases. This macro | ||
/// can be invoked without parameters to generate the delimiter. | ||
#[macro_export] | ||
macro_rules! kunit_case { | ||
() => { | ||
$crate::bindings::kunit_case { | ||
run_case: None, | ||
name: core::ptr::null_mut(), | ||
generate_params: None, | ||
status: $crate::bindings::kunit_status_KUNIT_SUCCESS, | ||
log: core::ptr::null_mut(), | ||
} | ||
}; | ||
($name:ident, $run_case:ident) => { | ||
$crate::bindings::kunit_case { | ||
run_case: Some($run_case), | ||
name: $crate::c_str!(core::stringify!($name)).as_char_ptr(), | ||
generate_params: None, | ||
status: $crate::bindings::kunit_status_KUNIT_SUCCESS, | ||
log: core::ptr::null_mut(), | ||
} | ||
}; | ||
} | ||
|
||
/// Registers a KUnit test suite. | ||
/// | ||
/// # Safety | ||
/// | ||
/// `test_cases` must be a NULL terminated array of test cases. | ||
/// | ||
/// # Examples | ||
/// | ||
/// ```ignore | ||
/// unsafe extern "C" fn test_fn(_test: *mut crate::bindings::kunit) { | ||
/// let actual = 1 + 1; | ||
/// let expected = 2; | ||
/// assert_eq!(actual, expected); | ||
/// } | ||
/// | ||
/// static mut KUNIT_TEST_CASE: crate::bindings::kunit_case = crate::kunit_case!(name, test_fn); | ||
/// static mut KUNIT_NULL_CASE: crate::bindings::kunit_case = crate::kunit_case!(); | ||
/// static mut KUNIT_TEST_CASES: &mut[crate::bindings::kunit_case] = unsafe { | ||
/// &mut[KUNIT_TEST_CASE, KUNIT_NULL_CASE] | ||
/// }; | ||
/// crate::kunit_unsafe_test_suite!(suite_name, KUNIT_TEST_CASES); | ||
/// ``` | ||
#[macro_export] | ||
macro_rules! kunit_unsafe_test_suite { | ||
($name:ident, $test_cases:ident) => { | ||
const _: () = { | ||
static KUNIT_TEST_SUITE_NAME: [i8; 256] = { | ||
let name_u8 = core::stringify!($name).as_bytes(); | ||
let mut ret = [0; 256]; | ||
|
||
let mut i = 0; | ||
while i < name_u8.len() { | ||
ret[i] = name_u8[i] as i8; | ||
i += 1; | ||
} | ||
|
||
ret | ||
}; | ||
|
||
// SAFETY: `test_cases` is valid as it should be static. | ||
static mut KUNIT_TEST_SUITE: core::cell::UnsafeCell<$crate::bindings::kunit_suite> = | ||
core::cell::UnsafeCell::new($crate::bindings::kunit_suite { | ||
name: KUNIT_TEST_SUITE_NAME, | ||
test_cases: unsafe { $test_cases.as_mut_ptr() }, | ||
suite_init: None, | ||
suite_exit: None, | ||
init: None, | ||
exit: None, | ||
status_comment: [0; 256usize], | ||
debugfs: core::ptr::null_mut(), | ||
log: core::ptr::null_mut(), | ||
suite_init_err: 0, | ||
}); | ||
|
||
// SAFETY: `KUNIT_TEST_SUITE` is static. | ||
#[used] | ||
#[link_section = ".kunit_test_suites"] | ||
static mut KUNIT_TEST_SUITE_ENTRY: *const $crate::bindings::kunit_suite = | ||
unsafe { KUNIT_TEST_SUITE.get() }; | ||
}; | ||
}; | ||
} | ||
|
||
/// In some cases, you need to call test-only code from outside the test case, for example, to | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may want to distinguish between this sort of "mocking", which only replaces direct calls with other KUnit mocking functionality, like the upcoming The main differences are that the static stub feature redirects all calls to a function, even if it's indirect (e.g., from a different module, crate, file, etc). It also automatically does the in_kunit_test() check, and the replacement can be swapped in and out at runtime. To be clear, I think this is fine as-is, but we will want to come up with a consistent story about when to use which features (and probably a more consistent naming scheme) once the static stub patches land. In the meantime, we've used the term "function redirection" to refer to the general concept of replacing a function, and then had more specific names for different implementations (mostly "stubbing"-related). A somewhat-outdated document gives some of the rationale behind this, mostly that "mocking" as a term is a bit overloaded. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added an entry in my to-do list to document mocking and add a link to the general KUnit docs in #935 once this PR gets merged. At work, I write JavaScript and the terminology makes sense to me. It's similar to the one used by the popular testing frameworks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah: personally, I'd rather we focus this PR on getting the basics of running tests going, and leave any extensive documentation of mocking or related things to a separate one. Terminology-wise, I'm not too worried about matching that old document, which was mostly based on an abandoned "mocking" proposal which had some more advanced "mocking" features, like autogenerating stubs, and being able to assert easily that a given call will receive certain arguments. Since that was abandoned, and the "stubs" system (which is much simpler) hasn't quite landed yet, none of the nomenclature here is set in stone. I don't think we need to hold this PR back over some slightly inconsistent, but not actually conflicting, terminology in a comment. If "mock" works well here, there's no reason we can't adopt it for the C side either. |
||
/// create a function mock. This function can be invoked to know whether we are currently running a | ||
/// KUnit test or not. | ||
/// | ||
/// # Examples | ||
/// | ||
/// This example shows how a function can be mocked to return a well-known value while testing: | ||
/// | ||
/// ``` | ||
/// # use kernel::kunit::in_kunit_test; | ||
/// # | ||
/// fn fn_mock_example(n: i32) -> i32 { | ||
/// if in_kunit_test() { | ||
/// 100 | ||
/// } else { | ||
/// n + 1 | ||
/// } | ||
/// } | ||
/// | ||
/// let mock_res = fn_mock_example(5); | ||
/// assert_eq!(mock_res, 100); | ||
/// ``` | ||
/// | ||
/// Sometimes, you don't control the code that needs to be mocked. This example shows how the | ||
/// `bindings` module can be mocked: | ||
/// | ||
/// ``` | ||
/// // Import our mock naming it as the real module. | ||
/// #[cfg(CONFIG_KUNIT)] | ||
/// use bindings_mock_example as bindings; | ||
/// | ||
/// // This module mocks `bindings`. | ||
/// mod bindings_mock_example { | ||
/// use kernel::kunit::in_kunit_test; | ||
/// use kernel::bindings::u64_; | ||
/// | ||
/// // Make the other binding functions available. | ||
/// pub(crate) use kernel::bindings::*; | ||
/// | ||
/// // Mock `ktime_get_boot_fast_ns` to return a well-known value when running a KUnit test. | ||
/// pub(crate) unsafe fn ktime_get_boot_fast_ns() -> u64_ { | ||
/// if in_kunit_test() { | ||
/// 1234 | ||
/// } else { | ||
/// unsafe { kernel::bindings::ktime_get_boot_fast_ns() } | ||
/// } | ||
/// } | ||
/// } | ||
/// | ||
/// // This is the function we want to test. Since `bindings` has been mocked, we can use its | ||
/// // functions seamlessly. | ||
/// fn get_boot_ns() -> u64 { | ||
/// unsafe { bindings::ktime_get_boot_fast_ns() } | ||
/// } | ||
/// | ||
/// let time = get_boot_ns(); | ||
/// assert_eq!(time, 1234); | ||
/// ``` | ||
pub fn in_kunit_test() -> bool { | ||
if cfg!(CONFIG_KUNIT) { | ||
// SAFETY: By the type invariant, we know that `*Task::current().deref().0` is valid. | ||
let test = unsafe { (*Task::current().deref().0.get()).kunit_test }; | ||
JoseExposito marked this conversation as resolved.
Show resolved
Hide resolved
|
||
!test.is_null() | ||
} else { | ||
false | ||
} | ||
} | ||
|
||
#[kunit_tests(rust_kernel_kunit)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn rust_test_kunit_kunit_tests() { | ||
let running = true; | ||
assert_eq!(running, true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, I think that someday we'll want to be able to use the KUNIT assertion system (and perhaps encourage more use of expectations over assertions, due to the issues with stack unwinding), but I think this can definitely be a separate patch. |
||
} | ||
|
||
#[test] | ||
fn rust_test_kunit_in_kunit_test() { | ||
let in_kunit = in_kunit_test(); | ||
assert_eq!(in_kunit, true); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
// 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 proc_macro::{Delimiter, Group, TokenStream, TokenTree}; | ||
use std::fmt::Write; | ||
|
||
pub(crate) fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream { | ||
if attr.to_string().is_empty() { | ||
panic!("Missing test name in #[kunit_tests(test_name)] macro") | ||
} | ||
|
||
let mut tokens: Vec<_> = ts.into_iter().collect(); | ||
|
||
// Scan for the "mod" keyword. | ||
tokens | ||
.iter() | ||
.find_map(|token| match token { | ||
TokenTree::Ident(ident) => match ident.to_string().as_str() { | ||
"mod" => Some(true), | ||
_ => None, | ||
}, | ||
_ => None, | ||
}) | ||
.expect("#[kunit_tests(test_name)] attribute should only be applied to modules"); | ||
|
||
// Retrieve the main body. The main body should be the last token tree. | ||
let body = match tokens.pop() { | ||
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => group, | ||
_ => panic!("cannot locate main body of module"), | ||
}; | ||
|
||
// Get the functions set as tests. Search for `[test]` -> `fn`. | ||
let mut body_it = body.stream().into_iter(); | ||
let mut tests = Vec::new(); | ||
while let Some(token) = body_it.next() { | ||
match token { | ||
TokenTree::Group(ident) if ident.to_string() == "[test]" => match body_it.next() { | ||
Some(TokenTree::Ident(ident)) if ident.to_string() == "fn" => { | ||
let test_name = match body_it.next() { | ||
Some(TokenTree::Ident(ident)) => ident.to_string(), | ||
_ => continue, | ||
}; | ||
tests.push(test_name); | ||
} | ||
_ => continue, | ||
}, | ||
_ => (), | ||
} | ||
} | ||
|
||
// Add `#[cfg(CONFIG_KUNIT)]` before the module declaration. | ||
let config_kunit = "#[cfg(CONFIG_KUNIT)]".to_owned().parse().unwrap(); | ||
tokens.insert( | ||
0, | ||
TokenTree::Group(Group::new(Delimiter::None, config_kunit)), | ||
); | ||
|
||
// 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(); | ||
// } | ||
// static mut KUNIT_CASE_FOO: kernel::bindings::kunit_case = | ||
// kernel::kunit_case!(foo, kunit_rust_wrapper_foo); | ||
// | ||
// unsafe extern "C" fn kunit_rust_wrapper_bar(_test: * mut kernel::bindings::kunit) { | ||
// bar(); | ||
// } | ||
// static mut KUNIT_CASE_BAR: kernel::bindings::kunit_case = | ||
// kernel::kunit_case!(bar, kunit_rust_wrapper_bar); | ||
// | ||
// static mut KUNIT_CASE_NULL: kernel::bindings::kunit_case = kernel::kunit_case!(); | ||
// | ||
// static mut TEST_CASES : &mut[kernel::bindings::kunit_case] = unsafe { | ||
// &mut [KUNIT_CASE_FOO, KUNIT_CASE_BAR, KUNIT_CASE_NULL] | ||
// }; | ||
// | ||
// kernel::kunit_unsafe_test_suite!(kunit_test_suit_name, TEST_CASES); | ||
// ``` | ||
let mut kunit_macros = "".to_owned(); | ||
let mut test_cases = "".to_owned(); | ||
for test in tests { | ||
let kunit_wrapper_fn_name = format!("kunit_rust_wrapper_{}", test); | ||
let kunit_case_name = format!("KUNIT_CASE_{}", test.to_uppercase()); | ||
let kunit_wrapper = format!( | ||
"unsafe extern \"C\" fn {}(_test: *mut kernel::bindings::kunit) {{ {}(); }}", | ||
kunit_wrapper_fn_name, test | ||
); | ||
let kunit_case = format!( | ||
"static mut {}: kernel::bindings::kunit_case = kernel::kunit_case!({}, {});", | ||
kunit_case_name, test, kunit_wrapper_fn_name | ||
); | ||
writeln!(kunit_macros, "{kunit_wrapper}").unwrap(); | ||
writeln!(kunit_macros, "{kunit_case}").unwrap(); | ||
writeln!(test_cases, "{kunit_case_name},").unwrap(); | ||
} | ||
|
||
writeln!( | ||
kunit_macros, | ||
"static mut KUNIT_CASE_NULL: kernel::bindings::kunit_case = kernel::kunit_case!();" | ||
) | ||
.unwrap(); | ||
|
||
writeln!( | ||
kunit_macros, | ||
"static mut TEST_CASES : &mut[kernel::bindings::kunit_case] = unsafe {{ &mut[{test_cases} KUNIT_CASE_NULL] }};" | ||
) | ||
.unwrap(); | ||
|
||
writeln!( | ||
kunit_macros, | ||
"kernel::kunit_unsafe_test_suite!({attr}, TEST_CASES);" | ||
) | ||
.unwrap(); | ||
|
||
let new_body: TokenStream = vec![body.stream(), kunit_macros.parse().unwrap()] | ||
.into_iter() | ||
.collect(); | ||
|
||
// Remove the `#[test]` macros. | ||
let new_body = new_body.to_string().replace("#[test]", ""); | ||
tokens.push(TokenTree::Group(Group::new( | ||
Delimiter::Brace, | ||
new_body.parse().unwrap(), | ||
))); | ||
|
||
tokens.into_iter().collect() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be
const
, but keeping it asstatic
is fine foo.