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}
134
135impl InstrumentedDriver {
136    pub fn new(driver: Box<dyn Driver>) -> Self {
137        Self {
138            inner: driver,
139            handle: InstrumentedHandle::default(),
140        }
141    }
142
143    /// Get the single control handle for this driver. The handle exposes
144    /// both the operations log and the fault-injection queue.
145    pub fn handle(&self) -> InstrumentedHandle {
146        self.handle.clone()
147    }
148}
149
150#[async_trait]
151impl Driver for InstrumentedDriver {
152    fn url(&self) -> Cow<'_, str> {
153        self.inner.url()
154    }
155
156    fn capability(&self) -> &'static Capability {
157        self.inner.capability()
158    }
159
160    async fn connect(&self, cx: &ConnectContext) -> Result<Box<dyn Connection>> {
161        Ok(Box::new(InstrumentedConnection {
162            inner: self.inner.connect(cx).await?,
163            handle: self.handle.clone(),
164            valid: AtomicBool::new(true),
165        }))
166    }
167
168    fn generate_migration(&self, schema_diff: &diff::Schema<'_>) -> Migration {
169        self.inner.generate_migration(schema_diff)
170    }
171
172    async fn reset_db(&self) -> Result<()> {
173        self.inner.reset_db().await
174    }
175}
176
177/// Per-connection counterpart of [`InstrumentedDriver`]: records each
178/// `exec` and consults the shared fault queue before delegating.
179#[derive(Debug)]
180pub struct InstrumentedConnection {
181    /// The underlying driver that actually executes operations
182    inner: Box<dyn Connection>,
183
184    /// Shared handle: ops log + fault queue.
185    handle: InstrumentedHandle,
186
187    /// Set to `false` once an injected `ConnectionLost` fault has fired
188    /// against this connection. Surfaced through [`Connection::is_valid`]
189    /// so the pool evicts it the same way it would after a real
190    /// connection-lost error.
191    valid: AtomicBool,
192}
193
194#[async_trait]
195impl Connection for InstrumentedConnection {
196    async fn exec(&mut self, schema: &Arc<Schema>, operation: Operation) -> Result<ExecResponse> {
197        // Pop a queued fault, if any, and short-circuit before reaching
198        // the underlying driver.
199        let fault = self
200            .handle
201            .inner
202            .faults
203            .lock()
204            .expect("Failed to acquire faults lock")
205            .pop_front();
206        if let Some(fault) = fault {
207            match fault {
208                Fault::OperationFailed => {
209                    return Err(toasty_core::Error::driver_operation_failed(
210                        std::io::Error::other("injected operation failure"),
211                    ));
212                }
213                Fault::ConnectionLost => {
214                    self.valid.store(false, Ordering::Release);
215                    return Err(toasty_core::Error::connection_lost(std::io::Error::other(
216                        "injected connection-lost fault",
217                    )));
218                }
219            }
220        }
221
222        // Clone the operation for logging
223        let operation_clone = operation.clone();
224
225        // Execute the operation on the underlying driver
226        let mut response = self.inner.exec(schema, operation).await?;
227
228        // Duplicate the response for logging
229        let duplicated_response = duplicate_response_mut(&mut response).await?;
230
231        // Log the operation and response
232        let driver_op = DriverOp {
233            operation: operation_clone,
234            response: duplicated_response,
235        };
236
237        self.handle
238            .inner
239            .ops_log
240            .lock()
241            .expect("Failed to acquire ops log lock")
242            .push(driver_op);
243
244        Ok(response)
245    }
246
247    async fn push_schema(&mut self, schema: &Schema) -> Result<()> {
248        self.inner.push_schema(schema).await
249    }
250
251    async fn applied_migrations(&mut self) -> Result<Vec<AppliedMigration>> {
252        self.inner.applied_migrations().await
253    }
254
255    async fn apply_migration(&mut self, id: u64, name: &str, migration: &Migration) -> Result<()> {
256        self.inner.apply_migration(id, name, migration).await
257    }
258
259    fn is_valid(&self) -> bool {
260        self.valid.load(Ordering::Acquire) && self.inner.is_valid()
261    }
262
263    async fn ping(&mut self) -> Result<()> {
264        // Consume a queued fault before delegating, mirroring `exec`.
265        // A `ConnectionLost` fault here lets tests target the sweep's
266        // ping path the same way they target user query paths.
267        let fault = self
268            .handle
269            .inner
270            .faults
271            .lock()
272            .expect("Failed to acquire faults lock")
273            .pop_front();
274        if let Some(fault) = fault {
275            match fault {
276                Fault::OperationFailed => {
277                    return Err(toasty_core::Error::driver_operation_failed(
278                        std::io::Error::other("injected operation failure"),
279                    ));
280                }
281                Fault::ConnectionLost => {
282                    self.valid.store(false, Ordering::Release);
283                    return Err(toasty_core::Error::connection_lost(std::io::Error::other(
284                        "injected connection-lost fault",
285                    )));
286                }
287            }
288        }
289        self.inner.ping().await
290    }
291}
292
293/// Duplicate an ExecResponse, using ValueStream::dup() for value streams
294/// This version takes a mutable reference so we can call dup() on the ValueStream
295async fn duplicate_response_mut(response: &mut ExecResponse) -> Result<ExecResponse> {
296    let values = match &mut response.values {
297        Rows::Count(count) => Rows::Count(*count),
298        Rows::Value(_) => todo!(),
299        Rows::Stream(stream) => {
300            // Duplicate the value stream
301            let duplicated_stream = stream.dup().await?;
302            Rows::Stream(duplicated_stream)
303        }
304    };
305
306    Ok(ExecResponse::from_rows(values))
307}