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:
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.
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?
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.