Skip to content

Commit

Permalink
Merge patch series "rust: kunit: Support KUnit tests with a user-spac…
Browse files Browse the repository at this point in the history
…e like syntax"

David Gow <davidgow@google.com> says:

This series was originally written by José Expósito, and can be found
here:
Rust-for-Linux#950

Add support for writing KUnit tests in Rust. While Rust doctests are
already converted to KUnit tests and run, they're really better suited
for examples, rather than as first-class unit tests.

This series implements a series of direct Rust bindings for KUnit tests,
as well as a new macro which allows KUnit tests to be written using a
close variant of normal Rust unit test syntax. The only change required
is replacing '#[cfg(test)]' with '#[kunit_tests(kunit_test_suite_name)]'

An example test would look like:
	#[kunit_tests(rust_kernel_hid_driver)]
	mod tests {
	    use super::*;
	    use crate::{c_str, driver, hid, prelude::*};
	    use core::ptr;

	    struct SimpleTestDriver;
	    impl Driver for SimpleTestDriver {
	        type Data = ();
	    }

	    #[test]
	    fn rust_test_hid_driver_adapter() {
	        let mut hid = bindings::hid_driver::default();
	        let name = c_str!("SimpleTestDriver");
	        static MODULE: ThisModule = unsafe { ThisModule::from_ptr(ptr::null_mut()) };

        	let res = unsafe {
	            <hid::Adapter<SimpleTestDriver> as driver::DriverOps>::register(&mut hid, name, &MODULE)
	        };
	        assert_eq!(res, Err(ENODEV)); // The mock returns -19
	    }
	}

Changes since the GitHub PR:
- Rebased on top of kselftest/kunit
- Add const_mut_refs feature
  This may conflict with https://lore.kernel.org/lkml/20230503090708.2524310-6-nmi@metaspace.dk/
- Add rust/macros/kunit.rs to the KUnit MAINTAINERS entry

Link: https://lore.kernel.org/r/20230720-rustbind-v1-0-c80db349e3b5@google.com
  • Loading branch information
matthewtgilbride committed May 17, 2024
2 parents 97ab3e8 + b24f5f7 commit 3137389
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 0 deletions.
1 change: 1 addition & 0 deletions MAINTAINERS
Original file line number Diff line number Diff line change
Expand Up @@ -11837,6 +11837,7 @@ F: Documentation/dev-tools/kunit/
F: include/kunit/
F: lib/kunit/
F: rust/kernel/kunit.rs
F: rust/macros/kunit.rs
F: scripts/rustdoc_test_*
F: tools/testing/kunit/

Expand Down
181 changes: 181 additions & 0 deletions rust/kernel/kunit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ pub fn info(args: fmt::Arguments<'_>) {
}
}

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.
Expand Down Expand Up @@ -161,3 +165,180 @@ macro_rules! kunit_assert_eq {
$crate::kunit_assert!($name, $file, $diff, $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
/// 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);
}
}
1 change: 1 addition & 0 deletions rust/kernel/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#![feature(new_uninit)]
#![feature(receiver_trait)]
#![feature(unsize)]
#![feature(const_mut_refs)]

// Ensure conditional compilation based on the kernel configuration works;
// otherwise we may silently break things like initcall handling.
Expand Down
149 changes: 149 additions & 0 deletions rust/macros/kunit.rs
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()
}
29 changes: 29 additions & 0 deletions rust/macros/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
mod quote;
mod concat_idents;
mod helpers;
mod kunit;
mod module;
mod paste;
mod pin_data;
Expand Down Expand Up @@ -405,3 +406,31 @@ pub fn paste(input: TokenStream) -> TokenStream {
pub fn derive_zeroable(input: TokenStream) -> TokenStream {
zeroable::derive(input)
}

/// 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)
}

0 comments on commit 3137389

Please sign in to comment.