aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar bors[bot] <26634292+bors[bot]@users.noreply.github.com> 2022-04-04 05:59:34 +0000
committerGravatar GitHub <noreply@github.com> 2022-04-04 05:59:34 +0000
commitb9e18327ea5e620c9205f3d90627eb6504a0a872 (patch)
tree7a5c98d8a9df87f8a79072f1bb1a48061b8efcbd
parentb4eaadcfa5c2600b0337e3a6f5e887a078523c49 (diff)
parentb4181a8a313ef7686da3f64e3947ce96b9788037 (diff)
downloadcortex-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.toml2
-rw-r--r--.github/workflows/ci.yml2
-rw-r--r--.github/workflows/on-target.yml83
-rw-r--r--Cargo.toml7
-rw-r--r--src/peripheral/dwt.rs12
-rw-r--r--testsuite/.cargo/config.toml6
-rw-r--r--testsuite/Cargo.toml23
-rw-r--r--testsuite/README.md69
-rw-r--r--testsuite/build.rs18
-rw-r--r--testsuite/minitest/Cargo.toml23
-rw-r--r--testsuite/minitest/README.md7
-rw-r--r--testsuite/minitest/macros/Cargo.toml18
-rw-r--r--testsuite/minitest/macros/src/lib.rs331
-rw-r--r--testsuite/minitest/src/export.rs13
-rw-r--r--testsuite/minitest/src/lib.rs70
-rw-r--r--testsuite/src/main.rs54
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
diff --git a/Cargo.toml b/Cargo.toml
index 914bd62..b4f23c0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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());
+ }
+ }
+}