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}