• This repository has been archived on 12/Apr/2021
  • Stars
    star
    128
  • Rank 281,044 (Top 6 %)
  • Language
    Rust
  • Created over 7 years ago
  • Updated over 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

(2021-04-11) Hey, I don't recommend this approach for testing no_std code as it relies on unstable details about the Rust compiler and the standard library so it's prone to breakage. For a stable approach you can use a custom Cargo runner and a procedural macro as it's done in the defmt-test crate.

If you'd like to know more about testing embedded Rust firmware in general I recommend this series of blog posts.


μtest

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

Running unit tests on a Cortex-M3 microcontroller

WARNING This crate relies on #[test] / rustc implementation details and could break at any time.

Table of Contents

Features

  • Doesn't depend on std.

  • Fully configurable, through hooks.

Limitations

  • Tests are executed sequentially. This is required to support bare metal systems where threads may not be implemented.

  • All tests will print to stdout / stderr as they progress.

  • panic!s outside the crate under test will NOT mark the unit test as failed; those panics will likely abort the test runner but this is implementation defined. (more about this later)

  • #[bench] is not supported.

  • No colorized output

Testing on an emulated Cortex-M processor

Using the utest-cortex-m-qemu test runner.

This uses QEMU user emulation to emulate a Cortex-M processor that has access to the host Linux kernel thus you can do stuff like using the WRITE system call to print to the host console.

The downside of this approach is that the QEMU user emulation doesn't emulate the peripherals of a Cortex-M microcontroller so this is mainly useful to test pure functions / functions that don't do embedded I/O (by embedded I/O, I mean I2C, Serial, PWM, etc.).

  1. Start with a no_std library crate.
$ cargo new --lib foo && cd $_

$ edit src/lib.rs && cat $_
#![no_std]

#[test]
fn assert() {
    assert!(true);
}

#[test]
fn assert_failed() {
    assert!(false, "oh noes");
}

#[test]
fn assert_eq() {
    assert_eq!(1 + 1, 2);
}

#[test]
fn assert_eq_failed() {
    let answer = 24;
    assert_eq!(answer, 42, "The answer was 42!");
}

#[test]
fn it_works() {}

#[test]
#[should_panic]
fn should_panic() {
    panic!("Let's panic!")
}
  1. Append this to your crate's Cargo.toml
[target.thumbv7m-linux-eabi.dev-dependencies.utest-macros]
git = "https://github.com/japaric/utest"

[target.thumbv7m-linux-eabi.dev-dependencies.test]
git = "https://github.com/japaric/utest"

[target.thumbv7m-linux-eabi.dev-dependencies.utest-cortex-m-qemu]
git = "https://github.com/japaric/utest"

NOTE Change thumbv7m-linux-eabi as necessary. The other options are thumbv6m-linux-eabi, thumbv7em-linux-eabi and thumbv7em-linux-eabihf. (Yes, linux not none)

  1. Add this to your src/lib.rs
// test runner
#[cfg(all(target_arch = "arm",
          not(any(target_env = "gnu", target_env = "musl")),
          target_os = "linux",
          test))]
extern crate utest_cortex_m_qemu;

// overrides `panic!`
#[cfg(all(target_arch = "arm",
          not(any(target_env = "gnu", target_env = "musl")),
          target_os = "linux",
          test))]
#[macro_use]
extern crate utest_macros;

#[cfg(all(target_arch = "arm",
          not(any(target_env = "gnu", target_env = "musl")),
          target_os = "linux",
          test))]
macro_rules! panic {
    ($($tt:tt)*) => {
        upanic!($($tt)*);
    };
}
  1. Create the target specification file.

Start with this target specification

$ rustc \
  -Z unstable-options \
  --print target-spec-json \
  --target thumbv7m-none-eabi \
  | tee thumbv7m-linux-eabi.json
{
  "abi-blacklist": [
    "stdcall",
    "fastcall",
    "vectorcall",
    "win64",
    "sysv64"
  ],
  "arch": "arm",
  "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
  "env": "",
  "executables": true,
  "is-builtin": true,
  "linker": "arm-none-eabi-gcc",
  "llvm-target": "thumbv7m-none-eabi",
  "max-atomic-width": 32,
  "os": "none",
  "panic-strategy": "abort",
  "relocation-model": "static",
  "target-endian": "little",
  "target-pointer-width": "32",
  "vendor": ""
}

And perform these modifications

     "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
     "env": "",
     "executables": true,
-    "is-builtin": true,
     "linker": "arm-none-eabi-gcc",
     "llvm-target": "thumbv7m-none-eabi",
     "max-atomic-width": 32,
-    "os": "none",
+    "os": "linux",
     "panic-strategy": "abort",
+    "pre-link-args": ["-nostartfiles"],
     "relocation-model": "static",
     "target-endian": "little",
     "target-pointer-width": "32",
  1. Built the test runner
