It was one of the earliest computer communication devices, used to attach teletypewriters for an operator console. It was also an early hardware system for the Internet.
Two Parts
We will proceed in two parts: 1. [ ] Configure Rust to print to the serial port. 2. [ ] Configure QEMU to display the messages relayed via the serial port to a terminal.
There is also a mysterious third part, panic handling.
Rust
Popular
UART 16550 is popular, nominally, so we’ll use that.
The various UARTs are all basically the same, like ASCII vs. Code Page ???
Make a new file, src/serial.rs, that will implement functions which print to the serial ports.
Not to the VGA buffer!
We will hope to eventually see the text in on host device.
That is, the same command line we invoke cargo t from.
To begin, we will use the same print and println macros from src/vga.rs, but prepend them with “serial” so know they are for special external usage.
This will probably look roughly as expected.
It’s a headache so I’m providing it.
src/serial.rs
/// Prints to the host through the serial interface.#[macro_export]macro_rules! serial_print { ($($arg:tt)*) =>{$crate::serial::_print(format_args!($($arg)*));};}/// Prints to the host through the serial interface, appending a newline.#[macro_export]macro_rules! serial_println { () => ($crate::serial_print!("\n")); ($fmt:expr) => ($crate::serial_print!(concat!($fmt,"\n"))); ($fmt:expr,$($arg:tt)*) => ($crate::serial_print!(concat!($fmt,"\n"),$($arg)*));}
_print
With these macros furnished, all that remains is to implement the _print function which is called internally within the macros, namely in the most indented line of serial_print.
Vs. VGA, this will be much easier.
We don’t need to worry about linbreaks.
We don’t need to worry about newlines.
We don’t need to worry about scrolling.
In fact, we don’t have to do any formatting at all!
The uart_16550::SerialPortalready has the .write_fmt Trait that we needed to implement println for the VGA buffer.
All that remains is to properly set up the serial port for printing (and to print to it).
Recall
My haters, of which there are many, may have been opinionated on how I got access to .write_fmt for the VGA buffer.
Instead of a Dummy we need a uart_16500::SerialPort.
Left as Exercise
Getting the rest of this to work is left as an exercise, and not too bad.
But you can’t test it yet.
Testing
Usage of Serial
Previously, we worked with the following test_runner
src/main.rs
#[cfg(test)]fn test_runner(_tests:&[&dynFn()]) {let fs = [_ex];for i in0..fs.len() {print!("Running test case {:0x}... ", i); fs[i]();println!("Success.");}}
There would be some benefit to either of the following formulations.
Print to Serial Only
This works well once you know your serial printer is working as intended.
src/main.rs
#[cfg(test)]fn test_runner(_tests:&[&dynFn()]) {let fs = [_ex];for i in0..fs.len() {serial_print!("Running test case {:0x}... ", i); fs[i]();serial_println!("Success.");}}
Double Print
When getting my code to work, I used both print and serial_print.
This reduced my error handling cases (as I could check the VGA buffer for text as well as host device).
I was only using this during debugging or I would’ve written a wrapper.
src/main.rs
#[cfg(test)]fn test_runner(_tests:&[&dynFn()]) {let fs = [_ex];for i in0..fs.len() {serial_print!("Running test case {:0x}... ", i);print!("Running test case {:0x}... ", i); fs[i]();println!("Success.");serial_println!("Success.");}}
Takeaways
In any case, make sure you have some that (1) prints to serial in the (2) test case.
QEMU
A Fun Story
I spent several hours thinking I didn’t get serial ports working when, in fact, I had, I simply had configured QEMU to display the results of serial port writes, so I wasn’t see anything.
Do not be like me.
QEMU Options
So going forward, we assume the correctness of the serial_println macro.
You won’t be able to test it until you’ve done this section!
That is okay, we can do two things at once.
Return to Cargo
To see the serial output from QEMU, we need to use the -serial argument to redirect the output to “Standard I/O” stdio.
For now, we are only using stdout but stdout\(\in\)stdio
This is not only less annoying, but also allows our test framework to run in environments without a graphical user interface, such as CI services or SSH connections.
A test that never returns can block the test runner forever.
That’s unfortunate, but not a big problem in practice since it’s usually easy to avoid endless loops.
We have no cases of loop {} in our code, right?
Systems Design
In our case, however, endless loops can occur in various situations: - The bootloader fails to load our kernel - The BIOS/UEFI firmware fails to load the bootloader - The CPU enters a loop {} statement. - Perhaps after QEMU bug. - The hardware causes a system reset.
Set a Timeout
By default, apparently timeout is 300 seconds or 5 minutes.
That is way too long.
I give up on problems instantly.
We can manually set it to be shorter, like 1 second.
The takeaway here is that I get my console back in a finite amount of time (5 minutes is infinite).
$ cargo t Finished`test` profile [unoptimized + debuginfo] target(s)in0.07sRunning unittests src/main.rs (target/x86_64-osirs/debug/deps/osirs-f4313fd1ed2d08da)Building bootloaderFinished`release` profile [optimized + debuginfo] target(s)in0.07sRunning:`qemu-system-x86_64-drive format=raw,file=/home/calvin/tmp/work/target/x86_64-osirs/debug/deps/bootimage-osirs-f4313fd1ed2d08da.bin -no-reboot-device isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio -display none`Beginning test 0x00...Error: Test timed outerror: 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-f4313fd1ed2d08da` (exit status: 1)note: test exited abnormally; to see the full output pass --no-capture to the harness.
Panics
One More
Okay, so a test can pass or stall infinitely, but what about fail.
src/main.rs
fn _bad() {assert!(false);}
Huh?
What is happening here?
Beginning test 0x00... [Pass]Beginning test 0x01...Error: Test timed out
An assertion is made.
The assertion fails.
The failed assertion triggers a panic.
The panic handler broadcasts an error message to the VGA buffer, which is not being displayed.
The panic handler enters an infinite loop.
Eventually, the timeout occurs and terminates the test suite.
Instead
We should write a custom panic handler for the testing context.
We use conditional compilation.
I used the following template:
src/main.rs
#[cfg(not(test))]#[panic_handler]fn panic(info:&core::panic::PanicInfo) ->!{println!("{}", info);loop{}}#[cfg(test)]#[panic_handler]fn panic(info:&core::panic::PanicInfo) ->!{// Your code hereloop{}}
You should:
Print a message denoting the test failed.
Print a message decribing what the failure was.
Exit QEMU.
With a status code other than success.
I used 0xF vs. 0xA which I used in my test_runner.
Here is an example output:
$ cargo tFinished`test` profile [unoptimized + debuginfo] target(s)in0.10sRunning unittests src/main.rs (target/x86_64-osirs/debug/deps/osirs-f4313fd1ed2d08da)Building bootloaderFinished`release` profile [optimized + debuginfo] target(s)in0.07sRunning:`qemu-system-x86_64-drive format=raw,file=/home/calvin/tmp/work/target/x86_64-osirs/debug/deps/bootimage-osirs-f4313fd1ed2d08da.bin -no-reboot-device isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio -display none`Beginning test 0x00... [Pass]Beginning test 0x01... [Fail]panicked at src/main.rs:51:5:assertion failed: falseerror: 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-f4313fd1ed2d08da` (exit status: 31)note: test exited abnormally; to see the full output pass --no-capture to the harness.
---title: Serial format: html---# Your Task- We are trying to get automated testing working.- We can quickly open and close QEMU.- We want to blast QEMU to `stdout`- And ideally not open QEMU.- We just recently turned to the *serial port*.## Serial Port- Easy mode is a [serial port](https://en.wikipedia.org/wiki/Serial_port) - An *old* interface standard which is no longer found in modern computers. - It is easy to program and...- QEMU can redirect the bytes sent over serial to the host.## UART- The chips implementing a serial interface are called UARTs - [Universal asynchronous receiver-transmitter](https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter)> It was one of the earliest computer communication devices, used to attach teletypewriters for an operator console. It was also an early hardware system for the Internet. ## Two PartsWe will proceed in two parts:1. [ ] Configure Rust to print to the serial port.2. [ ] Configure QEMU to display the messages relayed via the serial port to a terminal.There is also a mysterious third part, panic handling.# Rust## Popular- UART 16550 is popular, nominally, so we'll use that. - The various UARTs are all basically the same, like ASCII vs. Code Page ???- There is, naturally, a [`uart_165001`](https://docs.rs/uart_16550) crate. - Add it to your dependencies in the expect manner (in `Cargo.toml`).```{.toml filename="cargo.toml" code-line-numbers="4"}[dependencies]bootloader = "0.9"x86_64 = "0.14.2"uart_16550 = "0.2.0"```## Using UART- `uart_16550` implements a `SerialPort` struct. - It *represents* the UART registers.- Like `println`, I do this in a new file.### Add to Main```{.rs filename="src/main.rs"}mod serial;```## Create Serial File- Make a new file, `src/serial.rs`, that will implement functions which print to the serial ports. - Not to the VGA buffer! - We will hope to eventually see the text in on host device. - That is, the same command line we invoke `cargo t` from.- To begin, we will use the same `print` and `println` macros from `src/vga.rs`, but prepend them with "serial" so know they are for special external usage. - This will probably look roughly as expected. - It's a headache so I'm providing it.```{.rs filename="src/serial.rs"}/// Prints to the host through the serial interface.#[macro_export]macro_rules! serial_print { ($($arg:tt)*) => { $crate::serial::_print(format_args!($($arg)*)); };}/// Prints to the host through the serial interface, appending a newline.#[macro_export]macro_rules! serial_println { () => ($crate::serial_print!("\n")); ($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n"))); ($fmt:expr, $($arg:tt)*) => ($crate::serial_print!( concat!($fmt, "\n"), $($arg)*));}```## `_print`- With these macros furnished, all that remains is to implement the `_print` function which is called internally within the macros, namely in the most indented line of `serial_print`.- Vs. VGA, this will be much easier. - We don't need to worry about linbreaks. - We don't need to worry about newlines. - We don't need to worry about scrolling.- In fact, we don't have to do any formatting at all! - The `uart_16550::SerialPort` *already has* the `.write_fmt` Trait that we needed to implement `println` for the VGA buffer.- All that remains is to properly set up the serial port for printing (and to print to it).## Recall- My haters, of which there are many, may have been opinionated on how I got access to `.write_fmt` for the VGA buffer.```{.rs filename="src/vga.rs"}pub struct Dummy {}impl core::fmt::Write for Dummy { fn write_str(&mut self, s: &str) -> core::fmt::Result { str_to_vga(s); Ok(()) }}pub fn _print(args: core::fmt::Arguments) { use core::fmt::Write; let mut d = Dummy {}; d.write_fmt(args).unwrap();}```- We will only need to borrow `_print`, as `SerialPort` is already a (1) `struct` that (2) `impl core::fmt::Write`.## So...- Something like this.```{.rs filename="src/serial.rs"}pub fn _print(args: core::fmt::Arguments) { use core::fmt::Write; let mut serial_port = Dummy {}; serial_port.write_fmt(args).unwrap();}```- Instead of a `Dummy` we need a `uart_16500::SerialPort`.## Left as Exercise- Getting the rest of this to work is left as an exercise, and not too bad.- But you can't test it yet.# Testing## Usage of Serial- Previously, we worked with the following `test_runner````{.rs filename="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."); }}```- There would be some benefit to either of the following formulations.### Print to Serial Only- This works well once you know your serial printer is working as intended.```{.rs filename="src/main.rs"}#[cfg(test)]fn test_runner(_tests: &[&dyn Fn()]) { let fs = [_ex]; for i in 0..fs.len() { serial_print!("Running test case {:0x}... ", i); fs[i](); serial_println!("Success."); }}```### Double Print- When getting my code to work, I used both `print` and `serial_print`. - This reduced my error handling cases (as I could check the VGA buffer for text as well as host device). - I was only using this during debugging or I would've written a wrapper.```{.rs filename="src/main.rs"}#[cfg(test)]fn test_runner(_tests: &[&dyn Fn()]) { let fs = [_ex]; for i in 0..fs.len() { serial_print!("Running test case {:0x}... ", i); print!("Running test case {:0x}... ", i); fs[i](); println!("Success."); serial_println!("Success."); }}```### Takeaways- In any case, make sure you have some that (1) prints to serial in the (2) test case.# QEMU## A Fun Story- I spent several hours thinking I didn't get serial ports working when, in fact, I had, I simply had configured QEMU to display the results of serial port writes, so I wasn't see anything.- Do not be like me.## QEMU Options- So going forward, we assume the correctness of the `serial_println` macro. - You won't be able to test it until you've done this section! - That is okay, we can do two things at once.## Return to Cargo- To see the serial output from QEMU, we need to use the `-serial` argument to redirect the output to "Standard I/O" `stdio`. - For now, we are only using `stdout` but `stdout` $\in$ `stdio````{.toml filename="Cargo.toml"}[package.metadata.bootimage]test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"]```## Massaging Main- I also wanted to do multiple tests, so I updated `test_runner`.- It wasn't *too* exciting, I just added some formatting to look nice.- I also run the test multiple times, to mimic running multiple tests.```{.rs filename="src/main.rs"}fn test_runner(_tests: &[&dyn Fn()]) { let fs = [_ex, _ex, _ex]; for i in 0..fs.len() { serial_print!("Beginning test 0x{:02x}...", i); fs[i](); serial_println!(" [Pass]"); } unsafe { x86_64::instructions::port::Port::new(0xf4).write(0xAu32) };}```## Testing Tests- When we run `cargo test` now, we see the test output directly in the console:```{.sh}$ cargo t Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s Running unittests src/main.rs (target/x86_64-osirs/debug/deps/osirs-f4313fd1ed2d08da)Building bootloader Finished `release` profile [optimized + debuginfo] target(s) in 0.07sRunning: `qemu-system-x86_64 -drive format=raw,file=/home/calvin/tmp/work/target/x86_64-osirs/debug/deps/bootimage-osirs-f4313fd1ed2d08da.bin -no-reboot -device isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio -display none`Beginning test 0x00... [Pass]Beginning test 0x01... [Pass]Beginning test 0x02... [Pass]```## Good Enough?- Surely that is a good enough... right? - Wrong.- It is extremely annoying to have the graphics window rapidly open and close. - It is visually distracting. - I won't work well in all environments, such as GitHub Actions - It is resource intensive.## Hiding QEMU- We can prevent the display window launch by passing the `-display none` argument to QEMU... - Though we would *only* want to do this when conducting automated testing.```{.toml filename="Cargo.toml"}[package.metadata.bootimage]test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio", "-display", "none"]```## Quoth Blog> This is not only less annoying, but also allows our test framework to run in environments without a graphical user interface, such as CI services or SSH connections.- [CI - Continuious integration with GitHub Actions](https://docs.github.com/en/actions/get-started/continuous-integration)- [SSH - Secure Shell](https://en.wikipedia.org/wiki/Secure_Shell)## Good Enough?- Imagine this test:```{.rs filename="src/main.rs"}fn _ex() { loop {}}```## Timeouts- `cargo t` waits until the test runner exits.- A test that never returns can block the test runner forever. - That's unfortunate, but not a big problem in practice since it's usually easy to avoid endless loops.- We have no cases of `loop {}` in our code, right?## Systems DesignIn our case, however, endless loops can occur in various situations:- The bootloader fails to load our kernel- The BIOS/UEFI firmware fails to load the bootloader- The CPU enters a `loop {}` statement. - Perhaps after QEMU bug.- The hardware causes a system reset.## Set a Timeout- By default, apparently timeout is 300 seconds or 5 minutes. - That is *way* too long. - I give up on problems instantly.- We can manually set it to be shorter, like 1 second.- [bootimage config](https://github.com/rust-osdev/bootimage#configuration)```{.toml filename="Cargo.toml"}[package.metadata.bootimage]test-timeout = 1 ```## Test It- Try this now:```{.rs filename="src/main.rs"}fn _ex() { loop {}}```## What I see- The takeaway here is that I get my console back in a finite amount of time (5 minutes is infinite).```{.sh}$ cargo t Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s Running unittests src/main.rs (target/x86_64-osirs/debug/deps/osirs-f4313fd1ed2d08da)Building bootloader Finished `release` profile [optimized + debuginfo] target(s) in 0.07sRunning: `qemu-system-x86_64 -drive format=raw,file=/home/calvin/tmp/work/target/x86_64-osirs/debug/deps/bootimage-osirs-f4313fd1ed2d08da.bin -no-reboot -device isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio -display none`Beginning test 0x00...Error: Test timed outerror: 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-f4313fd1ed2d08da` (exit status: 1)note: test exited abnormally; to see the full output pass --no-capture to the harness.```# Panics## One More- Okay, so a test can pass or stall infinitely, but what about fail.```{.rs filename="src/main.rs"}fn _bad() { assert!(false);}```## Huh?- What is happening here?```{.sh}Beginning test 0x00... [Pass]Beginning test 0x01...Error: Test timed out```1. An assertion is made.2. The assertion fails.3. The failed assertion triggers a panic.4. The panic handler broadcasts an error message to the VGA buffer, which is not being displayed.5. The panic handler enters an infinite loop.6. Eventually, the timeout occurs and terminates the test suite.## Instead- We should write a *custom panic handler* for the testing context.- We use *conditional compilation*.- I used the following template:```{.rs filename="src/main.rs"}#[cfg(not(test))]#[panic_handler]fn panic(info: &core::panic::PanicInfo) -> ! { println!("{}", info); loop {}}#[cfg(test)]#[panic_handler]fn panic(info: &core::panic::PanicInfo) -> ! { // Your code here loop {}}```- You should: - Print a message denoting the test failed. - Print a message decribing what the failure was. - Exit QEMU. - With a status code *other* than success. - I used `0xF` vs. `0xA` which I used in my `test_runner`.- Here is an example output:```{.sh}$ cargo t Finished `test` profile [unoptimized + debuginfo] target(s) in 0.10s Running unittests src/main.rs (target/x86_64-osirs/debug/deps/osirs-f4313fd1ed2d08da)Building bootloader Finished `release` profile [optimized + debuginfo] target(s) in 0.07sRunning: `qemu-system-x86_64 -drive format=raw,file=/home/calvin/tmp/work/target/x86_64-osirs/debug/deps/bootimage-osirs-f4313fd1ed2d08da.bin -no-reboot -device isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio -display none`Beginning test 0x00... [Pass]Beginning test 0x01... [Fail]panicked at src/main.rs:51:5:assertion failed: falseerror: 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-f4313fd1ed2d08da` (exit status: 31)note: test exited abnormally; to see the full output pass --no-capture to the harness.```# Fin## Deliverables- [ ] Ensure you have a testing suite that can run without a graphics window and handle pass, fail, and divergence.<!--# TODO## Testing the VGA BufferNow that we have a working test framework, we can create a few tests for our VGA buffer implementation. First, we create a very simple test to verify that `println` works without panicking:```rust// in src/vga_buffer.rs#[test_case]fn test_println_simple() { println!("test_println_simple output");}```The test just prints something to the VGA buffer. If it finishes without panicking, it means that the `println` invocation did not panic either.To ensure that no panic occurs even if many lines are printed and lines are shifted off the screen, we can create another test:```rust// in src/vga_buffer.rs#[test_case]fn test_println_many() { for _ in 0..200 { println!("test_println_many output"); }}```We can also create a test function to verify that the printed lines really appear on the screen:```rust// in src/vga_buffer.rs#[test_case]fn test_println_output() { let s = "Some test string that fits on a single line"; println!("{}", s); for (i, c) in s.chars().enumerate() { let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read(); assert_eq!(char::from(screen_char.ascii_character), c); }}```The function defines a test string, prints it using `println`, and then iterates over the screen characters of the static `WRITER`, which represents the VGA text buffer. Since `println` prints to the last screen line and then immediately appends a newline, the string should appear on line `BUFFER_HEIGHT - 2`.By using [`enumerate`], we count the number of iterations in the variable `i`, which we then use for loading the screen character corresponding to `c`. By comparing the `ascii_character` of the screen character with `c`, we ensure that each character of the string really appears in the VGA text buffer.[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerateAs you can imagine, we could create many more test functions. For example, a function that tests that no panic occurs when printing very long lines and that they're wrapped correctly, or a function for testing that newlines, non-printable characters, and non-unicode characters are handled correctly.For the rest of this post, however, we will explain how to create _integration tests_ to test the interaction of different components together.## Integration TestsThe convention for [integration tests] in Rust is to put them into a `tests` directory in the project root (i.e., next to the `src` directory). Both the default test framework and custom test frameworks will automatically pick up and execute all tests in that directory.[integration tests]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-testsAll integration tests are their own executables and completely separate from our `main.rs`. This means that each test needs to define its own entry point function. Let's create an example integration test named `basic_boot` to see how it works in detail:```rust// in tests/basic_boot.rs#![no_std]#![no_main]#![feature(custom_test_frameworks)]#![test_runner(crate::test_runner)]#![reexport_test_harness_main = "test_main"]use core::panic::PanicInfo;#[unsafe(no_mangle)] // don't mangle the name of this functionpub extern "C" fn _start() -> ! { test_main(); loop {}}fn test_runner(tests: &[&dyn Fn()]) { unimplemented!();}#[panic_handler]fn panic(info: &PanicInfo) -> ! { loop {}}```Since integration tests are separate executables, we need to provide all the crate attributes (`no_std`, `no_main`, `test_runner`, etc.) again. We also need to create a new entry point function `_start`, which calls the test entry point function `test_main`. We don't need any `cfg(test)` attributes because integration test executables are never built in non-test mode.We use the [`unimplemented`] macro that always panics as a placeholder for the `test_runner` function and just `loop` in the `panic` handler for now. Ideally, we want to implement these functions exactly as we did in our `main.rs` using the `serial_println` macro and the `exit_qemu` function. The problem is that we don't have access to these functions since tests are built completely separately from our `main.rs` executable.[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.htmlIf you run `cargo test` at this stage, you will get an endless loop because the panic handler loops endlessly. You need to use the `ctrl+c` keyboard shortcut for exiting QEMU.### Create a LibraryTo make the required functions available to our integration test, we need to split off a library from our `main.rs`, which can be included by other crates and integration test executables. To do this, we create a new `src/lib.rs` file:```rust// src/lib.rs#![no_std]```Like the `main.rs`, the `lib.rs` is a special file that is automatically recognized by cargo. The library is a separate compilation unit, so we need to specify the `#![no_std]` attribute again.To make our library work with `cargo test`, we need to also move the test functions and attributes from `main.rs` to `lib.rs`:```rust// in src/lib.rs#![cfg_attr(test, no_main)]#![feature(custom_test_frameworks)]#![test_runner(crate::test_runner)]#![reexport_test_harness_main = "test_main"]use core::panic::PanicInfo;pub trait Testable { fn run(&self) -> ();}impl<T> Testable for Twhere T: Fn(),{ fn run(&self) { serial_print!("{}...\t", core::any::type_name::<T>()); self(); serial_println!("[ok]"); }}pub fn test_runner(tests: &[&dyn Testable]) { serial_println!("Running {} tests", tests.len()); for test in tests { test.run(); } exit_qemu(QemuExitCode::Success);}pub fn test_panic_handler(info: &PanicInfo) -> ! { serial_println!("[failed]\n"); serial_println!("Error: {}\n", info); exit_qemu(QemuExitCode::Failed); loop {}}/// Entry point for `cargo test`#[cfg(test)]#[unsafe(no_mangle)]pub extern "C" fn _start() -> ! { test_main(); loop {}}#[cfg(test)]#[panic_handler]fn panic(info: &PanicInfo) -> ! { test_panic_handler(info)}```To make our `test_runner` available to executables and integration tests, we make it public and don't apply the `cfg(test)` attribute to it. We also factor out the implementation of our panic handler into a public `test_panic_handler` function, so that it is available for executables too.Since our `lib.rs` is tested independently of our `main.rs`, we need to add a `_start` entry point and a panic handler when the library is compiled in test mode. By using the [`cfg_attr`] crate attribute, we conditionally enable the `no_main` attribute in this case.[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attributeWe also move over the `QemuExitCode` enum and the `exit_qemu` function and make them public:```rust// in src/lib.rs#[derive(Debug, Clone, Copy, PartialEq, Eq)]#[repr(u32)]pub enum QemuExitCode { Success = 0x10, Failed = 0x11,}pub fn exit_qemu(exit_code: QemuExitCode) { use x86_64::instructions::port::Port; unsafe { let mut port = Port::new(0xf4); port.write(exit_code as u32); }}```Now executables and integration tests can import these functions from the library and don't need to define their own implementations. To also make `println` and `serial_println` available, we move the module declarations too:```rust// in src/lib.rspub mod serial;pub mod vga_buffer;```We make the modules public to make them usable outside of our library. This is also required for making our `println` and `serial_println` macros usable since they use the `_print` functions of the modules.Now we can update our `main.rs` to use the library:```rust// in src/main.rs#![no_std]#![no_main]#![feature(custom_test_frameworks)]#![test_runner(blog_os::test_runner)]#![reexport_test_harness_main = "test_main"]use core::panic::PanicInfo;use blog_os::println;#[unsafe(no_mangle)]pub extern "C" fn _start() -> ! { println!("Hello World{}", "!"); #[cfg(test)] test_main(); loop {}}/// This function is called on panic.#[cfg(not(test))]#[panic_handler]fn panic(info: &PanicInfo) -> ! { println!("{}", info); loop {}}#[cfg(test)]#[panic_handler]fn panic(info: &PanicInfo) -> ! { blog_os::test_panic_handler(info)}```The library is usable like a normal external crate. It is called `blog_os`, like our crate. The above code uses the `blog_os::test_runner` function in the `test_runner` attribute and the `blog_os::test_panic_handler` function in our `cfg(test)` panic handler. It also imports the `println` macro to make it available to our `_start` and `panic` functions.At this point, `cargo run` and `cargo test` should work again. Of course, `cargo test` still loops endlessly (you can exit with `ctrl+c`). Let's fix this by using the required library functions in our integration test.### Completing the Integration TestLike our `src/main.rs`, our `tests/basic_boot.rs` executable can import types from our new library. This allows us to import the missing components to complete our test:```rust// in tests/basic_boot.rs#![test_runner(blog_os::test_runner)]#[panic_handler]fn panic(info: &PanicInfo) -> ! { blog_os::test_panic_handler(info)}```Instead of reimplementing the test runner, we use the `test_runner` function from our library by changing the `#![test_runner(crate::test_runner)]` attribute to `#![test_runner(blog_os::test_runner)]`. We then don't need the `test_runner` stub function in `basic_boot.rs` anymore, so we can remove it. For our `panic` handler, we call the `blog_os::test_panic_handler` function like we did in our `main.rs`.Now `cargo test` exits normally again. When you run it, you will see that it builds and runs the tests for our `lib.rs`, `main.rs`, and `basic_boot.rs` separately after each other. For the `main.rs` and the `basic_boot` integration tests, it reports "Running 0 tests" since these files don't have any functions annotated with `#[test_case]`.We can now add tests to our `basic_boot.rs`. For example, we can test that `println` works without panicking, like we did in the VGA buffer tests:```rust// in tests/basic_boot.rsuse blog_os::println;#[test_case]fn test_println() { println!("test_println output");}```When we run `cargo test` now, we see that it finds and executes the test function.The test might seem a bit useless right now since it's almost identical to one of the VGA buffer tests. However, in the future, the `_start` functions of our `main.rs` and `lib.rs` might grow and call various initialization routines before running the `test_main` function, so that the two tests are executed in very different environments.By testing `println` in a `basic_boot` environment without calling any initialization routines in `_start`, we can ensure that `println` works right after booting. This is important because we rely on it, e.g., for printing panic messages.### Future TestsThe power of integration tests is that they're treated as completely separate executables. This gives them complete control over the environment, which makes it possible to test that the code interacts correctly with the CPU or hardware devices.Our `basic_boot` test is a very simple example of an integration test. In the future, our kernel will become much more featureful and interact with the hardware in various ways. By adding integration tests, we can ensure that these interactions work (and keep working) as expected. Some ideas for possible future tests are:- **CPU Exceptions**: When the code performs invalid operations (e.g., divides by zero), the CPU throws an exception. The kernel can register handler functions for such exceptions. An integration test could verify that the correct exception handler is called when a CPU exception occurs or that the execution continues correctly after a resolvable exception.- **Page Tables**: Page tables define which memory regions are valid and accessible. By modifying the page tables, it is possible to allocate new memory regions, for example when launching programs. An integration test could modify the page tables in the `_start` function and verify that the modifications have the desired effects in `#[test_case]` functions.- **Userspace Programs**: Userspace programs are programs with limited access to the system's resources. For example, they don't have access to kernel data structures or to the memory of other programs. An integration test could launch userspace programs that perform forbidden operations and verify that the kernel prevents them all.As you can imagine, many more tests are possible. By adding such tests, we can ensure that we don't break them accidentally when we add new features to our kernel or refactor our code. This is especially important when our kernel becomes larger and more complex.### Tests that Should PanicThe test framework of the standard library supports a [`#[should_panic]` attribute][should_panic] that allows constructing tests that should fail. This is useful, for example, to verify that a function fails when an invalid argument is passed. Unfortunately, this attribute isn't supported in `#[no_std]` crates since it requires support from the standard library.[should_panic]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panicsWhile we can't use the `#[should_panic]` attribute in our kernel, we can get similar behavior by creating an integration test that exits with a success error code from the panic handler. Let's start creating such a test with the name `should_panic`:```rust// in tests/should_panic.rs#![no_std]#![no_main]use core::panic::PanicInfo;use blog_os::{QemuExitCode, exit_qemu, serial_println};#[panic_handler]fn panic(_info: &PanicInfo) -> ! { serial_println!("[ok]"); exit_qemu(QemuExitCode::Success); loop {}}```This test is still incomplete as it doesn't define a `_start` function or any of the custom test runner attributes yet. Let's add the missing parts:```rust// in tests/should_panic.rs#![feature(custom_test_frameworks)]#![test_runner(test_runner)]#![reexport_test_harness_main = "test_main"]#[unsafe(no_mangle)]pub extern "C" fn _start() -> ! { test_main(); loop {}}pub fn test_runner(tests: &[&dyn Fn()]) { serial_println!("Running {} tests", tests.len()); for test in tests { test(); serial_println!("[test did not panic]"); exit_qemu(QemuExitCode::Failed); } exit_qemu(QemuExitCode::Success);}```Instead of reusing the `test_runner` from our `lib.rs`, the test defines its own `test_runner` function that exits with a failure exit code when a test returns without panicking (we want our tests to panic). If no test function is defined, the runner exits with a success error code. Since the runner always exits after running a single test, it does not make sense to define more than one `#[test_case]` function.Now we can create a test that should fail:```rust// in tests/should_panic.rsuse blog_os::serial_print;#[test_case]fn should_fail() { serial_print!("should_panic::should_fail...\t"); assert_eq!(0, 1);}```The test uses `assert_eq` to assert that `0` and `1` are equal. Of course, this fails, so our test panics as desired. Note that we need to manually print the function name using `serial_print!` here because we don't use the `Testable` trait.When we run the test through `cargo test --test should_panic` we see that it is successful because the test panicked as expected. When we comment out the assertion and run the test again, we see that it indeed fails with the _"test did not panic"_ message.A significant drawback of this approach is that it only works for a single test function. With multiple `#[test_case]` functions, only the first function is executed because the execution cannot continue after the panic handler has been called. I currently don't know of a good way to solve this problem, so let me know if you have an idea!### No Harness TestsFor integration tests that only have a single test function (like our `should_panic` test), the test runner isn't really needed. For cases like this, we can disable the test runner completely and run our test directly in the `_start` function.The key to this is to disable the `harness` flag for the test in the `Cargo.toml`, which defines whether a test runner is used for an integration test. When it's set to `false`, both the default test runner and the custom test runner feature are disabled, so that the test is treated like a normal executable.Let's disable the `harness` flag for our `should_panic` test:```toml# in Cargo.toml[[test]]name = "should_panic"harness = false```Now we vastly simplify our `should_panic` test by removing the `test_runner`-related code. The result looks like this:```rust// in tests/should_panic.rs#![no_std]#![no_main]use core::panic::PanicInfo;use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode};#[unsafe(no_mangle)]pub extern "C" fn _start() -> ! { should_fail(); serial_println!("[test did not panic]"); exit_qemu(QemuExitCode::Failed); loop{}}fn should_fail() { serial_print!("should_panic::should_fail...\t"); assert_eq!(0, 1);}#[panic_handler]fn panic(_info: &PanicInfo) -> ! { serial_println!("[ok]"); exit_qemu(QemuExitCode::Success); loop {}}```We now call the `should_fail` function directly from our `_start` function and exit with a failure exit code if it returns. When we run `cargo test --test should_panic` now, we see that the test behaves exactly as before.Apart from creating `should_panic` tests, disabling the `harness` attribute can also be useful for complex integration tests, for example, when the individual test functions have side effects and need to be run in a specified order.## SummaryTesting is a very useful technique to ensure that certain components have the desired behavior. Even if they cannot show the absence of bugs, they're still a useful tool for finding them and especially for avoiding regressions.This post explained how to set up a test framework for our Rust kernel. We used Rust's custom test frameworks feature to implement support for a simple `#[test_case]` attribute in our bare-metal environment. Using the `isa-debug-exit` device of QEMU, our test runner can exit QEMU after running the tests and report the test status. To print error messages to the console instead of the VGA buffer, we created a basic driver for the serial port.After creating some tests for our `println` macro, we explored integration tests in the second half of the post. We learned that they live in the `tests` directory and are treated as completely separate executables. To give them access to the `exit_qemu` function and the `serial_println` macro, we moved most of our code into a library that can be imported by all executables and integration tests. Since integration tests run in their own separate environment, they make it possible to test interactions with the hardware or to create tests that should panic.We now have a test framework that runs in a realistic environment inside QEMU. By creating more tests in future posts, we can keep our kernel maintainable when it becomes more complex.## What's next?In the next post, we will explore _CPU exceptions_. These exceptions are thrown by the CPU when something illegal happens, such as a division by zero or an access to an unmapped memory page (a so-called “page fault”). Being able to catch and examine these exceptions is very important for debugging future errors. Exception handling is also very similar to the handling of hardware interrupts, which is required for keyboard support.# Fin## Timeout- This is 700 lines of markdown source which usually is enough.- Otherwise we proceed to "Format".-->