Skip to main content

toasty_driver_integration_suite/
instrumented_driver.rs

1use async_trait::async_trait;
2use std::{
3    borrow::Cow,
4    collections::VecDeque,
5    fmt,
6    sync::{
7        Arc, Mutex,
8        atomic::{AtomicBool, Ordering},
9    },
10};
11use toasty_core::{
12    Result, Schema,
13    driver::{Capability, ConnectContext, Connection, Driver, ExecResponse, Operation, Rows},
14    schema::{
15        db::{AppliedMigration, Migration},
16        diff,
17    },
18};
19
20/// A fault that can be injected into the next operation routed through
21/// the driver. Faults are consumed in FIFO order: each `exec` call pops
22/// at most one fault off the queue before delegating (or short-circuiting
23/// past) the underlying driver.
24#[derive(Debug, Clone)]
25pub enum Fault {
26    /// Causes the next `exec` to return `Error::driver_operation_failed`
27    /// without touching the underlying connection or marking it invalid.
28    OperationFailed,
29
30    /// Causes the next `exec` to return `Error::connection_lost` without
31    /// touching the underlying connection. The wrapping
32    /// `InstrumentedConnection`'s `is_valid` flips to `false`, mirroring
33    /// what a real connection-lost error would do and prompting the pool
34    /// to evict the connection.
35    ConnectionLost,
36}
37
38#[derive(Debug)]
39pub struct DriverOp {
40    pub operation: Operation,
41    pub response: ExecResponse,
42}
43
44/// Single control handle for the [`InstrumentedDriver`] test middleware.
45/// Exposes both the operation log (for assertions) and the fault queue
46/// (for injecting failures). Cheaply cloneable; every clone refers to
47/// the same shared state.
48#[derive(Clone, Default)]
49pub struct InstrumentedHandle {
50    inner: Arc<InstrumentedState>,
51}
52
53#[derive(Default)]
54struct InstrumentedState {
55    ops_log: Mutex<Vec<DriverOp>>,
56    faults: Mutex<VecDeque<Fault>>,
57}
58
59impl InstrumentedHandle {
60    /// Get the number of logged operations
61    pub fn len(&self) -> usize {
62        self.inner.ops_log.lock().unwrap().len()
63    }
64
65    /// Check if the log is empty
66    pub fn is_empty(&self) -> bool {
67        self.inner.ops_log.lock().unwrap().is_empty()
68    }
69
70    /// Clear the log
71    pub fn clear(&self) {
72        self.inner.ops_log.lock().unwrap().clear();
73    }
74
75    /// Remove and return the first operation from the log
76    #[track_caller]
77    pub fn pop(&self) -> (Operation, ExecResponse) {
78        let mut ops = self.inner.ops_log.lock().unwrap();
79        if ops.is_empty() {
80            panic!("no operations in log");
81        }
82        let driver_op = ops.remove(0);
83        (driver_op.operation, driver_op.response)
84    }
85
86    #[track_caller]
87    pub fn pop_op(&self) -> Operation {
88        self.pop().0
89    }
90
91    /// Remove and return the last operation from the log
92    #[track_caller]
93    pub fn pop_last(&self) -> (Operation, ExecResponse) {
94        let mut ops = self.inner.ops_log.lock().unwrap();
95        let Some(driver_op) = ops.pop() else {
96            panic!("no operations in log");
97        };
98        (driver_op.operation, driver_op.response)
99    }
100
101    #[track_caller]
102    pub fn pop_last_op(&self) -> Operation {
103        self.pop_last().0
104    }
105
106    /// Queue a fault to fire on the next driver `exec` call. Faults fire
107    /// in FIFO order across all connections produced by the driver.
108    pub fn inject_fault(&self, fault: Fault) {
109        self.inner
110            .faults
111            .lock()
112            .expect("Failed to acquire faults lock")
113            .push_back(fault);
114    }
115}
116
117impl fmt::Debug for InstrumentedHandle {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        let ops = self.inner.ops_log.lock().unwrap();
120        f.debug_struct("InstrumentedHandle")
121            .field("ops", &*ops)
122            .finish()
123    }
124}
125
126/// Test-only driver wrapper that instruments an underlying driver: it
127/// records every operation for later assertion and can inject faults
128/// (connection loss, etc.) to exercise error-handling paths.
129#[derive(Debug)]
130pub struct InstrumentedDriver {
131    inner: Box<dyn Driver>,
132    handle: InstrumentedHandle,
133    /// Reported in place of the inner driver's capability when set.
134    capability: Option<&'static Capability>,
135}
136
137impl InstrumentedDriver {
138    pub fn new(driver: Box<dyn Driver>) -> Self {
139        Self {
140            inner: driver,
141            handle: InstrumentedHandle::default(),
142            capability: None,
143        }
144    }
145
146    /// Report `capability` to the engine instead of the inner driver's own,
147    /// so a test can watch how the engine plans for a backend it does not
148    /// have. The inner driver still executes whatever the engine asks of it.
149    pub fn with_capability(mut self, capability: &'static Capability) -> Self {
150        self.capability = Some(capability);
151        self
152    }
153
154    /// Get the single control handle for this driver. The handle exposes
155    /// both the operations log and the fault-injection queue.
156    pub fn handle(&self) -> InstrumentedHandle {
157        self.handle.clone()
158    }
159}
160
161#[async_trait]
162impl Driver for InstrumentedDriver {
163    fn url(&self) -> Cow<'_, str> {
164        self.inner.url()
165    }
166
167    fn capability(&self) -> &'static Capability {
168        self.capability.unwrap_or_else(|| self.inner.capability())
169    }
170
171    async fn connect(&self, cx: &ConnectContext) -> Result<Box<dyn Connection>> {
172        Ok(Box::new(InstrumentedConnection {
173            inner: self.inner.connect(cx).await?,
174            handle: self.handle.clone(),
175            valid: AtomicBool::new(true),
176        }))
177    }
178
179    fn generate_migration(&self, schema_diff: &diff::Schema<'_>) -> Migration {
180        self.inner.generate_migration(schema_diff)
181    }
182
183    async fn reset_db(&self) -> Result<()> {
184        self.inner.reset_db().await
185    }
186}
187
188/// Per-connection counterpart of [`InstrumentedDriver`]: records each
189/// `exec` and consults the shared fault queue before delegating.
190#[derive(Debug)]
191pub struct InstrumentedConnection {
192    /// The underlying driver that actually executes operations
193    inner: Box<dyn Connection>,
194
195    /// Shared handle: ops log + fault queue.
196    handle: InstrumentedHandle,
197
198    /// Set to `false` once an injected `ConnectionLost` fault has fired
199    /// against this connection. Surfaced through [`Connection::is_valid`]
200    /// so the pool evicts it the same way it would after a real
201    /// connection-lost error.
202    valid: AtomicBool,
203}
204
205#[async_trait]
206impl Connection for InstrumentedConnection {
207    async fn exec(&mut self, schema: &Arc<Schema>, operation: Operation) -> Result<ExecResponse> {
208        // Pop a queued fault, if any, and short-circuit before reaching
209        // the underlying driver.
210        let fault = self
211            .handle
212            .inner
213            .faults
214            .lock()
215            .expect("Failed to acquire faults lock")
216            .pop_front();
217        if let Some(fault) = fault {
218            match fault {
219                Fault::OperationFailed => {
220                    return Err(toasty_core::Error::driver_operation_failed(
221                        std::io::Error::other("injected operation failure"),
222                    ));
223                }
224                Fault::ConnectionLost => {
225                    self.valid.store(false, Ordering::Release);
226                    return Err(toasty_core::Error::connection_lost(std::io::Error::other(
227                        "injected connection-lost fault",
228                    )));
229                }
230            }
231        }
232
233        // Clone the operation for logging
234        let operation_clone = operation.clone();
235
236        // Execute the operation on the underlying driver
237        let mut response = self.inner.exec(schema, operation).await?;
238
239        // Duplicate the response for logging
240        let duplicated_response = duplicate_response_mut(&mut response).await?;
241
242        // Log the operation and response
243        let driver_op = DriverOp {
244            operation: operation_clone,
245            response: duplicated_response,
246        };
247
248        self.handle
249            .inner
250            .ops_log
251            .lock()
252            .expect("Failed to acquire ops log lock")
253            .push(driver_op);
254
255        Ok(response)
256    }
257
258    async fn push_schema(&mut self, schema: &Schema) -> Result<()> {
259        self.inner.push_schema(schema).await
260    }
261
262    async fn applied_migrations(&mut self) -> Result<Vec<AppliedMigration>> {
263        self.inner.applied_migrations().await
264    }
265
266    async fn apply_migration(&mut self, id: u64, name: &str, migration: &Migration) -> Result<()> {
267        self.inner.apply_migration(id, name, migration).await
268    }
269
270    fn is_valid(&self) -> bool {
271        self.valid.load(Ordering::Acquire) && self.inner.is_valid()
272    }
273
274    async fn ping(&mut self) -> Result<()> {
275        // Consume a queued fault before delegating, mirroring `exec`.
276        // A `ConnectionLost` fault here lets tests target the sweep's
277        // ping path the same way they target user query paths.
278        let fault = self
279            .handle
280            .inner
281            .faults
282            .lock()
283            .expect("Failed to acquire faults lock")
284            .pop_front();
285        if let Some(fault) = fault {
286            match fault {
287                Fault::OperationFailed => {
288                    return Err(toasty_core::Error::driver_operation_failed(
289                        std::io::Error::other("injected operation failure"),
290                    ));
291                }
292                Fault::ConnectionLost => {
293                    self.valid.store(false, Ordering::Release);
294                    return Err(toasty_core::Error::connection_lost(std::io::Error::other(
295                        "injected connection-lost fault",
296                    )));
297                }
298            }
299        }
300        self.inner.ping().await
301    }
302}
303
304/// Duplicate an ExecResponse, using ValueStream::dup() for value streams
305/// This version takes a mutable reference so we can call dup() on the ValueStream
306async fn duplicate_response_mut(response: &mut ExecResponse) -> Result<ExecResponse> {
307    let values = match &mut response.values {
308        Rows::Count(count) => Rows::Count(*count),
309        Rows::Value(_) => todo!(),
310        Rows::Stream(stream) => {
311            // Duplicate the value stream
312            let duplicated_stream = stream.dup().await?;
313            Rows::Stream(duplicated_stream)
314        }
315    };
316
317    Ok(ExecResponse::from_rows(values))
318}