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;
70#[cfg(not(test))]
71use alloc::{boxed::Box, string::String};
72
73#[derive(Clone, Debug)]
74pub struct PanicInfo {
75 pub message: String,
76 pub file: String,
77 pub line: usize,
78 pub col: usize,
79}
80
81impl core::fmt::Display for PanicInfo {
82 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
83 writeln!(f, "Panicked at {}:{}:{}", self.file, self.line, self.col)?;
84 writeln!(f, "{}", self.message)
85 }
86}
87
88/// The error that may occur during the test.
89#[derive(Clone)]
90pub enum KtestError {
91 Panic(Box<PanicInfo>),
92 ShouldPanicButNoPanic,
93 ExpectedPanicNotMatch(&'static str, Box<PanicInfo>),
94 Unknown,
95}
96
97/// The information of the unit test.
98#[repr(C)]
99#[derive(Clone, Debug, PartialEq)]
100pub struct KtestItemInfo {
101 /// The path of the module, not including the function name.
102 ///
103 /// It would be separated by `::`.
104 pub module_path: &'static str,
105 /// The name of the unit test function.
106 pub fn_name: &'static str,
107 /// The name of the crate.
108 pub package: &'static str,
109 /// The source file where the test function resides.
110 pub source: &'static str,
111 /// The line number of the test function in the file.
112 pub line: usize,
113 /// The column number of the test function in the file.
114 pub col: usize,
115}
116
117#[repr(C)]
118#[derive(Clone, Debug)]
119pub struct KtestItem {
120 fn_: fn() -> (),
121 should_panic: (bool, Option<&'static str>),
122 info: KtestItemInfo,
123}
124
125type CatchUnwindImpl = fn(f: fn() -> ()) -> Result<(), Box<dyn core::any::Any + Send>>;
126
127impl KtestItem {
128 /// Create a new [`KtestItem`].
129 ///
130 /// Do not use this function directly. Instead, use the `#[ktest]`
131 /// attribute to mark the test function.
132 #[doc(hidden)]
133 pub const fn new(
134 fn_: fn() -> (),
135 should_panic: (bool, Option<&'static str>),
136 info: KtestItemInfo,
137 ) -> Self {
138 Self {
139 fn_,
140 should_panic,
141 info,
142 }
143 }
144
145 /// Get the information of the test.
146 pub fn info(&self) -> &KtestItemInfo {
147 &self.info
148 }
149
150 /// Run the test with a given catch_unwind implementation.
151 pub fn run(&self, catch_unwind_impl: &CatchUnwindImpl) -> Result<(), KtestError> {
152 let test_result = catch_unwind_impl(self.fn_);
153 if !self.should_panic.0 {
154 // Should not panic.
155 match test_result {
156 Ok(()) => Ok(()),
157 Err(e) => match e.downcast::<PanicInfo>() {
158 Ok(s) => Err(KtestError::Panic(s)),
159 Err(_payload) => Err(KtestError::Unknown),
160 },
161 }
162 } else {
163 // Should panic.
164 match test_result {
165 Ok(()) => Err(KtestError::ShouldPanicButNoPanic),
166 Err(e) => match e.downcast::<PanicInfo>() {
167 Ok(s) => {
168 if let Some(expected) = self.should_panic.1 {
169 // The expected message should appear in the actual panic message. Reference:
170 // <https://doc.rust-lang.org/reference/attributes/testing.html#the-should_panic-attribute>
171 if s.message.contains(expected) {
172 Ok(())
173 } else {
174 Err(KtestError::ExpectedPanicNotMatch(expected, s))
175 }
176 } else {
177 Ok(())
178 }
179 }
180 Err(_payload) => Err(KtestError::Unknown),
181 },
182 }
183 }
184 }
185}
186
187macro_rules! ktest_array {
188 () => {{
189 unsafe extern "C" {
190 fn __ktest_array();
191 fn __ktest_array_end();
192 }
193 let array_ptr = __ktest_array as *const () as *const KtestItem;
194 let array_end_ptr = __ktest_array_end as *const () as *const KtestItem;
195 // SAFETY: The pointer arithmetic is valid since both pointers point to
196 // the same section.
197 let l = unsafe { array_end_ptr.offset_from(array_ptr) as usize };
198 // SAFETY: `array_ptr` points to a valid static section with `l`
199 // `KtestItem` elements, and there are no write accesses.
200 unsafe { core::slice::from_raw_parts(array_ptr, l) }
201 }};
202}
203
204/// The iterator of the ktest array.
205pub struct KtestIter {
206 index: usize,
207}
208
209impl Default for KtestIter {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215impl KtestIter {
216 /// Create a new [`KtestIter`].
217 ///
218 /// It will iterate over all the tests (marked with `#[ktest]`).
219 pub fn new() -> Self {
220 Self { index: 0 }
221 }
222}
223
224impl Iterator for KtestIter {
225 type Item = KtestItem;
226
227 fn next(&mut self) -> Option<Self::Item> {
228 let ktest_item = ktest_array!().get(self.index)?;
229 self.index += 1;
230 Some(ktest_item.clone())
231 }
232}
233
234// The whitelists that will be generated by the OSDK as static consts.
235// They deliver the target tests that the user wants to run.
236unsafe extern "Rust" {
237 static KTEST_TEST_WHITELIST: Option<&'static [&'static str]>;
238 static KTEST_CRATE_WHITELIST: Option<&'static [&'static str]>;
239}
240
241/// Get the whitelist of the tests.
242///
243/// The whitelist is generated by the OSDK runner, indicating name of the
244/// target tests that the user wants to run.
245pub fn get_ktest_test_whitelist() -> Option<&'static [&'static str]> {
246 // SAFETY: The two extern statics in the base crate are generated by OSDK.
247 unsafe { KTEST_TEST_WHITELIST }
248}
249
250/// Get the whitelist of the crates.
251///
252/// The whitelist is generated by the OSDK runner, indicating the target crate
253/// that the user wants to test.
254pub fn get_ktest_crate_whitelist() -> Option<&'static [&'static str]> {
255 // SAFETY: The two extern statics in the base crate are generated by OSDK.
256 unsafe { KTEST_CRATE_WHITELIST }
257}