Skip to main content

ostd_test/
lib.rs

1// SPDX-License-Identifier: MPL-2.0
2
3//! # The kernel mode testing framework of OSTD.
4//!
5//! `ostd-test` stands for kernel-mode testing framework for OSTD. Its goal is to provide a
6//! `cargo test`-like experience for any `#![no_std]` bare metal crates.
7//!
8//! In OSTD, all the tests written in the source tree of the crates will be run
9//! immediately after the initialization of `ostd`. Thus you can use any
10//! feature provided by the frame including the heap allocator, etc.
11//!
12//! By all means, ostd-test is an individual crate that only requires:
13//!  - a custom linker script section `.ktest_array`,
14//!  - and an alloc implementation.
15//!
16//! And the OSTD happens to provide both of them. Thus, any crates depending
17//! on the OSTD can use ostd-test without any extra dependency.
18//!
19//! ## Usage
20//!
21//! To write a unit test for any crates, it is recommended to create a new test
22//! module, e.g.:
23//!
24//! ```rust
25//! #[cfg(ktest)]
26//! mod test {
27//!     use ostd::prelude::*;
28//!
29//!     #[ktest]
30//!     fn trivial_assertion() {
31//!         assert_eq!(0, 0);
32//!     }
33//!     #[ktest]
34//!     #[should_panic]
35//!     fn failing_assertion() {
36//!         assert_eq!(0, 1);
37//!     }
38//!     #[ktest]
39//!     #[should_panic(expected = "expected panic message")]
40//!     fn expect_panic() {
41//!         panic!("expected panic message");
42//!     }
43//! }
44//! ```
45//!
46//! Any crates using the ostd-test framework should be linked with ostd.
47//!
48//! By the way, `#[ktest]` attribute along also works, but it hinders test control
49//! using cfgs since plain attribute marked test will be executed in all test runs
50//! no matter what cfgs are passed to the compiler. More importantly, using `#[ktest]`
51//! without cfgs occupies binary real estate since the `.ktest_array` section is not
52//! explicitly stripped in normal builds.
53//!
54//! Rust cfg is used to control the compilation of the test module. In cooperation
55//! with the `ktest` framework, OSDK will set the `RUSTFLAGS` environment variable
56//! to pass the cfgs to all rustc invocations. To run the tests, you simply need
57//! to use the command `cargo osdk test` in the crate directory. For more information,
58//! please refer to the OSDK documentation.
59//!
60//! We support the `#[should_panic]` attribute just in the same way as the standard
61//! library do, but the implementation is quite slow currently. Use it with cautious.
62//!
63//! Doctest is not taken into consideration yet, and the interface is subject to
64//! change.
65//!
66
67#![cfg_attr(not(test), no_std)]
68
69extern crate alloc;
70use alloc::{boxed::Box, string::String};
71
72#[derive(Clone, Debug)]
73pub struct PanicInfo {
74    pub message: String,
75    pub file: String,
76    pub line: usize,
77    pub col: usize,
78}
79
80impl core::fmt::Display for PanicInfo {
81    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
82        writeln!(f, "Panicked at {}:{}:{}", self.file, self.line, self.col)?;
83        writeln!(f, "{}", self.message)
84    }
85}
86
87/// The error that may occur during the test.
88#[derive(Clone)]
89pub enum KtestError {
90    Panic(Box<PanicInfo>),
91    ShouldPanicButNoPanic,
92    ExpectedPanicNotMatch(&'static str, Box<PanicInfo>),
93    Unknown,
94}
95
96/// The information of the unit test.
97#[derive(Clone, PartialEq, Debug)]
98#[repr(C)]
99pub struct KtestItemInfo {
100    /// The path of the module, not including the function name.
101    ///
102    /// It would be separated by `::`.
103    pub module_path: &'static str,
104    /// The name of the unit test function.
105    pub fn_name: &'static str,
106    /// The name of the crate.
107    pub package: &'static str,
108    /// The source file where the test function resides.
109    pub source: &'static str,
110    /// The line number of the test function in the file.
111    pub line: usize,
112    /// The column number of the test function in the file.
113    pub col: usize,
114}
115
116#[derive(Clone, Debug)]
117#[repr(C)]
118pub struct KtestItem {
119    fn_: fn() -> (),
120    should_panic: (bool, Option<&'static str>),
121    info: KtestItemInfo,
122}
123
124type CatchUnwindImpl = fn(f: fn() -> ()) -> Result<(), Box<dyn core::any::Any + Send>>;
125
126impl KtestItem {
127    /// Create a new [`KtestItem`].
128    ///
129    /// Do not use this function directly. Instead, use the `#[ktest]`
130    /// attribute to mark the test function.
131    #[doc(hidden)]
132    pub const fn new(
133        fn_: fn() -> (),
134        should_panic: (bool, Option<&'static str>),
135        info: KtestItemInfo,
136    ) -> Self {
137        Self {
138            fn_,
139            should_panic,
140            info,
141        }
142    }
143
144    /// Get the information of the test.
145    pub fn info(&self) -> &KtestItemInfo {
146        &self.info
147    }
148
149    /// Run the test with a given catch_unwind implementation.
150    pub fn run(&self, catch_unwind_impl: &CatchUnwindImpl) -> Result<(), KtestError> {
151        let test_result = catch_unwind_impl(self.fn_);
152        if !self.should_panic.0 {
153            // Should not panic.
154            match test_result {
155                Ok(()) => Ok(()),
156                Err(e) => match e.downcast::<PanicInfo>() {
157                    Ok(s) => Err(KtestError::Panic(s)),
158                    Err(_payload) => Err(KtestError::Unknown),
159                },
160            }
161        } else {
162            // Should panic.
163            match test_result {
164                Ok(()) => Err(KtestError::ShouldPanicButNoPanic),
165                Err(e) => match e.downcast::<PanicInfo>() {
166                    Ok(s) => {
167                        if let Some(expected) = self.should_panic.1 {
168                            // The expected message should appear in the actual panic message. Reference:
169                            // <https://doc.rust-lang.org/reference/attributes/testing.html#the-should_panic-attribute>
170                            if s.message.contains(expected) {
171                                Ok(())
172                            } else {
173                                Err(KtestError::ExpectedPanicNotMatch(expected, s))
174                            }
175                        } else {
176                            Ok(())
177                        }
178                    }
179                    Err(_payload) => Err(KtestError::Unknown),
180                },
181            }
182        }
183    }
184}
185
186macro_rules! ktest_array {
187    () => {{
188        unsafe extern "C" {
189            fn __ktest_array();
190            fn __ktest_array_end();
191        }
192        let array_ptr = __ktest_array as *const () as *const KtestItem;
193        let array_end_ptr = __ktest_array_end as *const () as *const KtestItem;
194        // SAFETY: The pointer arithmetic is valid since both pointers point to
195        // the same section.
196        let l = unsafe { array_end_ptr.offset_from(array_ptr) as usize };
197        // SAFETY: `array_ptr` points to a valid static section with `l`
198        // `KtestItem` elements, and there are no write accesses.
199        unsafe { core::slice::from_raw_parts(array_ptr, l) }
200    }};
201}
202
203/// The iterator of the ktest array.
204pub struct KtestIter {
205    index: usize,
206}
207
208impl Default for KtestIter {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214impl KtestIter {
215    /// Create a new [`KtestIter`].
216    ///
217    /// It will iterate over all the tests (marked with `#[ktest]`).
218    pub fn new() -> Self {
219        Self { index: 0 }
220    }
221}
222
223impl core::iter::Iterator for KtestIter {
224    type Item = KtestItem;
225
226    fn next(&mut self) -> Option<Self::Item> {
227        let ktest_item = ktest_array!().get(self.index)?;
228        self.index += 1;
229        Some(ktest_item.clone())
230    }
231}
232
233// The whitelists that will be generated by the OSDK as static consts.
234// They deliver the target tests that the user wants to run.
235unsafe extern "Rust" {
236    static KTEST_TEST_WHITELIST: Option<&'static [&'static str]>;
237    static KTEST_CRATE_WHITELIST: Option<&'static [&'static str]>;
238}
239
240/// Get the whitelist of the tests.
241///
242/// The whitelist is generated by the OSDK runner, indicating name of the
243/// target tests that the user wants to run.
244pub fn get_ktest_test_whitelist() -> Option<&'static [&'static str]> {
245    // SAFETY: The two extern statics in the base crate are generated by OSDK.
246    unsafe { KTEST_TEST_WHITELIST }
247}
248
249/// Get the whitelist of the crates.
250///
251/// The whitelist is generated by the OSDK runner, indicating the target crate
252/// that the user wants to test.
253pub fn get_ktest_crate_whitelist() -> Option<&'static [&'static str]> {
254    // SAFETY: The two extern statics in the base crate are generated by OSDK.
255    unsafe { KTEST_CRATE_WHITELIST }
256}