mirror of
https://github.com/asterinas/asterinas.git
synced 2025-06-20 23:26:32 +00:00
Implement should_panic expectation and refactor ktest
This commit is contained in:
committed by
Tate, Hongliang Tian
parent
45a6b2f46c
commit
8eb1e06c2a
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -842,6 +842,14 @@ version = "0.1.0"
|
||||
[[package]]
|
||||
name = "ktest"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ktest-proc-macro",
|
||||
"owo-colors",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ktest-proc-macro"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1018,6 +1026,12 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
|
@ -58,7 +58,7 @@ make run
|
||||
|
||||
### Unit Test
|
||||
|
||||
#### User mode unit test
|
||||
#### User mode unit tests
|
||||
|
||||
Many of our crates does not require running on bare metal environment and can be tested through the standard Cargo testing framework. A specific list of which crates can be tested with `cargo test` is listed in the `[workspace.metadata.usermode_testable]` entry in the `Cargo.toml` file of the root workspace.
|
||||
|
||||
@ -69,7 +69,7 @@ make test
|
||||
|
||||
Nevertheless, you could enter the directory of a specific crate and invoke `cargo test` to perform user mode unit tests and doctests.
|
||||
|
||||
#### Kernel mode unit test
|
||||
#### Kernel mode unit tests
|
||||
|
||||
We can run unit tests in kernel mode for crates like `jinux-frame` or `jinux-std`. This is powered by our [ktest](framework/libs/ktest) framework.
|
||||
```bash
|
||||
|
@ -1,4 +1,4 @@
|
||||
//! QEMU isa debug device.
|
||||
//! Providing the ability to exit QEMU and return a value as debug result.
|
||||
|
||||
/// The exit code of x86 QEMU isa debug device. In `qemu-system-x86_64` the
|
||||
/// exit code will be `(code << 1) | 1`. So you could never let QEMU invoke
|
||||
@ -12,11 +12,18 @@ pub enum QemuExitCode {
|
||||
Failed = 0x20,
|
||||
}
|
||||
|
||||
/// Exit QEMU with the given exit code.
|
||||
///
|
||||
/// This function assumes that the kernel is run in QEMU with the following
|
||||
/// QEMU command line arguments that specifies the ISA debug exit device:
|
||||
/// `-device isa-debug-exit,iobase=0xf4,iosize=0x04`.
|
||||
pub fn exit_qemu(exit_code: QemuExitCode) -> ! {
|
||||
use x86_64::instructions::port::Port;
|
||||
let mut port = Port::new(0xf4);
|
||||
|
||||
// Safety: The write to the ISA debug exit port is safe and `0xf4` should
|
||||
// be the port number.
|
||||
unsafe {
|
||||
let mut port = Port::new(0xf4);
|
||||
port.write(exit_code as u32);
|
||||
}
|
||||
unreachable!()
|
||||
|
@ -3,9 +3,10 @@
|
||||
//!
|
||||
|
||||
pub mod kcmdline;
|
||||
pub mod memory_region;
|
||||
|
||||
use kcmdline::KCmdlineArg;
|
||||
|
||||
pub mod memory_region;
|
||||
use self::memory_region::MemoryRegion;
|
||||
|
||||
use alloc::{string::String, vec::Vec};
|
||||
@ -116,7 +117,16 @@ pub fn call_jinux_main() -> ! {
|
||||
}
|
||||
#[cfg(ktest)]
|
||||
{
|
||||
use crate::arch::qemu::{exit_qemu, QemuExitCode};
|
||||
use alloc::boxed::Box;
|
||||
use core::any::Any;
|
||||
crate::init();
|
||||
ktest::do_ktests!();
|
||||
let fn_catch_unwind = &(unwinding::panic::catch_unwind::<(), fn()>
|
||||
as fn(fn()) -> Result<(), Box<(dyn Any + Send + 'static)>>);
|
||||
use ktest::runner::{run_ktests, KtestResult};
|
||||
match run_ktests(crate::console::print, fn_catch_unwind) {
|
||||
KtestResult::Ok => exit_qemu(QemuExitCode::Success),
|
||||
KtestResult::Failed => exit_qemu(QemuExitCode::Failed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ fn invoke_ffi_init_funcs() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Unit test for the ktest framework and functions of the frame.
|
||||
/// Simple unit tests for the ktest framework.
|
||||
#[if_cfg_ktest]
|
||||
mod test {
|
||||
#[ktest]
|
||||
@ -95,4 +95,10 @@ mod test {
|
||||
fn failing_assertion() {
|
||||
assert_eq!(0, 1);
|
||||
}
|
||||
|
||||
#[ktest]
|
||||
#[should_panic(expected = "expected panic message")]
|
||||
fn expect_panic() {
|
||||
panic!("expected panic message");
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,13 @@
|
||||
//! Panic support in Jinux Frame.
|
||||
//! Panic support.
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::{boxed::Box, string::ToString};
|
||||
|
||||
use crate::arch::qemu::{exit_qemu, QemuExitCode};
|
||||
use crate::println;
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
pub fn panic_handler(info: &core::panic::PanicInfo) -> ! {
|
||||
let throw_info = PanicInfo {
|
||||
fn panic_handler(info: &core::panic::PanicInfo) -> ! {
|
||||
let throw_info = ktest::PanicInfo {
|
||||
message: info.message().unwrap().to_string(),
|
||||
file: info.location().unwrap().file().to_string(),
|
||||
line: info.location().unwrap().line() as usize,
|
||||
|
15
framework/libs/ktest-proc-macro/Cargo.toml
Normal file
15
framework/libs/ktest-proc-macro/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "ktest-proc-macro"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.69"
|
||||
quote = "1.0.33"
|
||||
rand = "0.8.5"
|
||||
syn = { version = "2.0.29", features = ["full"] }
|
128
framework/libs/ktest-proc-macro/src/lib.rs
Normal file
128
framework/libs/ktest-proc-macro/src/lib.rs
Normal file
@ -0,0 +1,128 @@
|
||||
#![feature(proc_macro_span)]
|
||||
|
||||
extern crate proc_macro2;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use syn::{parse_macro_input, Expr, Ident, ItemFn, ItemMod};
|
||||
|
||||
/// The conditional compilation attribute macro to control the compilation of test
|
||||
/// modules.
|
||||
#[proc_macro_attribute]
|
||||
pub fn if_cfg_ktest(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Assuming that the item is a module declearation, otherwise panics.
|
||||
let input = parse_macro_input!(item as ItemMod);
|
||||
|
||||
let crate_name = std::env::var("CARGO_PKG_NAME").unwrap();
|
||||
|
||||
let output = quote! {
|
||||
#[cfg(all(ktest, any(ktest = "all", ktest = #crate_name)))]
|
||||
#input
|
||||
};
|
||||
|
||||
TokenStream::from(output)
|
||||
}
|
||||
|
||||
/// The test attribute macro to mark a test function.
|
||||
#[proc_macro_attribute]
|
||||
pub fn ktest(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Assuming that the item has type `fn() -> ()`, otherwise panics.
|
||||
let input = parse_macro_input!(item as ItemFn);
|
||||
assert!(
|
||||
input.sig.inputs.is_empty(),
|
||||
"ktest function should have no arguments"
|
||||
);
|
||||
assert!(
|
||||
matches!(input.sig.output, syn::ReturnType::Default),
|
||||
"ktest function should return `()`"
|
||||
);
|
||||
|
||||
// Generate a random identifier to avoid name conflicts.
|
||||
let fn_id: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(8)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let fn_name = &input.sig.ident;
|
||||
let fn_ktest_item_name = Ident::new(
|
||||
&format!("{}_ktest_item_{}", &input.sig.ident, &fn_id),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
|
||||
let is_should_panic_attr = |attr: &&syn::Attribute| {
|
||||
attr.path()
|
||||
.segments
|
||||
.iter()
|
||||
.any(|segment| segment.ident == "should_panic")
|
||||
};
|
||||
let mut attr_iter = input.attrs.iter();
|
||||
let should_panic = attr_iter.find(is_should_panic_attr);
|
||||
let (should_panic, expectation) = match should_panic {
|
||||
Some(attr) => {
|
||||
assert!(
|
||||
!attr_iter.any(|attr: &syn::Attribute| is_should_panic_attr(&attr)),
|
||||
"multiple `should_panic` attributes"
|
||||
);
|
||||
match &attr.meta {
|
||||
syn::Meta::List(l) => {
|
||||
let arg_err_message = "`should_panic` attribute should only have zero or one `expected` argument, with the format of `expected = \"<panic message>\"`";
|
||||
let expected_assign =
|
||||
syn::parse2::<syn::ExprAssign>(l.tokens.clone()).expect(arg_err_message);
|
||||
let Expr::Lit(s) = *expected_assign.right else {
|
||||
panic!("{}", arg_err_message);
|
||||
};
|
||||
let syn::Lit::Str(expectation) = s.lit else {
|
||||
panic!("{}", arg_err_message);
|
||||
};
|
||||
(true, Some(expectation))
|
||||
}
|
||||
_ => (true, None),
|
||||
}
|
||||
}
|
||||
None => (false, None),
|
||||
};
|
||||
let expectation_tokens = if let Some(s) = expectation {
|
||||
quote! {
|
||||
Some(#s)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let package_name = std::env::var("CARGO_PKG_NAME").unwrap();
|
||||
let span = proc_macro::Span::call_site();
|
||||
let source = span.source_file().path();
|
||||
let source = source.to_str().unwrap();
|
||||
let line = span.line();
|
||||
let col = span.column();
|
||||
|
||||
let register_ktest_item = quote! {
|
||||
#[cfg(ktest)]
|
||||
#[used]
|
||||
#[link_section = ".ktest_array"]
|
||||
static #fn_ktest_item_name: ktest::KtestItem = ktest::KtestItem::new(
|
||||
#fn_name,
|
||||
(#should_panic, #expectation_tokens),
|
||||
ktest::KtestItemInfo {
|
||||
module_path: module_path!(),
|
||||
fn_name: stringify!(#fn_name),
|
||||
package: #package_name,
|
||||
source: #source,
|
||||
line: #line,
|
||||
col: #col,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
let output = quote! {
|
||||
#input
|
||||
|
||||
#register_ktest_item
|
||||
};
|
||||
|
||||
TokenStream::from(output)
|
||||
}
|
@ -5,11 +5,6 @@ edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.69"
|
||||
quote = "1.0.33"
|
||||
rand = "0.8.5"
|
||||
syn = { version = "2.0.29", features = ["full"] }
|
||||
owo-colors = "3.5.0"
|
||||
ktest-proc-macro = { path = "../ktest-proc-macro" }
|
||||
|
@ -1,12 +1,17 @@
|
||||
//! # The kernel mode testing framework of Jinux.
|
||||
//!
|
||||
//! `ktest` stands for kernel-mode testing framework. Its goal is to provide a
|
||||
//! `cargo test`-like experience for any crates that depends on jinux-frame.
|
||||
//! `cargo test`-like experience for any `#![no_std]` bare metal crates.
|
||||
//!
|
||||
//! All the tests written in the source tree of the crates will be run using the
|
||||
//! `do_ktests!()` macro immediately after the initialization of jinux-frame.
|
||||
//! Thus you can use any feature provided by the frame including the heap
|
||||
//! allocator, etc.
|
||||
//! In Jinux, all the tests written in the source tree of the crates will be run
|
||||
//! immediately after the initialization of jinux-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
|
||||
//!
|
||||
@ -26,6 +31,11 @@
|
||||
//! fn failing_assertion() {
|
||||
//! assert_eq!(0, 1);
|
||||
//! }
|
||||
//! #[ktest]
|
||||
//! #[should_panic(expected = "expected panic message")]
|
||||
//! fn expect_panic() {
|
||||
//! panic!("expected panic message");
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@ -59,198 +69,153 @@
|
||||
//! a default conditional compilation setting:
|
||||
//! `#[cfg(all(ktest, any(ktest = "all", ktest = #crate_name)))]`
|
||||
//!
|
||||
//! We do not support `#[should_panic]` attribute, but the implementation is quite
|
||||
//! slow currently. Use it with cautious.
|
||||
//! 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.
|
||||
//!
|
||||
//! ## How it works
|
||||
//!
|
||||
//! The `ktest` framework is implemented using the procedural macro feature of Rust.
|
||||
//! The `ktest` attribute macro will generate a static fn pointer variable linked in
|
||||
//! the `.ktest_array` section. The `do_ktests!()` macro will iterate over all the
|
||||
//! static variables in the section and run the tests.
|
||||
//!
|
||||
|
||||
#![feature(proc_macro_span)]
|
||||
#![no_std]
|
||||
#![feature(panic_info_message)]
|
||||
|
||||
extern crate proc_macro2;
|
||||
pub mod runner;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use syn::{parse_macro_input, Ident, ItemFn, ItemMod};
|
||||
extern crate alloc;
|
||||
use alloc::{boxed::Box, string::String};
|
||||
use core::result::Result;
|
||||
|
||||
/// The conditional compilation attribute macro to control the compilation of test
|
||||
/// modules.
|
||||
#[proc_macro_attribute]
|
||||
pub fn if_cfg_ktest(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Assuming that the item is a module declearation, otherwise panics.
|
||||
let input = parse_macro_input!(item as ItemMod);
|
||||
pub use ktest_proc_macro::{if_cfg_ktest, ktest};
|
||||
|
||||
let crate_name = std::env::var("CARGO_PKG_NAME").unwrap();
|
||||
|
||||
let output = quote! {
|
||||
#[cfg(all(ktest, any(ktest = "all", ktest = #crate_name)))]
|
||||
#input
|
||||
};
|
||||
|
||||
TokenStream::from(output)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PanicInfo {
|
||||
pub message: String,
|
||||
pub file: String,
|
||||
pub line: usize,
|
||||
pub col: usize,
|
||||
}
|
||||
|
||||
/// The test attribute macro to mark a test function.
|
||||
#[proc_macro_attribute]
|
||||
pub fn ktest(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Assuming that the item has type `fn() -> ()`, otherwise panics.
|
||||
let input = parse_macro_input!(item as ItemFn);
|
||||
assert!(
|
||||
input.sig.inputs.is_empty(),
|
||||
"ktest function should have no arguments"
|
||||
);
|
||||
assert!(
|
||||
matches!(input.sig.output, syn::ReturnType::Default),
|
||||
"ktest function should return `()`"
|
||||
);
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a random identifier to avoid name conflicts.
|
||||
let fn_id: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(8)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
#[derive(Clone)]
|
||||
pub enum KtestError {
|
||||
Panic(Box<PanicInfo>),
|
||||
ShouldPanicButNoPanic,
|
||||
ExpectedPanicNotMatch(&'static str, Box<PanicInfo>),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
let fn_name = &input.sig.ident;
|
||||
let fn_ktest_item_name = Ident::new(
|
||||
&format!("{}_ktest_item_{}", &input.sig.ident, &fn_id),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
#[derive(Clone)]
|
||||
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,
|
||||
}
|
||||
|
||||
// Since Rust does not support unamed structures, we have to generate a
|
||||
// unique name for each test item structure.
|
||||
let ktest_item_struct = Ident::new(
|
||||
&format!("KtestItem{}", &fn_id),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
#[derive(Clone)]
|
||||
pub struct KtestItem {
|
||||
fn_: fn() -> (),
|
||||
should_panic: (bool, Option<&'static str>),
|
||||
info: KtestItemInfo,
|
||||
}
|
||||
|
||||
let should_panic = input.attrs.iter().any(|attr| {
|
||||
attr.path()
|
||||
.segments
|
||||
.iter()
|
||||
.any(|segment| segment.ident == "should_panic")
|
||||
});
|
||||
type CatchUnwindImpl = fn(f: fn() -> ()) -> Result<(), Box<dyn core::any::Any + Send>>;
|
||||
|
||||
let package_name = std::env::var("CARGO_PKG_NAME").unwrap();
|
||||
let span = proc_macro::Span::call_site();
|
||||
let source = span.source_file().path();
|
||||
let source = source.to_str().unwrap();
|
||||
let line = span.line();
|
||||
let col = span.column();
|
||||
|
||||
let register = quote! {
|
||||
struct #ktest_item_struct {
|
||||
fn_: fn() -> (),
|
||||
should_panic: bool,
|
||||
module_path: &'static str,
|
||||
fn_name: &'static str,
|
||||
package: &'static str,
|
||||
source: &'static str,
|
||||
line: usize,
|
||||
col: usize,
|
||||
impl KtestItem {
|
||||
pub const fn new(
|
||||
fn_: fn() -> (),
|
||||
should_panic: (bool, Option<&'static str>),
|
||||
info: KtestItemInfo,
|
||||
) -> Self {
|
||||
Self {
|
||||
fn_,
|
||||
should_panic,
|
||||
info,
|
||||
}
|
||||
#[cfg(ktest)]
|
||||
#[used]
|
||||
#[link_section = ".ktest_array"]
|
||||
static #fn_ktest_item_name: #ktest_item_struct = #ktest_item_struct {
|
||||
fn_: #fn_name,
|
||||
should_panic: #should_panic,
|
||||
module_path: module_path!(),
|
||||
fn_name: stringify!(#fn_name),
|
||||
package: #package_name,
|
||||
source: #source,
|
||||
line: #line,
|
||||
col: #col,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let output = quote! {
|
||||
#input
|
||||
pub fn info(&self) -> &KtestItemInfo {
|
||||
&self.info
|
||||
}
|
||||
|
||||
#register
|
||||
};
|
||||
|
||||
TokenStream::from(output)
|
||||
/// 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The procedural macro to run all the tests.
|
||||
#[proc_macro]
|
||||
pub fn do_ktests(_item: TokenStream) -> TokenStream {
|
||||
let body = quote! {
|
||||
use crate::arch::qemu::{exit_qemu, QemuExitCode};
|
||||
|
||||
struct KtestItem {
|
||||
fn_: fn() -> (),
|
||||
should_panic: bool,
|
||||
module_path: &'static str,
|
||||
fn_name: &'static str,
|
||||
package: &'static str,
|
||||
source: &'static str,
|
||||
line: usize,
|
||||
col: usize,
|
||||
};
|
||||
macro_rules! ktest_array {
|
||||
() => {{
|
||||
extern "C" {
|
||||
fn __ktest_array();
|
||||
fn __ktest_array_end();
|
||||
};
|
||||
let item_size = core::mem::size_of::<KtestItem>() as u64;
|
||||
let l = (__ktest_array_end as u64 - __ktest_array as u64) / item_size;
|
||||
crate::println!("Running {} tests", l);
|
||||
for i in 0..l {
|
||||
unsafe {
|
||||
let item_ptr = (__ktest_array as u64 + item_size * i) as *const u64;
|
||||
let item = item_ptr as *const KtestItem;
|
||||
crate::print!("[{}] test {}::{} ...", (*item).package, (*item).module_path, (*item).fn_name);
|
||||
let test_result = unwinding::panic::catch_unwind((*item).fn_);
|
||||
let print_failure_heading = || {
|
||||
crate::println!("\nfailures:\n");
|
||||
crate::println!("---- {}:{}:{} - {} ----", (*item).source, (*item).line, (*item).col, (*item).fn_name);
|
||||
};
|
||||
if !(*item).should_panic {
|
||||
match test_result {
|
||||
Ok(()) => {
|
||||
crate::println!(" ok");
|
||||
},
|
||||
Err(e) => {
|
||||
crate::println!(" FAILED");
|
||||
print_failure_heading();
|
||||
match e.downcast::<crate::panicking::PanicInfo>() {
|
||||
Ok(s) => {
|
||||
crate::println!("[caught panic] {}", s);
|
||||
},
|
||||
Err(payload) => {
|
||||
crate::println!("[caught panic] unknown panic payload: {:#?}", payload);
|
||||
},
|
||||
}
|
||||
exit_qemu(QemuExitCode::Failed);
|
||||
},
|
||||
}
|
||||
} else {
|
||||
match test_result {
|
||||
Ok(()) => {
|
||||
crate::println!(" FAILED");
|
||||
print_failure_heading();
|
||||
crate::println!("test did not panic as expected");
|
||||
exit_qemu(QemuExitCode::Failed);
|
||||
},
|
||||
Err(_) => {
|
||||
crate::println!(" ok");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exit_qemu(QemuExitCode::Success);
|
||||
};
|
||||
|
||||
TokenStream::from(body)
|
||||
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 Some(ktest_item) = ktest_array!().get(self.index) else {
|
||||
return None;
|
||||
};
|
||||
self.index += 1;
|
||||
Some(ktest_item.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ktest_tests() -> (usize, KtestIter) {
|
||||
(ktest_array!().len(), KtestIter::new())
|
||||
}
|
||||
|
88
framework/libs/ktest/src/runner.rs
Normal file
88
framework/libs/ktest/src/runner.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use crate::{CatchUnwindImpl, KtestError, KtestItem};
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use core::format_args;
|
||||
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
pub enum KtestResult {
|
||||
Ok,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Run all the tests registered by `#[ktest]` in the `.ktest_array` section.
|
||||
///
|
||||
/// Need to provide a print function to print the test result, and a `catch_unwind`
|
||||
/// implementation to catch the panic.
|
||||
///
|
||||
/// Returns the test result interpreted as `ok` or `FAILED`.
|
||||
pub fn run_ktests<PrintFn>(print: PrintFn, catch_unwind: &CatchUnwindImpl) -> KtestResult
|
||||
where
|
||||
PrintFn: Fn(core::fmt::Arguments),
|
||||
{
|
||||
macro_rules! print {
|
||||
($fmt: literal $(, $($arg: tt)+)?) => {
|
||||
print(format_args!($fmt $(, $($arg)+)?))
|
||||
}
|
||||
}
|
||||
|
||||
let (n, ktests) = crate::get_ktest_tests();
|
||||
print!("\nrunning {} tests\n\n", n);
|
||||
let mut passed: usize = 0;
|
||||
let mut failed_tests: Vec<(KtestItem, KtestError)> = Vec::new();
|
||||
for test in ktests {
|
||||
print!(
|
||||
"[{}] test {}::{} ...",
|
||||
test.info().package,
|
||||
test.info().module_path,
|
||||
test.info().fn_name
|
||||
);
|
||||
match test.run(catch_unwind) {
|
||||
Ok(()) => {
|
||||
print!(" {}\n", "ok".green());
|
||||
passed += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
print!(" {}\n", "FAILED".red());
|
||||
failed_tests.push((test.clone(), e.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let failed = failed_tests.len();
|
||||
if failed == 0 {
|
||||
print!("\ntest result: {}.", "ok".green());
|
||||
} else {
|
||||
print!("\ntest result: {}.", "FAILED".red());
|
||||
}
|
||||
print!(" {} passed; {} failed.\n", passed, failed);
|
||||
if failed > 0 {
|
||||
print!("\nfailures:\n\n");
|
||||
for (t, e) in failed_tests {
|
||||
print!(
|
||||
"---- {}:{}:{} - {} ----\n\n",
|
||||
t.info().source,
|
||||
t.info().line,
|
||||
t.info().col,
|
||||
t.info().fn_name
|
||||
);
|
||||
match e {
|
||||
KtestError::Panic(s) => {
|
||||
print!("[caught panic] {}\n", s);
|
||||
}
|
||||
KtestError::ShouldPanicButNoPanic => {
|
||||
print!("test did not panic as expected\n");
|
||||
}
|
||||
KtestError::ExpectedPanicNotMatch(expected, s) => {
|
||||
print!("[caught panic] expected panic not match\n");
|
||||
print!("expected: {}\n", expected);
|
||||
print!("caught: {}\n", s);
|
||||
}
|
||||
KtestError::Unknown => {
|
||||
print!("[caught panic] unknown panic payload! (fatal panic handling error in ktest)\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
return KtestResult::Failed;
|
||||
}
|
||||
KtestResult::Ok
|
||||
}
|
Reference in New Issue
Block a user