diff options
author | 2022-04-04 05:59:34 +0000 | |
---|---|---|
committer | 2022-04-04 05:59:34 +0000 | |
commit | b9e18327ea5e620c9205f3d90627eb6504a0a872 (patch) | |
tree | 7a5c98d8a9df87f8a79072f1bb1a48061b8efcbd | |
parent | b4eaadcfa5c2600b0337e3a6f5e887a078523c49 (diff) | |
parent | b4181a8a313ef7686da3f64e3947ce96b9788037 (diff) | |
download | cortex-m-b9e18327ea5e620c9205f3d90627eb6504a0a872.tar.gz cortex-m-b9e18327ea5e620c9205f3d90627eb6504a0a872.tar.zst cortex-m-b9e18327ea5e620c9205f3d90627eb6504a0a872.zip |
Merge #355
355: Add on-target tests to CI r=thejpster a=newAM
A week ago in the rust-embedded matrix chat `@adamgreig` mentioned that on-target CI would be helpful for the `cortex-m` and `cortex-m-rt` crates. This is a pull-request to make that happen.
## History: Bootstrapping and `cortex-m-rt`
The first problem I came across was the amount of boilerplate required to build an on-target binary without `cortex-m-rt`.
There are two paths forward here:
1. Introduce a lot of boilerplate in `cortex-m`, which will largely be copy-pasted from `cortex-m-rt`.
2. Relocate `cortex-m-rt` to this workspace.
In (#391) `cortex-m-rt` was relocated to this workspace to fix the bootstrapping problem.
## Test Execution
Tests run with QEMU and physical hardware.
QEMU uses the LM3S6965 Cortex-M3 target because it is widely used.
The physical hardware is a NUCLEO-F070RB connected to a self-hosted runner. In the future more hardware can be added to cover more CPUs.
Due to reliability concerns with self-hosted runners only QEMU will be a required status check in CI, the physical hardware checks will be informational only.
### CI Software
The CI software for running on physical hardware is simply a self-hosted github actions runner. A working demonstration of this can be found [here](https://github.com/newAM/totally-not-a-cortex-m-fork/runs/5345451343?check_suite_focus=true) (the repository does not appear as a fork to work around github actions limitations with forks).
The runner uses [`probe-run`] to execute the tests on embedded hardware. [`probe-run`] was chosen for several reasons:
* Actively maintained by [knurling-rs], with an actively maintained back-end from [`probe-rs`].
* Written in rust. Understanding the code does not require contributors to learn a new language.
* Designed with on-target testing as a primary goal (for use with [`defmt-test`]).
* Automatic unwinding and backtrace display.
## Test Harness
This PR introduces a test harness, `minitest`.
`minitest` is almost identical to [`defmt-test`], the only difference is that it replaces [`defmt`] with [`rtt-target`] because [`defmt`] introduces a dependency cycle on `cortex-m`.
This is harness is very minimal, adding only 327 lines of rust:
```console
$ tokei testsuite/minitest
===============================================================================
Language Files Lines Code Comments Blanks
===============================================================================
Markdown 1 7 0 4 3
TOML 2 41 34 0 7
-------------------------------------------------------------------------------
Rust 3 406 350 5 51
|- Markdown 1 8 0 7 1
(Total) 414 350 12 52
===============================================================================
Total 6 454 384 9 61
===============================================================================
```
The test harness does introduce some abstraction, and may not be suitable for all tests. Lower-level tests are still possible without the harness using `asm::udf` to fail the test, and `asm::bkpt` to exit without failure.
## Reliability and Uptime
I have been doing automatic on-target testing for the [`stm32wlxx-hal`] using [`probe-run`]. Over hundreds of automatic runs spanning several months I have had no failures as a result of external factors (USB connectivity, programming errors, ect.). I do not anticipate on-target CI being perfect, but at the same time I do not anticipate frequent problems.
[`defmt-test`]: https://github.com/knurling-rs/defmt/tree/main/firmware/defmt-test
[`defmt`]: https://ferrous-systems.com/blog/defmt/
[`probe-rs`]: https://probe.rs/
[`probe-run`]: https://github.com/knurling-rs/probe-run
[`rtt-target`]: https://crates.io/crates/rtt-target
[`stm32wlxx-hal`]: https://github.com/stm32-rs/stm32wlxx-hal
[knurling-rs]: https://knurling.ferrous-systems.com/
Co-authored-by: Alex Martens <alex@thinglab.org>
-rw-r--r-- | .github/bors.toml | 2 | ||||
-rw-r--r-- | .github/workflows/ci.yml | 2 | ||||
-rw-r--r-- | .github/workflows/on-target.yml | 83 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | src/peripheral/dwt.rs | 12 | ||||
-rw-r--r-- | testsuite/.cargo/config.toml | 6 | ||||
-rw-r--r-- | testsuite/Cargo.toml | 23 | ||||
-rw-r--r-- | testsuite/README.md | 69 | ||||
-rw-r--r-- | testsuite/build.rs | 18 | ||||
-rw-r--r-- | testsuite/minitest/Cargo.toml | 23 | ||||
-rw-r--r-- | testsuite/minitest/README.md | 7 | ||||
-rw-r--r-- | testsuite/minitest/macros/Cargo.toml | 18 | ||||
-rw-r--r-- | testsuite/minitest/macros/src/lib.rs | 331 | ||||
-rw-r--r-- | testsuite/minitest/src/export.rs | 13 | ||||
-rw-r--r-- | testsuite/minitest/src/lib.rs | 70 | ||||
-rw-r--r-- | testsuite/src/main.rs | 54 |
16 files changed, 735 insertions, 3 deletions
diff --git a/.github/bors.toml b/.github/bors.toml index 218ec03..17cef85 100644 --- a/.github/bors.toml +++ b/.github/bors.toml @@ -8,6 +8,8 @@ status = [ "rt-ci-linux (1.59.0)", "rt-ci-other-os (macOS-latest)", "rt-ci-other-os (windows-latest)", + "hil-qemu", + "hil-compile-rtt", "rustfmt", "clippy", ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 701e46a..14a917d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,6 @@ jobs: toolchain: ${{ matrix.rust }} override: true - name: Run tests - run: cargo test --all --exclude cortex-m-rt + run: cargo test --all --exclude cortex-m-rt --exclude testsuite # FIXME: test on macOS and Windows diff --git a/.github/workflows/on-target.yml b/.github/workflows/on-target.yml new file mode 100644 index 0000000..e880796 --- /dev/null +++ b/.github/workflows/on-target.yml @@ -0,0 +1,83 @@ +on: + push: + branches: [ staging, trying, master ] + pull_request: + # allows manual triggering + workflow_dispatch: + +name: cortex-m on-target tests + +jobs: + + hil-qemu: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + target: thumbv7m-none-eabi + - name: Build testsuite + env: + RUSTFLAGS: -C link-arg=-Tlink.x -D warnings + run: cargo build -p testsuite --target thumbv7m-none-eabi --features testsuite/semihosting + - name: Install QEMU + run: sudo apt-get update && sudo apt-get install qemu qemu-system-arm + - name: Run testsuite + run: | + qemu-system-arm \ + -cpu cortex-m3 \ + -machine lm3s6965evb \ + -nographic \ + -semihosting-config enable=on,target=native \ + -kernel target/thumbv7m-none-eabi/debug/testsuite + + hil-compile-rtt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + target: thumbv6m-none-eabi + - name: Modify linkerfile + run: | + sed -i 's/FLASH : ORIGIN = 0x00000000, LENGTH = 256K/FLASH : ORIGIN = 0x8000000, LENGTH = 128K/g' memory.x + sed -i 's/RAM : ORIGIN = 0x20000000, LENGTH = 64K/RAM : ORIGIN = 0x20000000, LENGTH = 16K/g' memory.x + - name: Build testsuite + env: + RUSTFLAGS: -C link-arg=-Tlink.x -D warnings + run: cargo build -p testsuite --target thumbv6m-none-eabi --features testsuite/rtt + - name: Upload testsuite binaries + uses: actions/upload-artifact@v2 + with: + name: testsuite-bin + if-no-files-found: error + retention-days: 1 + path: target/thumbv6m-none-eabi/debug/testsuite + + hil-stm32: + runs-on: self-hosted + needs: + - hil-compile-rtt + steps: + - uses: actions/checkout@v2 + - name: Display probe-run version + run: probe-run --version + - name: List probes + run: probe-run --list-probes + - uses: actions/download-artifact@v2 + with: + name: testsuite-bin + path: testsuite-bin + - name: Run on-target tests + timeout-minutes: 5 + run: | + probe-run \ + --chip STM32F070RBTx \ + --connect-under-reset \ + testsuite-bin/testsuite @@ -35,11 +35,14 @@ std = [] [workspace] members = [ - "xtask", "cortex-m-rt", "cortex-m-semihosting", + "panic-itm", "panic-semihosting", - "panic-itm" + "testsuite", + "testsuite/minitest", + "testsuite/minitest/macros", + "xtask", ] [package.metadata.docs.rs] diff --git a/src/peripheral/dwt.rs b/src/peripheral/dwt.rs index c5f7bc9..72575d3 100644 --- a/src/peripheral/dwt.rs +++ b/src/peripheral/dwt.rs @@ -155,6 +155,18 @@ impl DWT { } } + /// Disables the cycle counter + #[cfg(not(armv6m))] + #[inline] + pub fn disable_cycle_counter(&mut self) { + unsafe { + self.ctrl.modify(|mut r| { + r.set_cyccntena(false); + r + }); + } + } + /// Returns `true` if the cycle counter is enabled #[cfg(not(armv6m))] #[inline] diff --git a/testsuite/.cargo/config.toml b/testsuite/.cargo/config.toml new file mode 100644 index 0000000..cce98a9 --- /dev/null +++ b/testsuite/.cargo/config.toml @@ -0,0 +1,6 @@ +[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" + +[build] +target = "thumbv7m-none-eabi" diff --git a/testsuite/Cargo.toml b/testsuite/Cargo.toml new file mode 100644 index 0000000..17f1562 --- /dev/null +++ b/testsuite/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = ["The Cortex-M Team <cortex-m@teams.rust-embedded.org>"] +name = "testsuite" +publish = false +edition = "2018" +version = "0.1.0" + +[features] +rtt = ["rtt-target", "minitest/rtt"] +semihosting = ["cortex-m-semihosting", "minitest/semihosting"] + +[dependencies] +cortex-m-rt.path = "../cortex-m-rt" +cortex-m.path = ".." +minitest.path = "minitest" + +[dependencies.rtt-target] +version = "0.3.1" +optional = true + +[dependencies.cortex-m-semihosting] +path = "../cortex-m-semihosting" +optional = true diff --git a/testsuite/README.md b/testsuite/README.md new file mode 100644 index 0000000..c11d850 --- /dev/null +++ b/testsuite/README.md @@ -0,0 +1,69 @@ +# Testsuite + +This workspace contains tests that run on physical and simulated Cortex-M CPUs. + +## Building + +Exactly one of these features are required: + +* `semihosting` Use semihosting for logging, this is used for QEMU. +* `rtt` Use RTT for logging, this is used with physical cortex-m CPUs. + +Assuming you are at the root of the repository you can build like this: + +```console +$ cd testsuite +$ cargo build --features semihosting + Compiling testsuite v0.1.0 (cortex-m/testsuite) + Finished dev [unoptimized + debuginfo] target(s) in 0.08 +``` + +## 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]. + +```console +$ cd testsuite +$ cargo run --features semihosting + 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! +``` + +## Running with Physical Hardware + +No implementation-specific features are tested right now; any physical `thumbv7m` target should work. + +Tests are executed with [probe-run](https://github.com/knurling-rs/probe-run). + +* Update `memory.x` in the root of the repository to match your target memory layout. +* Change the `probe-run` chip argument to match your chip, supported chips can be found with `probe-run --list-chips` +* 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-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 +``` + +[The Embedded Rust Book]: https://docs.rust-embedded.org/book/start/qemu.html +[probe-run]: https://github.com/knurling-rs/probe-run diff --git a/testsuite/build.rs b/testsuite/build.rs new file mode 100644 index 0000000..c0662b9 --- /dev/null +++ b/testsuite/build.rs @@ -0,0 +1,18 @@ +fn main() { + let target = std::env::var("TARGET").unwrap(); + + if target.starts_with("thumbv6m-") { + println!("cargo:rustc-cfg=armv6m"); + } else if target.starts_with("thumbv7m-") { + println!("cargo:rustc-cfg=armv7m"); + } else if target.starts_with("thumbv7em-") { + println!("cargo:rustc-cfg=armv7m"); + println!("cargo:rustc-cfg=armv7em"); // (not currently used) + } else if target.starts_with("thumbv8m.base") { + println!("cargo:rustc-cfg=armv8m"); + println!("cargo:rustc-cfg=armv8m_base"); + } else if target.starts_with("thumbv8m.main") { + println!("cargo:rustc-cfg=armv8m"); + println!("cargo:rustc-cfg=armv8m_main"); + } +} diff --git a/testsuite/minitest/Cargo.toml b/testsuite/minitest/Cargo.toml new file mode 100644 index 0000000..bf2c2eb --- /dev/null +++ b/testsuite/minitest/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = ["The Cortex-M Team <cortex-m@teams.rust-embedded.org>"] +name = "minitest" +publish = false +edition = "2018" +version = "0.1.0" + +[features] +semihosting = ["cortex-m-semihosting", "minitest-macros/semihosting"] +rtt = ["rtt-target", "minitest-macros/rtt"] + +[dependencies] +cortex-m.path = "../.." +cortex-m-rt.path = "../../cortex-m-rt" +minitest-macros.path = "macros" + +[dependencies.rtt-target] +version = "0.3.1" +optional = true + +[dependencies.cortex-m-semihosting] +path = "../../cortex-m-semihosting" +optional = true diff --git a/testsuite/minitest/README.md b/testsuite/minitest/README.md new file mode 100644 index 0000000..0a456a8 --- /dev/null +++ b/testsuite/minitest/README.md @@ -0,0 +1,7 @@ +# 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 new file mode 100644 index 0000000..077e316 --- /dev/null +++ b/testsuite/minitest/macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +authors = ["The Cortex-M Team <cortex-m@teams.rust-embedded.org>"] +name = "minitest-macros" +publish = false +edition = "2018" +version = "0.1.0" + +[lib] +proc-macro = true + +[features] +semihosting = [] +rtt = [] + +[dependencies] +proc-macro2 = "1.0.29" +quote = "1.0.10" +syn = { version = "1.0.80", features = ["extra-traits", "full"] } diff --git a/testsuite/minitest/macros/src/lib.rs b/testsuite/minitest/macros/src/lib.rs new file mode 100644 index 0000000..6570502 --- /dev/null +++ b/testsuite/minitest/macros/src/lib.rs @@ -0,0 +1,331 @@ +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<TokenStream> { + 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!({ + let channels = ::rtt_target::rtt_init! { + up: { + 0: { + size: 256 + mode: BlockIfFull + name: "minitest" + } + } + }; + unsafe { + ::rtt_target::set_print_channel_cs( + channels.up.0, + &((|arg, f| cortex_m::interrupt::free(|_| f(arg))) + as rtt_target::CriticalSectionFunc), + ); + } + }); + + #[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::<Vec<_>>(); + 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<Box<Type>>, +} + +struct Test { + func: ItemFn, + cfgs: Vec<Attribute>, + input: Option<Input>, + 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<Attribute> { + 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 new file mode 100644 index 0000000..4b04fda --- /dev/null +++ b/testsuite/minitest/src/export.rs @@ -0,0 +1,13 @@ +use crate::TestOutcome; +use cortex_m_rt as _; + +pub fn check_outcome<T: TestOutcome>(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 new file mode 100644 index 0000000..d98fb64 --- /dev/null +++ b/testsuite/minitest/src/lib.rs @@ -0,0 +1,70 @@ +#![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<T, E> Sealed for Result<T, E> {} +} + +/// 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<T: Debug, E: Debug> TestOutcome for Result<T, E> { + fn is_success(&self) -> bool { + self.is_ok() + } +} + +#[macro_export] +macro_rules! log { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "semihosting")] + ::cortex_m_semihosting::hprintln!($s $(, $x)*); + #[cfg(feature = "rtt")] + ::rtt_target::rprintln!($s $(, $x)*); + #[cfg(not(any(feature = "semihosting", feature="rtt")))] + let _ = ($( & $x ),*); + } + }; +} + +/// Stop all tests without failure. +pub fn exit() -> ! { + #[cfg(feature = "rtt")] + cortex_m::asm::bkpt(); + #[cfg(feature = "semihosting")] + cortex_m_semihosting::debug::exit(cortex_m_semihosting::debug::EXIT_SUCCESS); + + unreachable!() +} + +/// Stop all tests and report a failure. +pub fn fail() -> ! { + #[cfg(feature = "rtt")] + cortex_m::asm::udf(); + #[cfg(feature = "semihosting")] + cortex_m_semihosting::debug::exit(cortex_m_semihosting::debug::EXIT_FAILURE); + + #[cfg(not(feature = "rtt"))] + unreachable!() +} diff --git a/testsuite/src/main.rs b/testsuite/src/main.rs new file mode 100644 index 0000000..46ab629 --- /dev/null +++ b/testsuite/src/main.rs @@ -0,0 +1,54 @@ +#![no_main] +#![no_std] + +extern crate cortex_m_rt; + +#[cfg(target_env = "")] // appease clippy +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + cortex_m::interrupt::disable(); + minitest::log!("{}", info); + minitest::fail() +} + +#[minitest::tests] +mod tests { + 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(not(feature = "semihosting"))] // 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()); + } + } +} |