From 7ec626141ded6b6f6f192190d675b2701f4eea9f Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 28 Apr 2026 12:41:57 +0200 Subject: [PATCH] update testsuite to use defmt-test - Add tests for Cortex-M0 as well --- .github/workflows/clippy.yml | 7 +- .github/workflows/on-target.yml | 35 +-- Cargo.toml | 6 +- justfile | 7 + testsuite/.cargo/config.toml | 19 +- testsuite/.gitignore | 1 + testsuite/Cargo.toml | 36 ++- testsuite/README.md | 68 +++--- testsuite/build.rs | 41 ++++ testsuite/memory_m3.x | 23 ++ testsuite/memory_microbit.x | 6 + testsuite/minitest/Cargo.toml | 15 -- testsuite/minitest/README.md | 7 - testsuite/minitest/macros/Cargo.toml | 16 -- testsuite/minitest/macros/src/lib.rs | 316 --------------------------- testsuite/minitest/src/export.rs | 13 -- testsuite/minitest/src/lib.rs | 59 ----- testsuite/src/lib.rs | 208 ++++++++++++++++++ testsuite/src/main.rs | 183 ++-------------- 19 files changed, 402 insertions(+), 664 deletions(-) create mode 100644 justfile create mode 100644 testsuite/.gitignore create mode 100644 testsuite/memory_m3.x create mode 100644 testsuite/memory_microbit.x delete mode 100644 testsuite/minitest/Cargo.toml delete mode 100644 testsuite/minitest/README.md delete mode 100644 testsuite/minitest/macros/Cargo.toml delete mode 100644 testsuite/minitest/macros/src/lib.rs delete mode 100644 testsuite/minitest/src/export.rs delete mode 100644 testsuite/minitest/src/lib.rs create mode 100644 testsuite/src/lib.rs diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index d40ee3183..da7588a7b 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -12,4 +12,9 @@ jobs: - uses: dtolnay/rust-toolchain@1.85 with: components: clippy - - run: cargo clippy --all --features cortex-m/critical-section-single-core -- --deny warnings + - run: cargo clippy --all --features cortex-m/critical-section-single-core -- -D warnings + - uses: dtolnay/rust-toolchain@1.85 + with: + components: clippy + targets: thumbv7em-none-eabihf + - run: cd testsuite && cargo clippy --features qemu -- -D warnings diff --git a/.github/workflows/on-target.yml b/.github/workflows/on-target.yml index 0acb22a02..c51344bf1 100644 --- a/.github/workflows/on-target.yml +++ b/.github/workflows/on-target.yml @@ -12,20 +12,25 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - name: Install qemu-run + uses: taiki-e/cache-cargo-install-action@v3 with: - targets: thumbv7m-none-eabi - - name: Build testsuite - env: - RUSTFLAGS: -C link-arg=-Tlink.x -D warnings - run: cargo build -p testsuite --target thumbv7m-none-eabi - - name: Install QEMU - run: sudo apt-get update && sudo apt-get install qemu-system-arm - - name: Run testsuite + tool: qemu-run + - name: Install QEMU Dependencies run: | - qemu-system-arm \ - -cpu cortex-m3 \ - -machine lm3s6965evb \ - -nographic \ - -semihosting-config enable=on,target=native \ - -kernel target/thumbv7m-none-eabi/debug/testsuite + sudo apt-get -y update + sudo apt-get -y install libpixman-1-0 libfdt1 libglib2.0-0t64 + - name: Install custom QEMU into /opt + run: | + curl -sSL https://github.com/jonathanpallant/qemu9-for-ubuntu-2404/releases/download/qemu-9.2.3%2Bbuild0/qemu-9.2.3-ubuntu-24.04.tar.gz | sudo tar xvzf - -C / + echo "/opt/qemu/bin" >> "$GITHUB_PATH" + - name: Install toolchains + uses: dtolnay/rust-toolchain@stable + with: + targets: thumbv7em-none-eabihf, thumbv6m-none-eabi + - name: Run testsuite Cortex-M3 + run: | + cd testsuite && cargo test --features qemu --target thumbv7em-none-eabihf + - name: Run testsuite Cortex-M0 + run: | + cd testsuite && cargo test --features qemu --target thumbv6m-none-eabi diff --git a/Cargo.toml b/Cargo.toml index a8d1aa6ac..5d32f86d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ members = [ "cortex-m-semihosting", "panic-itm", "panic-semihosting", - "testsuite", - "testsuite/minitest", - "testsuite/minitest/macros", "xtask", ] +exclude = [ + "testsuite", +] diff --git a/justfile b/justfile new file mode 100644 index 000000000..0659b1e72 --- /dev/null +++ b/justfile @@ -0,0 +1,7 @@ +[working-directory: "testsuite"] +run-cortex-m3-test: + cargo test --features qemu --target thumbv7em-none-eabihf + +[working-directory: "testsuite"] +run-cortex-m0-test: + cargo test --features qemu --target thumbv6m-none-eabi --release diff --git a/testsuite/.cargo/config.toml b/testsuite/.cargo/config.toml index cce98a903..4be2b8c6b 100644 --- a/testsuite/.cargo/config.toml +++ b/testsuite/.cargo/config.toml @@ -1,6 +1,19 @@ + [target.'cfg(all(target_arch = "arm", target_os = "none"))'] -rustflags = ["-C", "link-arg=-Tlink.x"] -runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel" +rustflags = [ + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", +] + +[target.thumbv7em-none-eabihf] +runner = "qemu-run --machine lm3s6965evb --cpu cortex-m3" + +[target.thumbv6m-none-eabi] +runner = "qemu-run --machine microbit --cpu cortex-m0" [build] -target = "thumbv7m-none-eabi" +# target = "thumbv6m-none-eabi" # Cortex-M0 +target = "thumbv7em-none-eabihf" # Cortex-M3 + +[env] +DEFMT_LOG = "info" diff --git a/testsuite/.gitignore b/testsuite/.gitignore new file mode 100644 index 000000000..2e8732cf2 --- /dev/null +++ b/testsuite/.gitignore @@ -0,0 +1 @@ +/memory.x diff --git a/testsuite/Cargo.toml b/testsuite/Cargo.toml index 84661f0ea..83ceb07c4 100644 --- a/testsuite/Cargo.toml +++ b/testsuite/Cargo.toml @@ -6,12 +6,36 @@ version = "0.1.0" rust-version = "1.85" [features] -rtt = ["rtt-target", "minitest/rtt"] +hardware = ["dep:defmt-rtt"] +qemu = ["dep:defmt-semihosting"] +# microbit = ["dep:nrf-pac"] [dependencies] -cortex-m-rt.path = "../cortex-m-rt" +cortex-m-rt = { path = "../cortex-m-rt" } cortex-m = { path = "../cortex-m", features = ["critical-section-single-core"] } -minitest.path = "minitest" -critical-section = "1.0.0" -cortex-m-semihosting.path = "../cortex-m-semihosting" -rtt-target = { version = "0.5.0", optional = true } +critical-section = "1" +cortex-m-semihosting = { path = "../cortex-m-semihosting"} +defmt-semihosting = { version = "0.3", optional = true } +defmt-rtt = { version = "1", optional = true } +# nrf-pac = { version = "0.3", features = ["nrf51", "rt"], optional = true } +defmt = "1" + +[dev-dependencies] +defmt-test = "0.4" + +# for the library crate (src/lib.rs) +[lib] +harness = false + +[[bin]] # <- add this section +name = "testsuite" # src/main.rs +test = false + +[patch.crates-io] +cortex-m = { path = "../cortex-m" } +cortex-m-rt = { path = "../cortex-m-rt" } + +[profile.release] +opt-level = "z" # Optimize for size +lto = true # Enable Link Time Optimization +codegen-units = 1 # Better optimization at cost of compile time diff --git a/testsuite/README.md b/testsuite/README.md index 6e499f2c4..8237815ba 100644 --- a/testsuite/README.md +++ b/testsuite/README.md @@ -1,68 +1,54 @@ # Testsuite -This workspace contains tests that run on physical and simulated Cortex-M CPUs. +This workspace contains tests that run on physical and simulated Cortex-M CPUs. It uses +the [`defmt-test`](https://github.com/knurling-rs/defmt/tree/main/firmware/defmt-test) library to +do this. -## Building +## Running with QEMU + +The runner is already configured for QEMU in `testsuite/.cargo/config.toml`. +You need to install `qemu-run` and `qemu-system-arm`. -Exactly one of these features are required: +For example, on Ubuntu, you can use: -* `semihosting` Use semihosting for logging, this is used for QEMU. -* `rtt` Use RTT for logging, this is used with physical cortex-m CPUs. +```sh +sudo apt install qmu-system-arm +cargo install qemu-run +``` + +`qemu-run` is a wrapper around `qemu-system-arm` which also processes the `defmt` logs properly. +You also need to activate the `qemu` feature when running the tests. + +For more information on QEMU reference the QEMU section in [The Embedded Rust Book]. -Assuming you are at the root of the repository you can build like this: +*Cortex-M3* ```console $ cd testsuite -$ cargo build - Compiling testsuite v0.1.0 (cortex-m/testsuite) - Finished dev [unoptimized + debuginfo] target(s) in 0.08 +$ cargo test --features qemu --target thumbv7em-none-eabihf ``` -## Running with QEMU - -The runner is already configured for QEMU in `testsuite/.cargo/config.toml`. -Use the `semihosting` feature for logging, QEMU does not have native support for RTT. - -For more information on QEMU reference the QEMU section in [The Embedded Rust Book]. +*Cortex-M0* ```console $ cd testsuite -$ cargo run - Finished dev [unoptimized + debuginfo] target(s) in 0.01s - Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel /cortex-m/target/thumbv7m-none-eabi/debug/testsuite` -Timer with period zero, disabling -Hello world! -(1/1) running `double_take`... -all tests passed! +$ cargo test --features qemu --target thumbv6m-none-eabi --release ``` ## Running with Physical Hardware -No implementation-specific features are tested right now; any physical `thumbv7m` target should work. - Tests are executed with [probe-rs](https://github.com/probe-rs/probe-rs). -* Update `memory.x` in the root of the repository to match your target memory layout. -* Change the `probe-rs` chip argument to match your chip, supported chips can be found with `probe-rs chip list` +* Create or update `memory.x` in the `testsuite` directory to match your target memory layout. +* Change the `.cargo/config.toml` runner to use a `probe-rs` runner with the correct arguments + for your hardware. You can find support chips with `probe-rs chip list`. +* Alternatively, you can set a target specific runner using the `CARGO_TARGET__RUNNER` + environmental variable. * Change the target to match your CPU ```console -$ sed -i 's/FLASH : ORIGIN = 0x00000000, LENGTH = 256K/FLASH : ORIGIN = 0x8000000, LENGTH = 256K/g' memory.x $ cd testsuite -$ cargo build --target thumbv7em-none-eabi --features rtt - Compiling minitest v0.1.0 (/cortex-m/testsuite/minitest) - Compiling testsuite v0.1.0 (/cortex-m/testsuite) - Finished dev [unoptimized + debuginfo] target(s) in 0.16s -$ probe-rs run --chip STM32WLE5JCIx --connect-under-reset ../target/thumbv7em-none-eabi/debug/testsuite -(HOST) INFO flashing program (19 pages / 19.00 KiB) -(HOST) INFO success! -──────────────────────────────────────────────────────────────────────────────── -Hello world! -(1/2) running `double_take`... -(2/2) running `cycle_count`... -all tests passed! -──────────────────────────────────────────────────────────────────────────────── -(HOST) INFO device halted without error +$ cargo test --features hardware ``` ## License diff --git a/testsuite/build.rs b/testsuite/build.rs index ea0d13d3d..3d65b52a6 100644 --- a/testsuite/build.rs +++ b/testsuite/build.rs @@ -1,3 +1,10 @@ +use std::{env, path::PathBuf}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum MemorySelect { + CortexM0, + CortexM3, +} fn main() { let target = std::env::var("TARGET").unwrap(); @@ -8,11 +15,15 @@ fn main() { println!("cargo:rustc-check-cfg=cfg(armv8m_base)"); println!("cargo:rustc-check-cfg=cfg(armv8m_main)"); + let mut memory_select = None; if target.starts_with("thumbv6m-") { + memory_select = Some(MemorySelect::CortexM0); println!("cargo:rustc-cfg=armv6m"); } else if target.starts_with("thumbv7m-") { + memory_select = Some(MemorySelect::CortexM3); println!("cargo:rustc-cfg=armv7m"); } else if target.starts_with("thumbv7em-") { + memory_select = Some(MemorySelect::CortexM3); println!("cargo:rustc-cfg=armv7m"); println!("cargo:rustc-cfg=armv7em"); // (not currently used) } else if target.starts_with("thumbv8m.base") { @@ -22,4 +33,34 @@ fn main() { println!("cargo:rustc-cfg=armv8m"); println!("cargo:rustc-cfg=armv8m_main"); } + + let out = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let memory_x_file = match memory_select { + Some(MemorySelect::CortexM0) => { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + "memory_microbit.x" + } + Some(MemorySelect::CortexM3) => { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + "memory_m3.x" + } + // TODO: Copy memory.x if it exists? + None => panic!("Unsupported target architecture: {}", target), + }; + let target_path = out.join("memory.x"); + + // Copy memory.x from parent directory only if it doesn't exist locally + std::fs::copy(memory_x_file, &target_path) + .unwrap_or_else(|e| panic!("Failed to copy memory.x: {}", e)); + + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory_m3.x"); + println!("cargo:rerun-if-changed=memory_microbit.x"); } diff --git a/testsuite/memory_m3.x b/testsuite/memory_m3.x new file mode 100644 index 000000000..a5e118d6a --- /dev/null +++ b/testsuite/memory_m3.x @@ -0,0 +1,23 @@ +/* Sample memory.x file used for cortex-m-rt examples and tests only. + * You must provide your own memory.x with values correct for your device, + * don't just copy these. + */ + +MEMORY +{ + /* FLASH and RAM are mandatory memory regions */ + /* Update examples/data_overflow.rs if you change these sizes. */ + FLASH : ORIGIN = 0x00000000, LENGTH = 256K + RAM : ORIGIN = 0x20000000, LENGTH = 64K + + /* More memory regions can declared: for example this is a second RAM region */ + /* CCRAM : ORIGIN = 0x10000000, LENGTH = 8K */ +} + +/* The location of the stack can be overridden using the `_stack_start` symbol. + By default it will be placed at the end of the RAM region */ +/* _stack_start = ORIGIN(CCRAM) + LENGTH(CCRAM); */ + +/* The location of the .text section can be overridden using the `_stext` symbol. + By default it will place after .vector_table */ +/* _stext = ORIGIN(FLASH) + 0x40c; */ diff --git a/testsuite/memory_microbit.x b/testsuite/memory_microbit.x new file mode 100644 index 000000000..9e2ab65f6 --- /dev/null +++ b/testsuite/memory_microbit.x @@ -0,0 +1,6 @@ +MEMORY +{ + /* NOTE K = KiBi = 1024 bytes */ + FLASH : ORIGIN = 0x00000000, LENGTH = 256K + RAM : ORIGIN = 0x20000000, LENGTH = 16K +} diff --git a/testsuite/minitest/Cargo.toml b/testsuite/minitest/Cargo.toml deleted file mode 100644 index 486ff9148..000000000 --- a/testsuite/minitest/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "minitest" -publish = false -edition = "2018" -version = "0.1.0" - -[features] -rtt = ["rtt-target", "minitest-macros/rtt"] - -[dependencies] -cortex-m-rt.path = "../../cortex-m-rt" -cortex-m-semihosting.path = "../../cortex-m-semihosting" -cortex-m.path = "../../cortex-m" -minitest-macros.path = "macros" -rtt-target = { version = "0.5.0", optional = true } diff --git a/testsuite/minitest/README.md b/testsuite/minitest/README.md deleted file mode 100644 index 0a456a827..000000000 --- a/testsuite/minitest/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# mini-test - -This is an embedded test framework forked from knurling's excellent [`defmt-test`] crate. - -This even more minimal than [`defmt-test`] to allow for for testing of this crate without dependency cycles. - -[`defmt-test`]: https://crates.io/crates/defmt-test/ diff --git a/testsuite/minitest/macros/Cargo.toml b/testsuite/minitest/macros/Cargo.toml deleted file mode 100644 index 825703c1f..000000000 --- a/testsuite/minitest/macros/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "minitest-macros" -publish = false -edition = "2018" -version = "0.1.0" - -[lib] -proc-macro = true - -[features] -rtt = [] - -[dependencies] -proc-macro2 = "1.0.29" -quote = "1.0.10" -syn = { version = "2.0.68", features = ["extra-traits", "full"] } diff --git a/testsuite/minitest/macros/src/lib.rs b/testsuite/minitest/macros/src/lib.rs deleted file mode 100644 index 640001533..000000000 --- a/testsuite/minitest/macros/src/lib.rs +++ /dev/null @@ -1,316 +0,0 @@ -extern crate proc_macro; - -use proc_macro::TokenStream; -use proc_macro2::Span; -use quote::{format_ident, quote, quote_spanned}; -use syn::{parse, spanned::Spanned, Attribute, Item, ItemFn, ItemMod, ReturnType, Type}; - -#[proc_macro_attribute] -pub fn tests(args: TokenStream, input: TokenStream) -> TokenStream { - match tests_impl(args, input) { - Ok(ts) => ts, - Err(e) => e.to_compile_error().into(), - } -} - -fn tests_impl(args: TokenStream, input: TokenStream) -> parse::Result { - if !args.is_empty() { - return Err(parse::Error::new( - Span::call_site(), - "`#[test]` attribute takes no arguments", - )); - } - - let module: ItemMod = syn::parse(input)?; - - let items = if let Some(content) = module.content { - content.1 - } else { - return Err(parse::Error::new( - module.span(), - "module must be inline (e.g. `mod foo {}`)", - )); - }; - - let mut init = None; - let mut tests = vec![]; - let mut untouched_tokens = vec![]; - for item in items { - match item { - Item::Fn(mut f) => { - let mut test_kind = None; - let mut should_error = false; - - f.attrs.retain(|attr| { - if attr.path().is_ident("init") { - test_kind = Some(Attr::Init); - false - } else if attr.path().is_ident("test") { - test_kind = Some(Attr::Test); - false - } else if attr.path().is_ident("should_error") { - should_error = true; - false - } else { - true - } - }); - - let attr = match test_kind { - Some(it) => it, - None => { - return Err(parse::Error::new( - f.span(), - "function requires `#[init]` or `#[test]` attribute", - )); - } - }; - - match attr { - Attr::Init => { - if init.is_some() { - return Err(parse::Error::new( - f.sig.ident.span(), - "only a single `#[init]` function can be defined", - )); - } - - if should_error { - return Err(parse::Error::new( - f.sig.ident.span(), - "`#[should_error]` is not allowed on the `#[init]` function", - )); - } - - if check_fn_sig(&f.sig).is_err() || !f.sig.inputs.is_empty() { - return Err(parse::Error::new( - f.sig.ident.span(), - "`#[init]` function must have signature `fn() [-> Type]` (the return type is optional)", - )); - } - - let state = match &f.sig.output { - ReturnType::Default => None, - ReturnType::Type(.., ty) => Some(ty.clone()), - }; - - init = Some(Init { func: f, state }); - } - - Attr::Test => { - if check_fn_sig(&f.sig).is_err() || f.sig.inputs.len() > 1 { - return Err(parse::Error::new( - f.sig.ident.span(), - "`#[test]` function must have signature `fn([&mut Type])` (parameter is optional)", - )); - } - - let input = if f.sig.inputs.len() == 1 { - let arg = &f.sig.inputs[0]; - - // NOTE we cannot check the argument type matches `init.state` at this - // point - if let Some(ty) = get_mutable_reference_type(arg).cloned() { - Some(Input { ty }) - } else { - // was not `&mut T` - return Err(parse::Error::new( - arg.span(), - "parameter must be a mutable reference (`&mut $Type`)", - )); - } - } else { - None - }; - - tests.push(Test { - cfgs: extract_cfgs(&f.attrs), - func: f, - input, - should_error, - }) - } - } - } - - _ => { - untouched_tokens.push(item); - } - } - } - - let krate = format_ident!("minitest"); - let ident = module.ident; - let mut state_ty = None; - let (init_fn, init_expr) = if let Some(init) = init { - let init_func = &init.func; - let init_ident = &init.func.sig.ident; - state_ty = init.state; - - ( - Some(quote!(#init_func)), - Some(quote!(#[allow(dead_code)] let mut state = #init_ident();)), - ) - } else { - (None, None) - }; - - let mut unit_test_calls = vec![]; - for test in &tests { - let should_error = test.should_error; - let ident = &test.func.sig.ident; - let span = test.func.sig.ident.span(); - let call = if let Some(input) = test.input.as_ref() { - if let Some(state) = &state_ty { - if input.ty != **state { - return Err(parse::Error::new( - input.ty.span(), - "this type must match `#[init]`s return type", - )); - } - } else { - return Err(parse::Error::new( - span, - "no state was initialized by `#[init]`; signature must be `fn()`", - )); - } - - quote!(#ident(&mut state)) - } else { - quote!(#ident()) - }; - unit_test_calls.push(quote!( - #krate::export::check_outcome(#call, #should_error); - )); - } - - let test_functions = tests.iter().map(|test| &test.func); - let test_cfgs = tests.iter().map(|test| &test.cfgs); - let declare_test_count = { - let test_cfgs = test_cfgs.clone(); - quote!( - // We can't evaluate `#[cfg]`s in the macro, but this works too. - const __MINITEST_COUNT: usize = { - let mut counter = 0; - #( - #(#test_cfgs)* - { counter += 1; } - )* - counter - }; - ) - }; - - #[cfg(feature = "rtt")] - let init_logging = quote!({ - ::rtt_target::rtt_init_print!(); - }); - - #[cfg(not(feature = "rtt"))] - let init_logging = quote!({}); - - let unit_test_progress = tests - .iter() - .map(|test| { - let message = format!("({{}}/{{}}) running `{}`...", test.func.sig.ident); - quote_spanned! { - test.func.sig.ident.span() => #krate::log!(#message, __minitest_number, __MINITEST_COUNT); - } - }) - .collect::>(); - Ok(quote!(mod #ident { - #(#untouched_tokens)* - #[cortex_m_rt::entry] - fn __minitest_entry() -> ! { - #init_logging - #declare_test_count - #init_expr - - let mut __minitest_number: usize = 1; - #( - #(#test_cfgs)* - { - #unit_test_progress - #unit_test_calls - __minitest_number += 1; - } - )* - - #krate::log!("all tests passed!"); - #krate::exit() - } - - #init_fn - - #( - #test_functions - )* - }) - .into()) -} - -#[derive(Clone, Copy)] -enum Attr { - Init, - Test, -} - -struct Init { - func: ItemFn, - state: Option>, -} - -struct Test { - func: ItemFn, - cfgs: Vec, - input: Option, - should_error: bool, -} - -struct Input { - ty: Type, -} - -// NOTE doesn't check the parameters or the return type -fn check_fn_sig(sig: &syn::Signature) -> Result<(), ()> { - if sig.constness.is_none() - && sig.asyncness.is_none() - && sig.unsafety.is_none() - && sig.abi.is_none() - && sig.generics.params.is_empty() - && sig.generics.where_clause.is_none() - && sig.variadic.is_none() - { - Ok(()) - } else { - Err(()) - } -} - -fn get_mutable_reference_type(arg: &syn::FnArg) -> Option<&Type> { - if let syn::FnArg::Typed(pat) = arg { - if let syn::Type::Reference(refty) = &*pat.ty { - if refty.mutability.is_some() { - Some(&refty.elem) - } else { - None - } - } else { - None - } - } else { - None - } -} - -fn extract_cfgs(attrs: &[Attribute]) -> Vec { - let mut cfgs = vec![]; - - for attr in attrs { - if attr.path().is_ident("cfg") { - cfgs.push(attr.clone()); - } - } - - cfgs -} diff --git a/testsuite/minitest/src/export.rs b/testsuite/minitest/src/export.rs deleted file mode 100644 index 4b04fdade..000000000 --- a/testsuite/minitest/src/export.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::TestOutcome; -use cortex_m_rt as _; - -pub fn check_outcome(outcome: T, should_error: bool) { - if outcome.is_success() == should_error { - let note: &str = if should_error { - "`#[should_error]` " - } else { - "" - }; - panic!("{}test failed with outcome: {:?}", note, outcome); - } -} diff --git a/testsuite/minitest/src/lib.rs b/testsuite/minitest/src/lib.rs deleted file mode 100644 index 8aeeeb6ce..000000000 --- a/testsuite/minitest/src/lib.rs +++ /dev/null @@ -1,59 +0,0 @@ -#![no_std] - -use core::fmt::Debug; -pub use minitest_macros::tests; - -/// Private implementation details used by the proc macro. -#[doc(hidden)] -pub mod export; - -mod sealed { - pub trait Sealed {} - impl Sealed for () {} - impl Sealed for Result {} -} - -/// Indicates whether a test succeeded or failed. -/// -/// This is comparable to the `Termination` trait in libstd, except stable and tailored towards the -/// needs of defmt-test. It is implemented for `()`, which always indicates success, and `Result`, -/// where `Ok` indicates success. -pub trait TestOutcome: Debug + sealed::Sealed { - fn is_success(&self) -> bool; -} - -impl TestOutcome for () { - fn is_success(&self) -> bool { - true - } -} - -impl TestOutcome for Result { - fn is_success(&self) -> bool { - self.is_ok() - } -} - -#[macro_export] -macro_rules! log { - ($s:literal $(, $x:expr)* $(,)?) => { - { - #[cfg(not(feature = "rtt"))] - ::cortex_m_semihosting::hprintln!($s $(, $x)*); - #[cfg(feature = "rtt")] - ::rtt_target::rprintln!($s $(, $x)*); - } - }; -} - -/// Stop all tests without failure. -pub fn exit() -> ! { - cortex_m_semihosting::debug::exit(cortex_m_semihosting::debug::EXIT_SUCCESS); - unreachable!() -} - -/// Stop all tests and report a failure. -pub fn fail() -> ! { - cortex_m_semihosting::debug::exit(cortex_m_semihosting::debug::EXIT_FAILURE); - unreachable!() -} diff --git a/testsuite/src/lib.rs b/testsuite/src/lib.rs new file mode 100644 index 000000000..27deafa55 --- /dev/null +++ b/testsuite/src/lib.rs @@ -0,0 +1,208 @@ +#![no_main] +#![no_std] + +use core::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "hardware")] +use defmt_rtt as _; +#[cfg(feature = "qemu")] +use defmt_semihosting as _; + +#[cfg(all(not(feature = "hardware"), not(feature = "qemu")))] +compile_error!("Either the `hardware` or `qemu` feature must be enabled"); + +#[cfg(target_env = "")] // appease clippy +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + cortex_m::interrupt::disable(); + defmt::info!("{}", info); + fail() +} + +static PENDSV_FLAG: AtomicBool = AtomicBool::new(false); +static SVCALL_FLAG: AtomicBool = AtomicBool::new(false); +static WANT_FAULT: AtomicBool = AtomicBool::new(false); + +#[cortex_m_rt::exception] +fn PendSV() { + defmt::info!("Hit PendSV!"); + PENDSV_FLAG.store(true, Ordering::SeqCst); +} + +#[cortex_m_rt::exception] +fn SVCall() { + defmt::info!("Handling SWI :)"); + SVCALL_FLAG.store(true, Ordering::SeqCst); +} + +#[cortex_m_rt::exception] +unsafe fn HardFault(frame: &cortex_m_rt::ExceptionFrame) -> ! { + defmt::info!( + "Exception Frame {{ r0: {:08x}, r1: {:08x}, r2: {:08x}, r3: {:08x}, r12: {:08x}, lr: {:08x}, pc: {:08x}, xpsr: {:08x} }}", + frame.r0(), + frame.r1(), + frame.r2(), + frame.r3(), + frame.r12(), + frame.lr(), + frame.pc(), + frame.xpsr() + ); + if WANT_FAULT.load(Ordering::Relaxed) { + defmt::info!("Trapped breakpoint OK!"); + exit() + } else { + fail() + } +} + +#[defmt_test::tests] +#[cfg(test)] +mod tests { + const STACK_SIZE_WORDS: usize = 1024; + static STACK: cortex_m::psp::Stack = cortex_m::psp::Stack::new(); + + use crate::{Ordering, PENDSV_FLAG}; + use defmt::info; + + #[init] + fn init() -> cortex_m::Peripherals { + info!("Hello world!"); + cortex_m::Peripherals::take().unwrap() + } + + #[test] + fn double_take() { + assert!(cortex_m::Peripherals::take().is_none()); + } + + #[test] + #[cfg(feature = "hardware")] // QEMU does not model the cycle counter + fn cycle_count(p: &mut cortex_m::Peripherals) { + #[cfg(not(armv6m))] + { + use cortex_m::peripheral::DWT; + + assert!(DWT::has_cycle_counter()); + + p.DCB.enable_trace(); + p.DWT.disable_cycle_counter(); + + const TEST_COUNT: u32 = 0x5555_AAAA; + p.DWT.set_cycle_count(TEST_COUNT); + assert_eq!(DWT::cycle_count(), TEST_COUNT); + + p.DWT.enable_cycle_counter(); + assert!(DWT::cycle_count() > TEST_COUNT); + } + + #[cfg(armv6m)] + { + assert!(!p.DWT.has_cycle_counter()); + } + } + + #[test] + fn critical_section_nesting() { + PENDSV_FLAG.store(false, Ordering::SeqCst); + critical_section::with(|_| { + critical_section::with(|_| { + cortex_m::peripheral::SCB::set_pendsv(); + assert!(!PENDSV_FLAG.load(Ordering::SeqCst)); + }); + assert!(!PENDSV_FLAG.load(Ordering::SeqCst)); + }); + assert!(PENDSV_FLAG.load(Ordering::SeqCst)); + } + + #[test] + fn interrupt_free_nesting() { + PENDSV_FLAG.store(false, Ordering::SeqCst); + cortex_m::interrupt::free(|_| { + cortex_m::interrupt::free(|_| { + cortex_m::peripheral::SCB::set_pendsv(); + assert!(!PENDSV_FLAG.load(Ordering::SeqCst)); + }); + assert!(!PENDSV_FLAG.load(Ordering::SeqCst)); + }); + assert!(PENDSV_FLAG.load(Ordering::SeqCst)); + } + + #[test] + fn check_stack_handles() { + let mut handle = STACK.take_handle(); + let top = handle.top(); + let bottom = handle.bottom(); + let delta = unsafe { top.offset_from(bottom) }; + assert_eq!(delta as usize, STACK_SIZE_WORDS); + } + + #[test] + fn check_asm() { + // Data Memory Barrier - harmless + cortex_m::asm::dmb(); + // Data Sync Barrier - harmless + cortex_m::asm::dsb(); + // Instruction Sync Barrier - harmless + cortex_m::asm::isb(); + // A NOP loop - harmless + cortex_m::asm::delay(100); + // A single NOP - harmless + cortex_m::asm::nop(); + // Set the event flag + cortex_m::asm::sev(); + // Wait for Event (will not block - flag is set) + cortex_m::asm::wfe(); + // Pend an interrupt, the wait for it + cortex_m::peripheral::SCB::set_pendsv(); + cortex_m::interrupt::free(|_| { + cortex_m::peripheral::SCB::set_pendsv(); + // wfi will turn interrupts back on + cortex_m::asm::wfi(); + }); + // Print to the debug console with a semihosting syscall + let msg = c"This is a test\n"; + const SYS_WRITE0: u32 = 0x04; + unsafe { + cortex_m::asm::semihosting_syscall(SYS_WRITE0, msg.as_ptr() as usize as u32); + } + } + + // this test must be last! + #[test] + fn run_psp() { + static STACK: cortex_m::psp::Stack<2048> = cortex_m::psp::Stack::new(); + info!("Switching to PSP..."); + cortex_m::psp::switch_to_unprivileged_psp(STACK.take_handle(), crate::user_fn); + } +} + +/// This code runs on the Process Stack Pointer (i.e. "User mode") +#[cfg(test)] +extern "C" fn user_fn() -> ! { + // should not be set + assert!(!SVCALL_FLAG.load(Ordering::SeqCst)); + // this should fire the SVCall handler + unsafe { + core::arch::asm!("swi 0x00"); + } + // check we hit the SVCall handler + assert!(SVCALL_FLAG.load(Ordering::SeqCst)); + // now test breakpoints, and exit the tests at the same time + // (bkpt will trip the HardFault handler) + crate::WANT_FAULT.store(true, Ordering::Relaxed); + loop { + cortex_m::asm::bkpt(); + } +} + +/// Stop all tests without failure. +pub fn exit() -> ! { + cortex_m_semihosting::debug::exit(cortex_m_semihosting::debug::EXIT_SUCCESS); + unreachable!() +} + +/// Stop all tests and report a failure. +pub fn fail() -> ! { + cortex_m_semihosting::debug::exit(cortex_m_semihosting::debug::EXIT_FAILURE); + unreachable!() +} diff --git a/testsuite/src/main.rs b/testsuite/src/main.rs index 259c2f3af..bae7f29d2 100644 --- a/testsuite/src/main.rs +++ b/testsuite/src/main.rs @@ -1,179 +1,24 @@ -#![no_main] #![no_std] +#![no_main] -extern crate cortex_m_rt; -use core::sync::atomic::{AtomicBool, Ordering}; - -#[cfg(target_env = "")] // appease clippy -#[panic_handler] -fn panic(info: &core::panic::PanicInfo) -> ! { - cortex_m::interrupt::disable(); - minitest::log!("{}", info); - minitest::fail() -} - -static PENDSV_FLAG: AtomicBool = AtomicBool::new(false); -static SVCALL_FLAG: AtomicBool = AtomicBool::new(false); -static WANT_FAULT: AtomicBool = AtomicBool::new(false); - -const STACK_SIZE_WORDS: usize = 1024; - -static STACK: cortex_m::psp::Stack = cortex_m::psp::Stack::new(); - -#[cortex_m_rt::exception] -fn PendSV() { - minitest::log!("Hit PendSV!"); - PENDSV_FLAG.store(true, Ordering::SeqCst); -} - -#[cortex_m_rt::exception] -fn SVCall() { - minitest::log!("Handling SWI :)"); - SVCALL_FLAG.store(true, Ordering::SeqCst); -} - -#[cortex_m_rt::exception] -unsafe fn HardFault(frame: &cortex_m_rt::ExceptionFrame) -> ! { - minitest::log!("{:?}", frame); - if WANT_FAULT.load(Ordering::Relaxed) { - minitest::log!("Trapped breakpoint OK!"); - minitest::exit() - } else { - minitest::fail() - } -} - -#[minitest::tests] -mod tests { - use crate::{Ordering, PENDSV_FLAG}; - use minitest::log; - - #[init] - fn init() -> cortex_m::Peripherals { - log!("Hello world!"); - cortex_m::Peripherals::take().unwrap() - } - - #[test] - fn double_take() { - assert!(cortex_m::Peripherals::take().is_none()); - } - - #[test] - #[cfg(feature = "rtt")] // QEMU does not model the cycle counter - fn cycle_count(p: &mut cortex_m::Peripherals) { - #[cfg(not(armv6m))] - { - use cortex_m::peripheral::DWT; - - assert!(p.DWT.has_cycle_counter()); - - p.DCB.enable_trace(); - p.DWT.disable_cycle_counter(); - - const TEST_COUNT: u32 = 0x5555_AAAA; - p.DWT.set_cycle_count(TEST_COUNT); - assert_eq!(DWT::cycle_count(), TEST_COUNT); - - p.DWT.enable_cycle_counter(); - assert!(DWT::cycle_count() > TEST_COUNT); - } - - #[cfg(armv6m)] - { - assert!(!p.DWT.has_cycle_counter()); - } - } - - #[test] - fn critical_section_nesting() { - PENDSV_FLAG.store(false, Ordering::SeqCst); - critical_section::with(|_| { - critical_section::with(|_| { - cortex_m::peripheral::SCB::set_pendsv(); - assert!(!PENDSV_FLAG.load(Ordering::SeqCst)); - }); - assert!(!PENDSV_FLAG.load(Ordering::SeqCst)); - }); - assert!(PENDSV_FLAG.load(Ordering::SeqCst)); - } - - #[test] - fn interrupt_free_nesting() { - PENDSV_FLAG.store(false, Ordering::SeqCst); - cortex_m::interrupt::free(|_| { - cortex_m::interrupt::free(|_| { - cortex_m::peripheral::SCB::set_pendsv(); - assert!(!PENDSV_FLAG.load(Ordering::SeqCst)); - }); - assert!(!PENDSV_FLAG.load(Ordering::SeqCst)); - }); - assert!(PENDSV_FLAG.load(Ordering::SeqCst)); - } - - #[test] - fn check_stack_handles() { - let mut handle = super::STACK.take_handle(); - let top = handle.top(); - let bottom = handle.bottom(); - let delta = unsafe { top.offset_from(bottom) }; - assert_eq!(delta as usize, super::STACK_SIZE_WORDS); - } +use cortex_m_rt::entry; +#[cfg(feature = "hardware")] +use defmt_rtt as _; +#[cfg(feature = "qemu")] +use defmt_semihosting as _; - #[test] - fn check_asm() { - // Data Memory Barrier - harmless - cortex_m::asm::dmb(); - // Data Sync Barrier - harmless - cortex_m::asm::dsb(); - // Instruction Sync Barrier - harmless - cortex_m::asm::isb(); - // A NOP loop - harmless - cortex_m::asm::delay(100); - // A single NOP - harmless +#[entry] +fn main() -> ! { + defmt::warn!("=== Use `cargo test` to run the tests ==="); + loop { cortex_m::asm::nop(); - // Set the event flag - cortex_m::asm::sev(); - // Wait for Event (will not block - flag is set) - cortex_m::asm::wfe(); - // Pend an interrupt, the wait for it - cortex_m::peripheral::SCB::set_pendsv(); - cortex_m::interrupt::free(|_| { - cortex_m::peripheral::SCB::set_pendsv(); - // wfi will turn interrupts back on - cortex_m::asm::wfi(); - }); - // Print to the debug console with a semihosting syscall - let msg = c"This is a test\n"; - const SYS_WRITE0: u32 = 0x04; - unsafe { - cortex_m::asm::semihosting_syscall(SYS_WRITE0, msg.as_ptr() as usize as u32); - } - } - - // this test must be last! - #[test] - fn run_psp() { - static STACK: cortex_m::psp::Stack<4096> = cortex_m::psp::Stack::new(); - minitest::log!("Switching to PSP..."); - cortex_m::psp::switch_to_unprivileged_psp(STACK.take_handle(), crate::user_fn); } } -/// This code runs on the Process Stack Pointer (i.e. "User mode") -extern "C" fn user_fn() -> ! { - // should not be set - assert!(!SVCALL_FLAG.load(Ordering::SeqCst)); - // this should fire the SVCall handler - unsafe { - core::arch::asm!("swi 0x00"); - } - // check we hit the SVCall handler - assert!(SVCALL_FLAG.load(Ordering::SeqCst)); - // now test breakpoints, and exit the tests at the same time - // (bkpt will trip the HardFault handler) - crate::WANT_FAULT.store(true, Ordering::Relaxed); +#[cfg(target_env = "")] // appease clippy +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { loop { - cortex_m::asm::bkpt(); + cortex_m::asm::nop(); } }