blob: e24b0c1e4a5edd139c4344d330bbf6dc439c2b8a [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::{Context as _, Error},
fuchsia_async as fasync,
futures::future::BoxFuture,
futures::{future, select, stream::TryStreamExt, Future, FutureExt},
log::{error, info},
pin_utils::pin_mut,
};
/// A `TestHarness` is a type that provides an interface to test cases for interacting with
/// functionality under test. For example, a WidgetHarness might provide controls for interacting
/// with and meausuring a Widget, allowing us to easily write tests for Widget functionality.
///
/// A `TestHarness` defines how to initialize (via `init()`) the harness resources and how to
/// terminate (via `terminate()`) them when done. The `init()` function can also provide some
/// environment resources (`env`) to be held for the test duration, and also a task (`runner`) that
/// can be executed to perform asynchronous behavior necessary for the test harness to function
/// correctly.
pub trait TestHarness: Sized {
/// The type of environment needed to be held whilst the test runs. This is normally used to
/// keep resources open during the test, and allow a graceful shutdown in `terminate`.
type Env: Send + 'static;
/// A future that models any background computation the harness needs to process. If no
/// processing is needed, implementations should use `future::Pending` to model a future that
/// never returns Poll::Ready
type Runner: Future<Output = Result<(), Error>> + Unpin + Send + 'static;
/// Initialize a TestHarness, creating the harness itself, any hidden environment, and a runner
/// task to execute background behavior
fn init() -> BoxFuture<'static, Result<(Self, Self::Env, Self::Runner), Error>>;
/// Terminate the TestHarness. This should clear up any and all resources that were created by
/// init()
fn terminate(env: Self::Env) -> BoxFuture<'static, Result<(), Error>>;
}
/// We can run any test which is an async function from some harness `H` to a result
pub async fn run_with_harness<H, F, Fut>(test_func: F) -> Result<(), Error>
where
H: TestHarness,
F: FnOnce(H) -> Fut + Send + 'static,
Fut: Future<Output = Result<(), Error>> + Send + 'static,
{
let (harness, env, runner) = H::init().await.context("Error initializing harness")?;
let run_test = test_func(harness);
pin_mut!(run_test);
pin_mut!(runner);
let result = select! {
test_result = run_test.fuse() => test_result,
runner_result = runner.fuse() => runner_result.context("Error running harness background task"),
};
let term_result = H::terminate(env).await.context("Error terminating harness");
// Return test failure if it exists, else return terminate failure, else return the
// successful test result
result.and_then(|ok| term_result.map(|_| ok))
}
/// Sets up the test environment and the given test case. Each integration test case is an
/// asynchronous function from some harness `H` to the result of the test run.
pub fn run_test<H, Fut>(
test: impl FnOnce(H) -> Fut + Send + 'static,
name: &str,
) -> Result<(), Error>
where
Fut: Future<Output = Result<(), Error>> + Send + 'static,
H: TestHarness,
{
let mut executor = fasync::Executor::new().context("error creating event loop")?;
info!("[ RUN ] {}...", name);
let result = executor.run_singlethreaded(run_with_harness(test));
if let Err(err) = &result {
error!("[ \x1b[31mFAILED\x1b[0m ] {}: Error running test: {:?}", name, err);
} else {
info!("[ \x1b[32mPASSED\x1b[0m ] {}", name);
}
result.context(format!("Error running test '{}'", name))
}
/// The Unit type can be used as the empty test-harness - it does no initialization and no
/// termination. This is largely useful when using the `run_suite!` macro, which takes a sequence
/// of tests to run with harnesses - if there are tests that don't actually need a harness, then a
/// unit parameter can be passed.
impl TestHarness for () {
type Env = ();
type Runner = future::Pending<Result<(), Error>>;
fn init() -> BoxFuture<'static, Result<(Self, Self::Env, Self::Runner), Error>> {
async { Ok(((), (), future::pending())) }.boxed()
}
fn terminate(_env: Self::Env) -> BoxFuture<'static, Result<(), Error>> {
future::ok(()).boxed()
}
}
// Prints out the test name and runs the test.
#[macro_export]
macro_rules! run_test {
($name:ident) => {{
test_harness::run_test($name, stringify!($name))
}};
}
#[macro_export]
macro_rules! run_suite {
($name:tt, [$($test:ident),+]) => {{
log::info!(">>> Running {} tests:", $name);
{
use fuchsia_bluetooth::util::CollectExt;
let res = vec![$( test_harness::run_test($test, stringify!($test)), )*]
.into_iter()
.collect_results();
anyhow::Context::context(res, format!("Error running suite {}", $name))?;
}
Ok(())
}}
}
/// We can implement TestHarness for any tuples of types that are TestHarnesses - this macro can
/// generate boilerplate implementations for tuples of arities by combining the init() and
/// terminate() functions of the tuple components.
///
/// ----
///
/// To elaborate: This implementation is implied by the nature of _applicative functors_, which are
/// a class of types that support 'tupling' of their type members. In particular, Functions, Tuples,
/// Futures and Results are all applicative, and compositions of applicatives are _also_ applicative
/// by definition. So a `Function that returns a Result of a Tuple` is applicative (e.g. `init()`),
/// as is a `Function that returns a Future of a Result` (such as `terminate()`).
///
/// A TestHarness impl itself is really equivalent to a tuple of two functions - init() and
/// terminate(). Again, by the composition of applicative functors, this itself is applicative, and
/// therefore a tuple of TestHarness impls can be turned into an TestHarness impl of a tuple. Rustc
/// isn't able to derive this automatically for us so we write this macro here to do the heavy
/// lifting.
///
/// (Further caveat: The tupling of terminate() is technically slightly more complex as it's a
/// function indexed by a type parameter (Self::Env), but it still shakes out much the same)
macro_rules! generate_tuples {
($(
($($Harness:ident),*),
)*) => ($(
// The impl below re-uses type names as identifiers, so we allow non_snake_case to
// suppress warnings over using 'A' instead of 'a', etc.
#[allow(non_snake_case)]
impl<$($Harness: TestHarness + Send),*> TestHarness for ($($Harness),*) {
type Env = ($($Harness::Env),*);
type Runner = BoxFuture<'static, Result<(), Error>>;
fn init() -> BoxFuture<'static, Result<(Self, Self::Env, Self::Runner), Error>> {
async {
$(
let $Harness = $Harness::init().await?;
)*
// Create a stream of the result of each future
let runners = futures::stream::select_all(
vec![
$( $Harness.2.boxed().into_stream()),*
]
);
// Use try_collect to return first error or continue to completion
let runner = runners.try_collect::<()>().boxed();
let harness = ($($Harness.0),*);
let env = ($($Harness.1),*);
Ok((harness, env, runner))
}
.boxed()
}
fn terminate(environment: Self::Env) -> BoxFuture<'static, Result<(), Error>> {
let ($($Harness),*) = environment;
async {
$(
let $Harness = $Harness::terminate($Harness).await;
)*
let done = Ok(());
$(
let done = done.and($Harness);
)*
done
}.boxed()
}
}
)*)
}
// Generate TestHarness impls for tuples up to arity-6
generate_tuples! {
(A, B),
(A, B, C),
(A, B, C, D),
(A, B, C, D, E),
(A, B, C, D, E, F),
}
// import the macro from the macro crate
pub use test_harness_macro::run_singlethreaded_test;
// Re-export from fuchsia_async to provide an unambiguous direct include from test_harness_macro
/// Runs a test in an executor, potentially repeatedly and concurrently
pub fn run_singlethreaded_test<F, Fut, R>(test: F) -> R
where
F: 'static + Send + Sync + Fn(usize) -> Fut,
Fut: 'static + Future<Output = R>,
R: fasync::test_support::TestResult,
{
fasync::test_support::run_singlethreaded_test(test)
}