1use std::collections::HashMap;
10use std::future::Future;
11use std::io;
12use std::panic::resume_unwind;
13use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
14use std::time::{Duration, SystemTime};
15
16use ruststream::testing::{Backlog, Coordinator, InProcess, TestableBroker};
17use ruststream::{HeaderMap, OutgoingMessage, RawMessage};
18use ruststream_sqlx_dialect::Dialect;
19use tokio::runtime::Handle;
20use tokio::sync::oneshot;
21use tokio::task::spawn_blocking;
22use tokio::time::Instant;
23
24use super::broker::{ConnectedSqlxBroker, Shared, SqlxBroker};
25use super::database::QueueDatabase;
26use super::engine::Now;
27use super::error::SqlxBrokerError;
28use super::publish::insert_routed;
29
30#[derive(Debug, Clone, Copy)]
33pub(crate) struct TestClock {
34 system: SystemTime,
35 instant: Instant,
36}
37
38impl TestClock {
39 fn start() -> Self {
40 Self {
41 system: SystemTime::now(),
42 instant: Instant::now(),
43 }
44 }
45
46 pub(crate) fn now(self) -> SystemTime {
47 self.system + self.instant.elapsed()
48 }
49}
50
51pub(crate) async fn off_clock<T, Work>(work: Work) -> Option<T>
56where
57 T: Send + 'static,
58 Work: Future<Output = T> + Send + 'static,
59{
60 let runtime = Handle::current();
61 match spawn_blocking(move || runtime.block_on(work)).await {
62 Ok(output) => Some(output),
63 Err(join) if join.is_panic() => resume_unwind(join.into_panic()),
64 Err(_) => None,
65 }
66}
67
68pub(crate) fn cancelled() -> sqlx::Error {
70 sqlx::Error::Io(io::Error::other(
71 "the runtime shut down during a database call",
72 ))
73}
74
75#[derive(Debug, Default)]
78pub(crate) struct Harness {
79 coordinator: OnceLock<Coordinator>,
80 log: Mutex<Vec<RawMessage>>,
81 clock: OnceLock<TestClock>,
83 credits: Mutex<HashMap<String, usize>>,
85 writes: Mutex<Option<oneshot::Receiver<()>>>,
87}
88
89impl Harness {
90 pub(crate) fn in_process(&self) -> bool {
92 self.clock.get().is_some()
93 }
94
95 pub(crate) fn now(&self) -> Now {
97 Now::test(self.clock.get().copied())
98 }
99
100 fn books(&self) -> Option<&Coordinator> {
102 self.clock.get().and_then(|_| self.coordinator.get())
103 }
104
105 fn credits(&self) -> MutexGuard<'_, HashMap<String, usize>> {
106 self.credits.lock().unwrap_or_else(PoisonError::into_inner)
107 }
108
109 pub(crate) fn opened(&self, name: &str) {
111 self.credits().entry(name.to_owned()).or_insert(0);
112 }
113
114 pub(crate) fn closed(&self, name: &str) {
116 self.credits().remove(name);
117 }
118
119 pub(crate) fn expect(&self, name: &str) {
122 let Some(coordinator) = self.books() else {
123 return;
124 };
125 if let Some(credit) = self.credits().get_mut(name) {
126 *credit += 1;
127 coordinator.enqueued();
128 }
129 }
130
131 pub(crate) fn refused(&self, name: &str) {
133 let Some(coordinator) = self.books() else {
134 return;
135 };
136 if let Some(credit) = self.credits().get_mut(name)
137 && *credit > 0
138 {
139 *credit -= 1;
140 coordinator.consumed();
141 }
142 }
143
144 pub(crate) fn claimed(&self, name: &str, count: usize) {
146 let Some(coordinator) = self.books() else {
147 return;
148 };
149 let covered = self.credits().get_mut(name).map_or(0, |credit| {
150 let covered = (*credit).min(count);
151 *credit -= covered;
152 covered
153 });
154 for _ in covered..count {
155 coordinator.enqueued();
156 }
157 }
158
159 pub(crate) fn released(&self) {
161 if let Some(coordinator) = self.books() {
162 coordinator.consumed();
163 }
164 }
165
166 pub(crate) fn returned(&self, name: &str) {
168 self.expect(name);
169 self.released();
170 }
171
172 pub(crate) fn published(&self, message: &OutgoingMessage<'_>) {
175 self.record(message);
176 }
177
178 fn record(&self, message: &OutgoingMessage<'_>) {
179 self.log
180 .lock()
181 .unwrap_or_else(PoisonError::into_inner)
182 .push(
183 RawMessage::new(message.name().to_owned(), message.payload().to_vec())
184 .with_headers(message.headers().clone()),
185 );
186 }
187}
188
189pub(crate) fn returns_after<DB: QueueDatabase>(
192 connection: &Arc<Shared<DB>>,
193 name: &'static str,
194 delay: Duration,
195) {
196 let Some(coordinator) = connection.harness.books() else {
197 return;
198 };
199 let connection = Arc::clone(connection);
200 coordinator.schedule_redelivery(delay, move || connection.harness.expect(name));
201}
202
203impl<DB: QueueDatabase, D: Dialect + 'static> InProcess for SqlxBroker<DB, D> {
204 async fn connect_in_process(self) -> Result<ConnectedSqlxBroker<DB, D>, SqlxBrokerError> {
207 let (pool, choice) = (self.pool.clone(), self.dialect.clone());
208 let dialect = off_clock(async move {
209 let conn = pool
210 .acquire()
211 .await
212 .map_err(|source| SqlxBrokerError::Connect { source })?;
213 choice.resolve(&conn)
214 })
215 .await
216 .unwrap_or_else(|| {
217 Err(SqlxBrokerError::Connect {
218 source: cancelled(),
219 })
220 })?;
221 let listening = match self.listen {
222 Some(start) => {
223 let pool = self.pool.clone();
224 Some(
225 off_clock(start(pool))
226 .await
227 .unwrap_or_else(|| Err(cancelled()))
228 .map_err(|source| SqlxBrokerError::Connect { source })?,
229 )
230 }
231 None => None,
232 };
233 let connected = ConnectedSqlxBroker::new(self, dialect, Handle::current(), listening);
234 let _ = connected.shared.harness.clock.set(TestClock::start());
235 connected.shared.closing.run_in_process();
236 Ok(connected)
237 }
238}
239
240impl<DB: QueueDatabase, D: Dialect + 'static> TestableBroker for ConnectedSqlxBroker<DB, D> {
241 fn install_coordinator(&self, coordinator: Coordinator) {
242 let _ = self.shared.harness.coordinator.set(coordinator);
243 }
244
245 fn inject(&self, message: OutgoingMessage<'_>) {
246 let harness = &self.shared.harness;
247 harness.record(&message);
248 harness.expect(message.name());
249 let (name, payload, headers) = message.into_parts();
250 let (name, payload) = (name.to_owned(), payload.to_vec());
251 let (written, next) = oneshot::channel();
254 let previous = harness
255 .writes
256 .lock()
257 .unwrap_or_else(PoisonError::into_inner)
258 .replace(next);
259 let shared = Arc::clone(&self.shared);
260 tokio::spawn(async move {
261 if let Some(previous) = previous {
262 let _ = previous.await;
263 }
264 inject(&shared, &name, &payload, headers).await;
265 let _ = written.send(());
266 });
267 }
268
269 fn published(&self, name: &str) -> Vec<RawMessage> {
270 self.shared
271 .harness
272 .log
273 .lock()
274 .unwrap_or_else(PoisonError::into_inner)
275 .iter()
276 .filter(|message| message.name() == name)
277 .cloned()
278 .collect()
279 }
280
281 fn backlog(&self) -> Backlog {
282 Backlog::Delivered
284 }
285}
286
287async fn inject<DB: QueueDatabase>(
289 shared: &Shared<DB>,
290 name: &str,
291 payload: &[u8],
292 headers: HeaderMap,
293) {
294 let message = OutgoingMessage::new(name, payload).with_headers(headers);
295 let written = match shared.routes.find_with_wake(name) {
296 Some((route, wake)) => insert_routed(shared, route, wake, &message).await,
297 None => Err(SqlxBrokerError::NoRoute {
298 name: name.to_owned(),
299 }),
300 };
301 if let Err(error) = written {
302 shared.harness.refused(name);
303 tracing::warn!(target: "ruststream_sqlx", %error, name, "an injected message was refused");
304 }
305}
306
307#[cfg(feature = "postgres")]
308ruststream::register_testable_broker!(SqlxBroker<sqlx::Postgres>);
309#[cfg(feature = "mysql")]
310ruststream::register_testable_broker!(SqlxBroker<sqlx::MySql>);
311#[cfg(feature = "sqlite")]
312ruststream::register_testable_broker!(SqlxBroker<sqlx::Sqlite>);
313#[cfg(feature = "any")]
314ruststream::register_testable_broker!(SqlxBroker<sqlx::Any>);