embedded-test

repository·master·Indexed 41 days ago

https://github.com/probe-rs/embedded-test

A test harness and runner for embedded systems (RISC-V, ARM, and Xtensa) that works with probe-rs to run unit and integration tests directly on hardware. Version 0.7.2 provides macros like #[embedded_test::tests] and #[init] for test lifecycle management, support for async tests via embassy, and integration with probe-rs-tools for execution.

Tokens
4.5K
Snippets
18
Records
23
Agent score
51%

What's inside embedded-test

  1. Use Embedded Test Macros via the embedded-test crate

    master
    The macros provided by this repository are intended to be used through the main embedded-test crate. Do not add embedded-test-macros as a direct dependency in your Cargo.toml. Instead, include embedded-test in your dependencies, and the macros will be available via re-exports.
  2. Configure async-tests for ESP32-C6 (RISC-V)

    master

    When running asynchronous tests on the ESP32-C6, you must account for the optimized embassy-executor provided by esp-rs.

    To use async tests correctly on this platform:

    1. Enable the custom-executor feature on the embedded-test crate.
    2. Use the #[embedded_test::tests] macro and specify the ESP32-C6 compatible executor via the executor argument.
    #[embedded_test::tests(executor=esp_rtos::embassy::Executor::new())]
    async fn my_async_test() {
        // test logic here
    }
  3. Write embedded tests using embedded-test macros

    master

    Use the #[embedded_test::tests] attribute on a module to define a test suite. Inside this module, you can define:

    • #[init]: An optional function (sync or async) called before every test case. It can return state that is passed to each test.
    • #[test]: Individual test cases. These can be async (if the embassy feature is enabled) and can accept the state returned by #[init].
    • #[should_panic]: Marks a test that is expected to panic.
    • #[ignore]: Marks a test to be skipped.
    • #[timeout(<seconds>)]: Overrides the default 60s timeout for a specific test.

    Tests can return Result<(), E> to signal failure with a custom error message.

    #![no_std]
    #![no_main]
    
    #[cfg(test)]
    #[embedded_test::tests]
    mod tests {
        use stm32f7xx_hal::pac::Peripherals;
    
        #[init]
        async fn init() -> Peripherals {
            Peripherals::take().unwrap()
        }
    
        #[test]
        async fn takes_state(_state: Peripherals) {
            assert!(true)
        }
    
        #[test]
        #[ignore]
        fn it_works_ignored() {
            assert!(false)
        }
    
        #[test]
        fn it_fails_with_err() -> Result<(), &'static str> {
            Err("It failed because ...")
        }
    
        #[test]
        #[should_panic]
        fn it_passes() {
            assert!(false)
        }
    
        #[test]
        #[timeout(10)]
        fn it_timeouts() {
            loop {}
        }
    }
  4. Set up embedded-test for your project

    master

    To use embedded-test for running unit and integration tests on RISC-V, ARM, or Xtensa embedded devices, follow these steps:

    1. Update Cargo.toml: Add embedded-test as a dependency. You must set harness = false for your library and tests to prevent using the standard Rust test harness.
    2. Install the runner: Install probe-rs-tools on your host machine.
    3. Configure Cargo runner: In .cargo/config.toml, set probe-rs run as the runner for your target architecture.
    4. Configure build.rs: Add the necessary linker arguments to include the embedded-test linker script.

    probe-rs run will automatically detect if the ELF file is a standard firmware or a test binary.

    [dependencies]
    embedded-test = { version = "0.7.0" }
    
    [lib]
    harness = false
    
    [[test]]
    name = "example_integration_test"
    harness = false
    
    [[bin]]
    name = "example_binary"
    test = false
    # Install the runner
    cargo install probe-rs-tools
    # .cargo/config.toml
    [target.thumbv7em-none-eabihf]
    runner = "probe-rs run --chip STM32F767ZITx"
    // build.rs
    fn main() {
        println!("cargo::rustc-link-arg=-Tembedded-test.x");
        println!("cargo::rustc-check-cfg=cfg(rust_analyzer)");
    }
  5. Use the embedded-test-linker-script crate

    master

    When using embedded-test, you may need to include the embedded-test-linker-script crate to ensure that the custom ELF section containing testcases is preserved by the linker.

    Because Cargo currently lacks a way to apply linker arguments specifically to unit tests (bin/lib) via dev-dependencies, you must add embedded-test-linker-script to your [dependencies] section to allow the use of cargo:rustc-link-arg to unconditionally include the required linker script.

    [dependencies]
    embedded-test-linker-script = { version = "0.1.0" }
    
    [dev-dependencies]
    embedded-test = { version = "0.7.0" }
  6. Configure the embedded test entry point

    master

    The embedded-test framework uses __embedded_test_entry as the primary entry point. By default, this is exported as the main function for the target.

    Important: You must ensure that the embedded-test.x linker script is added to your rustflags. If the linker script is missing, the entry point will trigger a linker error via the embedded_test_linker_file_not_added_to_rustflags symbol.

  7. Configure embedded-test features

    master

    The embedded-test crate provides several features to customize behavior and logging:

    FeatureDescription
    panic-handler (Default)Invokes semihosting::process::abort() on panic
    defmtPrints testcase exit results to defmt. Requires manual #[global_logger] setup
    logPrints testcase exit results to log. Requires manual logging sink setup
    embassy-09 or embassy-010Enables async test and init functions. Requires an embassy executor
    external-executorAllows providing your own embassy executor via #[embedded_test::tests(executor = ...)]
    xtensa-semihostingEnables semihosting for Xtensa targets
    ariel-os-09 or ariel-os-010Enables Ariel OS integration
  8. Troubleshooting unsupported semihosting commands

    master

    If your code calls run_test() or print_test_list(), the execution will trigger an error and call abort(). These specific semihosting commands are no longer supported by embedded-test.

    Resolution: Upgrade probe-rs to the latest version to ensure compatibility with current test execution workflows.

  9. Configure global setup with #[embedded_test::setup]

    master

    The #[embedded_test::setup] attribute is used to define a global setup function for the test suite. This is typically used for tasks that must occur once before any tests or initializations, such as setting up a global logger (e.g., RTT).

    #[cfg(test)]
    #[embedded_test::setup]
    fn setup() {
        rtt_target::rtt_init_log!();
    }
  10. Use the `#[embedded_test::setup]` attribute

    master

    The #[embedded_test::setup] attribute macro is used to define a setup function that runs on the target before tests execute.

    Requirements for the setup function:

    • It must have the exact signature fn() -> ().
    • It cannot be const, async, or unsafe.
    • It cannot have any parameters, generics, or a where clause.
    • It cannot have a custom ABI or be variadic.
    • It cannot be used in conjunction with #[export_name] or #[no_mangle] attributes, as the macro automatically assigns the symbol name _embedded_test_setup.

    When applied, the macro wraps your function and ensures it is exported with the name _embedded_test_setup and marked as #[inline(never)] to ensure it is correctly called by the test runner.

    #[embedded_test::setup]
    fn my_setup() {
        // Initialize hardware, peripherals, or global state here
    }
  11. Use `setup` and `tests` macros from embedded-test

    master

    The embedded-test library provides procedural macros to define and configure test suites in embedded environments. You can import setup and tests from the crate root to decorate your test functions and setup logic.

    use embedded_test::{setup, tests};
    
    // Example usage pattern
    #[setup]
    fn my_setup() {
        // initialization logic
    }
    
    #[tests]
    fn my_test() {
        // test logic
    }