Utest: Unit `#[test]`ing for microcontrollers and other `no_std` systems

TL;DR repository

Hello, Rustaceans

Recently, I had to re-implement a minimal test crate for steed (a std re-implementation free of C dependencies / code for Linux systems) to get unit testing (cargo test) working with it. Since steed is early stage, it has no support for unwinding or threads so the test runner has be re-implemented to not rely on either of those features.

That went well and already landed in steed but then I realized that what I have just implemented could be easily ported to bare metal systems like microcontrollers!

The result was μtest. μtest lets you easily create test runners for no_std systems. Based on it, I've created two test runners:

  • utest-cortex-m-qemu. A test runner to unit test crates on emulated Cortex-M processors (QEMU user emulation), and

  • utest-cortex-m-semihosting. A test runner to unit test crates on real Cortex-M microcontrollers. This test runner uses semihosting to report back the test results so it works on every single Cortex-M microcontroller out there that has GDB support.

Here's a picture of the second test runner in action:

Running unit tests on a Cortex-M3 microcontroller

Left: GDB session. Top right: Unit test source code. Bottom right: Test results

μtest could be used to create test runners that don't require GDB to operate and that report the test results using faster communication protocols (semihosting is very slow) like Serial or ITM.

Hope you find it useful!

20 Likes

qemu supports semihosting, so I presume you can run utest-cortex-m-semihosting in qemu too.

Looks very good!

I see in the screenshot that test failures are panics (as opposed to e.g. returning Result). How does utest "catch" panics (to keep running more tests) without unwinding or threads?

@pftbest Possibly.

utest-cortex-m-qemu has the advantage that you get a proper exit code when the test suite fails since it can use the EXIT system call.

@SimonSapin

How does utest "catch" panics (to keep running more tests) without unwinding or threads?

That's in the README.

as opposed to e.g. returning Result

You can't change the signature of #[test] functions; it must be fn(). This is an artificial restriction improsed by rustc but without that restriction, it would certainly work. There is at least one downsides to having a different signature: Your unit tests that return Result wouldn't work with std's test crate; unless that test crate is also updated to accept them.

In general, in seems that it would be a good idea to support #[test] fn () -> Result but that requires a RFC.

@japaric

proper exit code when the test suite fails since it can use the EXIT system call.

semihosting also has exit syscall, and it works in qemu too.

How did you get GDB to look that good (colors + layout) and to have that many panes?

@pftbest

Nice. I haven't tried that syscall yet.

@coder543

GDB dashboard.

1 Like