1use crate::job::{BoxFuture, FailedJob, QueuedJob, ReservedJob};
20use crate::queue::{Queue, record_pushed};
21use rustlavel_core::{Error, Result};
22use std::sync::Mutex;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::time::Duration;
25
26pub fn fake() -> FakeQueue {
28 FakeQueue::new()
29}
30
31#[derive(Default)]
33pub struct FakeQueue {
34 pushed: Mutex<Vec<QueuedJob>>,
35 next_id: AtomicU64,
36}
37
38impl FakeQueue {
39 pub fn new() -> Self {
40 FakeQueue { pushed: Mutex::new(Vec::new()), next_id: AtomicU64::new(1) }
41 }
42
43 pub fn all(&self) -> Vec<QueuedJob> {
45 self.pushed.lock().expect("the fake queue was poisoned").clone()
46 }
47
48 pub fn pushed(&self, name: &str) -> Vec<QueuedJob> {
50 self.all().into_iter().filter(|job| job.name == name).collect()
51 }
52
53 pub fn count(&self) -> usize {
55 self.pushed.lock().expect("the fake queue was poisoned").len()
56 }
57
58 pub fn reset(&self) {
60 self.pushed.lock().expect("the fake queue was poisoned").clear();
61 }
62
63 #[track_caller]
65 pub fn assert_pushed(&self, name: &str) -> &Self {
66 let found = self.pushed(name);
67 assert!(
68 !found.is_empty(),
69 "expected `{name}` to have been pushed, but {}",
70 self.summary()
71 );
72 self
73 }
74
75 #[track_caller]
77 pub fn assert_pushed_times(&self, name: &str, times: usize) -> &Self {
78 let found = self.pushed(name).len();
79 assert_eq!(
80 found, times,
81 "expected `{name}` to have been pushed {times} time(s), but it was pushed {found} \
82 time(s); {}",
83 self.summary()
84 );
85 self
86 }
87
88 #[track_caller]
90 pub fn assert_not_pushed(&self, name: &str) -> &Self {
91 self.assert_pushed_times(name, 0)
92 }
93
94 #[track_caller]
96 pub fn assert_nothing_pushed(&self) -> &Self {
97 assert!(self.count() == 0, "expected nothing to have been pushed, but {}", self.summary());
98 self
99 }
100
101 #[track_caller]
103 pub fn assert_pushed_on(&self, queue: &str, name: &str) -> &Self {
104 let found = self.pushed(name);
105 assert!(
106 found.iter().any(|job| job.queue == queue),
107 "expected `{name}` to have been pushed on `{queue}`, but it went to {:?}; {}",
108 found.iter().map(|job| job.queue.as_str()).collect::<Vec<_>>(),
109 self.summary()
110 );
111 self
112 }
113
114 #[track_caller]
116 pub fn assert_pushed_later(&self, name: &str, delay: Duration) -> &Self {
117 let found = self.pushed(name);
118 assert!(
119 found.iter().any(|job| job.delay >= delay),
120 "expected `{name}` to have been delayed by at least {delay:?}, but the delays were \
121 {:?}; {}",
122 found.iter().map(|job| job.delay).collect::<Vec<_>>(),
123 self.summary()
124 );
125 self
126 }
127
128 fn summary(&self) -> String {
130 let all = self.all();
131 if all.is_empty() {
132 return "nothing was pushed at all".to_string();
133 }
134
135 let names: Vec<String> =
136 all.iter().map(|job| format!("{} on `{}`", job.name, job.queue)).collect();
137 format!("what was pushed: {}", names.join(", "))
138 }
139}
140
141impl Queue for FakeQueue {
142 fn driver(&self) -> &'static str {
143 "fake"
144 }
145
146 fn push(&self, job: QueuedJob) -> BoxFuture<'_, Result<String>> {
147 Box::pin(async move {
148 let id = self.next_id.fetch_add(1, Ordering::SeqCst).to_string();
149 self.pushed.lock().expect("the fake queue was poisoned").push(job.clone());
150
151 record_pushed(self.driver(), &job, &id);
154 Ok(id)
155 })
156 }
157
158 fn pop<'a>(&'a self, _queue: &'a str) -> BoxFuture<'a, Result<Option<ReservedJob>>> {
161 Box::pin(async { Ok(None) })
162 }
163
164 fn size<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>> {
165 Box::pin(async move {
166 Ok(self.all().iter().filter(|job| job.queue == queue).count() as u64)
167 })
168 }
169
170 fn delete<'a>(&'a self, _job: &'a ReservedJob) -> BoxFuture<'a, Result<()>> {
171 Box::pin(async { Ok(()) })
172 }
173
174 fn release<'a>(&'a self, _job: &'a ReservedJob, _delay: Duration) -> BoxFuture<'a, Result<()>> {
175 Box::pin(async {
176 Err(Error::msg("a faked queue never reserves a job, so it cannot release one"))
177 })
178 }
179
180 fn fail<'a>(&'a self, _job: &'a ReservedJob, _error: &'a str) -> BoxFuture<'a, Result<()>> {
181 Box::pin(async {
182 Err(Error::msg("a faked queue never reserves a job, so it cannot fail one"))
183 })
184 }
185
186 fn failed_jobs(&self) -> BoxFuture<'_, Result<Vec<FailedJob>>> {
187 Box::pin(async { Ok(Vec::new()) })
188 }
189
190 fn clear<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>> {
191 Box::pin(async move {
192 let mut pushed = self.pushed.lock().expect("the fake queue was poisoned");
193 let before = pushed.len();
194 pushed.retain(|job| job.queue != queue);
195 Ok((before - pushed.len()) as u64)
196 })
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::queue::QueueExt;
204 use crate::tests_support::{CountingJob, counter};
205 use rustlavel_core::Json;
206
207 #[tokio::test]
208 async fn a_faked_queue_records_a_dispatch_without_running_it() {
209 let queue = fake();
210 let tally = counter();
211
212 queue.dispatch(&CountingJob::new(tally, 5)).await.unwrap();
213
214 queue.assert_pushed("counting");
215 assert_eq!(tally.runs(), 0, "a fake must never run the job");
216 assert_eq!(queue.count(), 1);
217 }
218
219 #[tokio::test]
220 async fn a_faked_queue_hands_nothing_to_a_worker() {
221 let queue = fake();
222 queue.push(QueuedJob::new("anything", Json::Null)).await.unwrap();
223
224 assert!(queue.pop("default").await.unwrap().is_none());
225 }
226
227 #[tokio::test]
228 async fn counting_assertions_distinguish_between_job_names() {
229 let queue = fake();
230 for _ in 0..3 {
231 queue.push(QueuedJob::new("a", Json::Null)).await.unwrap();
232 }
233 queue.push(QueuedJob::new("b", Json::Null)).await.unwrap();
234
235 queue.assert_pushed_times("a", 3).assert_pushed_times("b", 1).assert_not_pushed("c");
236 }
237
238 #[tokio::test]
239 async fn nothing_pushed_holds_on_a_fresh_fake_and_after_a_reset() {
240 let queue = fake();
241 queue.assert_nothing_pushed();
242
243 queue.push(QueuedJob::new("a", Json::Null)).await.unwrap();
244 queue.reset();
245 queue.assert_nothing_pushed();
246 }
247
248 #[tokio::test]
249 async fn the_queue_and_delay_a_job_was_given_are_both_recorded() {
250 let queue = fake();
251 queue.push_on("emails", QueuedJob::new("welcome", Json::Null)).await.unwrap();
252 queue
253 .later(Duration::from_secs(600), QueuedJob::new("digest", Json::Null))
254 .await
255 .unwrap();
256
257 queue.assert_pushed_on("emails", "welcome");
258 queue.assert_pushed_later("digest", Duration::from_secs(300));
259 assert_eq!(queue.size("emails").await.unwrap(), 1);
260 }
261
262 #[tokio::test]
263 async fn the_recorded_payload_is_available_for_inspection() {
264 let queue = fake();
265 queue
266 .push(QueuedJob::new("invoice", Json::object([("order", Json::from(42))])))
267 .await
268 .unwrap();
269
270 let pushed = queue.pushed("invoice");
271 assert_eq!(pushed.len(), 1);
272 assert_eq!(pushed[0].payload.get("order").and_then(Json::as_i64), Some(42));
273 }
274
275 #[tokio::test]
276 async fn a_failed_assertion_says_what_was_pushed_instead() {
277 let queue = fake();
278 queue.push(QueuedJob::new("actual", Json::Null)).await.unwrap();
279
280 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
281 queue.assert_pushed("expected");
282 }))
283 .unwrap_err();
284
285 let message = rustlavel_http::panic::message_of(panic.as_ref());
286 assert!(message.contains("expected `expected` to have been pushed"), "{message}");
287 assert!(message.contains("actual on `default`"), "{message}");
288 }
289
290 #[tokio::test]
291 async fn a_fake_is_usable_wherever_a_real_queue_is() {
292 let queue: std::sync::Arc<dyn Queue> = std::sync::Arc::new(fake());
293
294 queue.dispatch(&CountingJob::new(counter(), 1)).await.unwrap();
295
296 assert_eq!(queue.driver(), "fake");
297 assert_eq!(queue.size("default").await.unwrap(), 1);
298 assert!(queue.failed_jobs().await.unwrap().is_empty());
299 }
300}