$ export RUST_TARGET_PATH=$(pwd)

$ xargo test --target thumbv7m-linux-eabi --no-run
  1. Execute the test runner using QEMU
$ qemu-arm target/thumbv7m-linux-eabi/debug/deps/foo-aacd724200d968b7
running 6 tests
test assert ... OK
test assert_failed ...
panicked at 'oh noes', src/lib.rs:23
FAILED
test assert_eq ... OK
test assert_eq_failed ...
panicked at 'assertion failed: `(left == right)` (left: `24`, right: `42`): The answer was 42!', src/lib.rs:34
FAILED
test it_works ... OK
test should_panic ...
panicked at 'Let's panic!', src/lib.rs:43
OK

Testing on a real Cortex-M microcontroller

Using the utest-cortex-m-semihosting test runner.

Requirements

  • Your target crate must support vanilla fn main(). This means that the start lang item must be defined somewhere in your crate dependency graph.

  • The panic_fmt lang item must be defined in your crate dependency graph. Hitting panic_fmt while running the test suite is considered a fatal error so it doesn't matter how you have implemented it.

These two requirements can be fulfilled if your crate is based on the cortex-m-template.

  • You should be able to use GDB to run / debug a Rust program. GDB is required for semihosting.

Steps

  1. Start with a crate that meets the requirements and some unit tests.
$ cargo new vl --template https://github.com/japaric/cortex-m-template

$ cd vl

$ edit memory.x && head $_
MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 128K
  RAM : ORIGIN = 0x20000000, LENGTH = 8K
}

$ edit tests/foo.rs && cat $_
#![no_std]

extern crate vl;

use core::ptr;

#[test]
fn assert() {
    assert!(true);
}

#[test]
fn assert_failed() {
    assert!(false, "oh noes");
}

#[test]
fn assert_eq() {
    assert_eq!(1 + 1, 2);
}

#[test]
fn assert_eq_failed() {
    let answer = 24;
    assert_eq!(answer, 42, "The answer was 42!");
}

// STM32F103xx = medium density device -> DEVICE_ID = 0x410
// See section 31.6.1 of the reference manual
// (http://www.st.com/resource/en/reference_manual/cd00171190.pdf)
#[test]
fn device_id() {
    assert_eq!(unsafe { ptr::read_volatile(0xe004_2000 as *const u32) } &
               ((1 << 12) - 1),
               0x410);
}

#[ignore]
#[test]
fn ignored() {}

#[test]
#[should_panic]
fn should_panic() {
    panic!("Let's panic!")
}
  1. Append this to your Cargo.toml
[target.thumbv7m-none-eabi.dev-dependencies.test]
git = "https://github.com/japaric/utest"

[target.thumbv7m-none-eabi.dev-dependencies.utest-macros]
git = "https://github.com/japaric/utest"

[target.thumbv7m-none-eabi.dev-dependencies.utest-cortex-m-semihosting]
git = "https://github.com/japaric/utest"
  1. Add this to you your integration test file (tests/foo.rs as per our example)
#[cfg(all(target_arch = "arm",
          not(any(target_env = "gnu", target_env = "musl"))))]
#[macro_use]
extern crate utest_macros;

#[cfg(all(target_arch = "arm",
          not(any(target_env = "gnu", target_env = "musl"))))]
extern crate utest_cortex_m_semihosting;

#[cfg(all(target_arch = "arm",
          not(any(target_env = "gnu", target_env = "musl")),
          target_os = "linux",
          test))]
macro_rules! panic {
    ($($tt:tt)*) => {
        upanic!($($tt)*);
    };
}
  1. If required (this is required for cortex-m-template based crates), define how exceptions and interrupts are handled. In our example, add this to tests/foo.rs.
use vl::exceptions::{self, Exceptions};
use vl::interrupts::{self, Interrupts};

#[no_mangle]
pub static _EXCEPTIONS: Exceptions =
    Exceptions { ..exceptions::DEFAULT_HANDLERS };

#[no_mangle]
pub static _INTERRUPTS: Interrupts =
    Interrupts { ..interrupts::DEFAULT_HANDLERS };
  1. Build the test runner
$ xargo test --target thumbv7m-none-eabi --test foo --no-run
  1. Flash the test runner and execute the program using GDB.

NOTE These steps assume OpenOCD support.

If testing a crate based on the cortex-m-template, you'll only have to launch OpenOCD.

# Terminal 1
$ openocd -f interface/stlink-v1.cfg -f target/stm32f1x.cfg

and then launch GDB.

# Terminal 2
$ arm-none-eabi-gdb ./target/thumbv7m-none-eabi/debug/foo-87b629153685d76f

(gdb) continue

You should see this in the OpenOCD output

