Test

OS in Rust

Announcements

  • Action Items:
    • How is graphics?
      • Are you learning NumPy?
      • Anyone using CUDA?

Citations

  • I am geniunely convinced of the usefulness of automated testing when developing a system.
  • The most high profile case of this is the testing framework for 4096_t.c
  • Hard to persuade students of the usefulness of this on small class assignments.
  • \(\therefore\) I’ve never written anything of any reasonable size in Rust, so
  • Testing

Today

  • Testing under no_std
  • QEMU
  • bootimage
    • Still not in love with this dependency, but we do what we can.

Pre-work

  • Make sure you have a .cargo/config.toml
  • No idea how you wouldn’t have one of these yet!
  • Just make sure you have it.

Modern Language

  • Like Python with Pytest, and unlike the only other systems language, Rust has a testing framework.
  • It’s covered in Rust book
  • Naturally I skipped this chapter.

Framework

  • Use the #[test] attribute on some functions.
  • Including assertions within those functions.
  • cargo test or, if you see someone around, cargo t, will check the assertions.
  • The crate will otherwise be non-impacted.

Review

  • To enable testing for our kernel binary, we can set the test flag in the Cargo.toml to true.
  • We recall our Cargo.toml - mine no longer has an allusions to panic with the introduction of a JSON target.
Cargo.toml
[package]
name = "osirs"
version = "0.1.0"
edition = "2024"

[dependencies]
bootloader = "0.9"

Enable

  • This is necessary but not sufficient.
Cargo.toml
[package]
name = "osirs"
version = "0.1.0"
edition = "2024"

[dependencies]
bootloader = "0.9"

[[bin]]
name = "osirs"
test = true
bench = false

On [[bin]]

The double-bracket sections like [[bin]] are array-of-table of TOML, which means you can write more than one [[bin]] section to make several executables in your crate.

  • Just one for us for now.

Working within no_std

  • Testing is complicated by no_std.
  • Rust implicitly uses the test library, which depends on std
$ cargo t
   Compiling bootloader v0.9.34
error[E0463]: can't find crate for `core`
  |
  = note: the `x86_64-osirs` target may not be installed
  = help: consider downloading the target with `rustup target add x86_64-osirs`

For more information about this error, try `rustc --explain E0463`.
error: could not compile `bootloader` (lib) due to 1 previous error

What We Lose

  • Vs. std, we lose e.g. should_panic tests.
  • That’s okay, we are crafty (and wrote our own panic handler anyway).

To Main

  • My main looks like this:
src/main.rs
#![no_std]
#![no_main]

mod colors;
mod vga;

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
    println!("{}", info);
    loop {}
}

#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
    colors::colors();
    colors::image();
    loop {}
}

Just a note

  • When you make a new version for today (60)
    • Probably leave out colors (52) and
    • Instead start with something with println! (likely 51)

Today’s Starting Point

src/main.rs
#![no_std]
#![no_main]

mod vga;

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
    println!("{}", info);
    loop {}
}

#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
    println!("Hello world!");
    loop {}
}

Adding to Main

src/main.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]

mod vga;

#[cfg(test)]
pub fn test_runner(tests: &[&dyn Fn()]) {
    println!("Running {} tests", tests.len());
    for test in tests {
        test();
    }
}

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
    println!("{}", info);
    loop {}
}

#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
    println!("Hello world!");
    loop {}
}

Wait a minute

The dyn keyword is used to highlight that calls to methods on the associated Trait are dynamically dispatched.

Dynamic Dispatch

In computer science, dynamic dispatch is the process of selecting which implementation of a polymorphic operation (method or function) to call at run time. It is commonly employed in, and considered a prime characteristic of, object-oriented programming (OOP) languages and systems.

OOP

  • Yeah I’m not doing that.
  • We’ll maintain a list of functions.
  • If you want to learn about OOP, here is a tutorial on writing a Java app for Android.

Write Two Functions

  • Doesn’t matter but nice to have externally observable functions.
src/main.rs
fn hi() {
    println!("Hello world!");
    return;
}

fn bye() {
    println!("Goodbye space!");
    return;
}

Make an array

  • Just in _start for now.
src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
    let fs = [hi, bye];

Loop over the array

  • Same as any other for each loop.
    • I am willing to use the for each loop, but still regard it with suspicion.
src/main.rs
    let fs = [hi, bye];
    for f in fs {
        todo!();
    }

Call each function

  • What do you see?
src/main.rs
    let fs = [hi, bye];
    for f in fs {
        f();
    }

Sample out

Aside: Bits

