From a7e8f076864fdb7b3891abcb94bd17e5257987bc Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Wed, 18 Feb 2026 12:44:15 -0800 Subject: [PATCH 1/3] unit testing --- .github/workflows/test.yml | 8 +++- Cargo.toml | 5 ++ src/clock/realtime.rs | 17 +++++++ src/main.rs | 10 ++++ src/testing/mod.rs | 95 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/testing/mod.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 509b60b..d2d850e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,11 +23,15 @@ jobs: # If all features are disabled - name: Run tests inside docker container if: ${{ matrix.smp-feature == '' }} - run: docker run moss "/bin/bash" -c "cargo run -r --no-default-features -- /bin/usertest" >> out.log + run: | + docker run moss "/bin/bash" -c "cargo run -r --no-default-features -- /bin/usertest" >> out.log + docker run moss "/bin/bash" -c "cargo test -r --no-default-features" >> out.log # If any feature is enabled - name: Run tests inside docker container if: ${{ matrix.smp-feature == 'smp' }} - run: docker run moss "/bin/bash" -c "cargo run -r --no-default-features --features "${{ matrix.smp-feature }}" -- /bin/usertest" >> out.log + run: | + docker run moss "/bin/bash" -c "cargo run -r --no-default-features --features "${{ matrix.smp-feature }}" -- /bin/usertest" >> out.log + docker run moss "/bin/bash" -c "cargo test -r --no-default-features --features "${{ matrix.smp-feature }}"" >> out.log - name: Display test output run: cat out.log - name: Check for success line diff --git a/Cargo.toml b/Cargo.toml index d434ecf..dc857d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,11 @@ name = "moss" version = "0.1.0" edition = "2024" +[[bin]] +name = "moss" +test = true +bench = false + [dependencies] libkernel = { path = "libkernel" } aarch64-cpu = "11.1.0" diff --git a/src/clock/realtime.rs b/src/clock/realtime.rs index 837595a..fd4a8d5 100644 --- a/src/clock/realtime.rs +++ b/src/clock/realtime.rs @@ -27,3 +27,20 @@ pub fn set_date(duration: Duration) { // Represents a known duration since the epoch at the assoicated instant. static EPOCH_DURATION: SpinLock> = SpinLock::new(None); + +#[cfg(test)] +mod tests { + use super::*; + use crate::ktest; + + ktest! { + fn test_date_and_set_date() { + let initial_date = date(); + let new_date = Duration::from_secs(1_000_000); + set_date(new_date); + let updated_date = date(); + assert_ne!(initial_date, updated_date, "Date should change after set_date"); + assert!(updated_date >= new_date, "Updated date should be at least the new date set"); + } + } +} diff --git a/src/main.rs b/src/main.rs index 1c8541f..5b6cc43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,11 @@ #![feature(used_with_arg)] #![feature(likely_unlikely)] #![feature(box_as_ptr)] +#![expect(internal_features)] +#![feature(core_intrinsics)] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(crate::testing::test_runner)] use alloc::{ boxed::Box, @@ -45,6 +50,8 @@ mod memory; mod process; mod sched; mod sync; +#[cfg(test)] +pub mod testing; #[panic_handler] fn on_panic(info: &PanicInfo) -> ! { @@ -161,6 +168,9 @@ async fn launch_init(mut opts: KOptions) { .expect("Could not clone FD"); } + #[cfg(test)] + test_main(); + drop(task); let mut init_args = vec![init.as_str().to_string()]; diff --git a/src/testing/mod.rs b/src/testing/mod.rs new file mode 100644 index 0000000..154846d --- /dev/null +++ b/src/testing/mod.rs @@ -0,0 +1,95 @@ +use crate::arch::{Arch, ArchImpl}; +use crate::console::write_fmt; +use crate::drivers::timer::uptime; +use alloc::format; +use core::fmt::Display; + +const TEXT_GREEN: &str = "\x1b[32m"; +const TEXT_RED: &str = "\x1b[31m"; +const TEXT_RESET: &str = "\x1b[0m"; + +pub enum TestResult { + Ok, + Failed, + Skipped, +} + +impl Display for TestResult { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + TestResult::Ok => write!(f, "{TEXT_GREEN}ok{TEXT_RESET}"), + TestResult::Failed => write!(f, "{TEXT_GREEN}failed{TEXT_RESET}"), + TestResult::Skipped => write!(f, "skipped"), + } + } +} + +pub struct Test { + pub name: &'static str, + pub test_fn: fn() -> TestResult, +} + +#[cfg(test)] +pub fn test_runner(tests: &[&Test]) { + write_fmt(format_args!("\nrunning {} tests\n", tests.len())).unwrap(); + let mut passed = 0; + let mut failed = 0; + let mut ignored = 0; + let start = uptime(); + for test in tests { + let result = (test.test_fn)(); + match result { + TestResult::Ok => passed += 1, + TestResult::Failed => failed += 1, + TestResult::Skipped => ignored += 1, + } + write_fmt(format_args!("test {} ... {}\n", test.name, result)).unwrap(); + } + let duration = uptime() - start; + write_fmt(format_args!( + "\ntest result: {}. {passed} passed; {failed} failed; {ignored} ignored; finished in {}.{}s\n", + if failed == 0 { + format!("{TEXT_GREEN}ok{TEXT_RESET}") + } else { + format!("{TEXT_RED}FAILED{TEXT_RESET}") + }, + duration.as_secs(), + duration.subsec_millis() / 10 + )) + .unwrap(); + ArchImpl::power_off(); +} + +pub fn panic_noop(_: *mut u8, _: *mut u8) {} + +#[macro_export] +macro_rules! ktest { + (fn $name:ident() $body:block) => { + #[cfg(test)] + fn $name(_: *mut u8) { + $body + } + + paste::paste! { + #[cfg(test)] + #[test_case] + static [<__TEST_ $name>]: crate::testing::Test = crate::testing::Test { + name: concat!(module_path!(), "::", stringify!($name)), + test_fn: || { + let result = unsafe { + core::intrinsics::catch_unwind( + $name as fn(*mut u8), + core::ptr::null_mut(), + crate::testing::panic_noop, + ) + }; + match result { + 0 => crate::testing::TestResult::Ok, + 1 => crate::testing::TestResult::Failed, + _ => unreachable!("catch_unwind should only return 0 or 1"), + } + }, + }; + } + }; +} From d2d1ba2cbfb5c9cd05cdcdea95fcf8cb7fb7632b Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Wed, 18 Feb 2026 22:40:17 -0800 Subject: [PATCH 2/3] async unit testing --- src/fs/mod.rs | 12 ++++++++++++ src/kernel/kpipe.rs | 21 +++++++++++++++++++++ src/testing/mod.rs | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 4566f19..cd5438f 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -626,3 +626,15 @@ impl VFS { fs.sync().await } } + +#[cfg(test)] +mod tests { + use crate::fs::VFS; + use crate::ktest; + + ktest! { + async fn test_sync_all() { + VFS.sync_all().await.unwrap(); + } + } +} diff --git a/src/kernel/kpipe.rs b/src/kernel/kpipe.rs index 84c1acf..1bac517 100644 --- a/src/kernel/kpipe.rs +++ b/src/kernel/kpipe.rs @@ -96,3 +96,24 @@ impl KPipe { self.inner.splice_from(&source.inner, count).await } } + +#[cfg(test)] +mod tests { + use crate::kernel::kpipe::KPipe; + use crate::ktest; + + ktest! { + async fn kpipe_basic() { + let pipe = KPipe::new().unwrap(); + pipe.push(1).await; + pipe.push(2).await; + pipe.push(3).await; + let val1 = pipe.pop().await; + let val2 = pipe.pop().await; + let val3 = pipe.pop().await; + assert_eq!(val1, 1); + assert_eq!(val2, 2); + assert_eq!(val3, 3); + } + } +} diff --git a/src/testing/mod.rs b/src/testing/mod.rs index 154846d..efd5ee8 100644 --- a/src/testing/mod.rs +++ b/src/testing/mod.rs @@ -64,9 +64,9 @@ pub fn panic_noop(_: *mut u8, _: *mut u8) {} #[macro_export] macro_rules! ktest { - (fn $name:ident() $body:block) => { + ($name:ident, fn $fn_name:ident() $body:block) => { #[cfg(test)] - fn $name(_: *mut u8) { + fn $fn_name(_: *mut u8) { $body } @@ -78,7 +78,7 @@ macro_rules! ktest { test_fn: || { let result = unsafe { core::intrinsics::catch_unwind( - $name as fn(*mut u8), + $fn_name as fn(*mut u8), core::ptr::null_mut(), crate::testing::panic_noop, ) @@ -92,4 +92,31 @@ macro_rules! ktest { }; } }; + (fn $name:ident() $body:block) => { + crate::ktest!($name, fn $name() $body); + }; + (async fn $name:ident() $body:block) => { + async fn $name() { + $body + } + + paste::paste! { + crate::ktest! { + $name, + fn [<__sync_ $name>]() { + let mut fut = alloc::boxed::Box::pin($name()); + let desc = crate::process::TaskDescriptor::from_tgid_tid(crate::process::thread_group::Tgid(0), crate::process::Tid(0)); + let waker = crate::sched::waker::create_waker(desc); + let mut ctx = core::task::Context::from_waker(&waker); + loop { + match fut.as_mut().poll(&mut ctx) { + core::task::Poll::Ready(()) => break, + _ => {}, + } + } + } + } + } + + } } From 303041553aa6fee894fe0e4531ee120a769332ff Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Thu, 19 Feb 2026 10:14:00 -0800 Subject: [PATCH 3/3] ensure pass on CI --- .github/workflows/test.yml | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d2d850e..8b811f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,23 +24,33 @@ jobs: - name: Run tests inside docker container if: ${{ matrix.smp-feature == '' }} run: | - docker run moss "/bin/bash" -c "cargo run -r --no-default-features -- /bin/usertest" >> out.log - docker run moss "/bin/bash" -c "cargo test -r --no-default-features" >> out.log + docker run moss "/bin/bash" -c "cargo run -r --no-default-features -- /bin/usertest" >> usertest.log + docker run moss "/bin/bash" -c "cargo test -r --no-default-features" >> unittest.log # If any feature is enabled - name: Run tests inside docker container if: ${{ matrix.smp-feature == 'smp' }} run: | - docker run moss "/bin/bash" -c "cargo run -r --no-default-features --features "${{ matrix.smp-feature }}" -- /bin/usertest" >> out.log - docker run moss "/bin/bash" -c "cargo test -r --no-default-features --features "${{ matrix.smp-feature }}"" >> out.log - - name: Display test output - run: cat out.log - - name: Check for success line - run: grep -q "All tests passed in " out.log || (echo "Tests failed" && exit 1) - - name: Upload test output as artifact + docker run moss "/bin/bash" -c "cargo run -r --no-default-features --features "${{ matrix.smp-feature }}" -- /bin/usertest" >> usertest.log + docker run moss "/bin/bash" -c "cargo test -r --no-default-features --features "${{ matrix.smp-feature }}"" >> unittest.log + - name: Display usertest output + run: cat usertest.log + - name: Display unit test output + run: cat unittest.log + - name: Check for usertest success line + run: grep -q "All tests passed in " usertest.log || (echo "Usertests failed" && exit 1) + - name: Check for unit test success line + run: | + grep -q "test result: .*ok.*\..*passed" unittest.log || (echo "Unit tests failed" && exit 1) + - name: Upload usertest output as artifact uses: actions/upload-artifact@v6 with: - name: test-output-${{ matrix.smp-feature || 'up' }} - path: out.log + name: usertest-output-${{ matrix.smp-feature || 'up' }} + path: usertest.log + - name: Upload unittest output as artifact + uses: actions/upload-artifact@v6 + with: + name: unittest-output-${{ matrix.smp-feature || 'up' }} + path: unittest.log upload-image: runs-on: ubuntu-latest