From fa4f936c338eb383970bd1eef130966b44f27cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Exp=C3=B3sito?= Date: Sun, 22 Jan 2023 17:44:50 +0100 Subject: [PATCH 1/3] rust: kunit: add KUnit case and suite macros MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a couple of Rust macros to allow to develop KUnit tests without relying on generated C code: - The `kunit_unsafe_test_suite!` Rust macro is similar to the `kunit_test_suite` C macro. - The `kunit_case!` Rust macro is similar to the `KUNIT_CASE` C macro. It can be used with parameters to generate a test case or without parameters to be used as delimiter in `kunit_test_suite!`. While these macros can be used on their own, a future patch will introduce another macro to create KUnit tests using a user-space like syntax. Co-developed-by: David Gow Signed-off-by: David Gow Signed-off-by: José Expósito --- rust/kernel/kunit.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/rust/kernel/kunit.rs b/rust/kernel/kunit.rs index ab04a2a226be1f..32f07674e3735d 100644 --- a/rust/kernel/kunit.rs +++ b/rust/kernel/kunit.rs @@ -88,3 +88,95 @@ 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() }; + }; + }; +} From 587d3dfd5b3ba0ade36596d1b54b16935a1ca053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Exp=C3=B3sito?= Date: Sun, 22 Jan 2023 17:44:50 +0100 Subject: [PATCH 2/3] rust: macros: add macro to easily run KUnit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: José Expósito --- rust/kernel/kunit.rs | 11 ++++ rust/kernel/lib.rs | 2 + rust/macros/kunit.rs | 149 +++++++++++++++++++++++++++++++++++++++++++ rust/macros/lib.rs | 29 +++++++++ 4 files changed, 191 insertions(+) create mode 100644 rust/macros/kunit.rs diff --git a/rust/kernel/kunit.rs b/rust/kernel/kunit.rs index 32f07674e3735d..ce5f642f575a62 100644 --- a/rust/kernel/kunit.rs +++ b/rust/kernel/kunit.rs @@ -6,6 +6,8 @@ //! //! Reference: +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. @@ -180,3 +182,12 @@ macro_rules! kunit_unsafe_test_suite { }; }; } + +#[kunit_tests(rust_kernel_kunit)] +mod tests { + #[test] + fn rust_test_kunit_kunit_tests() { + let running = true; + assert_eq!(running, true); + } +} diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs index 596119bfe5ab89..b78a5629ba2f82 100644 --- a/rust/kernel/lib.rs +++ b/rust/kernel/lib.rs @@ -31,6 +31,8 @@ #[cfg(not(CONFIG_RUST))] compile_error!("Missing kernel configuration for conditional compilation"); +extern crate self as kernel; + #[cfg(not(test))] #[cfg(not(testlib))] mod allocator; diff --git a/rust/macros/kunit.rs b/rust/macros/kunit.rs new file mode 100644 index 00000000000000..69dac253232f3e --- /dev/null +++ b/rust/macros/kunit.rs @@ -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 + +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() +} diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs index 397e1496da28be..73b9952f9c96a5 100644 --- a/rust/macros/lib.rs +++ b/rust/macros/lib.rs @@ -4,6 +4,7 @@ mod concat_idents; mod helpers; +mod kunit; mod module; mod vtable; @@ -189,3 +190,31 @@ pub fn vtable(attr: TokenStream, ts: TokenStream) -> TokenStream { pub fn concat_idents(ts: TokenStream) -> TokenStream { concat_idents::concat_idents(ts) } + +/// Registers a KUnit test suite and its test cases using a user-space like syntax. +/// +/// This macro should be used on modules. If `CONFIG_KUNIT` (in `.config`) is `n`, the target module +/// is ignored. +/// +/// # Examples +/// +/// ```ignore +/// # use macros::kunit_tests; +/// +/// #[kunit_tests(kunit_test_suit_name)] +/// mod tests { +/// #[test] +/// fn foo() { +/// assert_eq!(1, 1); +/// } +/// +/// #[test] +/// fn bar() { +/// assert_eq!(2, 2); +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream { + kunit::kunit_tests(attr, ts) +} From c3930088fbf76e747860caa725bc3b50fb2a37ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Exp=C3=B3sito?= Date: Sun, 22 Jan 2023 17:44:50 +0100 Subject: [PATCH 3/3] rust: kunit: allow to know if we are in a test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In some cases, you need to call test-only code from outside the test case, for example, to mock a function or a module. In order to check whether we are in a test or not, we need to test if `CONFIG_KUNIT` is set. Unfortunately, we cannot rely only on this condition because some distros compile KUnit in production kernels, so checking at runtime that `current->kunit_test != NULL` is required. Note that the C function `kunit_get_current_test()` can not be used because it is not present in the current Rust tree yet. Once it is available we might want to change our Rust wrapper to use it. This patch adds a function to know whether we are in a KUnit test or not and examples showing how to mock a function and a module. Reviewed-by: David Gow Signed-off-by: José Expósito --- rust/kernel/kunit.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/rust/kernel/kunit.rs b/rust/kernel/kunit.rs index ce5f642f575a62..4a5506e94e816a 100644 --- a/rust/kernel/kunit.rs +++ b/rust/kernel/kunit.rs @@ -6,6 +6,8 @@ //! //! Reference: +use crate::task::Task; +use core::ops::Deref; use macros::kunit_tests; /// Asserts that a boolean expression is `true` at runtime. @@ -183,11 +185,87 @@ macro_rules! kunit_unsafe_test_suite { }; } +/// In some cases, you need to call test-only code from outside the test case, for example, to +/// 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 }; + !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); } + + #[test] + fn rust_test_kunit_in_kunit_test() { + let in_kunit = in_kunit_test(); + assert_eq!(in_kunit, true); + } }