Microdragon / Kernel

A microkernel written in Rust
Mozilla Public License 2.0
0 stars 1 forks source link

Feature: DragonTest Unit Testing Framework #1

Open Rain336 opened 10 months ago

Rain336 commented 10 months ago

Feature: DragonTest Unit Testing Framework

Microdragon needs unit testing, but the default libtest framework cannot run without libstd, so a new testing framework needs to be written using the custom_test_frameworks nightly feature. This new test framework should offer most of the features of libtest and maybe some extensions specific to testing kernels.

Writing Tests

Writing tests should be as similar to libtest as possible. To do this we need to overwrite a few builtin macros normally used by libtest with our own proc-macros.

pub enum ShouldPanic { No, Yes, YesWithMessage(&'static str), }

pub struct TestSpec { pub func: TestFn, pub name: &'static str, pub ignore: bool, pub ignore_message: Option<&'static str>, pub source_file: &'static str, pub should_panic: ShouldPanic, }

Here a few examples of how functions would be translated:
```rust
#[test]
fn sample_test() {}

#[bench]
fn sample_bench() {}
fn sample_test() {}

#[test_case]
static SAMPLE_TEST_SPEC: TestSpec = TestSpec {
    func: TestFn::Test(|| dragontest::test_result(sample_test())),
    name: concat!(module_path!(), "::", stringify!(sample_test)),
    ignore: false,
    ignore_message: None,
    source_file: file!(),
    should_panic: ShouldPanic::No
};

fn sample_bench() {}

#[test_case]
static SAMPLE_BENCH_SPEC: TestSpec = TestSpec {
    func: TestFn::Benchmark(|b| dragontest::test_result(sample_bench(b))),
    name: concat!(module_path!(), "::", stringify!(sample_bench)),
    ignore: false,
    ignore_message: None,
    source_file: file!(),
    should_panic: ShouldPanic::No
};

[test]

[shuld_panic(expected = "panic message contains this")]

fn sample_test_message() {}


```rust
fn sample_test() {}

#[test_case]
static SAMPLE_TEST_SPEC: TestSpec = TestSpec {
    func: TestFn::Test(|| dragontest::test_result(sample_test())),
    name: concat!(module_path!(), "::", stringify!(sample_test)),
    ignore: false,
    ignore_message: None,
    source_file: file!(),
    should_panic: ShouldPanic::Yes
};

fn sample_test_message() {}

#[test_case]
static SAMPLE_TEST_MESSAGE_SPEC: TestSpec = TestSpec {
    func: TestFn::Test(|| dragontest::test_result(sample_test_message())),
    name: concat!(module_path!(), "::", stringify!(sample_test_message)),
    ignore: false,
    ignore_message: None,
    source_file: file!(),
    should_panic: ShouldPanic::YesWithMessage("panic message contains this")
};

[test]

[ignore = "This test is ignored"]

fn sample_test_message() {}


```rust
fn sample_test() {}

#[test_case]
static SAMPLE_TEST_SPEC: TestSpec = TestSpec {
    func: TestFn::Test(|| dragontest::test_result(sample_test())),
    name: concat!(module_path!(), "::", stringify!(sample_test)),
    ignore: true,
    ignore_message: None,
    source_file: file!(),
    should_panic: ShouldPanic::No
};

fn sample_test_message() {}

#[test_case]
static SAMPLE_TEST_MESSAGE_SPEC: TestSpec = TestSpec {
    func: TestFn::Test(|| dragontest::test_result(sample_test_message())),
    name: concat!(module_path!(), "::", stringify!(sample_test_message)),
    ignore: true,
    ignore_message: Some("This test is ignored"),
    source_file: file!(),
    should_panic: ShouldPanic::No
};

Running Tests

The plan is to integrate with the cargo test command, which requires us to write a runner that can package and iso and start qemu for running the tests. This would require either a complete reimplementation of the Justfile in rust or modifying the Justfile to be able to act as our test runner. I would go the route of rewriting the Justfile in rust and actually replacing it completely with it. Then we could also define a runner for normal builds and make cargo run work.

Additionally, cargo test accepts different options that are processed by libtest, most of these have to be implemented by us too to provide adequate support. The following are not supported:

For the command line options to be considered, they also need to be passed into the VM, this can be done by passing the options as bootloader kernel arguments. Most bootloaders support this and it can be easily prepared by the test runner's packing step.

Integrating the Test Framework

The test framework supplies a main entry point that can be passed to the #![test_runner] macro. Then the re-exported test main function just has to be called to start the test framework.

Handling panics requires another function from the test framework to process the panic and continue testing.

Open Questions

Rain336 commented 10 months ago

Considering we are running in a VM, getting a known good timer is actually easy, since the testing framework only needs to support one timer that is guaranteed to be provided by QEMU. For x86_64 this will most likely be the TimeStamp Counter (TSC) which can be easily used to implement a monotonic clock, similar to Instant. This is basically all we need for time tracking.