Skip to main content

embassy_mock/
executor.rs

1//! A mocked version of the `embassy-executor` crate.
2//!
3//! # Examples
4//! ```
5//! use embassy_mock::executor::Spawner;
6//!
7//! #[embassy_executor::task]
8//! async fn example_task() {}
9//!
10//! // Generic over the `Spawner` trait
11//! pub fn spawn_tasks<S: Spawner>(spawner: &S) {
12//!     spawner.spawn(example_task()).unwrap();
13//! }
14//!
15//! // The real main that runs on the Embassy executor.
16//! // #[embassy_executor::main]
17//! // async fn main(spawner: embassy_executor::Spawner) {
18//! //     spawn_tasks(&spawner);
19//! // }
20//!
21//! # test_spawning_of_tasks();
22//! // The unit tests that use the `MockSpawner`.
23//! #[cfg(test)]
24//! mod tests {
25//!     use super::*;
26//! # }
27//!     use embassy_mock::executor::MockSpawner;
28//!
29//!     #[test]
30//!     # fn hidden_fake_test(){}
31//!     fn test_spawning_of_tasks() {
32//!         let spawner = MockSpawner::expect(1);
33//!         spawn_tasks(&spawner);
34//!
35//!         assert_eq!(spawner.done(), Ok(()));
36//!     }
37//! # mod closing {
38//! }
39//! ```
40
41use core::sync::atomic::{AtomicUsize, Ordering};
42use embassy_executor::{SpawnError, SpawnToken, Spawner as EmbassySpawner};
43use snafu::prelude::*;
44
45/// The trait to replace the [`embassy_executor::Spawner`] in code to allow the [`MockSpawner`] to
46/// be used in its place for tests.
47pub trait Spawner {
48    /// Wrapper for [`embassy_executor::Spawner::spawn()`].
49    fn spawn<S>(&self, token: SpawnToken<S>) -> Result<(), SpawnError>;
50}
51
52impl Spawner for EmbassySpawner {
53    /// Spawn a task into an executor.
54    ///
55    /// You obtain the `token` by calling a task function (i.e. one marked with `#[embassy_executor::task]`).
56    fn spawn<S>(&self, token: SpawnToken<S>) -> Result<(), SpawnError> {
57        self.spawn(token)
58    }
59}
60
61/// The errors that are reported by [`MockSpawner`].
62#[derive(Debug, Snafu, PartialEq)]
63pub enum MockSpawnerError {
64    /// The [`MockSpawner::spawn()`] method was called the wrong number of times.
65    #[snafu(display("expected to spawn {expected} task(s), actually spawned {actual}"))]
66    WrongNumberOfTasks {
67        /// The expected number of calls to [`MockSpawner::spawn()`].
68        expected: usize,
69
70        /// The actual number of times [`MockSpawner::spawn()`] was called.
71        actual: usize,
72    },
73}
74
75/// A mocked version of [`embassy_executor::Spawner`] that can be used in its place for unit tests.
76///
77/// This mocked version counts how many times [`Self::spawn()`] is called and can be checked that
78/// [`Self::spawn()`] was called the correct number of times using [`Self::done()`]. If
79/// [`Self::done()`] is not called then it asserts that [`Self::spawn()`] was called the correct
80/// number of times when dropped which causes a panic if incorrect.
81///
82/// # Panics
83///
84/// Panics if [`Self::spawn()`] called the wrong number of times and [`Self`] is dropped before
85/// calling [`Self::done()`].
86///
87/// # Examples
88///
89/// ```
90/// use embassy_mock::executor::{MockSpawner, MockSpawnerError, Spawner};
91///
92/// #[embassy_executor::task]
93/// async fn example_task() {}
94///
95/// let spawner = MockSpawner::expect(4);
96/// spawner.spawn(example_task()).unwrap();
97///
98/// let res = spawner.done();
99///
100/// let expected = Err(MockSpawnerError::WrongNumberOfTasks {
101///     expected: 4,
102///     actual: 1,
103/// });
104/// assert_eq!(res, expected);
105/// ```
106///
107/// ```
108/// use embassy_mock::executor::{MockSpawner, Spawner};
109///
110/// #[embassy_executor::task]
111/// async fn example_task() {}
112///
113/// let spawner = MockSpawner::expect(1); // Expects `spawn()` to be called once.
114/// spawner.spawn(example_task()).unwrap(); // `spawn()` is called once.
115///
116/// // `spawner` is dropped but doesn't panic.
117/// ```
118///
119/// ```should_panic
120/// use embassy_mock::executor::{MockSpawner, Spawner};
121///
122/// #[embassy_executor::task]
123/// async fn example_task() {}
124///
125/// let spawner = MockSpawner::expect(2); // Expects `spawn()` to be called twice.
126/// spawner.spawn(example_task()).unwrap(); // `spawn()` is called only once.
127///
128/// // `spawner` is dropped and will panic.
129/// ```
130#[derive(Debug)]
131pub struct MockSpawner {
132    /// The number of expected calls to [`Self::spawn()`].
133    expected: usize,
134
135    /// The number of times [`Self::spawn()`] has been called.
136    times_called: AtomicUsize,
137
138    /// Has this mock been checked with a call to [`Self::done()`].
139    /// If true it is not checked when dropped.
140    is_done: bool,
141}
142
143impl MockSpawner {
144    /// Create a [`MockSpawner`], providing the expected number of calls to [`Self::spawn()`].
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use embassy_mock::executor::MockSpawner;
150    ///
151    /// # const X: usize = 0;
152    /// let spawner = MockSpawner::expect(X); // Where `X` is the number of times `spawn()` should be called
153    /// ```
154    pub const fn expect(expected: usize) -> Self {
155        Self {
156            expected,
157            times_called: AtomicUsize::new(0),
158            is_done: false,
159        }
160    }
161
162    /// Mark the [`MockSpawner`] as done and check if [`Self::spawn()`] was called the correct
163    /// number of times.
164    ///
165    /// This is a cleaner way of testing that [`Self::spawn()`] is called the correct number of
166    /// times as [`MockSpawner`] doesn't cause a panic when dropped if this method is called,
167    /// it also returns a [`Result<(), MockSpawnerError>`] which allows checking the outcome of the
168    /// mock.
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// use embassy_mock::executor::{MockSpawner, Spawner};
174    ///
175    /// #[embassy_executor::task]
176    /// async fn example_task() {}
177    ///
178    /// let spawner = MockSpawner::expect(1);
179    /// spawner.spawn(example_task()).unwrap();
180    ///
181    /// let res = spawner.done();
182    ///
183    /// assert_eq!(res, Ok(()));
184    /// ```
185    ///
186    /// ```
187    /// use embassy_mock::executor::{MockSpawner, MockSpawnerError, Spawner};
188    ///
189    /// #[embassy_executor::task]
190    /// async fn example_task() {}
191    ///
192    /// let spawner = MockSpawner::expect(4);
193    /// spawner.spawn(example_task()).unwrap();
194    ///
195    /// let res = spawner.done();
196    ///
197    /// let expected = Err(MockSpawnerError::WrongNumberOfTasks {
198    ///     expected: 4,
199    ///     actual: 1,
200    /// });
201    /// assert_eq!(res, expected);
202    ///
203    /// // This doesn't panic when `spawner` is dropped as `spawner.done()` was called.
204    /// ```
205    pub fn done(mut self) -> Result<(), MockSpawnerError> {
206        let times_called = self.times_called.load(Ordering::Relaxed);
207        let res = if times_called != self.expected {
208            Err(MockSpawnerError::WrongNumberOfTasks {
209                expected: self.expected,
210                actual: times_called,
211            })
212        } else {
213            Ok(())
214        };
215
216        self.is_done = true;
217        res
218    }
219}
220
221impl Drop for MockSpawner {
222    /// If [`Self::done()`] has not been called before being dropped then check that the number of
223    /// times [`Self::spawn()`] was called is as expected.
224    fn drop(&mut self) {
225        if !self.is_done {
226            let times_called = self.times_called.load(Ordering::Relaxed);
227            assert_eq!(
228                self.expected, times_called,
229                "expected to spawn {} task(s), actually spawned {}",
230                self.expected, times_called
231            );
232        }
233    }
234}
235
236impl Spawner for MockSpawner {
237    /// Increment an internal counter of how many times this method is called.
238    fn spawn<S>(&self, token: SpawnToken<S>) -> Result<(), SpawnError> {
239        // Need to forget the token so that it is not dropped which causes a panic
240        core::mem::forget(token);
241        let times_called = self
242            .times_called
243            .load(Ordering::Relaxed)
244            .checked_add(1)
245            .unwrap();
246        self.times_called.store(times_called, Ordering::Relaxed);
247
248        Ok(())
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[embassy_executor::task]
257    async fn example_task() {}
258
259    #[test]
260    fn can_spawn_single_task_just_drop() {
261        let spawner = MockSpawner::expect(1);
262        spawner.spawn(example_task()).unwrap();
263    }
264
265    #[test]
266    fn can_spawn_multiple_tasks_just_drop() {
267        let spawner = MockSpawner::expect(3);
268        spawner.spawn(example_task()).unwrap();
269        spawner.spawn(example_task()).unwrap();
270        spawner.spawn(example_task()).unwrap();
271    }
272
273    #[test]
274    #[should_panic(expected = "expected to spawn 1 task(s), actually spawned 3")]
275    fn spawn_too_many_tasks_just_drop() {
276        let spawner = MockSpawner::expect(1);
277        spawner.spawn(example_task()).unwrap();
278        spawner.spawn(example_task()).unwrap();
279        spawner.spawn(example_task()).unwrap();
280    }
281
282    #[test]
283    #[should_panic(expected = "expected to spawn 3 task(s), actually spawned 1")]
284    fn spawn_too_few_tasks_just_drop() {
285        let spawner = MockSpawner::expect(3);
286        spawner.spawn(example_task()).unwrap();
287    }
288
289    #[test]
290    fn done_returns_ok() {
291        let spawner = MockSpawner::expect(1);
292        spawner.spawn(example_task()).unwrap();
293
294        let res = spawner.done();
295
296        assert_eq!(res, Ok(()));
297    }
298
299    #[test]
300    fn done_returns_err_does_not_panic_on_drop() {
301        let spawner = MockSpawner::expect(3);
302        spawner.spawn(example_task()).unwrap();
303
304        let res = spawner.done();
305
306        let expected = Err(MockSpawnerError::WrongNumberOfTasks {
307            expected: 3,
308            actual: 1,
309        });
310        assert_eq!(res, expected);
311    }
312}