Pulling back the curtain

  • I’m insane.
  • I ran this then screendumped (in QEMU) to fs.ppm
  • I converted to .png
python3 -c "from PIL import Image; Image.open('fs.ppm').save('fs.png')"
  • I base64 the PNG.
    • See next slide.

It’s tiny!

$ base64 fs.png | wc
     25      25    1861
$ base64 fs.png
iVBORw0KGgoAAAANSUhEUgAAAtAAAAGQCAIAAAAIhcA6AAAFJ0lEQVR4nO3dyXaDIBQAUMzp//9y
ujAlVgw+p4TovasaFKEb8DGlBAAAAPDtuvv9nlLquq6/Hl3W9TdP3l9JakS9hLNVa7ZeANCg25aH
K41u++1xvYSvUnNHJP8BAMza1OEAAIj4qSePvuO3xy1WZFgOYQx/eZVhvmfF4I7oBQDs6xHhuP8Z
puU2u5c2t8TvzHB4uehd9Ska+ff2x4wAoB2PCMcoMDD07Z/7i3oGJoQCwBFmhlRSS63vlt5AO7UA
gAua73Awou8CAEstWKVSTvJ4p3bWo372/wAA32hm46/6opJhajBp3bKXcu7nbAkrZQuWMK9wmaya
OAcAAAA0pKuMDviIBwB2YadRAAAAAAAAAIDPiy4iXaE8NW3FOWqnYTEtAFd2S/+PQEv77axVNq6X
bW7b2bUMAD7iNvryvmyfAAA4zsRZKsGNQeObkJbK8YUtG57W31I+lTOfHOIJbp9aeSpeQgC4iNo+
HMOhltFoSyUpBeYrLBq7qb9rxVP5cjLD2Uot/W8kASQALu8Z4dhr3mhwdmQfYOhb/Xi2caP84zUK
vqjM0PwMAHjl2eHYd8bo7lb0gXKfI/54eUrcoSUEgIs4+dbmuy+92V4SALigWyPtcVwOWsRvO6KO
lWJMJgWLDQCnNLEl13Bexbp1GaPchqnxDINPVXyw8K9meAhyAEBDjo4HiDcAwDu1OIdDMAAATqaV
Rv2dG2cdcXAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ/ULzGetS97yp6YAAAAA
SUVORK5CYII=

To HTML

<div>
  <p>Taken from wikpedia</p>
  <img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA
    AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
        9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />
</div>
  • I only need this:
<img src="data:image/png;base64, ">

Here it is

  • And then I placed that exact HTML within my Markdown.
    • With the base64 PNG data within.

By the way

  • You can use that as a URL.
  • Click me
  • Okay back to work.

Wait Just Kidding

  • Manually edit the data.
  • Delete exactly 24 A values in the big block.
    • 24 values at 6 bits of information each yields 144 bits or 18 bytes.
    • What do you see?
    • What if you delete not a multiple of 24?
    • What if you delete a different 24?

Quote Me

“They’re my bits, in my computer - let me use them!”

Back to Work

Recall

  • Rust is trying to get us to write Java instead of C.
src/main.rs
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Fn()]) {
    println!("Running {} tests", tests.len());
    for test in tests {
        test();
    }
}

Outsmart Rust

  • Ignore the argument via _
  • Include the stuff from inside _start.
src/main.rs
#[cfg(test)]
pub fn test_runner(_tests: &[&dyn Fn()]) {
    let fs = [hi, bye];
    println!("Running {} tests", fs.len());
    for f in fs {
        f();
    }
}
  • Probably change main too so you know what is running, mine says “I’m main”.

Check it

  • You can run the tests via:
cargo +nightly t
  • Wow that +nightly is getting annoying.
    • Time for an aside.

Aside: Nightly

Back to Stack

  • I love stackoverflow.
    • Too bad it is slowly being incinerated by generative AI
    • Surely this will lead to no long-term problems.
  • Check out this answer

It states

you can add a rust-toolchain.toml file, for example

rust-toolchain.toml
[toolchain]
channel = "nightly"
  • I am doing this now.

What I see

  • I removed my target for clarity.
$ rm -rf target/
$ tree
.
├── Cargo.lock
├── Cargo.toml
├── rust-toolchain.toml
├── src
│   ├── main.rs
│   └── vga.rs
└── x86_64-osirs.json

1 directory, 6 files

The other way

  • If you wish to use nightly in all projects, you can check this answer.
  • There are a variety of intermediate ways, of course.
  • Okay back to work.

Back to Work

Recall

  • We were trying to run tests.
cargo t # FINALLY
  • You may notice this is just running _start.
  • That is because src/main.rs is a fan of the Black Eyed Peas.
    • Learn more
    • Note to Calvin - do not click that in class.

