From 4a5a8564963a72513873da0f636a96097e9d95c7 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 21 Dec 2023 13:18:10 +0100 Subject: [PATCH] Add #[test] macro --- embassy-executor-macros/src/lib.rs | 29 ++++++++++ embassy-executor-macros/src/macros/mod.rs | 1 + embassy-executor-macros/src/macros/test.rs | 66 ++++++++++++++++++++++ embassy-executor/CHANGELOG.md | 1 + embassy-executor/src/arch/std.rs | 16 ++++++ embassy-executor/src/lib.rs | 11 ++++ embassy-executor/src/testutils.rs | 35 ++++++++++++ 7 files changed, 159 insertions(+) create mode 100644 embassy-executor-macros/src/macros/test.rs create mode 100644 embassy-executor/src/testutils.rs diff --git a/embassy-executor-macros/src/lib.rs b/embassy-executor-macros/src/lib.rs index c9d58746..1da8c456 100644 --- a/embassy-executor-macros/src/lib.rs +++ b/embassy-executor-macros/src/lib.rs @@ -62,6 +62,35 @@ pub fn task(args: TokenStream, item: TokenStream) -> TokenStream { task::run(&args.meta, f).unwrap_or_else(|x| x).into() } +/// Declares a unit test which uses the embassy executor to run the test on std. +/// +/// The following restrictions apply: +/// +/// * The function may accept exactly 1 parameter, an `embassy_executor::Spawner` handle that it can use to spawn additional tasks. +/// * The function must be declared `async`. +/// * The function must not use generics. +/// * The function must not return anything +/// +/// ## Examples +/// Creating a testcase: +/// +/// ``` rust +///#[cfg(test)] +/// mod tests +/// { +//// #[embassy_executor::test] +/// async fn test1() { +/// // Test case body +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn test(_args: TokenStream, item: TokenStream) -> TokenStream { + let f = syn::parse_macro_input!(item as syn::ItemFn); + + test::run(f).unwrap_or_else(|x| x).into() +} + /// Creates a new `executor` instance and declares an application entry point for Cortex-M spawning the corresponding function body as an async task. /// /// The following restrictions apply: diff --git a/embassy-executor-macros/src/macros/mod.rs b/embassy-executor-macros/src/macros/mod.rs index 572094ca..ee718e30 100644 --- a/embassy-executor-macros/src/macros/mod.rs +++ b/embassy-executor-macros/src/macros/mod.rs @@ -1,2 +1,3 @@ pub mod main; pub mod task; +pub mod test; diff --git a/embassy-executor-macros/src/macros/test.rs b/embassy-executor-macros/src/macros/test.rs new file mode 100644 index 00000000..81fbff1b --- /dev/null +++ b/embassy-executor-macros/src/macros/test.rs @@ -0,0 +1,66 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{self, ReturnType}; + +use crate::util::ctxt::Ctxt; + +pub fn run(f: syn::ItemFn) -> Result { + let ctxt = Ctxt::new(); + + if f.sig.asyncness.is_none() { + ctxt.error_spanned_by(&f.sig, "test functions must be async"); + } + if !f.sig.generics.params.is_empty() { + ctxt.error_spanned_by(&f.sig, "test functions must not be generic"); + } + + let args = f.sig.inputs.clone(); + + if args.len() > 1 { + ctxt.error_spanned_by(f.sig.inputs, "test function must take zero or one (spawner) arguments"); + } + + if f.sig.output != ReturnType::Default { + ctxt.error_spanned_by(&f.sig.output, "test functions must not return a value"); + } + + ctxt.check()?; + + let test_name = f.sig.ident; + let task_fn_body = f.block; + let embassy_test_name = format_ident!("__embassy_test_{}", test_name); + let embassy_test_launcher = format_ident!("__embassy_test_launcher_{}", test_name); + + let invocation = if args.len() == 1 { + quote! { #embassy_test_name(spawner).await } + } else { + quote! { #embassy_test_name().await } + }; + + let result = quote! { + + #[::embassy_executor::task] + async fn #embassy_test_launcher(spawner: ::embassy_executor::Spawner, runner: &'static mut ::embassy_executor::_export_testutils::TestRunner) { + #invocation; + runner.done(); + } + + async fn #embassy_test_name(#args) { + #task_fn_body + } + + #[test] + fn #test_name() { + let r = ::embassy_executor::_export_testutils::TestRunner::default(); + + let r1: &'static mut ::embassy_executor::_export_testutils::TestRunner = unsafe { core::mem::transmute(&r) }; + + r1.initialize(|spawner| { + let r2: &'static mut ::embassy_executor::_export_testutils::TestRunner = unsafe { core::mem::transmute(&r) }; + spawner.spawn(#embassy_test_launcher(spawner, r2)).unwrap(); + }); + r1.run_until_done(); + } + }; + Ok(result.into()) +} diff --git a/embassy-executor/CHANGELOG.md b/embassy-executor/CHANGELOG.md index 5c674923..1878149c 100644 --- a/embassy-executor/CHANGELOG.md +++ b/embassy-executor/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +- Added #[test] macro for std. ## 0.4.0 - 2023-12-05 diff --git a/embassy-executor/src/arch/std.rs b/embassy-executor/src/arch/std.rs index b02b1598..dde07170 100644 --- a/embassy-executor/src/arch/std.rs +++ b/embassy-executor/src/arch/std.rs @@ -62,6 +62,22 @@ mod thread { self.signaler.wait() } } + + /// Returns the spawner of the executor + pub(crate) fn spawner(&'static self) -> Spawner { + self.inner.spawner() + } + + /// Run the executor until the closure returns true. + pub(crate) fn run_until(&'static self, f: impl Fn() -> bool) { + loop { + unsafe { self.inner.poll() }; + if f() { + break; + } + self.signaler.wait(); + } + } } struct Signaler { diff --git a/embassy-executor/src/lib.rs b/embassy-executor/src/lib.rs index 4c6900a6..09f0d9ef 100644 --- a/embassy-executor/src/lib.rs +++ b/embassy-executor/src/lib.rs @@ -7,6 +7,8 @@ pub(crate) mod fmt; pub use embassy_executor_macros::task; +#[cfg(feature = "arch-std")] +pub use embassy_executor_macros::test; macro_rules! check_at_most_one { (@amo [$($feats:literal)*] [] [$($res:tt)*]) => { @@ -38,6 +40,15 @@ pub mod raw; mod spawner; pub use spawner::*; +#[cfg(feature = "arch-std")] +mod testutils; + +#[cfg(feature = "arch-std")] +#[doc(hidden)] +pub mod _export_testutils { + pub use crate::testutils::*; +} + mod config { #![allow(unused)] include!(concat!(env!("OUT_DIR"), "/config.rs")); diff --git a/embassy-executor/src/testutils.rs b/embassy-executor/src/testutils.rs new file mode 100644 index 00000000..5f98bbf8 --- /dev/null +++ b/embassy-executor/src/testutils.rs @@ -0,0 +1,35 @@ +use core::sync::atomic::{AtomicBool, Ordering}; + +use crate::{Executor, Spawner}; + +/// Test runner that will be used by the #[test] macro (only supported for the `arch-std`) +pub struct TestRunner { + inner: Executor, + done: AtomicBool, +} + +impl Default for TestRunner { + fn default() -> Self { + Self { + inner: Executor::new(), + done: AtomicBool::new(false), + } + } +} + +impl TestRunner { + /// Call the closure with a spawner that can be used to spawn tasks. + pub fn initialize(&'static self, init: impl FnOnce(Spawner)) { + init(self.inner.spawner()); + } + + /// Run the executor until the test is done + pub fn run_until_done(&'static self) { + self.inner.run_until(|| self.done.load(Ordering::SeqCst)); + } + + /// Mark the test as done + pub fn done(&'static self) { + self.done.store(true, Ordering::SeqCst); + } +}