Skip to main content

coven_storage/cloud/
test_utils.rs

1//! In-process CloudHome implementation for tests. Records every write keyed
2//! by cloud_key so tests can read back exactly what landed, and serves reads
3//! from the same map — enough to simulate two devices sharing a cloud bucket.
4//!
5//! Available under `#[cfg(test)]` in this crate itself and to the crates above
6//! it that enable the `test-utils` feature.
7
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
10use std::sync::Arc;
11use std::sync::Mutex;
12
13use async_trait::async_trait;
14use bytes::Bytes;
15
16use super::{
17    BoxPartSink, CloudAccessOutcome, CloudAccessState, CloudFileReadError, CloudHome,
18    CloudHomeError, CloudObjectVersion, CloudVersionedObject, ConditionalWriteOutcome,
19    ExactSlotStorage, PartSink,
20};
21use coven_protocol::objects::ObjectSlot;
22
23#[derive(Clone)]
24struct AppendPause {
25    call: usize,
26    reached: Arc<tokio::sync::Notify>,
27    release: Arc<tokio::sync::Notify>,
28}
29
30struct ExactStreamReadGuard {
31    inflight: Arc<AtomicUsize>,
32}
33
34struct InflightGuard {
35    inflight: Arc<AtomicUsize>,
36}
37
38#[derive(Clone)]
39struct OperationPause {
40    reached: Arc<tokio::sync::Notify>,
41    release: Arc<tokio::sync::Notify>,
42}
43
44#[derive(Clone)]
45struct MemoryObject {
46    bytes: Vec<u8>,
47    version: u64,
48}
49
50struct MemoryObjects {
51    values: HashMap<String, MemoryObject>,
52    next_version: u64,
53}
54
55impl MemoryObjects {
56    fn new() -> Self {
57        Self {
58            values: HashMap::new(),
59            next_version: 0,
60        }
61    }
62
63    fn insert(&mut self, key: String, bytes: Vec<u8>) -> Option<MemoryObject> {
64        self.next_version = self
65            .next_version
66            .checked_add(1)
67            .expect("in-memory cloud object version overflow");
68        self.values.insert(
69            key,
70            MemoryObject {
71                bytes,
72                version: self.next_version,
73            },
74        )
75    }
76
77    fn bytes(&self, key: &str) -> Option<Vec<u8>> {
78        self.values.get(key).map(|object| object.bytes.clone())
79    }
80}
81
82impl Drop for ExactStreamReadGuard {
83    fn drop(&mut self) {
84        self.inflight.fetch_sub(1, Ordering::SeqCst);
85    }
86}
87
88impl Drop for InflightGuard {
89    fn drop(&mut self) {
90        self.inflight.fetch_sub(1, Ordering::SeqCst);
91    }
92}
93
94/// In-memory CloudHome backed by a HashMap. `Clone` shares one backing store, so
95/// clones act as separate devices reading and writing the same cloud bucket, and
96/// a test can keep its own handle for direct at-rest assertions while each device
97/// owns an exact cloud-home clone.
98///
99/// Beyond the happy path it carries fault-injection knobs
100/// ([`arm_write_failures`](Self::arm_write_failures),
101/// [`fail_next_range_reads`](Self::fail_next_range_reads),
102/// [`remove`](Self::remove)) so a host test can drive upload-failure,
103/// read-retry, and missing-blob paths without a bespoke `CloudHome` impl. The
104/// arming state is shared across clones, like the backing store.
105#[derive(Clone)]
106pub struct InMemoryCloudHome {
107    provider_binding: coven_protocol::objects::ResolvedProviderBinding,
108    writes: Arc<Mutex<MemoryObjects>>,
109    access_requests: Arc<Mutex<Vec<CloudAccessState>>>,
110    exact_slot_allocations: Arc<AtomicUsize>,
111    exact_slot_allocation_delay_millis: Arc<AtomicU64>,
112    exact_slot_allocation_inflight: Arc<AtomicUsize>,
113    exact_slot_allocation_max_inflight: Arc<AtomicUsize>,
114    deletes: Arc<Mutex<Vec<String>>>,
115    fail_writes: Arc<AtomicBool>,
116    fail_next_range_reads: Arc<AtomicUsize>,
117    fail_next_exact_stream_reads: Arc<AtomicUsize>,
118    sort_listings: Arc<AtomicBool>,
119    exact_create_count: Arc<AtomicUsize>,
120    exact_creates: Arc<Mutex<Vec<ObjectSlot>>>,
121    fail_exact_create_before: Arc<AtomicUsize>,
122    fail_exact_create_after: Arc<AtomicUsize>,
123    lose_next_conditional_replace_response: Arc<AtomicBool>,
124    conditional_replace_pause: Arc<Mutex<Option<OperationPause>>>,
125    exact_create_pause: Arc<Mutex<Option<AppendPause>>>,
126    probe_pause: Arc<Mutex<Option<OperationPause>>>,
127    probe_failure: Arc<Mutex<Option<coven_protocol::objects::StorageBackendFailure>>>,
128    exact_full_read_count: Arc<AtomicUsize>,
129    exact_full_read_delay_millis: Arc<AtomicU64>,
130    exact_list_count: Arc<AtomicUsize>,
131    exact_listed_prefixes: Arc<Mutex<Vec<String>>>,
132    exact_full_read_inflight: Arc<AtomicUsize>,
133    exact_full_read_max_inflight: Arc<AtomicUsize>,
134    exact_stream_read_count: Arc<AtomicUsize>,
135    exact_reads: Arc<Mutex<Vec<ObjectSlot>>>,
136    exact_stream_read_inflight: Arc<AtomicUsize>,
137    exact_stream_read_max_inflight: Arc<AtomicUsize>,
138    exact_stream_read_barrier: Arc<Mutex<Option<Arc<tokio::sync::Barrier>>>>,
139    exact_stream_read_chunk_bytes: Arc<AtomicUsize>,
140    exact_stream_read_chunk_delay_millis: Arc<AtomicU64>,
141    exact_delete_count: Arc<AtomicUsize>,
142    /// Every ranged exact read this home has served, as `(start, end)` stored
143    /// offsets. What a test counts to say a read cost the bytes it asked for and
144    /// no more — a full read is counted separately, by `exact_full_read_count`
145    /// and `exact_stream_read_count`, so reintroducing a whole-object fetch
146    /// shows up as a full read rather than hiding inside the range total.
147    exact_range_reads: Arc<Mutex<Vec<(u64, u64)>>>,
148    fail_exact_delete_on: Arc<AtomicUsize>,
149    fail_exact_delete_of: Arc<Mutex<Option<TargetedDeleteFailure>>>,
150}
151
152/// Fail the `countdown`-th delete of an object whose key is in `keys`, counting
153/// only those deletes. Set by [`InMemoryCloudHome::fail_nth_exact_delete_of`].
154struct TargetedDeleteFailure {
155    keys: std::collections::HashSet<String>,
156    countdown: usize,
157    failure: TargetedDeleteFailureKind,
158}
159
160/// What the armed delete returns: a transport fault a later attempt may clear,
161/// or a provider refusal no attempt can.
162enum TargetedDeleteFailureKind {
163    Transport,
164    Permanent,
165}
166
167impl InMemoryCloudHome {
168    pub fn new() -> Self {
169        Self {
170            provider_binding: coven_protocol::objects::ResolvedProviderBinding {
171                store: coven_protocol::objects::StoreProviderBinding::S3 {
172                    endpoint: coven_protocol::objects::S3EndpointBinding::Custom {
173                        origin: "https://in-memory.invalid".to_string(),
174                    },
175                    region: "test".to_string(),
176                    bucket: "in-memory".to_string(),
177                    key_prefix: None,
178                },
179                device: coven_protocol::objects::ProviderDeviceBinding {
180                    principal: coven_protocol::objects::ProviderPrincipalId::CustomS3Credential {
181                        access_key_id_hash: coven_protocol::store_commit::ObjectHash::digest(
182                            b"coven.s3-access-key-id.v1\0in-memory",
183                        ),
184                    },
185                },
186            },
187            writes: Arc::new(Mutex::new(MemoryObjects::new())),
188            access_requests: Arc::new(Mutex::new(Vec::new())),
189            exact_slot_allocations: Arc::new(AtomicUsize::new(0)),
190            exact_slot_allocation_delay_millis: Arc::new(AtomicU64::new(0)),
191            exact_slot_allocation_inflight: Arc::new(AtomicUsize::new(0)),
192            exact_slot_allocation_max_inflight: Arc::new(AtomicUsize::new(0)),
193            deletes: Arc::new(Mutex::new(Vec::new())),
194            fail_writes: Arc::new(AtomicBool::new(false)),
195            fail_next_range_reads: Arc::new(AtomicUsize::new(0)),
196            fail_next_exact_stream_reads: Arc::new(AtomicUsize::new(0)),
197            sort_listings: Arc::new(AtomicBool::new(false)),
198            exact_create_count: Arc::new(AtomicUsize::new(0)),
199            exact_creates: Arc::new(Mutex::new(Vec::new())),
200            fail_exact_create_before: Arc::new(AtomicUsize::new(0)),
201            fail_exact_create_after: Arc::new(AtomicUsize::new(0)),
202            lose_next_conditional_replace_response: Arc::new(AtomicBool::new(false)),
203            conditional_replace_pause: Arc::new(Mutex::new(None)),
204            exact_create_pause: Arc::new(Mutex::new(None)),
205            probe_pause: Arc::new(Mutex::new(None)),
206            probe_failure: Arc::new(Mutex::new(None)),
207            exact_full_read_count: Arc::new(AtomicUsize::new(0)),
208            exact_list_count: Arc::new(AtomicUsize::new(0)),
209            exact_listed_prefixes: Arc::new(Mutex::new(Vec::new())),
210            exact_full_read_delay_millis: Arc::new(AtomicU64::new(0)),
211            exact_full_read_inflight: Arc::new(AtomicUsize::new(0)),
212            exact_full_read_max_inflight: Arc::new(AtomicUsize::new(0)),
213            exact_stream_read_count: Arc::new(AtomicUsize::new(0)),
214            exact_reads: Arc::new(Mutex::new(Vec::new())),
215            exact_stream_read_inflight: Arc::new(AtomicUsize::new(0)),
216            exact_stream_read_max_inflight: Arc::new(AtomicUsize::new(0)),
217            exact_stream_read_barrier: Arc::new(Mutex::new(None)),
218            exact_stream_read_chunk_bytes: Arc::new(AtomicUsize::new(0)),
219            exact_stream_read_chunk_delay_millis: Arc::new(AtomicU64::new(0)),
220            exact_delete_count: Arc::new(AtomicUsize::new(0)),
221            exact_range_reads: Arc::new(Mutex::new(Vec::new())),
222            fail_exact_delete_on: Arc::new(AtomicUsize::new(0)),
223            fail_exact_delete_of: Arc::new(Mutex::new(None)),
224        }
225    }
226
227    pub fn with_provider_binding(
228        mut self,
229        binding: coven_protocol::objects::ResolvedProviderBinding,
230    ) -> Self {
231        binding
232            .validate()
233            .expect("in-memory provider binding must be valid");
234        self.provider_binding = binding;
235        self
236    }
237
238    /// Return `list` results in sorted key order instead of the backing map's
239    /// arbitrary order. A real bucket LIST has no defined order, so the pull's
240    /// cross-device apply order is arbitrary; a test that needs a fixed order (to
241    /// reproduce an order-dependent bug deterministically) arms this and picks the
242    /// order through its device ids.
243    pub fn sort_listings(&self) {
244        self.sort_listings.store(true, Ordering::SeqCst);
245    }
246
247    /// Arm every subsequent write (`put_object` and `open_multipart`) to fail
248    /// with a retryable transport error. A test can let a home's setup writes
249    /// land and then arm this before driving the path whose uploads must fail;
250    /// it stays armed for the store's lifetime.
251    pub fn arm_write_failures(&self) {
252        self.fail_writes.store(true, Ordering::SeqCst);
253    }
254
255    /// Let writes through again, so a test can drive the resume that follows a
256    /// failed one rather than only the failure.
257    pub fn clear_write_failures(&self) {
258        self.fail_writes.store(false, Ordering::SeqCst);
259    }
260
261    /// Make the next `n` `read_range` calls fail with a retryable transport
262    /// error before any serves bytes, to exercise a caller's read-retry path.
263    /// Each failed call consumes one; once `n` are spent, ranges serve
264    /// normally.
265    pub fn fail_next_range_reads(&self, n: usize) {
266        self.fail_next_range_reads.store(n, Ordering::SeqCst);
267    }
268
269    /// Make the next `n` exact streaming reads fail before creating their local
270    /// destination. This drives retry of operations such as snapshot bootstrap
271    /// without changing the durable object stored in the shared test home.
272    pub fn fail_next_exact_stream_reads(&self, n: usize) {
273        self.fail_next_exact_stream_reads.store(n, Ordering::SeqCst);
274    }
275
276    /// Serve exact streaming reads in chunks separated by `delay`. This lets a
277    /// test drive the real streaming progress path instead of calling an
278    /// observer directly.
279    pub fn stream_exact_reads_in_chunks(&self, chunk_bytes: usize, delay: std::time::Duration) {
280        assert!(chunk_bytes > 0, "stream chunk size must be nonzero");
281        self.exact_stream_read_chunk_bytes
282            .store(chunk_bytes, Ordering::SeqCst);
283        self.exact_stream_read_chunk_delay_millis.store(
284            u64::try_from(delay.as_millis()).expect("test stream delay fits u64 milliseconds"),
285            Ordering::SeqCst,
286        );
287    }
288
289    /// Reset the exact-create counter and fail before the selected call stores bytes.
290    pub fn fail_exact_create_before_call(&self, call: usize) {
291        assert!(call > 0, "create call numbers are 1-based");
292        self.exact_create_count.store(0, Ordering::SeqCst);
293        self.fail_exact_create_before.store(call, Ordering::SeqCst);
294    }
295
296    /// Reset the exact-create counter and lose the response after the selected create.
297    pub fn fail_exact_create_after_call(&self, call: usize) {
298        assert!(call > 0, "create call numbers are 1-based");
299        self.exact_create_count.store(0, Ordering::SeqCst);
300        self.fail_exact_create_after.store(call, Ordering::SeqCst);
301    }
302
303    /// Commit the next conditional replacement but return a transport error,
304    /// reproducing a response lost after the provider accepted the write.
305    pub fn lose_next_conditional_replace_response(&self) {
306        self.lose_next_conditional_replace_response
307            .store(true, Ordering::SeqCst);
308    }
309
310    /// Pause after the next successful conditional replacement becomes visible.
311    pub fn pause_next_conditional_replace(
312        &self,
313    ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
314        let reached = Arc::new(tokio::sync::Notify::new());
315        let release = Arc::new(tokio::sync::Notify::new());
316        *self.conditional_replace_pause.lock().unwrap() = Some(OperationPause {
317            reached: reached.clone(),
318            release: release.clone(),
319        });
320        (reached, release)
321    }
322
323    /// Pause after the selected exact create is physically visible.
324    pub fn pause_after_exact_create_call(
325        &self,
326        call: usize,
327    ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
328        assert!(call > 0, "create call numbers are 1-based");
329        self.exact_create_count.store(0, Ordering::SeqCst);
330        let reached = Arc::new(tokio::sync::Notify::new());
331        let release = Arc::new(tokio::sync::Notify::new());
332        *self.exact_create_pause.lock().unwrap() = Some(AppendPause {
333            call,
334            reached: reached.clone(),
335            release: release.clone(),
336        });
337        (reached, release)
338    }
339
340    /// Pause the next reachability probe after it starts and before it succeeds.
341    pub fn pause_next_probe(&self) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
342        let reached = Arc::new(tokio::sync::Notify::new());
343        let release = Arc::new(tokio::sync::Notify::new());
344        *self.probe_pause.lock().unwrap() = Some(OperationPause {
345            reached: reached.clone(),
346            release: release.clone(),
347        });
348        (reached, release)
349    }
350
351    pub fn fail_next_probe_with(&self, failure: coven_protocol::objects::StorageBackendFailure) {
352        *self.probe_failure.lock().unwrap() = Some(failure);
353    }
354
355    pub fn exact_create_count(&self) -> usize {
356        self.exact_create_count.load(Ordering::SeqCst)
357    }
358
359    pub fn delay_exact_slot_allocations(&self, delay: std::time::Duration) {
360        self.exact_slot_allocation_delay_millis.store(
361            u64::try_from(delay.as_millis()).expect("test allocation delay fits u64 milliseconds"),
362            Ordering::SeqCst,
363        );
364        self.exact_slot_allocation_inflight
365            .store(0, Ordering::SeqCst);
366        self.exact_slot_allocation_max_inflight
367            .store(0, Ordering::SeqCst);
368    }
369
370    pub fn exact_slot_allocation_max_inflight(&self) -> usize {
371        self.exact_slot_allocation_max_inflight
372            .load(Ordering::SeqCst)
373    }
374
375    pub fn exact_creates(&self) -> Vec<ObjectSlot> {
376        self.exact_creates.lock().unwrap().clone()
377    }
378
379    pub fn clear_exact_creates(&self) {
380        self.exact_creates.lock().unwrap().clear();
381    }
382
383    pub fn exact_full_read_count(&self) -> usize {
384        self.exact_full_read_count.load(Ordering::SeqCst)
385    }
386
387    pub fn exact_list_count(&self) -> usize {
388        self.exact_list_count.load(Ordering::SeqCst)
389    }
390
391    pub fn exact_listed_prefixes(&self) -> Vec<String> {
392        self.exact_listed_prefixes.lock().unwrap().clone()
393    }
394
395    pub fn clear_exact_listings(&self) {
396        self.exact_list_count.store(0, Ordering::SeqCst);
397        self.exact_listed_prefixes.lock().unwrap().clear();
398    }
399
400    /// Delay every whole-object exact read and measure their concurrency.
401    pub fn delay_exact_full_reads(&self, delay: std::time::Duration) {
402        self.exact_full_read_delay_millis.store(
403            u64::try_from(delay.as_millis()).expect("test read delay fits u64 milliseconds"),
404            Ordering::SeqCst,
405        );
406        self.exact_full_read_inflight.store(0, Ordering::SeqCst);
407        self.exact_full_read_max_inflight.store(0, Ordering::SeqCst);
408    }
409
410    pub fn exact_full_read_max_inflight(&self) -> usize {
411        self.exact_full_read_max_inflight.load(Ordering::SeqCst)
412    }
413
414    /// Every ranged exact read served so far, as `(start, end)` stored offsets.
415    pub fn exact_range_reads(&self) -> Vec<(u64, u64)> {
416        self.exact_range_reads.lock().unwrap().clone()
417    }
418
419    /// Total stored bytes ranged reads have transferred.
420    pub fn exact_range_read_bytes(&self) -> u64 {
421        self.exact_range_reads
422            .lock()
423            .unwrap()
424            .iter()
425            .map(|(start, end)| end - start)
426            .sum()
427    }
428
429    pub fn clear_exact_range_reads(&self) {
430        self.exact_range_reads.lock().unwrap().clear();
431    }
432
433    pub fn exact_stream_read_count(&self) -> usize {
434        self.exact_stream_read_count.load(Ordering::SeqCst)
435    }
436
437    pub fn exact_reads(&self) -> Vec<ObjectSlot> {
438        self.exact_reads.lock().unwrap().clone()
439    }
440
441    pub fn clear_exact_reads(&self) {
442        self.exact_reads.lock().unwrap().clear();
443    }
444
445    pub fn arm_exact_stream_read_concurrency_probe(&self, width: usize) {
446        assert!(width > 0, "exact stream read probe width must be positive");
447        self.exact_stream_read_inflight.store(0, Ordering::SeqCst);
448        self.exact_stream_read_max_inflight
449            .store(0, Ordering::SeqCst);
450        *self.exact_stream_read_barrier.lock().unwrap() =
451            Some(Arc::new(tokio::sync::Barrier::new(width)));
452    }
453
454    pub fn exact_stream_read_max_inflight(&self) -> usize {
455        self.exact_stream_read_max_inflight.load(Ordering::SeqCst)
456    }
457
458    pub fn exact_delete_count(&self) -> usize {
459        self.exact_delete_count.load(Ordering::SeqCst)
460    }
461
462    pub fn fail_exact_delete_on_call(&self, call: usize) {
463        assert!(call > 0, "exact-delete call numbers are 1-based");
464        self.exact_delete_count.store(0, Ordering::SeqCst);
465        self.fail_exact_delete_on.store(call, Ordering::SeqCst);
466    }
467
468    /// Fail the `nth` (1-based) delete of an object among `slots`, counting only
469    /// deletes of those objects, then disarm. Unlike `fail_exact_delete_on_call`,
470    /// which counts every exact delete (probes, candidate cleanup), this counts
471    /// only the identities that matter, so "fail the 2nd package delete" lands
472    /// deterministically however many unrelated deletes interleave and whatever
473    /// order the two package deletes arrive in.
474    pub fn fail_nth_exact_delete_of(&self, slots: &[&ObjectSlot], nth: usize) {
475        self.arm_targeted_delete_failure(slots, nth, TargetedDeleteFailureKind::Transport);
476    }
477
478    /// Fail the `nth` (1-based) delete of an object among `slots` with a
479    /// configuration error — a provider refusal retrying cannot clear — then
480    /// disarm. The permanent counterpart to [`Self::fail_nth_exact_delete_of`],
481    /// and what drives the deterministic-failure paths that treat an operation
482    /// as decided rather than as worth another cycle.
483    pub fn fail_nth_exact_delete_of_permanently(&self, slots: &[&ObjectSlot], nth: usize) {
484        self.arm_targeted_delete_failure(slots, nth, TargetedDeleteFailureKind::Permanent);
485    }
486
487    fn arm_targeted_delete_failure(
488        &self,
489        slots: &[&ObjectSlot],
490        nth: usize,
491        failure: TargetedDeleteFailureKind,
492    ) {
493        assert!(nth > 0, "targeted delete ordinals are 1-based");
494        let keys = slots
495            .iter()
496            .map(|slot| Self::exact_storage_key(slot).expect("test exact slot is valid"))
497            .collect();
498        *self.fail_exact_delete_of.lock().unwrap() = Some(TargetedDeleteFailure {
499            keys,
500            countdown: nth,
501            failure,
502        });
503    }
504
505    /// Drop `key`'s bytes out of band — as if the object vanished from the
506    /// bucket on its own, without a `delete` (which `deletes_seen` would
507    /// record). Drives missing-blob read failures.
508    pub fn remove(&self, key: &str) {
509        self.writes.lock().unwrap().values.remove(key);
510    }
511
512    /// Snapshot of every key currently in the cloud. Useful for assertions
513    /// that don't want to hold the lock across an await.
514    pub fn keys(&self) -> Vec<String> {
515        self.writes.lock().unwrap().values.keys().cloned().collect()
516    }
517
518    /// Snapshot of the bytes at `key`, or `None` if absent. Cloned so the
519    /// caller can hold the result across `await` points without retaining
520    /// the internal lock.
521    pub fn get(&self, key: &str) -> Option<Vec<u8>> {
522        self.writes.lock().unwrap().bytes(key)
523    }
524
525    /// Number of objects stored. Cheap snapshot.
526    pub fn len(&self) -> usize {
527        self.writes.lock().unwrap().values.len()
528    }
529
530    /// Returns true if the store is empty.
531    pub fn is_empty(&self) -> bool {
532        self.writes.lock().unwrap().values.is_empty()
533    }
534
535    /// Snapshot of every delete that's been requested, in arrival order.
536    pub fn deletes_seen(&self) -> Vec<String> {
537        self.deletes.lock().unwrap().clone()
538    }
539
540    pub fn access_requests(&self) -> Vec<CloudAccessState> {
541        self.access_requests.lock().unwrap().clone()
542    }
543
544    /// Insert caller-selected bytes at one exact logical slot.
545    pub fn insert_exact_object(&self, logical_key: &str, bytes: Vec<u8>) -> ObjectSlot {
546        let slot = ObjectSlot::logical(logical_key.to_string())
547            .map_err(CloudHomeError::from)
548            .expect("test logical key is non-empty");
549        self.writes
550            .lock()
551            .unwrap()
552            .insert(logical_key.to_string(), bytes);
553        slot
554    }
555
556    /// Snapshot the bytes stored at one exact slot, or `None` if absent.
557    pub fn stored_exact_bytes(&self, slot: &ObjectSlot) -> Option<Vec<u8>> {
558        let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
559        self.writes.lock().unwrap().bytes(&key)
560    }
561
562    /// Whether one exact protocol object is currently stored.
563    pub fn contains_exact_object(&self, object: &coven_protocol::objects::ExactObjectRef) -> bool {
564        let key = Self::exact_storage_key(object.slot()).expect("test exact slot is valid");
565        self.writes.lock().unwrap().values.contains_key(&key)
566    }
567
568    /// Re-insert bytes at one exact slot, restoring an object dropped by
569    /// [`remove_exact_object`](Self::remove_exact_object).
570    pub fn restore_exact_object(&self, slot: &ObjectSlot, bytes: Vec<u8>) {
571        let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
572        self.writes.lock().unwrap().insert(key, bytes);
573    }
574
575    /// Remove one exact object without recording a protocol delete.
576    pub fn remove_exact_object(&self, slot: &ObjectSlot) {
577        let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
578        self.writes.lock().unwrap().values.remove(&key);
579    }
580
581    /// The bytes currently stored at one exact slot, without counting a read.
582    pub fn stored_exact_object(&self, slot: &ObjectSlot) -> Vec<u8> {
583        self.writes
584            .lock()
585            .unwrap()
586            .bytes(&Self::exact_storage_key(slot).expect("test exact slot is valid"))
587            .expect("exact slot exists")
588    }
589
590    /// Replace bytes at one exact slot without changing its locator.
591    pub fn replace_exact_object(&self, slot: &ObjectSlot, bytes: Vec<u8>) {
592        let previous = self.writes.lock().unwrap().insert(
593            Self::exact_storage_key(slot).expect("test exact slot is valid"),
594            bytes,
595        );
596        assert!(previous.is_some(), "exact slot exists");
597    }
598
599    async fn put_object(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError> {
600        if self.fail_writes.load(Ordering::SeqCst) {
601            return Err(CloudHomeError::Transport(
602                "InMemoryCloudHome: armed write failure".into(),
603            ));
604        }
605        self.writes.lock().unwrap().insert(key.to_string(), data);
606        Ok(())
607    }
608
609    async fn open_multipart<'a>(
610        &'a self,
611        key: &str,
612        _total_len: u64,
613    ) -> Result<BoxPartSink<'a>, CloudHomeError> {
614        // Gate multipart too, so `arm_write_failures` fails a write whatever its
615        // size — `write_blob` routes blobs above `multipart_threshold` here.
616        if self.fail_writes.load(Ordering::SeqCst) {
617            return Err(CloudHomeError::Transport(
618                "InMemoryCloudHome: armed write failure".into(),
619            ));
620        }
621        Ok(Box::new(InMemoryPartSink {
622            writes: self.writes.clone(),
623            key: key.to_string(),
624            buf: Vec::new(),
625        }))
626    }
627
628    fn multipart_threshold(&self) -> u64 {
629        // A small threshold so tests exercise the multipart driver path; the part
630        // size matches so a multi-part blob ticks progress several times.
631        super::PROGRESS_CHUNK_SIZE as u64
632    }
633    fn validate_exact_slot(slot: &ObjectSlot) -> Result<(), CloudHomeError> {
634        slot.validate()?;
635        Ok(())
636    }
637
638    fn exact_storage_key(slot: &ObjectSlot) -> Result<String, CloudHomeError> {
639        Self::validate_exact_slot(slot)?;
640        Ok(match slot.physical() {
641            coven_protocol::objects::PhysicalObjectLocator::LogicalKey => {
642                slot.logical_key().to_string()
643            }
644            coven_protocol::objects::PhysicalObjectLocator::Opaque(provider_id) => {
645                format!("{}#exact#{provider_id}", slot.logical_key())
646            }
647        })
648    }
649
650    /// The slot [`exact_storage_key`](Self::exact_storage_key) built this
651    /// bucket key from, so a listing recovers the locator the writer allocated
652    /// the same way a real provider's listing does.
653    fn exact_slot_from_storage_key(key: &str) -> Result<ObjectSlot, CloudHomeError> {
654        match key.split_once("#exact#") {
655            Some((logical_key, provider_id)) => {
656                ObjectSlot::opaque(logical_key.to_string(), provider_id.to_string())
657            }
658            None => ObjectSlot::logical(key.to_string()),
659        }
660        .map_err(CloudHomeError::from)
661    }
662
663    async fn create_at_slot(
664        &self,
665        upload: &super::ExactUpload<'_>,
666        control: &super::UploadControl,
667    ) -> Result<super::ExactCreateOutcome, CloudHomeError> {
668        if self.fail_writes.load(Ordering::SeqCst) {
669            return Err(CloudHomeError::Transport(
670                "InMemoryCloudHome: armed write failure".into(),
671            ));
672        }
673        let slot = upload.object().slot();
674        self.exact_creates.lock().unwrap().push(slot.clone());
675        let key = Self::exact_storage_key(slot)?;
676        let call = self.exact_create_count.fetch_add(1, Ordering::SeqCst) + 1;
677        if self.fail_exact_create_before.load(Ordering::SeqCst) == call {
678            self.fail_exact_create_before.store(0, Ordering::SeqCst);
679            return Err(CloudHomeError::Transport(format!(
680                "InMemoryCloudHome: forced failure before exact create call {call}"
681            )));
682        }
683        let bytes = upload.body().await?.collect().await?;
684        control.report(bytes.len() as u64);
685        {
686            let mut writes = self.writes.lock().unwrap();
687            if let Some(existing) = writes.values.get(&key) {
688                return if upload.object().verify(&existing.bytes).is_ok() {
689                    Ok(super::ExactCreateOutcome::AlreadyPresent)
690                } else {
691                    Err(CloudHomeError::SlotCollision(key))
692                };
693            }
694            writes.insert(key.clone(), bytes);
695        }
696        let pause = self
697            .exact_create_pause
698            .lock()
699            .unwrap()
700            .clone()
701            .filter(|pause| pause.call == call);
702        if let Some(pause) = pause {
703            pause.reached.notify_one();
704            pause.release.notified().await;
705            self.exact_create_pause.lock().unwrap().take();
706        }
707        if self.fail_exact_create_after.load(Ordering::SeqCst) == call {
708            self.fail_exact_create_after.store(0, Ordering::SeqCst);
709            let stored_matches = self
710                .writes
711                .lock()
712                .unwrap()
713                .values
714                .get(&key)
715                .is_some_and(|stored| upload.object().verify(&stored.bytes).is_ok());
716            if !stored_matches {
717                return Err(CloudHomeError::Transport(format!(
718                    "InMemoryCloudHome: forced failure after exact create call {call}"
719                )));
720            }
721        }
722        let stored_matches = self
723            .writes
724            .lock()
725            .unwrap()
726            .values
727            .get(&key)
728            .is_some_and(|stored| upload.object().verify(&stored.bytes).is_ok());
729        if !stored_matches {
730            return Err(CloudHomeError::SlotCollision(key));
731        }
732        Ok(super::ExactCreateOutcome::Created)
733    }
734
735    async fn read_exact(&self, slot: &ObjectSlot) -> Result<Vec<u8>, CloudHomeError> {
736        self.exact_full_read_count.fetch_add(1, Ordering::SeqCst);
737        let inflight = self.exact_full_read_inflight.fetch_add(1, Ordering::SeqCst) + 1;
738        self.exact_full_read_max_inflight
739            .fetch_max(inflight, Ordering::SeqCst);
740        let _guard = InflightGuard {
741            inflight: self.exact_full_read_inflight.clone(),
742        };
743        let delay = self.exact_full_read_delay_millis.load(Ordering::SeqCst);
744        if delay > 0 {
745            tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
746        }
747        let key = Self::exact_storage_key(slot)?;
748        self.exact_reads.lock().unwrap().push(slot.clone());
749        self.writes
750            .lock()
751            .unwrap()
752            .bytes(&key)
753            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))
754    }
755
756    async fn read_exact_to_file(
757        &self,
758        slot: &ObjectSlot,
759        destination: &std::path::Path,
760        progress: super::DownloadProgress,
761    ) -> Result<(), CloudFileReadError> {
762        if self
763            .fail_next_exact_stream_reads
764            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
765            .is_ok()
766        {
767            return Err(CloudHomeError::Transport(
768                "InMemoryCloudHome: armed exact stream-read failure".into(),
769            )
770            .into());
771        }
772        self.exact_stream_read_count.fetch_add(1, Ordering::SeqCst);
773        self.exact_reads.lock().unwrap().push(slot.clone());
774        let inflight = self
775            .exact_stream_read_inflight
776            .fetch_add(1, Ordering::SeqCst)
777            + 1;
778        self.exact_stream_read_max_inflight
779            .fetch_max(inflight, Ordering::SeqCst);
780        let _guard = ExactStreamReadGuard {
781            inflight: self.exact_stream_read_inflight.clone(),
782        };
783        let barrier = self.exact_stream_read_barrier.lock().unwrap().clone();
784        if let Some(barrier) = barrier {
785            barrier.wait().await;
786        }
787        let key = Self::exact_storage_key(slot)?;
788        let bytes = self
789            .writes
790            .lock()
791            .unwrap()
792            .bytes(&key)
793            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))?;
794        let chunk_bytes = self.exact_stream_read_chunk_bytes.load(Ordering::SeqCst);
795        let chunk_delay = std::time::Duration::from_millis(
796            self.exact_stream_read_chunk_delay_millis
797                .load(Ordering::SeqCst),
798        );
799        let chunks = if chunk_bytes == 0 {
800            vec![bytes::Bytes::from(bytes)]
801        } else {
802            bytes
803                .chunks(chunk_bytes)
804                .map(bytes::Bytes::copy_from_slice)
805                .collect::<Vec<_>>()
806        };
807        let stream = futures_util::StreamExt::then(
808            futures_util::stream::iter(chunks),
809            move |chunk| async move {
810                if !chunk_delay.is_zero() {
811                    tokio::time::sleep(chunk_delay).await;
812                }
813                Ok(chunk)
814            },
815        );
816        super::write_cloud_object_stream(destination, Box::pin(stream), progress).await?;
817        Ok(())
818    }
819
820    async fn delete_exact(&self, slot: &ObjectSlot) -> Result<(), CloudHomeError> {
821        let key = Self::exact_storage_key(slot)?;
822        let call = self.exact_delete_count.fetch_add(1, Ordering::SeqCst) + 1;
823        if self.fail_exact_delete_on.load(Ordering::SeqCst) == call {
824            self.fail_exact_delete_on.store(0, Ordering::SeqCst);
825            return Err(CloudHomeError::Transport(format!(
826                "InMemoryCloudHome: forced exact delete failure on call {call}"
827            )));
828        }
829        {
830            let mut targeted = self.fail_exact_delete_of.lock().unwrap();
831            if let Some(failure) = targeted.as_mut() {
832                if failure.keys.contains(&key) {
833                    failure.countdown -= 1;
834                    if failure.countdown == 0 {
835                        let error = match failure.failure {
836                            TargetedDeleteFailureKind::Transport => CloudHomeError::Transport(
837                                format!("InMemoryCloudHome: forced exact delete failure of {key}"),
838                            ),
839                            TargetedDeleteFailureKind::Permanent => CloudHomeError::Configuration(
840                                format!("InMemoryCloudHome: refused to delete {key}"),
841                            ),
842                        };
843                        *targeted = None;
844                        return Err(error);
845                    }
846                }
847            }
848        }
849        self.writes.lock().unwrap().values.remove(&key);
850        self.deletes.lock().unwrap().push(key);
851        Ok(())
852    }
853    async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError> {
854        self.writes
855            .lock()
856            .unwrap()
857            .bytes(key)
858            .ok_or_else(|| CloudHomeError::NotFound(key.to_string()))
859    }
860
861    async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError> {
862        // An armed range read fails before touching the store. `checked_sub`
863        // returns `None` at zero, so `fetch_update` only succeeds (and errors)
864        // while the countdown is positive.
865        if self
866            .fail_next_range_reads
867            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
868            .is_ok()
869        {
870            return Err(CloudHomeError::Transport(
871                "InMemoryCloudHome: armed range-read failure".into(),
872            ));
873        }
874        let data = self.read(key).await?;
875        let s = start as usize;
876        let e = (end as usize).min(data.len());
877        if s > data.len() {
878            return Err(CloudHomeError::NotFound(format!("range past end of {key}")));
879        }
880        Ok(data[s..e].to_vec())
881    }
882
883    async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError> {
884        let mut keys: Vec<String> = self
885            .writes
886            .lock()
887            .unwrap()
888            .values
889            .keys()
890            .filter(|k| k.starts_with(prefix))
891            .cloned()
892            .collect();
893        if self.sort_listings.load(Ordering::SeqCst) {
894            keys.sort();
895        }
896        Ok(keys)
897    }
898
899    async fn delete(&self, key: &str) -> Result<(), CloudHomeError> {
900        self.writes.lock().unwrap().values.remove(key);
901        self.deletes.lock().unwrap().push(key.to_string());
902        Ok(())
903    }
904
905    async fn exists(&self, key: &str) -> Result<bool, CloudHomeError> {
906        Ok(self.writes.lock().unwrap().values.contains_key(key))
907    }
908
909    async fn set_access(
910        &self,
911        desired: super::CloudAccessState,
912    ) -> Result<super::CloudAccessOutcome, CloudHomeError> {
913        self.access_requests.lock().unwrap().push(desired.clone());
914        Ok(match desired {
915            super::CloudAccessState::Present { .. } => {
916                super::CloudAccessOutcome::Present(super::CloudHomeJoinInfo::S3 {
917                    bucket: "in-memory".to_string(),
918                    region: "test".to_string(),
919                    endpoint: Some("https://in-memory.invalid".to_string()),
920                    access_key: "in-memory".to_string(),
921                    secret_key: "in-memory".to_string(),
922                    key_prefix: None,
923                })
924            }
925            super::CloudAccessState::Absent { .. } => {
926                super::CloudAccessOutcome::Absent(super::RevokeOutcome::Unsupported)
927            }
928        })
929    }
930}
931
932impl Default for InMemoryCloudHome {
933    fn default() -> Self {
934        Self::new()
935    }
936}
937
938/// A [`PartSink`] for the in-memory backend: accumulate the streamed parts in
939/// order and store the assembled object on `finish`, so a multipart upload
940/// round-trips exactly like a single `put_object`.
941struct InMemoryPartSink {
942    writes: Arc<Mutex<MemoryObjects>>,
943    key: String,
944    buf: Vec<u8>,
945}
946
947#[async_trait]
948impl PartSink for InMemoryPartSink {
949    fn part_size(&self) -> usize {
950        super::PROGRESS_CHUNK_SIZE
951    }
952
953    async fn send_part(
954        &mut self,
955        part: Bytes,
956        _offset: u64,
957        _is_last: bool,
958        _control: &super::UploadControl,
959    ) -> Result<(), CloudHomeError> {
960        self.buf.extend_from_slice(&part);
961        Ok(())
962    }
963
964    async fn abort(&mut self) -> Result<(), CloudHomeError> {
965        Ok(())
966    }
967
968    async fn finish(self: Box<Self>) -> Result<(), CloudHomeError> {
969        self.writes.lock().unwrap().insert(self.key, self.buf);
970        Ok(())
971    }
972}
973
974#[async_trait]
975impl CloudHome for InMemoryCloudHome {
976    async fn probe(&self) -> Result<(), CloudHomeError> {
977        let pause = self.probe_pause.lock().unwrap().take();
978        if let Some(pause) = pause {
979            pause.reached.notify_one();
980            pause.release.notified().await;
981        }
982        if let Some(kind) = self.probe_failure.lock().unwrap().take() {
983            return Err(CloudHomeError::backend(
984                kind,
985                "probe in-memory cloud home",
986                std::io::Error::other("injected provider probe failure"),
987            ));
988        }
989        Ok(())
990    }
991
992    async fn put_object(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError> {
993        InMemoryCloudHome::put_object(self, key, data).await
994    }
995
996    async fn open_multipart<'a>(
997        &'a self,
998        key: &str,
999        total_len: u64,
1000    ) -> Result<BoxPartSink<'a>, CloudHomeError> {
1001        InMemoryCloudHome::open_multipart(self, key, total_len).await
1002    }
1003
1004    fn multipart_threshold(&self) -> u64 {
1005        InMemoryCloudHome::multipart_threshold(self)
1006    }
1007
1008    async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError> {
1009        InMemoryCloudHome::read(self, key).await
1010    }
1011
1012    async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError> {
1013        InMemoryCloudHome::read_range(self, key, start, end).await
1014    }
1015
1016    async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError> {
1017        InMemoryCloudHome::list(self, prefix).await
1018    }
1019
1020    async fn delete(&self, key: &str) -> Result<(), CloudHomeError> {
1021        InMemoryCloudHome::delete(self, key).await
1022    }
1023
1024    async fn exists(&self, key: &str) -> Result<bool, CloudHomeError> {
1025        InMemoryCloudHome::exists(self, key).await
1026    }
1027
1028    async fn set_access(
1029        &self,
1030        desired: CloudAccessState,
1031    ) -> Result<CloudAccessOutcome, CloudHomeError> {
1032        InMemoryCloudHome::set_access(self, desired).await
1033    }
1034}
1035
1036#[async_trait]
1037impl ExactSlotStorage for InMemoryCloudHome {
1038    async fn provider_binding(
1039        &self,
1040    ) -> Result<coven_protocol::objects::ResolvedProviderBinding, CloudHomeError> {
1041        Ok(self.provider_binding.clone())
1042    }
1043
1044    async fn list_slots(&self, prefix: &str) -> Result<Vec<ObjectSlot>, CloudHomeError> {
1045        self.exact_list_count.fetch_add(1, Ordering::SeqCst);
1046        self.exact_listed_prefixes
1047            .lock()
1048            .unwrap()
1049            .push(prefix.to_string());
1050        let mut keys: Vec<String> = self
1051            .writes
1052            .lock()
1053            .unwrap()
1054            .values
1055            .keys()
1056            .filter(|key| key.starts_with(prefix))
1057            .cloned()
1058            .collect();
1059        keys.sort();
1060        keys.iter()
1061            .map(|key| Self::exact_slot_from_storage_key(key))
1062            .collect()
1063    }
1064
1065    async fn allocate_slot(&self, logical_key: &str) -> Result<ObjectSlot, CloudHomeError> {
1066        let inflight = self
1067            .exact_slot_allocation_inflight
1068            .fetch_add(1, Ordering::SeqCst)
1069            + 1;
1070        self.exact_slot_allocation_max_inflight
1071            .fetch_max(inflight, Ordering::SeqCst);
1072        let _guard = InflightGuard {
1073            inflight: self.exact_slot_allocation_inflight.clone(),
1074        };
1075        let delay = self
1076            .exact_slot_allocation_delay_millis
1077            .load(Ordering::SeqCst);
1078        if delay > 0 {
1079            tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
1080        }
1081        match &self.provider_binding.store {
1082            coven_protocol::objects::StoreProviderBinding::GoogleDrive { .. } => {
1083                let allocation = self.exact_slot_allocations.fetch_add(1, Ordering::SeqCst) + 1;
1084                ObjectSlot::opaque(logical_key.to_string(), format!("in-memory-{allocation}"))
1085                    .map_err(CloudHomeError::from)
1086            }
1087            coven_protocol::objects::StoreProviderBinding::S3 { .. }
1088            | coven_protocol::objects::StoreProviderBinding::Dropbox { .. }
1089            | coven_protocol::objects::StoreProviderBinding::OneDrive { .. }
1090            | coven_protocol::objects::StoreProviderBinding::CloudKit { .. } => {
1091                ObjectSlot::logical(logical_key.to_string()).map_err(CloudHomeError::from)
1092            }
1093        }
1094    }
1095
1096    async fn create_at(
1097        &self,
1098        upload: &super::ExactUpload<'_>,
1099        control: &super::UploadControl,
1100    ) -> Result<super::ExactCreateOutcome, CloudHomeError> {
1101        InMemoryCloudHome::create_at_slot(self, upload, control).await
1102    }
1103
1104    async fn read_at(&self, slot: &ObjectSlot) -> Result<Vec<u8>, CloudHomeError> {
1105        InMemoryCloudHome::read_exact(self, slot).await
1106    }
1107
1108    async fn read_versioned_at(
1109        &self,
1110        slot: &ObjectSlot,
1111    ) -> Result<CloudVersionedObject, CloudHomeError> {
1112        let key = Self::exact_storage_key(slot)?;
1113        let writes = self.writes.lock().unwrap();
1114        let object = writes
1115            .values
1116            .get(&key)
1117            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))?;
1118        Ok(CloudVersionedObject {
1119            bytes: object.bytes.clone(),
1120            version: CloudObjectVersion::from_provider(object.version.to_string())?,
1121        })
1122    }
1123
1124    async fn replace_at_if_version(
1125        &self,
1126        slot: &ObjectSlot,
1127        expected: &CloudObjectVersion,
1128        bytes: Vec<u8>,
1129    ) -> Result<ConditionalWriteOutcome, CloudHomeError> {
1130        if self.fail_writes.load(Ordering::SeqCst) {
1131            return Err(CloudHomeError::Transport(
1132                "InMemoryCloudHome: armed write failure".into(),
1133            ));
1134        }
1135        let key = Self::exact_storage_key(slot)?;
1136        let (version, lose_response, pause) = {
1137            let mut writes = self.writes.lock().unwrap();
1138            let Some(current) = writes.values.get(&key) else {
1139                return Err(CloudHomeError::NotFound(slot.logical_key().to_string()));
1140            };
1141            if current.version.to_string() != expected.as_provider() {
1142                return Ok(ConditionalWriteOutcome::VersionChanged);
1143            }
1144            writes.insert(key, bytes);
1145            let version = writes.next_version;
1146            // Claim these faults before another writer can replace this record.
1147            let lose_response = self
1148                .lose_next_conditional_replace_response
1149                .swap(false, Ordering::SeqCst);
1150            let pause = self.conditional_replace_pause.lock().unwrap().take();
1151            (version, lose_response, pause)
1152        };
1153        if let Some(pause) = pause {
1154            pause.reached.notify_one();
1155            pause.release.notified().await;
1156        }
1157        if lose_response {
1158            return Err(CloudHomeError::Transport(
1159                "InMemoryCloudHome: conditional replacement response lost".to_string(),
1160            ));
1161        }
1162        Ok(ConditionalWriteOutcome::Replaced(
1163            CloudObjectVersion::from_provider(version.to_string())?,
1164        ))
1165    }
1166
1167    async fn read_range_at(
1168        &self,
1169        slot: &ObjectSlot,
1170        start: u64,
1171        end: u64,
1172    ) -> Result<Vec<u8>, CloudHomeError> {
1173        // Served straight out of the bucket rather than through `read_exact`, so
1174        // the full-read counter keeps meaning "something fetched a whole object"
1175        // and a ranged read never inflates it.
1176        let key = Self::exact_storage_key(slot)?;
1177        let bytes = self
1178            .writes
1179            .lock()
1180            .unwrap()
1181            .bytes(&key)
1182            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))?;
1183        // A range past the object's end is refused, not clamped: a short answer
1184        // to a range request is the provider ignoring it, which a caller must
1185        // see rather than splice.
1186        let window = bytes
1187            .get(start as usize..end as usize)
1188            .ok_or_else(|| {
1189                CloudHomeError::NotFound(format!(
1190                    "range {start}..{end} past the {} bytes of {}",
1191                    bytes.len(),
1192                    slot.logical_key()
1193                ))
1194            })?
1195            .to_vec();
1196        self.exact_range_reads.lock().unwrap().push((start, end));
1197        Ok(window)
1198    }
1199
1200    async fn read_at_to_file(
1201        &self,
1202        slot: &ObjectSlot,
1203        destination: &std::path::Path,
1204        progress: super::DownloadProgress,
1205    ) -> Result<(), CloudFileReadError> {
1206        InMemoryCloudHome::read_exact_to_file(self, slot, destination, progress).await
1207    }
1208
1209    async fn delete_at(&self, slot: &ObjectSlot) -> Result<(), CloudHomeError> {
1210        InMemoryCloudHome::delete_exact(self, slot).await
1211    }
1212}
1213
1214#[cfg(test)]
1215#[path = "test_utils_tests.rs"]
1216mod tests;
1217
1218/// An [`InMemoryCloudHome`] bound to a fixed Google Drive provider, for the
1219/// tests that need a cloud home but do not care which provider it claims to
1220/// be.
1221#[cfg(any(test, feature = "test-utils"))]
1222pub fn test_cloud_home() -> std::sync::Arc<InMemoryCloudHome> {
1223    test_cloud_home_with_binding(coven_protocol::objects::ResolvedProviderBinding {
1224        store: coven_protocol::objects::StoreProviderBinding::GoogleDrive {
1225            corpus: coven_protocol::objects::GoogleDriveCorpus::SharedDrive {
1226                drive_id: "test-drive".to_string(),
1227                folder_id: "test-folder".to_string(),
1228            },
1229        },
1230        device: coven_protocol::objects::ProviderDeviceBinding {
1231            principal: coven_protocol::objects::ProviderPrincipalId::GoogleDrive {
1232                permission_id: "test-permission".to_string(),
1233            },
1234        },
1235    })
1236}
1237
1238#[cfg(any(test, feature = "test-utils"))]
1239pub fn test_cloud_home_with_binding(
1240    binding: coven_protocol::objects::ResolvedProviderBinding,
1241) -> std::sync::Arc<InMemoryCloudHome> {
1242    std::sync::Arc::new(InMemoryCloudHome::new().with_provider_binding(binding))
1243}