Add #[test] macro

This commit is contained in:
Timo 2023-12-21 13:18:10 +01:00
parent 530ead5fde
commit 4a5a856496
7 changed files with 159 additions and 0 deletions

View File

@ -62,6 +62,35 @@ pub fn task(args: TokenStream, item: TokenStream) -> TokenStream {
task::run(&args.meta, f).unwrap_or_else(|x| x).into() 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. /// 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: /// The following restrictions apply:

View File

@ -1,2 +1,3 @@
pub mod main; pub mod main;
pub mod task; pub mod task;
pub mod test;

View File

@ -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<TokenStream, TokenStream> {
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())
}

View File

@ -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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## Unreleased
- Added #[test] macro for std.
## 0.4.0 - 2023-12-05 ## 0.4.0 - 2023-12-05

View File

@ -62,6 +62,22 @@ mod thread {
self.signaler.wait() 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 { struct Signaler {

View File

@ -7,6 +7,8 @@
pub(crate) mod fmt; pub(crate) mod fmt;
pub use embassy_executor_macros::task; pub use embassy_executor_macros::task;
#[cfg(feature = "arch-std")]
pub use embassy_executor_macros::test;
macro_rules! check_at_most_one { macro_rules! check_at_most_one {
(@amo [$($feats:literal)*] [] [$($res:tt)*]) => { (@amo [$($feats:literal)*] [] [$($res:tt)*]) => {
@ -38,6 +40,15 @@ pub mod raw;
mod spawner; mod spawner;
pub use 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 { mod config {
#![allow(unused)] #![allow(unused)]
include!(concat!(env!("OUT_DIR"), "/config.rs")); include!(concat!(env!("OUT_DIR"), "/config.rs"));

View File

@ -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);
}
}