Surprise!

  • Something not going as planned on the first try?
  • Our _start function is still used as entry point.
    • It is, after all, the _start function.

The Problem

  • The custom test frameworks feature generates a main function that calls test_runner
    • What is it for? Software?
    • Might as well use Kotlin!
  • Our OS, of course, never calls main.

Maybe problem

  • If you see “duplicate lang item” expand:

Note: There is currently a bug in cargo that leads to “duplicate lang item” errors on cargo test in some cases. It occurs when you have set panic = "abort" for a profile in your Cargo.toml. Try removing it, then cargo test should work. Alternatively, if that doesn’t work, then add panic-abort-tests = true to the [unstable] section of your .cargo/config.toml file. See the cargo issue for more information on this.

Fine, I’ll do it myself

  • Simply call test_runner from _start
    • It needs a borrowed array of whatevers, so give it one.
src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
    println!("I'm main.");
    test_runner(&[]);
    loop {}
}

Only problem

  • What if we run instead of test?
$ cargo r
   Compiling osirs v0.1.0 (/home/user/tmp/work)
error[E0423]: expected function, found built-in attribute `test_runner`
  --> src/main.rs:26:5
   |
26 |     test_runner(&[]);
   |     ^^^^^^^^^^^ not a function
   |
note: found an item that was configured out
  --> src/main.rs:9:4
   |
 8 | #[cfg(test)]
   |       ---- the item is gated here
 9 | fn test_runner(_tests: &[&dyn Fn()]) {
   |    ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0423`.
error: could not compile `osirs` (bin "osirs") due to 1 previous error

Conditional Compilation

  • We need to conditionally compile _start to either call test_runner or not.
  • No problem, we already know how to do this!
  • #[cfg(test)]
src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
    println!("I'm main{}", ".");

    #[cfg(test)]
    test_runner(&[]);

    loop {}
}

Only problem

  • What if we run instead of test?
$ cargo r
   Compiling osirs v0.1.0 (/home/user/tmp/work)
warning: function `hi` is never used
  --> src/main.rs:32:4
   |
32 | fn hi() {
   |    ^^
   |
   = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default

warning: function `bye` is never used
  --> src/main.rs:37:4
   |
37 | fn bye() {
   |    ^^^

Correct Way

  • There is a correct way to solve this.
  • Separately, there is the opposite: my way.
  • I renamed them with a _ prefix.
    for f in [_hi, _bye] {
        f();
    }
  • The correct way is left as an exercise to the interested student.

Writing Tests

  • By convention, we don’t simply print nonsense as a test.
    • It so happens in our OS, printing was a whole thing, but this is atypical for non-OS applications.
  • A much more common thing to test is an assertion.
    • An expression we claim to evaluate to true

A Test

  • Let’s just make a sample assertion.
    • I will take expression that will evaluate to true
    • I will apply the assert! macro to the expression
    • I will invoke this macro within a function.
    • I will include the function in the test_runner array of functions.

Example

src/main.rs
fn _ex() {
    assert(true);
}
  • There’s a minor annoyance here.
    • We can’t tell if it’s running at all.

Update test_runner

  • By the way, I remove _hi and _bye at this time.
  • I then just enumerate my tests in an array.
  • Loop over the range of the length of the array.
    • Say I’m running test \(i\)
    • Run test \(i\)
    • Say it’s gucci

My test_runner

  • For each could never.
src/main.rs
#[cfg(test)]
fn test_runner(_tests: &[&dyn Fn()]) {
    let fs = [_ex];
    for i in 0..fs.len() {
        print!("Running test case {:0x}", i);
        fs[i]();
        println!("Success.");
    }
}

Now what?

  • test_runner returns to _start function
  • _start contains an infinite loop
    • We recall the entry point function is not allowed to return.
  • We probably want cargo t to exit after running all tests.

Winners Never

  • We want to quit QEMU.
    • Yes, quitting, the thing winners never do.
  • This is extremely helpful if both:
  1. Have code you want to test, and
  2. Have a finite life expectancy.

OS Shutdown

  • Folks, its hard.
  • Big Blog references these two power management standards.
    • Both are good uses of your time outside of class.
  • APM
  • ACPI

QEMU to the RESQ

  • QEMU includes a debug exit!
  • isa-debug-exit.
    • We pass a -device argument to qemu
    • We pass via the bootimage crate.
    • We specify in Cargo.toml
  • This only occurs while testing!
Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]

Make Sure This Works

  • You can just cargo t (and also cargo r) and verify the qemu command differs.
  • (Scroll)
$ cargo t
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running unittests src/main.rs (target/x86_64-osirs/debug/deps/osirs-577ad3ee7fc7cc13)
Building bootloader
   Compiling bootloader v0.9.34 (/home/calvin/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bootloader-0.9.34)
    Finished `release` profile [optimized + debuginfo] target(s) in 0.83s
Running: `qemu-system-x86_64 -drive format=raw,file=/home/calvin/tmp/work/target/x86_64-osirs/debug/deps/bootimage-osirs-577ad3ee7fc7cc13.bin -no-reboot -device isa-debug-exit,iobase=0xf4,iosize=0x04`
$ cargo r
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running `bootimage runner target/x86_64-osirs/debug/osirs`
Building bootloader
   Compiling bootloader v0.9.34 (/home/calvin/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bootloader-0.9.34)
    Finished `release` profile [optimized + debuginfo] target(s) in 0.84s
Running: `qemu-system-x86_64 -drive format=raw,file=target/x86_64-osirs/debug/bootimage-osirs.bin`

I/O Ports

  • Recall memory-mapped I/O.
  • There is also port-mapped I/O.
  • This uses a separate I/O bus for communication.
    • It makes sense QEMU uses this as it is not actual physical hardware!

Imagine

  • Each connected peripheral (mouse, monitor, power supply) has one or more port numbers.
  • To communicate with such an I/O port, use special hardware level instructions.
    • in and out
  • These take a port number and a data byte

Usage

  • The isa-debug-exit device uses port-mapped I/O.
    • isa = instruction set architecture
    • An example instruction is in or out
  • The iobase parameter specifies the port address of the (imaginary) device
  • 0xf4 is a generally unused port
  • The iosize specifies the port size (0x04 means four bytes).

The Device

  • isa-debug-exit is simple.
  • When a value v is written to the I/O port specified by iobase, QEMU to exits with exit status (v << 1) | 1.
  • So when we write 0 to the port, QEMU will exit with (0 << 1) | 1 = 1,
  • Exit status is more interesting to C coders, and not used much anymore.

Handwave assembly

  • Vs. manually invoking in and out
    • That is for the compilers class
    • Live on YT Sp27 unless I get fired for not liking AI.
  • We use the x86_64 crate.
  • Add to [dependencies] in Cargo.toml:
Cargo.toml
[dependencies]
bootloader = "0.9"
x86_64 = "0.14.2"

Port

  • Not just a type of land!
  • We will use “Port” to gracefully exit.
  • Make one:
    unsafe {
        let mut port = x86_64::instructions::port::Port::new(0xf4);
    }

Write to it

  • I use 0xA, you can use anything.
    • Could e.g. return number of failed test.
    • Zero will collide with a general QEMU error.
    • Does need to be a u32 because we said we would.
    unsafe {
        x86_64::instructions::port::Port::new(0xf4).write(0xAu32);
    }

Test Exit

  • Just add those lines to the end of test_runner and you should be good to go!
$ cargo t
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s
     Running unittests src/main.rs (target/x86_64-osirs/debug/deps/osirs-429ccf0d82ba9f9c)
Building bootloader
    Finished `release` profile [optimized + debuginfo] target(s) in 0.07s
Running: `qemu-system-x86_64 -drive format=raw,file=/home/calvin/tmp/work/target/x86_64-osirs/debug/deps/bootimage-osirs-429ccf0d82ba9f9c.bin -no-reboot -device isa-debug-exit,iobase=0xf4,iosize=0x04`
error: test failed, to rerun pass `--bin osirs`

Caused by:
  process didn't exit successfully: `bootimage runner /home/calvin/tmp/work/target/x86_64-osirs/debug/deps/osirs-429ccf0d82ba9f9c` (exit status: 21)
note: test exited abnormally; to see the full output pass --no-capture to the harness.
$ 

Exit Status

  • We are providing an exit status other than cargo t expects and being admonished.
    • The audacity.
  • We just specify the expected exit status in Cargo.toml
Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
test-success-exit-code = 21
  • Absolute geniuses note ((0xA<<1)|1)==21

Annoyance

  • There remains two annoying things here:
    • The QEMU window opens and then closes before we can see it, so we can’t see results.
    • The QEMU window opens so we are opening a graphics window unnecessarily, which inhibits automation.

Printing to the Console

  • To see the test output on the console, we need to send the data from our kernel to the host system somehow.
  • There are various ways to achieve this, for example, by sending the data over a TCP network interface.
    • Networks, live on YouTube Spring 2028.

Fin

Serial Ports

  • I expect we will be out of time by here.
  • We continue these efforts in the lab: Serial.