1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
// SPDX-License-Identifier: MPL-2.0

//! # The kernel mode testing framework of Asterinas.
//!
//! `ktest` stands for kernel-mode testing framework. Its goal is to provide a
//! `cargo test`-like experience for any `#![no_std]` bare metal crates.
//!
//! In Asterinas, all the tests written in the source tree of the crates will be run
//! immediately after the initialization of aster-frame. Thus you can use any
//! feature provided by the frame including the heap allocator, etc.
//!
//! By all means, ktest is an individule crate that only requires:
//!  - a custom linker script section `.ktest_array`,
//!  - and an alloc implementation.
//! to work. And the frame happens to provide both of them. Thus, any crates depending
//! on the frame can use ktest without any extra dependency.
//!
//! ## Usage
//!
//! To write a unit test for any crates, it is recommended to create a new test
//! module, e.g.:
//!
//! ```rust
//! use ktest::ktest;
//! #[cfg(ktest)]
//! mod test {
//!     #[ktest]
//!     fn trivial_assertion() {
//!         assert_eq!(0, 0);
//!     }
//!     #[ktest]
//!     #[should_panic]
//!     fn failing_assertion() {
//!         assert_eq!(0, 1);
//!     }
//!     #[ktest]
//!     #[should_panic(expected = "expected panic message")]
//!     fn expect_panic() {
//!         panic!("expected panic message");
//!     }
//! }
//! ```
//!
//! And also, any crates using the ktest framework should be linked with aster-frame
//! and import the `ktest` crate:
//!
//! ```toml
//! # Cargo.toml
//! [dependencies]
//! ktest = { path = "relative/path/to/ktest" }
//! ```
//!
//! By the way, `#[ktest]` attribute along also works, but it hinders test control
//! using cfgs since plain attribute marked test will be executed in all test runs
//! no matter what cfgs are passed to the compiler. More importantly, using `#[ktest]`
//! without cfgs occupies binary real estate since the `.ktest_array` section is not
//! explicitly stripped in normal builds.
//!
//! Rust cfg is used to control the compilation of the test module. In cooperation
//! with the `ktest` framework, the Makefile will set the `RUSTFLAGS` environment
//! variable to pass the cfgs to all rustc invocations. To run the tests, you simply
//! need to set a list of cfgs by specifying `KTEST=1` to the Makefile, e.g.:
//!
//! ```bash
//! make run KTEST=1
//! ```
//!
//! Also, you can run a subset of tests by specifying the `KTEST_WHITELIST` variable.
//! This is achieved by a whitelist filter on the test name.
//!
//! ```bash
//! make run KTEST=1 KTEST_WHITELIST=failing_assertion,aster_frame::test::expect_panic
//! ```
//!
//! `KTEST_CRATES` variable is used to specify in which crates the tests to be run.
//! This is achieved by conditionally compiling the test module using the `#[cfg]`.
//!
//! ```bash
//! make run KTEST=1 KTEST_CRATES=aster-frame
//! ``
//!
//! We support the `#[should_panic]` attribute just in the same way as the standard
//! library do, but the implementation is quite slow currently. Use it with cautious.
//!
//! Doctest is not taken into consideration yet, and the interface is subject to
//! change.
//!

#![cfg_attr(not(test), no_std)]
#![feature(panic_info_message)]

pub mod path;
pub mod runner;
pub mod tree;

extern crate alloc;
use alloc::{boxed::Box, string::String};

pub use ktest_proc_macro::ktest;

#[derive(Clone, Debug)]
pub struct PanicInfo {
    pub message: String,
    pub file: String,
    pub line: usize,
    pub col: usize,
}

impl core::fmt::Display for PanicInfo {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        writeln!(f, "Panicked at {}:{}:{}", self.file, self.line, self.col)?;
        writeln!(f, "{}", self.message)
    }
}

#[derive(Clone)]
pub enum KtestError {
    Panic(Box<PanicInfo>),
    ShouldPanicButNoPanic,
    ExpectedPanicNotMatch(&'static str, Box<PanicInfo>),
    Unknown,
}

#[derive(Clone, PartialEq, Debug)]
pub struct KtestItemInfo {
    pub module_path: &'static str,
    pub fn_name: &'static str,
    pub package: &'static str,
    pub source: &'static str,
    pub line: usize,
    pub col: usize,
}

#[derive(Clone, PartialEq, Debug)]
pub struct KtestItem {
    fn_: fn() -> (),
    should_panic: (bool, Option<&'static str>),
    info: KtestItemInfo,
}

type CatchUnwindImpl = fn(f: fn() -> ()) -> Result<(), Box<dyn core::any::Any + Send>>;

impl KtestItem {
    pub const fn new(
        fn_: fn() -> (),
        should_panic: (bool, Option<&'static str>),
        info: KtestItemInfo,
    ) -> Self {
        Self {
            fn_,
            should_panic,
            info,
        }
    }

    pub fn info(&self) -> &KtestItemInfo {
        &self.info
    }

    /// Run the test with a given catch_unwind implementation.
    pub fn run(&self, catch_unwind_impl: &CatchUnwindImpl) -> Result<(), KtestError> {
        let test_result = catch_unwind_impl(self.fn_);
        if !self.should_panic.0 {
            // Should not panic.
            match test_result {
                Ok(()) => Ok(()),
                Err(e) => match e.downcast::<PanicInfo>() {
                    Ok(s) => Err(KtestError::Panic(s)),
                    Err(_payload) => Err(KtestError::Unknown),
                },
            }
        } else {
            // Should panic.
            match test_result {
                Ok(()) => Err(KtestError::ShouldPanicButNoPanic),
                Err(e) => match e.downcast::<PanicInfo>() {
                    Ok(s) => {
                        if let Some(expected) = self.should_panic.1 {
                            if s.message == expected {
                                Ok(())
                            } else {
                                Err(KtestError::ExpectedPanicNotMatch(expected, s))
                            }
                        } else {
                            Ok(())
                        }
                    }
                    Err(_payload) => Err(KtestError::Unknown),
                },
            }
        }
    }
}

macro_rules! ktest_array {
    () => {{
        extern "C" {
            fn __ktest_array();
            fn __ktest_array_end();
        }
        let item_size = core::mem::size_of::<KtestItem>();
        let l = (__ktest_array_end as usize - __ktest_array as usize) / item_size;
        // SAFETY: __ktest_array is a static section consisting of KtestItem.
        unsafe { core::slice::from_raw_parts(__ktest_array as *const KtestItem, l) }
    }};
}

pub struct KtestIter {
    index: usize,
}

impl KtestIter {
    fn new() -> Self {
        Self { index: 0 }
    }
}

impl core::iter::Iterator for KtestIter {
    type Item = KtestItem;

    fn next(&mut self) -> Option<Self::Item> {
        let ktest_item = ktest_array!().get(self.index)?;
        self.index += 1;
        Some(ktest_item.clone())
    }
}