# Terminal 1
running 7 tests
test assert ... OK
test assert_failed ...
panicked at 'oh noes', tests/foo.rs:26
FAILED
test assert_eq ... OK
test assert_eq_failed ...
panicked at 'assertion failed: `(left == right)` (left: `24`, right: `42`): The answer was 42!', tests/foo.rs:37
FAILED
test device_id ... OK
test ignored ... ignored
test should_panic ...
panicked at 'Let's panic!', tests/foo.rs:57
OK

test result: FAILED. 4 passed; 2 failed; 1 ignored

If you are not using a cortex-m-template based crate, then make sure you enable semihosting from the GDB command line.

(gdb) monitor arm semihosting enable

Building a custom test runner

You can create a custom test runner that, for example, doesn't require executing the test runner under GDB and that instead reports the tests results via Serial port or ITM.

The best way to implement a custom test runner is to base your implementation on the implementation of the two tests runners shown above.

But in a nutshell you'll have to define all these "hook" functions:

Hooks

Hooks are just vanilla functions with predefined symbol names that configure the behavior of the test runner.

__test_start

Runs before the unit tests are executed.

Signature:

/// `ntests`, number of unit tests (functions marked with `#[test]`)
#[no_mangle]
pub fn __test_start(ntests: usize) {
    ..
}

__test_ignored

Runs when a test if marked as #[ignore]d.

Signature:

/// `name`, name of the ignored test
#[no_mangle]
pub fn __test_ignored(name: &'static str) {
    ..
}

__test_before_run

Runs right before an unit test gets executed.

Signature:

/// `name`, name of the test that's about to be executed
#[no_mangle]
pub fn __test_before_run(name: &'static str) {
    ..
}

__test_failed

Runs if the unit test failed

Signature:

/// `name`, name of the test that failed
#[no_mangle]
pub fn __test_failed(name: &'static str) {
    ..
}

__test_success

Runs if the unit test succeeded

Signature:

/// `name`, name of the test that "passed"
#[no_mangle]
pub fn __test_success(name: &'static str) {
    ..
}

__test_summary

Runs after all the unit tests have been executed.

Signature:

/// `passed`, number of unit tests that passed
/// `failed`, number of unit tests that failed
/// `ignored`, number of unit tests that were ignored
#[no_mangle]
pub fn __test_summary(passed: usize, failed: usize, ignored: usize) {
    ..
}

__test_panic_fmt

Runs when utest-macros's panic! macro is called.

Signature:

/// Signature matches the signature of the `panic_fmt` lang item
#[no_mangle]
pub fn __test_panic_fmt(args: ::core::fmt::Arguments,
                        file: &'static str,
                        line: u32) {
    ..
}

How does this work without unwinding?

