test_harness/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
// 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::{format_err, Context as _, Error};
use fuchsia_async as fasync;
use fuchsia_sync::Mutex;
use futures::future::BoxFuture;
use futures::stream::TryStreamExt;
use futures::{future, select, Future, FutureExt};
use std::any::{Any, TypeId};
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::pin::pin;
use std::sync::Arc;

pub const SHARED_STATE_TEST_COMPONENT_INDEX: &str = "TEST-COMPONENT";

/// SharedState is a string-indexed map used to share state across multiple TestHarnesses that are
/// tupled together. For example, the state could be a Bluetooth emulator instance and core
/// component/driver hierarchy manipulated by two distinct Harnesses during a test. The map's value
/// types use runtime polymorphism (`Box<dyn Any>`) so that any type of state may be shared. Once
/// added, entries cannot be removed from the SharedState, although they may be modified.
type InnerSharedState = HashMap<String, Arc<dyn Any + Send + Sync + 'static>>;

#[derive(Default)]
pub struct SharedState(Mutex<InnerSharedState>);

impl SharedState {
    /// Returns None if no entry exists for `key`. Returns Some(Err) if an entry exists for `key`,
    /// but is not of type T. Returns Some(Ok(Arc<T>)) if the existing entry is successfully
    /// downcasted to T.
    pub fn get<T: Send + Sync + 'static>(&self, key: &str) -> Option<Result<Arc<T>, Error>> {
        self.0.lock().get(key).cloned().map(Self::downcast_with_err)
    }

    /// Insert `val` at `key` if `key` is not yet occupied. If SharedState did not have an entry
    /// for `key`, returns Ok(Arc<the inserted `val`>). Otherwise, does not insert `val` and returns
    /// Err(Arc<existing value>)
    pub fn try_insert<T: Send + Sync + 'static>(
        &self,
        key: &str,
        val: T,
    ) -> Result<Arc<T>, Arc<dyn Any + Send + Sync + 'static>> {
        match self.0.lock().entry(key.to_string()) {
            Entry::Occupied(existing) => Err(existing.get().clone()),
            Entry::Vacant(empty) => {
                let entry = Arc::new(val);
                let _ = empty.insert(entry.clone());
                Ok(entry)
            }
        }
    }

    /// This takes two type parameters, F and T. The inserter F is used in case `key` is not yet
    /// associated with an existing state value to create the value associated with `key`. T is the
    /// type of state expected to be associated with `key`. After possibly inserting the state
    /// associated with `key` in the map, we dynamically cast `key`s state into type T. Errors can
    /// stem from `inserter` failures, or existing types in the map not matching T.
    pub async fn get_or_insert_with<F, Fut, T>(
        &self,
        key: &str,
        inserter: F,
    ) -> Result<Arc<T>, Error>
    where
        F: FnOnce() -> Fut,
        Fut: Future<Output = Result<T, Error>>,
        T: Send + Sync + 'static,
    {
        if let Some(res) = self.get(key) {
            return res;
        }
        let state = inserter().await?;
        // It's possible and OK for us to be preempted while running inserter.
        self.try_insert(key, state).or_else(Self::downcast_with_err)
    }

    fn downcast_with_err<T: Send + Sync + 'static>(
        any: Arc<dyn Any + Send + Sync + 'static>,
    ) -> Result<Arc<T>, Error> {
        any.downcast()
            .map_err(|_| format_err!("failed to downcast to type {:?}", TypeId::of::<T>()))
    }
}

/// 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 measuring 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. May index into shared_state to access state that needs
    /// to be shared across Harnesses. If `init` needs to access `shared_state` inside the returned
    /// future, it should clone the state outside of the future and move it into the future.
    ///
    /// `shared_state` will be dropped before the test code executes, so if a shared state entry
    /// must exist for the duration of the test, Harnesses should add it to their `Env`.
    fn init(
        shared_state: &Arc<SharedState>,
    ) -> 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, test_component: Option<String>)
where
    H: TestHarness,
    F: FnOnce(H) -> Fut + Send + 'static,
    Fut: Future<Output = ()> + Send + 'static,
{
    diagnostics_log::initialize(diagnostics_log::PublishOptions::default()).expect("init logging");
    let state: Arc<SharedState> = Default::default();
    if test_component.is_some() {
        let _ = state
            .try_insert(SHARED_STATE_TEST_COMPONENT_INDEX, test_component.unwrap())
            .expect("insert test_component");
    }
    let (harness, env, runner) = H::init(&state).await.expect("couldn't initialize harness");
    // Drop `state` so that SharedState entries may be dropped if not needed during test execution.
    // See Harness::init doc comment for further explanation.
    drop(state);

    let run_test = test_func(harness);
    let run_test = pin!(run_test);
    let runner = pin!(runner);

    let result = select! {
        () = run_test.fuse() => Ok(()),
        runner_result = runner.fuse() => runner_result.context("error running harness background task"),
    };

    let () = H::terminate(env).await.expect("couldn't terminate harness");
    result.expect("error running test");
}

/// The Unit type can be used as the empty test-harness - it does no initialization and no
/// termination.
impl TestHarness for () {
    type Env = ();
    type Runner = future::Pending<Result<(), Error>>;

    fn init(
        _shared_state: &Arc<SharedState>,
    ) -> 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()
    }
}

/// 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(shared_state: &Arc<SharedState>) -> BoxFuture<'static, Result<(Self, Self::Env, Self::Runner), Error>> {
                    let shared_state = shared_state.clone();
                    async move {
                        $(
                            let $Harness = $Harness::init(&shared_state).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)
}