aboutsummaryrefslogtreecommitdiff
path: root/testsuite
diff options
context:
space:
mode:
authorGravatar Alex Martens <alex@thinglab.org> 2022-03-27 10:21:28 -0700
committerGravatar Alex Martens <alex@thinglab.org> 2022-03-27 10:21:28 -0700
commitb4181a8a313ef7686da3f64e3947ce96b9788037 (patch)
treee144a319fcd4762ad83586035e968985d44ce42c /testsuite
parentc2e77c5aee4a2c587f4bb3dc4a91a6bd02b322a6 (diff)
downloadcortex-m-b4181a8a313ef7686da3f64e3947ce96b9788037.tar.gz
cortex-m-b4181a8a313ef7686da3f64e3947ce96b9788037.tar.zst
cortex-m-b4181a8a313ef7686da3f64e3947ce96b9788037.zip
Add on-target tests
Diffstat (limited to 'testsuite')
-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
11 files changed, 632 insertions, 0 deletions
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());
+ }
+ }
+}