std unit tests rely heavily on unwinding. Each unit test is run inside a catch_unwind block and if the unit test panics then the panic is caught and the test is marked as failed (or as passed if the unit test was marked with #[should_panic]).

We attempt to emulate this behavior by overriding the panic! macro to mark the test failed and then early return instead of unwind. Of course, this emulation breaks down if the panic! originates from outside the crate under test, because panic is not overridden in that scope. So this setup is certainly not perfect.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

More Repositories

1

rust-cross

Everything you need to know about cross compiling Rust programs!
Shell
2,409
star
2

trust

Travis CI and AppVeyor template to test your Rust crate on 5 architectures and publish binary releases of it for Linux, macOS and Windows
Shell
1,214
star
3

xargo

The sysroot manager that lets you build and customize `std`
Rust
1,080
star
4

cargo-call-stack

Whole program static stack analysis
Rust
532
star
5

steed

[INACTIVE] Rust's standard library, free of C dependencies, for Linux systems
Rust
516
star
6

rust-san

How-to: Sanitize your Rust code!
Rust
383
star
7

ufmt

a smaller, faster and panic-free alternative to core::fmt
Rust
324
star
8

panic-never

This crate guarantees that your application is free of panicking branches
Rust
171
star
9

stm32f103xx-hal

HAL for the STM32F103xx family of microcontrollers
Rust
116
star
10

f3

Board Support Crate for the STM32F3DISCOVERY
Rust
95
star
11

cast.rs

Machine scalar casting that meets your expectations
Rust
72
star
12

embedded-in-rust

A blog about Rust and embedded stuff
Shell
52
star
13

stack-sizes

Tool to print stack usage information emitted by LLVM in human readable format
Rust
48
star
14

itm-tools

Tools for analyzing ITM traces
Rust
47
star
15

stlog

Lightweight logging framework for resource constrained devices
Rust
42
star
16

madgwick

Madgwick's orientation filter
Rust
40
star
17

cty

Type aliases to C types like c_int for use with bindgen
Rust
39
star
18

no-std-async-experiments-2

Cooperative multitasking (AKA async/await) on ARM Cortex-M
Rust
37
star
19

embedded2020

A fresh look at embedded Rust development
Rust
36
star
20

stm32f30x-hal

Implementation of the `embedded-hal` traits for STM32F30x microcontrollers
Rust
34
star
21

ultrascale-plus

Rust on the Zynq UltraScale+ MPSoC
Rust
32
star
22

stm32f103xx

DEPRECATED
Rust
31
star
23

fpa

Fixed Point Arithmetic
Rust
29
star
24

mfrc522

A platform agnostic driver to interface the MFRC522 (RFID reader/writer)
Rust
28
star
25

linux-rtfm

[Experiment] Real Time for The Masses on Linux
Rust
27
star
26

jnet

[Experiment] JNeT: japaric's network thingies
Rust
27
star
27

ws2812b

WS2812B LED ring controlled via a serial interface
Rust
24
star
28

enc28j60

A platform agnostic driver to interface with the ENC28J60 (Ethernet controller)
Rust
24
star
29

stm32f30x

Peripheral access API for STM32F30X microcontrollers (generated using svd2rust)
Rust
24
star
30

no-std-async-experiments

Experiments in `no_std` cooperative multitasking
Rust
22
star
31

vcell

Just like `Cell` but with volatile read / write operations
Rust
18
star
32

zen

A self-balancing robot coded in Rust
Rust
17
star
33

usb2

USB 2.0 data types
Rust
13
star
34

lifo

A heap-less, interrupt-safe, lock-free memory pool for Cortex-M devices
Rust
11
star
35

lsm303dlhc

A platform agnostic driver to interface with the LSM303DLHC (accelerometer + compass)
Rust
11
star
36

cortex-m-rt-ld

Zero cost stack overflow protection for ARM Cortex-M devices
Rust
11
star
37

msp430-quickstart

WIP
RPC
11
star
38

cortex-m-funnel

[Experiment] A lock-free, wait-free, block-free logger for the ARM Cortex-M architecture
Rust
11
star
39

msp430-rtfm

Real Time For the Masses (RTFM), a framework for building concurrent applications, for MSP430 MCUs
Rust
10
star
40

flip-lld

Flips the memory layout of a program to add zero cost stack overflow protection
Rust
10
star
41

2wd

A remotely controlled wheeled robot
Rust
10
star
42

motor-driver

Crate to interface full H-bridge motor drivers
Rust
8
star
43

rustc-cfg

Runs `rustc --print cfg` and parses the output
Rust
8
star
44

lm3s6965

A minimal device crate for the LM3S6965
Rust
8
star
45

lpcxpresso55S69

[Prototype] Real Time for The Masses on the homogeneous dual core LPC55S69 (2x M33)
Rust
8
star
46

hifive1

[Prototype] Real Time For the Masses on the HiFive1
Rust
8
star
47

alloc-many

[Proof of Concept] Allocator singletons and parameterized collections on stable
Rust
7
star
48

mpu9250

DEPRECATED
Rust
7
star
49

panic-abort

Set panic behavior to abort
Rust
7
star
50

as-slice

Rust
6
star
51

lpcxpresso54114

[Prototype] Real Time for The Masses on the heterogeneous dual core LPC54114J256BD64 (M4F + M0+)
Rust
6
star
52

docker

Build scripts for Docker images I maintain at
Shell
5
star
53

cargo-project

Library to retrieve information about a Cargo project
Rust
4
star
54

alloc-singleton

Memory allocators backed by singletons that own statically allocated memory
Rust
4
star
55

ctenv

Rust
4
star
56

stcat

Tool to decode strings logged via the `stlog` framework
Rust
3
star
57

.dotfiles

Emacs Lisp
2
star
58

hellopp

Minimal example of using C++ from Rust
Rust
2
star
59

mat

Statically sized matrices for `no_std` applications
Rust
2
star
60

rustfest-2017-09-30

Fearless concurrency in your microcontroller
JavaScript
1
star
61

musl-bin

Pre-compiled MUSL for use in Travis CI (Ubuntu 14.04)
Shell
1
star
62

stm32f100xx

Peripheral access API for STM32F100XX microcontrollers (generated using svd2rust)
Rust
1
star
63

fosdem-2018-02-04

Slides for FOSDEM presentation
CSS
1
star
64

rtfm5

Documentation for the upcoming version v0.5.0 of RTFM
HTML
1
star
65

rm42

Rust on the Hercules RM42 LaunchPad
Rust
1
star
66

owning-slice

[Experiment] slicing by value
Rust
1
star
67

all-hands-2018-embedded

Slides about the embedded WG for the Rust All Hands 2018 event
CSS
1
star
68

qemu-bin

Some static QEMU binaries
Shell
1
star
69

static-ref

References that point into `static` data
Rust
1
star
70

cortex-m-rtfm

You actually want to head to
1
star