diff --git a/.github/workflows/publish_osdk_and_ostd.yml b/.github/workflows/publish_osdk_and_ostd.yml index a6f2dcdc6..488a512e4 100644 --- a/.github/workflows/publish_osdk_and_ostd.yml +++ b/.github/workflows/publish_osdk_and_ostd.yml @@ -48,15 +48,18 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Check Publish OSTD + - name: Check Publish OSTD and the test runner # On pull request, set `--dry-run` to check whether OSDK can publish if: github.event_name == 'pull_request' run: | cd ostd cargo publish --target ${{ matrix.target }} --dry-run cargo doc --target ${{ matrix.target }} + cd osdk/test-kernel + cargo publish --target ${{ matrix.target }} --dry-run + cargo doc --target ${{ matrix.target }} - - name: Publish OSTD + - name: Publish OSTD and the test runner if: github.event_name == 'push' env: REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} @@ -66,4 +69,6 @@ jobs: run: | cd ostd cargo publish --target x86_64-unknown-none --token ${REGISTRY_TOKEN} - + cd osdk/test-kernel + cargo publish --target x86_64-unknown-none --token ${REGISTRY_TOKEN} + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 117acb1d2..507bc81cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1045,6 +1045,15 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "osdk-test-kernel" +version = "0.1.0" +dependencies = [ + "ostd", + "owo-colors 4.0.0", + "unwinding", +] + [[package]] name = "ostd" version = "0.7.0" @@ -1075,7 +1084,7 @@ dependencies = [ "ostd-macros", "ostd-pod", "ostd-test", - "owo-colors", + "owo-colors 3.5.0", "rsdp", "spin 0.9.8", "static_assertions", @@ -1119,9 +1128,6 @@ dependencies = [ [[package]] name = "ostd-test" version = "0.1.0" -dependencies = [ - "owo-colors", -] [[package]] name = "owo-colors" @@ -1129,6 +1135,12 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "owo-colors" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" + [[package]] name = "paste" version = "1.0.14" diff --git a/Cargo.toml b/Cargo.toml index 26ab93e79..edcb204ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "osdk/test-kernel", "ostd", "ostd/libs/align_ext", "ostd/libs/ostd-macros", diff --git a/Makefile b/Makefile index 8b85e848f..c70452dc0 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,7 @@ NON_OSDK_CRATES := \ # In contrast, OSDK crates depend on OSTD (or being `ostd` itself) # and need to be built or tested with OSDK. OSDK_CRATES := \ + osdk/test-kernel \ ostd \ ostd/libs/linux-bzimage/setup \ kernel \ @@ -130,7 +131,10 @@ all: build # To uninstall, do `cargo uninstall cargo-osdk` .PHONY: install_osdk install_osdk: - @cargo install cargo-osdk --path osdk + @# The `OSDK_LOCAL_DEV` environment variable is used for local development + @# without the need to publish the changes of OSDK's self-hosted + @# dependencies to `crates.io`. + @OSDK_LOCAL_DEV=1 cargo install cargo-osdk --path osdk # This will install OSDK if it is not already installed # To update OSDK, we need to run `install_osdk` manually diff --git a/kernel/aster-nix/src/lib.rs b/kernel/aster-nix/src/lib.rs index 72ea970ee..4a8faf02b 100644 --- a/kernel/aster-nix/src/lib.rs +++ b/kernel/aster-nix/src/lib.rs @@ -13,6 +13,7 @@ #![feature(int_roundings)] #![feature(iter_repeat_n)] #![feature(let_chains)] +#![feature(linkage)] #![feature(linked_list_remove)] #![feature(negative_impls)] #![feature(register_tool)] diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index dd59f76b0..328058278 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -2,6 +2,7 @@ #![no_std] #![no_main] +#![feature(linkage)] #![deny(unsafe_code)] extern crate ostd; diff --git a/osdk/src/base_crate/mod.rs b/osdk/src/base_crate/mod.rs index 4f1b727ae..f7a482052 100644 --- a/osdk/src/base_crate/mod.rs +++ b/osdk/src/base_crate/mod.rs @@ -1,8 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 //! The base crate is the OSDK generated crate that is ultimately built by cargo. -//! It will depend on the kernel crate. -//! +//! It will depend on the to-be-built kernel crate or the to-be-tested crate. use std::{ fs, @@ -12,10 +11,16 @@ use std::{ use crate::util::get_cargo_metadata; +/// Create a new base crate that will be built by cargo. +/// +/// The dependencies of the base crate will be the target crate. If +/// `link_unit_test_runner` is set to true, the base crate will also depend on +/// the `ostd-test-runner` crate. pub fn new_base_crate( base_crate_path: impl AsRef, dep_crate_name: &str, dep_crate_path: impl AsRef, + link_unit_test_runner: bool, ) { let workspace_root = { let meta = get_cargo_metadata(None::<&str>, None::<&[&str]>).unwrap(); @@ -82,7 +87,7 @@ pub fn new_base_crate( fs::write("src/main.rs", main_rs).unwrap(); // Add dependencies to the Cargo.toml - add_manifest_dependency(dep_crate_name, dep_crate_path); + add_manifest_dependency(dep_crate_name, dep_crate_path, link_unit_test_runner); // Copy the manifest configurations from the target crate to the base crate copy_profile_configurations(workspace_root); @@ -94,7 +99,11 @@ pub fn new_base_crate( std::env::set_current_dir(original_dir).unwrap(); } -fn add_manifest_dependency(crate_name: &str, crate_path: impl AsRef) { +fn add_manifest_dependency( + crate_name: &str, + crate_path: impl AsRef, + link_unit_test_runner: bool, +) { let mainfest_path = "Cargo.toml"; let mut manifest: toml::Table = { @@ -112,13 +121,26 @@ fn add_manifest_dependency(crate_name: &str, crate_path: impl AsRef) { let dependencies = manifest.get_mut("dependencies").unwrap(); - let dep = toml::Table::from_str(&format!( + let target_dep = toml::Table::from_str(&format!( "{} = {{ path = \"{}\", default-features = false }}", crate_name, crate_path.as_ref().display() )) .unwrap(); - dependencies.as_table_mut().unwrap().extend(dep); + dependencies.as_table_mut().unwrap().extend(target_dep); + + if link_unit_test_runner { + let dep_str = match option_env!("OSDK_LOCAL_DEV") { + Some("1") => "osdk-test-kernel = { path = \"../../../osdk/test-kernel\" }", + _ => concat!( + "osdk-test-kernel = { version = \"", + env!("CARGO_PKG_VERSION"), + "\" }" + ), + }; + let test_runner_dep = toml::Table::from_str(dep_str).unwrap(); + dependencies.as_table_mut().unwrap().extend(test_runner_dep); + } let content = toml::to_string(&manifest).unwrap(); fs::write(mainfest_path, content).unwrap(); diff --git a/osdk/src/commands/build/bin.rs b/osdk/src/commands/build/bin.rs index 10d907307..29194b0f2 100644 --- a/osdk/src/commands/build/bin.rs +++ b/osdk/src/commands/build/bin.rs @@ -171,7 +171,7 @@ fn install_setup_with_arch( cmd.arg("install").arg("linux-bzimage-setup"); cmd.arg("--force"); cmd.arg("--root").arg(install_dir.as_ref()); - if std::env::var("AUTO_TEST").is_ok() || std::env::var("OSDK_INTEGRATION_TEST").is_ok() { + if matches!(option_env!("OSDK_LOCAL_DEV"), Some("1")) { cmd.arg("--path") .arg("../../../ostd/libs/linux-bzimage/setup"); } diff --git a/osdk/src/commands/build/mod.rs b/osdk/src/commands/build/mod.rs index 8252402b2..7f680b214 100644 --- a/osdk/src/commands/build/mod.rs +++ b/osdk/src/commands/build/mod.rs @@ -72,6 +72,7 @@ pub fn create_base_and_cached_build( &base_crate_path, &get_current_crate_info().name, get_current_crate_info().path, + false, ); let original_dir = std::env::current_dir().unwrap(); std::env::set_current_dir(&base_crate_path).unwrap(); diff --git a/osdk/src/commands/new/kernel.template b/osdk/src/commands/new/kernel.template index bb5486add..c92050d16 100644 --- a/osdk/src/commands/new/kernel.template +++ b/osdk/src/commands/new/kernel.template @@ -1,4 +1,6 @@ #![no_std] +// The feature `linkage` is required for `ostd::main` to work. +#![feature(linkage)] #![deny(unsafe_code)] use ostd::prelude::*; diff --git a/osdk/src/commands/test.rs b/osdk/src/commands/test.rs index e3d0865e8..327ebd476 100644 --- a/osdk/src/commands/test.rs +++ b/osdk/src/commands/test.rs @@ -7,6 +7,8 @@ use crate::{ base_crate::new_base_crate, cli::TestArgs, config::{scheme::ActionChoice, Config}, + error::Errno, + error_msg, util::{ get_cargo_metadata, get_current_crate_info, get_target_directory, parse_package_id_string, }, @@ -25,7 +27,26 @@ pub fn test_current_crate(config: &Config, args: &TestArgs) { let cargo_target_directory = get_target_directory(); let osdk_output_directory = cargo_target_directory.join(DEFAULT_TARGET_RELPATH); let target_crate_dir = osdk_output_directory.join("base"); - new_base_crate(&target_crate_dir, ¤t_crate.name, ¤t_crate.path); + + // A special case is that we use OSDK to test the OSDK test runner crate + // itself. We check it by name. + let runner_self_test = if current_crate.name == "osdk-test-kernel" { + if matches!(option_env!("OSDK_LOCAL_DEV"), Some("1")) { + true + } else { + error_msg!("The tested crate name collides with the OSDK test runner crate"); + std::process::exit(Errno::BadCrateName as _); + } + } else { + false + }; + + new_base_crate( + &target_crate_dir, + ¤t_crate.name, + ¤t_crate.path, + !runner_self_test, + ); let main_rs_path = target_crate_dir.join("src").join("main.rs"); @@ -39,19 +60,29 @@ pub fn test_current_crate(config: &Config, args: &TestArgs) { ktest_crate_whitelist.push(name.clone()); } - let ktest_static_var = format!( + // Append the ktest static variable and the runner reference to the + // `main.rs` file. + let ktest_main_rs = format!( r#" + +{} + #[no_mangle] pub static KTEST_TEST_WHITELIST: Option<&[&str]> = {}; #[no_mangle] pub static KTEST_CRATE_WHITELIST: Option<&[&str]> = Some(&{:#?}); -"#, - ktest_test_whitelist, ktest_crate_whitelist, - ); - // Append the ktest static variable to the main.rs file +"#, + if runner_self_test { + "" + } else { + "extern crate osdk_test_kernel;" + }, + ktest_test_whitelist, + ktest_crate_whitelist, + ); let mut main_rs_content = fs::read_to_string(&main_rs_path).unwrap(); - main_rs_content.push_str(&ktest_static_var); + main_rs_content.push_str(&ktest_main_rs); fs::write(&main_rs_path, main_rs_content).unwrap(); // Build the kernel with the given base crate diff --git a/osdk/src/error.rs b/osdk/src/error.rs index b266c8f59..1dfdeaa17 100644 --- a/osdk/src/error.rs +++ b/osdk/src/error.rs @@ -10,6 +10,7 @@ pub enum Errno { ExecuteCommand = 5, BuildCrate = 6, RunBundle = 7, + BadCrateName = 8, } /// Print error message to console diff --git a/osdk/test-kernel/Cargo.toml b/osdk/test-kernel/Cargo.toml new file mode 100644 index 000000000..65e5803c3 --- /dev/null +++ b/osdk/test-kernel/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "osdk-test-kernel" +version = "0.1.0" +edition = "2021" +description = "The OSTD-based kernel for running unit tests with OSDK." +license = "MPL-2.0" +repository ="https://github.com/asterinas/asterinas" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ostd = { version = "0.7.0", path = "../../ostd" } +owo-colors = "4.0.0" +unwinding = { version = "0.2.2", default-features = false, features = ["fde-gnu-eh-frame-hdr", "hide-trace", "panic", "personality", "unwinder"] } diff --git a/ostd/libs/ostd-test/src/runner.rs b/osdk/test-kernel/src/lib.rs similarity index 57% rename from ostd/libs/ostd-test/src/runner.rs rename to osdk/test-kernel/src/lib.rs index 1fc16fd00..164ce20ce 100644 --- a/ostd/libs/ostd-test/src/runner.rs +++ b/osdk/test-kernel/src/lib.rs @@ -1,24 +1,59 @@ // SPDX-License-Identifier: MPL-2.0 -//! Test runner enabling control over the tests. -//! +//! The OSTD unit test runner is a kernel that runs the tests defined by the +//! `#[ostd::ktest]` attribute. The kernel should be automatically selected to +//! run when OSDK is used to test a specific crate. -use alloc::{collections::BTreeSet, string::String, vec::Vec}; -use core::format_args; +#![no_std] +#![forbid(unsafe_code)] -use owo_colors::OwoColorize; +extern crate alloc; -use crate::{ - path::{KtestPath, SuffixTrie}, - tree::{KtestCrate, KtestTree}, - CatchUnwindImpl, KtestError, KtestItem, KtestIter, +mod path; +mod tree; + +use alloc::{boxed::Box, collections::BTreeSet, string::String, vec::Vec}; +use core::{any::Any, format_args}; + +use ostd::{ + early_print, + ktest::{ + get_ktest_crate_whitelist, get_ktest_test_whitelist, KtestError, KtestItem, KtestIter, + }, }; +use owo_colors::OwoColorize; +use path::{KtestPath, SuffixTrie}; +use tree::{KtestCrate, KtestTree}; pub enum KtestResult { Ok, Failed, } +/// The entry point of the test runner. +#[ostd::ktest::main] +fn main() { + use ostd::task::TaskOptions; + + let test_task = move || { + use alloc::string::ToString; + + use ostd::arch::qemu::{exit_qemu, QemuExitCode}; + + match run_ktests( + get_ktest_test_whitelist().map(|s| s.iter().map(|s| s.to_string())), + get_ktest_crate_whitelist(), + ) { + KtestResult::Ok => exit_qemu(QemuExitCode::Success), + KtestResult::Failed => exit_qemu(QemuExitCode::Failed), + }; + }; + + let _ = TaskOptions::new(test_task).data(()).spawn(); + + unreachable!("The spawn method will NOT return in the boot context") +} + /// Run all the tests registered by `#[ktest]` in the `.ktest_array` section. /// /// Need to provide a print function `print` to print the test result, and a `catch_unwind` @@ -32,27 +67,18 @@ pub enum KtestResult { /// /// If a test inside a crate fails, the test runner will continue to run the rest of the tests /// inside the crate. But the tests in the following crates will not be run. -pub fn run_ktests( - print: &PrintFn, - catch_unwind: &CatchUnwindImpl, +fn run_ktests( test_whitelist: Option, crate_whitelist: Option<&[&str]>, ) -> KtestResult where - PrintFn: Fn(core::fmt::Arguments), PathsIter: Iterator, { - macro_rules! print { - ($fmt: literal $(, $($arg: tt)+)?) => { - print(format_args!($fmt $(, $($arg)+)?)) - } - } - let whitelist_trie = test_whitelist.map(|paths| SuffixTrie::from_paths(paths.map(|p| KtestPath::from(&p)))); let tree = KtestTree::from_iter(KtestIter::new()); - print!( + early_print!( "\n[ktest runner] running {} tests in {} crates\n", tree.nr_tot_tests(), tree.nr_tot_crates() @@ -62,36 +88,22 @@ where for crate_ in tree.iter() { if let Some(crate_set) = &crate_set { if !crate_set.contains(crate_.name()) { - print!("\n[ktest runner] skipping crate \"{}\".\n", crate_.name()); + early_print!("\n[ktest runner] skipping crate \"{}\".\n", crate_.name()); continue; } } - match run_crate_ktests(crate_, print, catch_unwind, &whitelist_trie) { + match run_crate_ktests(crate_, &whitelist_trie) { KtestResult::Ok => {} KtestResult::Failed => return KtestResult::Failed, } } - print!("\n[ktest runner] All crates tested.\n"); + early_print!("\n[ktest runner] All crates tested.\n"); KtestResult::Ok } -fn run_crate_ktests( - crate_: &KtestCrate, - print: &PrintFn, - catch_unwind: &CatchUnwindImpl, - whitelist: &Option, -) -> KtestResult -where - PrintFn: Fn(core::fmt::Arguments), -{ - macro_rules! print { - ($fmt: literal $(, $($arg: tt)+)?) => { - print(format_args!($fmt $(, $($arg)+)?)) - } - } - +fn run_crate_ktests(crate_: &KtestCrate, whitelist: &Option) -> KtestResult { let crate_name = crate_.name(); - print!( + early_print!( "\nrunning {} tests in crate \"{}\"\n\n", crate_.nr_tot_tests(), crate_name @@ -110,19 +122,22 @@ where continue; } } - print!( + early_print!( "test {}::{} ...", test.info().module_path, test.info().fn_name ); debug_assert_eq!(test.info().package, crate_name); - match test.run(catch_unwind) { + match test.run( + &(unwinding::panic::catch_unwind::<(), fn()> + as fn(fn()) -> Result<(), Box<(dyn Any + Send + 'static)>>), + ) { Ok(()) => { - print!(" {}\n", "ok".green()); + early_print!(" {}\n", "ok".green()); passed += 1; } Err(e) => { - print!(" {}\n", "FAILED".red()); + early_print!(" {}\n", "FAILED".red()); failed_tests.push((test.clone(), e.clone())); } } @@ -130,19 +145,21 @@ where } let failed = failed_tests.len(); if failed == 0 { - print!("\ntest result: {}.", "ok".green()); + early_print!("\ntest result: {}.", "ok".green()); } else { - print!("\ntest result: {}.", "FAILED".red()); + early_print!("\ntest result: {}.", "FAILED".red()); } - print!( + early_print!( " {} passed; {} failed; {} filtered out.\n", - passed, failed, filtered + passed, + failed, + filtered ); assert!(passed + failed + filtered == crate_.nr_tot_tests()); if failed > 0 { - print!("\nfailures:\n\n"); + early_print!("\nfailures:\n\n"); for (t, e) in failed_tests { - print!( + early_print!( "---- {}:{}:{} - {} ----\n\n", t.info().source, t.info().line, @@ -151,18 +168,18 @@ where ); match e { KtestError::Panic(s) => { - print!("[caught panic] {}\n", s); + early_print!("[caught panic] {}\n", s); } KtestError::ShouldPanicButNoPanic => { - print!("test did not panic as expected\n"); + early_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); + early_print!("[caught panic] expected panic not match\n"); + early_print!("expected: {}\n", expected); + early_print!("caught: {}\n", s); } KtestError::Unknown => { - print!("[caught panic] unknown panic payload! (fatal panic handling error in ktest)\n"); + early_print!("[caught panic] unknown panic payload! (fatal panic handling error in ktest)\n"); } } } diff --git a/ostd/libs/ostd-test/src/path.rs b/osdk/test-kernel/src/path.rs similarity index 98% rename from ostd/libs/ostd-test/src/path.rs rename to osdk/test-kernel/src/path.rs index 434acb49b..b6a973e71 100644 --- a/ostd/libs/ostd-test/src/path.rs +++ b/osdk/test-kernel/src/path.rs @@ -112,11 +112,13 @@ impl Display for KtestPath { } } -#[cfg(test)] +#[cfg(ktest)] mod path_test { + use ostd::prelude::ktest; + use super::*; - #[test] + #[ktest] fn test_ktest_path() { let mut path = KtestPath::new(); path.push_back("a"); @@ -129,7 +131,7 @@ mod path_test { assert_eq!(path.pop_back(), None); } - #[test] + #[ktest] fn test_ktest_path_starts_with() { let mut path = KtestPath::new(); path.push_back("a"); @@ -144,7 +146,7 @@ mod path_test { assert!(!path.starts_with(&KtestPath::from("d"))); } - #[test] + #[ktest] fn test_ktest_path_ends_with() { let mut path = KtestPath::new(); path.push_back("a"); @@ -238,8 +240,10 @@ impl Default for SuffixTrie { } } -#[cfg(test)] +#[cfg(ktest)] mod suffix_trie_test { + use ostd::prelude::ktest; + use super::*; static TEST_PATHS: &[&str] = &[ @@ -252,7 +256,7 @@ mod suffix_trie_test { "m::n", ]; - #[test] + #[ktest] fn test_contains() { let trie = SuffixTrie::from_paths(TEST_PATHS.iter().map(|&s| KtestPath::from(s))); @@ -269,7 +273,7 @@ mod suffix_trie_test { assert!(!trie.contains(KtestPath::from("n").iter())); } - #[test] + #[ktest] fn test_matches() { let trie = SuffixTrie::from_paths(TEST_PATHS.iter().map(|&s| KtestPath::from(s))); diff --git a/ostd/libs/ostd-test/src/tree.rs b/osdk/test-kernel/src/tree.rs similarity index 98% rename from ostd/libs/ostd-test/src/tree.rs rename to osdk/test-kernel/src/tree.rs index 3d908fd9a..85ac5c9f3 100644 --- a/ostd/libs/ostd-test/src/tree.rs +++ b/osdk/test-kernel/src/tree.rs @@ -213,8 +213,10 @@ impl<'a> Iterator for KtestModuleIter<'a> { } } -#[cfg(test)] +#[cfg(ktest)] mod tests { + use ostd::prelude::ktest; + use super::*; macro_rules! gen_test_case { @@ -227,7 +229,7 @@ mod tests { KtestItem::new( dummy_fn, (false, None), - crate::KtestItemInfo { + ostd::ktest::KtestItemInfo { module_path: m, fn_name: f, package: p, @@ -250,7 +252,7 @@ mod tests { }}; } - #[test] + #[ktest] fn test_tree_iter() { let tree = gen_test_case!(); let mut iter = tree.iter(); @@ -261,7 +263,7 @@ mod tests { assert!(iter.next().is_none()); } - #[test] + #[ktest] fn test_crate_iter() { let tree = gen_test_case!(); for crate_ in tree.iter() { @@ -285,7 +287,7 @@ mod tests { } } - #[test] + #[ktest] fn test_module_iter() { let tree = gen_test_case!(); let mut collection = Vec::<&KtestItem>::new(); diff --git a/osdk/tests/examples_in_book/work_in_workspace_templates/myos/src/lib.rs b/osdk/tests/examples_in_book/work_in_workspace_templates/myos/src/lib.rs index a8fc07f1d..a5cd52cd9 100644 --- a/osdk/tests/examples_in_book/work_in_workspace_templates/myos/src/lib.rs +++ b/osdk/tests/examples_in_book/work_in_workspace_templates/myos/src/lib.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 #![no_std] +#![feature(linkage)] #![deny(unsafe_code)] use ostd::prelude::*; diff --git a/osdk/tests/examples_in_book/write_a_kernel_in_100_lines_templates/lib.rs b/osdk/tests/examples_in_book/write_a_kernel_in_100_lines_templates/lib.rs index 9fbfde511..b7d923542 100644 --- a/osdk/tests/examples_in_book/write_a_kernel_in_100_lines_templates/lib.rs +++ b/osdk/tests/examples_in_book/write_a_kernel_in_100_lines_templates/lib.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 #![no_std] +// The feature `linkage` is required for `ostd::main` to work. +#![feature(linkage)] extern crate alloc; diff --git a/ostd/libs/ostd-macros/src/lib.rs b/ostd/libs/ostd-macros/src/lib.rs index 93466626b..869d75a18 100644 --- a/ostd/libs/ostd-macros/src/lib.rs +++ b/ostd/libs/ostd-macros/src/lib.rs @@ -7,12 +7,13 @@ use quote::quote; use rand::{distributions::Alphanumeric, Rng}; use syn::{parse_macro_input, Expr, Ident, ItemFn}; -/// This macro is used to mark the kernel entry point. +/// A macro attribute to mark the kernel entry point. /// /// # Example /// /// ```ignore /// #![no_std] +/// #![feature(linkage)] /// /// use ostd::prelude::*; /// @@ -28,8 +29,37 @@ pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream { quote!( #[no_mangle] - pub fn __ostd_main() -> ! { - ostd::init(); + #[linkage = "weak"] + extern "Rust" fn __ostd_main() -> ! { + // SAFETY: The function is called only once on the BSP. + unsafe { ostd::init() }; + #main_fn_name(); + ostd::prelude::abort(); + } + + #main_fn + ) + .into() +} + +/// A macro attribute for the unit test kernel entry point. +/// +/// This macro is used for internal OSDK implementation. Do not use it +/// directly. +/// +/// It is a strong version of the `main` macro attribute. So if it exists ( +/// which means the unit test kernel is linked to perform testing), the actual +/// kernel entry point will be replaced by this one. +#[proc_macro_attribute] +pub fn test_main(_attr: TokenStream, item: TokenStream) -> TokenStream { + let main_fn = parse_macro_input!(item as ItemFn); + let main_fn_name = &main_fn.sig.ident; + + quote!( + #[no_mangle] + extern "Rust" fn __ostd_main() -> ! { + // SAFETY: The function is called only once on the BSP. + unsafe { ostd::init() }; #main_fn_name(); ostd::prelude::abort(); } diff --git a/ostd/libs/ostd-test/Cargo.toml b/ostd/libs/ostd-test/Cargo.toml index 82d4bcacf..5b8e6f949 100644 --- a/ostd/libs/ostd-test/Cargo.toml +++ b/ostd/libs/ostd-test/Cargo.toml @@ -2,11 +2,8 @@ name = "ostd-test" version = "0.1.0" edition = "2021" -description = "The kernel mode testing framework of OSTD" +description = "The kernel mode unit testing framework of OSTD" license = "MPL-2.0" repository ="https://github.com/asterinas/asterinas" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -owo-colors = "3.5.0" diff --git a/ostd/libs/ostd-test/src/lib.rs b/ostd/libs/ostd-test/src/lib.rs index 3ec04c608..b4fd65d59 100644 --- a/ostd/libs/ostd-test/src/lib.rs +++ b/ostd/libs/ostd-test/src/lib.rs @@ -45,12 +45,6 @@ //! //! Any crates using the ostd-test framework should be linked with ostd. //! -//! ```toml -//! # Cargo.toml -//! [dependencies] -//! ostd = { path = "relative/path/to/ostd" } -//! ``` -//! //! 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]` @@ -58,27 +52,10 @@ //! 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,ostd::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=ostd -//! `` +//! with the `ktest` framework, OSDK will set the `RUSTFLAGS` environment variable +//! to pass the cfgs to all rustc invocations. To run the tests, you simply need +//! to use the command `cargo osdk test` in the crate directory. For more information, +//! please refer to the OSDK documentation. //! //! 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. @@ -90,10 +67,6 @@ #![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}; @@ -113,6 +86,7 @@ impl core::fmt::Display for PanicInfo { } } +/// The error that may occur during the test. #[derive(Clone)] pub enum KtestError { Panic(Box), @@ -121,13 +95,22 @@ pub enum KtestError { Unknown, } +/// The information of the unit test. #[derive(Clone, PartialEq, Debug)] pub struct KtestItemInfo { + /// The path of the module, not including the function name. + /// + /// It would be separated by `::`. pub module_path: &'static str, + /// The name of the unit test function. pub fn_name: &'static str, + /// The name of the crate. pub package: &'static str, + /// The source file where the test function resides. pub source: &'static str, + /// The line number of the test function in the file. pub line: usize, + /// The column number of the test function in the file. pub col: usize, } @@ -141,6 +124,11 @@ pub struct KtestItem { type CatchUnwindImpl = fn(f: fn() -> ()) -> Result<(), Box>; impl KtestItem { + /// Create a new [`KtestItem`]. + /// + /// Do not use this function directly. Instead, use the `#[ktest]` + /// attribute to mark the test function. + #[doc(hidden)] pub const fn new( fn_: fn() -> (), should_panic: (bool, Option<&'static str>), @@ -153,6 +141,7 @@ impl KtestItem { } } + /// Get the information of the test. pub fn info(&self) -> &KtestItemInfo { &self.info } @@ -206,12 +195,22 @@ macro_rules! ktest_array { }}; } +/// The iterator of the ktest array. pub struct KtestIter { index: usize, } +impl Default for KtestIter { + fn default() -> Self { + Self::new() + } +} + impl KtestIter { - fn new() -> Self { + /// Create a new [`KtestIter`]. + /// + /// It will iterate over all the tests (marked with `#[ktest]`). + pub fn new() -> Self { Self { index: 0 } } } @@ -225,3 +224,28 @@ impl core::iter::Iterator for KtestIter { Some(ktest_item.clone()) } } + +// The whitelists that will be generated by the OSDK as static consts. +// They deliver the target tests that the user wants to run. +extern "Rust" { + static KTEST_TEST_WHITELIST: Option<&'static [&'static str]>; + static KTEST_CRATE_WHITELIST: Option<&'static [&'static str]>; +} + +/// Get the whitelist of the tests. +/// +/// The whitelist is generated by the OSDK runner, indicating name of the +/// target tests that the user wants to run. +pub fn get_ktest_test_whitelist() -> Option<&'static [&'static str]> { + // SAFETY: The two extern statics in the base crate are generated by OSDK. + unsafe { KTEST_TEST_WHITELIST } +} + +/// Get the whitelist of the crates. +/// +/// The whitelist is generated by the OSDK runner, indicating the target crate +/// that the user wants to test. +pub fn get_ktest_crate_whitelist() -> Option<&'static [&'static str]> { + // SAFETY: The two extern statics in the base crate are generated by OSDK. + unsafe { KTEST_CRATE_WHITELIST } +} diff --git a/ostd/src/boot/mod.rs b/ostd/src/boot/mod.rs index 627c674aa..2bd462f25 100644 --- a/ostd/src/boot/mod.rs +++ b/ostd/src/boot/mod.rs @@ -115,8 +115,9 @@ pub fn init() { /// /// Any kernel that uses the `ostd` crate should define a function marked with /// `ostd::main` as the entrypoint. -pub fn call_ostd_main() -> ! { - #[cfg(not(ktest))] +/// +/// This function should be only called from the bootloader-specific module. +pub(crate) fn call_ostd_main() -> ! { unsafe { // The entry point of kernel code, which should be defined by the package that // uses OSTD. @@ -125,42 +126,4 @@ pub fn call_ostd_main() -> ! { } __ostd_main(); } - #[cfg(ktest)] - unsafe { - use crate::task::TaskOptions; - - crate::init(); - // The whitelists that will be generated by OSDK runner as static consts. - extern "Rust" { - static KTEST_TEST_WHITELIST: Option<&'static [&'static str]>; - static KTEST_CRATE_WHITELIST: Option<&'static [&'static str]>; - } - - let test_task = move || { - run_ktests(KTEST_TEST_WHITELIST, KTEST_CRATE_WHITELIST); - }; - let _ = TaskOptions::new(test_task).data(()).spawn(); - unreachable!("The spawn method will NOT return in the boot context") - } -} - -fn run_ktests(test_whitelist: Option<&[&str]>, crate_whitelist: Option<&[&str]>) -> ! { - use alloc::{boxed::Box, string::ToString}; - use core::any::Any; - - use crate::arch::qemu::{exit_qemu, QemuExitCode}; - - let fn_catch_unwind = &(unwinding::panic::catch_unwind::<(), fn()> - as fn(fn()) -> Result<(), Box<(dyn Any + Send + 'static)>>); - - use ostd_test::runner::{run_ktests, KtestResult}; - match run_ktests( - &crate::console::early_print, - fn_catch_unwind, - test_whitelist.map(|s| s.iter().map(|s| s.to_string())), - crate_whitelist, - ) { - KtestResult::Ok => exit_qemu(QemuExitCode::Success), - KtestResult::Failed => exit_qemu(QemuExitCode::Failed), - }; } diff --git a/ostd/src/lib.rs b/ostd/src/lib.rs index 215d57e4e..cff2f80e9 100644 --- a/ostd/src/lib.rs +++ b/ostd/src/lib.rs @@ -56,10 +56,15 @@ pub(crate) use crate::cpu::local::cpu_local_cell; /// This function represents the first phase booting up the system. It makes /// all functionalities of OSTD available after the call. /// -/// TODO: We need to refactor this function to make it more modular and -/// make inter-initialization-dependencies more clear and reduce usages of -/// boot stage only global variables. -pub fn init() { +/// # Safety +/// +/// This function should be called only once and only on the BSP. +// +// TODO: We need to refactor this function to make it more modular and +// make inter-initialization-dependencies more clear and reduce usages of +// boot stage only global variables. +#[doc(hidden)] +pub unsafe fn init() { arch::enable_cpu_features(); arch::serial::init(); @@ -131,8 +136,14 @@ mod test { } } -/// The module re-exports everything from the ktest crate -#[cfg(ktest)] +#[doc(hidden)] pub mod ktest { + //! The module re-exports everything from the [`ostd_test`] crate, as well + //! as the test entry point macro. + //! + //! It is rather discouraged to use the definitions here directly. The + //! `ktest` attribute is sufficient for all normal use cases. + + pub use ostd_macros::test_main as main; pub use ostd_test::*; } diff --git a/tools/bump_version.sh b/tools/bump_version.sh index ecf828d2b..8b01e437f 100755 --- a/tools/bump_version.sh +++ b/tools/bump_version.sh @@ -20,6 +20,13 @@ update_package_version() { sed -i "0,/${pattern}/s/${pattern}/version = \"${new_version}\"/1" $1 } +# Update the version of the `ostd` dependency (`ostd = { version = "", ...`) in file $1 +update_ostd_dep_version() { + echo "Updating file $1" + pattern="^ostd = { version = \"[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+\"" + sed -i "0,/${pattern}/s/${pattern}/ostd = { version = \"${new_version}\"/1" $1 +} + # Update Docker image versions (`asterinas/asterinas:{version}`) in file $1 update_image_versions() { echo "Updating file $1" @@ -98,6 +105,7 @@ ASTER_SRC_DIR=${SCRIPT_DIR}/.. DOCS_DIR=${ASTER_SRC_DIR}/docs OSTD_CARGO_TOML_PATH=${ASTER_SRC_DIR}/ostd/Cargo.toml OSDK_CARGO_TOML_PATH=${ASTER_SRC_DIR}/osdk/Cargo.toml +OSTD_TEST_RUNNER_CARGO_TOML_PATH=${ASTER_SRC_DIR}/osdk/test-kernel/Cargo.toml VERSION_PATH=${ASTER_SRC_DIR}/VERSION current_version=$(cat ${VERSION_PATH}) @@ -114,6 +122,8 @@ new_version=$(bump_version ${current_version}) # Update the package version in Cargo.toml update_package_version ${OSTD_CARGO_TOML_PATH} update_package_version ${OSDK_CARGO_TOML_PATH} +update_package_version ${OSTD_TEST_RUNNER_CARGO_TOML_PATH} +update_ostd_dep_version ${OSTD_TEST_RUNNER_CARGO_TOML_PATH} # Automatically bump Cargo.lock file cargo update -p asterinas --precise $new_version