Skip to main content

coven_core/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 coven itself and to downstream crates
6//! that enable the `test-utils` feature.
7
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
10use std::sync::Arc;
11use std::sync::Mutex;
12
13use async_trait::async_trait;
14use bytes::Bytes;
15
16use super::{
17    BlobBody, BoxPartSink, CloudAccessOutcome, CloudAccessState, CloudFileReadError, CloudHome,
18    CloudHomeError, ExactSlotStorage, ObjectSlot, PartSink, UploadProgress,
19};
20
21#[derive(Clone)]
22struct AppendPause {
23    call: usize,
24    reached: Arc<tokio::sync::Notify>,
25    release: Arc<tokio::sync::Notify>,
26}
27
28struct ExactStreamReadGuard {
29    inflight: Arc<AtomicUsize>,
30}
31
32#[derive(Clone)]
33struct ProbePause {
34    reached: Arc<tokio::sync::Notify>,
35    release: Arc<tokio::sync::Notify>,
36}
37
38impl Drop for ExactStreamReadGuard {
39    fn drop(&mut self) {
40        self.inflight.fetch_sub(1, Ordering::SeqCst);
41    }
42}
43
44/// In-memory CloudHome backed by a HashMap. `Clone` shares one backing store, so
45/// clones act as separate devices reading and writing the same cloud bucket, and
46/// a test can keep its own handle for direct at-rest assertions while each device
47/// owns a `Box<dyn CloudHome>` clone.
48///
49/// Beyond the happy path it carries fault-injection knobs
50/// ([`arm_write_failures`](Self::arm_write_failures),
51/// [`fail_next_range_reads`](Self::fail_next_range_reads),
52/// [`remove`](Self::remove)) so a host test can drive upload-failure,
53/// read-retry, and missing-blob paths without a bespoke `CloudHome` impl. The
54/// arming state is shared across clones, like the backing store.
55#[derive(Clone)]
56pub struct InMemoryCloudHome {
57    provider_binding: crate::sync::storage::ResolvedProviderBinding,
58    writes: Arc<Mutex<HashMap<String, Vec<u8>>>>,
59    exact_slot_allocations: Arc<AtomicUsize>,
60    deletes: Arc<Mutex<Vec<String>>>,
61    fail_writes: Arc<AtomicBool>,
62    fail_next_range_reads: Arc<AtomicUsize>,
63    sort_listings: Arc<AtomicBool>,
64    exact_create_count: Arc<AtomicUsize>,
65    fail_exact_create_before: Arc<AtomicUsize>,
66    fail_exact_create_after: Arc<AtomicUsize>,
67    corrupt_exact_readback: Arc<AtomicUsize>,
68    exact_create_pause: Arc<Mutex<Option<AppendPause>>>,
69    probe_pause: Arc<Mutex<Option<ProbePause>>>,
70    exact_full_read_count: Arc<AtomicUsize>,
71    exact_stream_read_count: Arc<AtomicUsize>,
72    exact_reads: Arc<Mutex<Vec<ObjectSlot>>>,
73    exact_stream_read_inflight: Arc<AtomicUsize>,
74    exact_stream_read_max_inflight: Arc<AtomicUsize>,
75    exact_stream_read_barrier: Arc<Mutex<Option<Arc<tokio::sync::Barrier>>>>,
76    exact_delete_count: Arc<AtomicUsize>,
77    fail_exact_delete_on: Arc<AtomicUsize>,
78    fail_exact_delete_of: Arc<Mutex<Option<TargetedDeleteFailure>>>,
79}
80
81/// Fail the `countdown`-th delete of an object whose key is in `keys`, counting
82/// only those deletes. Set by [`InMemoryCloudHome::fail_nth_exact_delete_of`].
83struct TargetedDeleteFailure {
84    keys: std::collections::HashSet<String>,
85    countdown: usize,
86}
87
88impl InMemoryCloudHome {
89    pub fn new() -> Self {
90        Self {
91            provider_binding: crate::sync::storage::ResolvedProviderBinding {
92                store: crate::sync::storage::StoreProviderBinding::S3 {
93                    endpoint: crate::sync::storage::S3EndpointBinding::Custom {
94                        origin: "https://in-memory.invalid".to_string(),
95                    },
96                    region: "test".to_string(),
97                    bucket: "in-memory".to_string(),
98                    key_prefix: None,
99                },
100                device: crate::sync::storage::ProviderDeviceBinding {
101                    principal: crate::sync::storage::ProviderPrincipalId::CustomS3Credential {
102                        access_key_id_hash: crate::sync::store_commit::ObjectHash::digest(
103                            b"coven.s3-access-key-id.v1\0in-memory",
104                        ),
105                    },
106                },
107            },
108            writes: Arc::new(Mutex::new(HashMap::new())),
109            exact_slot_allocations: Arc::new(AtomicUsize::new(0)),
110            deletes: Arc::new(Mutex::new(Vec::new())),
111            fail_writes: Arc::new(AtomicBool::new(false)),
112            fail_next_range_reads: Arc::new(AtomicUsize::new(0)),
113            sort_listings: Arc::new(AtomicBool::new(false)),
114            exact_create_count: Arc::new(AtomicUsize::new(0)),
115            fail_exact_create_before: Arc::new(AtomicUsize::new(0)),
116            fail_exact_create_after: Arc::new(AtomicUsize::new(0)),
117            corrupt_exact_readback: Arc::new(AtomicUsize::new(0)),
118            exact_create_pause: Arc::new(Mutex::new(None)),
119            probe_pause: Arc::new(Mutex::new(None)),
120            exact_full_read_count: Arc::new(AtomicUsize::new(0)),
121            exact_stream_read_count: Arc::new(AtomicUsize::new(0)),
122            exact_reads: Arc::new(Mutex::new(Vec::new())),
123            exact_stream_read_inflight: Arc::new(AtomicUsize::new(0)),
124            exact_stream_read_max_inflight: Arc::new(AtomicUsize::new(0)),
125            exact_stream_read_barrier: Arc::new(Mutex::new(None)),
126            exact_delete_count: Arc::new(AtomicUsize::new(0)),
127            fail_exact_delete_on: Arc::new(AtomicUsize::new(0)),
128            fail_exact_delete_of: Arc::new(Mutex::new(None)),
129        }
130    }
131
132    pub fn with_provider_binding(
133        mut self,
134        binding: crate::sync::storage::ResolvedProviderBinding,
135    ) -> Self {
136        binding
137            .validate()
138            .expect("in-memory provider binding must be valid");
139        self.provider_binding = binding;
140        self
141    }
142
143    /// Return `list` results in sorted key order instead of the backing map's
144    /// arbitrary order. A real bucket LIST has no defined order, so the pull's
145    /// cross-device apply order is arbitrary; a test that needs a fixed order (to
146    /// reproduce an order-dependent bug deterministically) arms this and picks the
147    /// order through its device ids.
148    pub fn sort_listings(&self) {
149        self.sort_listings.store(true, Ordering::SeqCst);
150    }
151
152    /// Arm every subsequent write (`put_object` and `open_multipart`) to fail
153    /// with a retryable transport error. A test can let a home's setup writes
154    /// land and then arm this before driving the path whose uploads must fail;
155    /// it stays armed for the store's lifetime.
156    pub fn arm_write_failures(&self) {
157        self.fail_writes.store(true, Ordering::SeqCst);
158    }
159
160    /// Make the next `n` `read_range` calls fail with a retryable transport
161    /// error before any serves bytes, to exercise a caller's read-retry path.
162    /// Each failed call consumes one; once `n` are spent, ranges serve
163    /// normally.
164    pub fn fail_next_range_reads(&self, n: usize) {
165        self.fail_next_range_reads.store(n, Ordering::SeqCst);
166    }
167
168    /// Reset the exact-create counter and fail before the selected call stores bytes.
169    pub fn fail_exact_create_before_call(&self, call: usize) {
170        assert!(call > 0, "create call numbers are 1-based");
171        self.exact_create_count.store(0, Ordering::SeqCst);
172        self.fail_exact_create_before.store(call, Ordering::SeqCst);
173    }
174
175    /// Reset the exact-create counter and lose the response after the selected create.
176    pub fn fail_exact_create_after_call(&self, call: usize) {
177        assert!(call > 0, "create call numbers are 1-based");
178        self.exact_create_count.store(0, Ordering::SeqCst);
179        self.fail_exact_create_after.store(call, Ordering::SeqCst);
180    }
181
182    /// Replace the selected exact object's bytes before its verification read.
183    pub fn corrupt_exact_readback_on_call(&self, call: usize) {
184        assert!(call > 0, "create call numbers are 1-based");
185        self.exact_create_count.store(0, Ordering::SeqCst);
186        self.corrupt_exact_readback.store(call, Ordering::SeqCst);
187    }
188
189    /// Pause after the selected exact create is physically visible.
190    pub fn pause_after_exact_create_call(
191        &self,
192        call: usize,
193    ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
194        assert!(call > 0, "create call numbers are 1-based");
195        self.exact_create_count.store(0, Ordering::SeqCst);
196        let reached = Arc::new(tokio::sync::Notify::new());
197        let release = Arc::new(tokio::sync::Notify::new());
198        *self.exact_create_pause.lock().unwrap() = Some(AppendPause {
199            call,
200            reached: reached.clone(),
201            release: release.clone(),
202        });
203        (reached, release)
204    }
205
206    /// Pause the next reachability probe after it starts and before it succeeds.
207    pub fn pause_next_probe(&self) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
208        let reached = Arc::new(tokio::sync::Notify::new());
209        let release = Arc::new(tokio::sync::Notify::new());
210        *self.probe_pause.lock().unwrap() = Some(ProbePause {
211            reached: reached.clone(),
212            release: release.clone(),
213        });
214        (reached, release)
215    }
216
217    pub fn exact_create_count(&self) -> usize {
218        self.exact_create_count.load(Ordering::SeqCst)
219    }
220
221    pub fn exact_full_read_count(&self) -> usize {
222        self.exact_full_read_count.load(Ordering::SeqCst)
223    }
224
225    pub fn exact_stream_read_count(&self) -> usize {
226        self.exact_stream_read_count.load(Ordering::SeqCst)
227    }
228
229    pub fn exact_reads(&self) -> Vec<ObjectSlot> {
230        self.exact_reads.lock().unwrap().clone()
231    }
232
233    pub fn clear_exact_reads(&self) {
234        self.exact_reads.lock().unwrap().clear();
235    }
236
237    pub fn arm_exact_stream_read_concurrency_probe(&self, width: usize) {
238        assert!(width > 0, "exact stream read probe width must be positive");
239        self.exact_stream_read_inflight.store(0, Ordering::SeqCst);
240        self.exact_stream_read_max_inflight
241            .store(0, Ordering::SeqCst);
242        *self.exact_stream_read_barrier.lock().unwrap() =
243            Some(Arc::new(tokio::sync::Barrier::new(width)));
244    }
245
246    pub fn exact_stream_read_max_inflight(&self) -> usize {
247        self.exact_stream_read_max_inflight.load(Ordering::SeqCst)
248    }
249
250    pub fn exact_delete_count(&self) -> usize {
251        self.exact_delete_count.load(Ordering::SeqCst)
252    }
253
254    pub fn fail_exact_delete_on_call(&self, call: usize) {
255        assert!(call > 0, "exact-delete call numbers are 1-based");
256        self.exact_delete_count.store(0, Ordering::SeqCst);
257        self.fail_exact_delete_on.store(call, Ordering::SeqCst);
258    }
259
260    /// Fail the `nth` (1-based) delete of an object among `slots`, counting only
261    /// deletes of those objects, then disarm. Unlike [`fail_exact_delete_on_call`],
262    /// which counts every exact delete (probes, candidate cleanup), this counts
263    /// only the identities that matter, so "fail the 2nd package delete" lands
264    /// deterministically however many unrelated deletes interleave and whatever
265    /// order the two package deletes arrive in.
266    pub fn fail_nth_exact_delete_of(&self, slots: &[&ObjectSlot], nth: usize) {
267        assert!(nth > 0, "targeted delete ordinals are 1-based");
268        let keys = slots
269            .iter()
270            .map(|slot| Self::exact_storage_key(slot).expect("test exact slot is valid"))
271            .collect();
272        *self.fail_exact_delete_of.lock().unwrap() = Some(TargetedDeleteFailure {
273            keys,
274            countdown: nth,
275        });
276    }
277
278    /// Drop `key`'s bytes out of band — as if the object vanished from the
279    /// bucket on its own, without a `delete` (which `deletes_seen` would
280    /// record). Drives missing-blob read failures.
281    pub fn remove(&self, key: &str) {
282        self.writes.lock().unwrap().remove(key);
283    }
284
285    /// Snapshot of every key currently in the cloud. Useful for assertions
286    /// that don't want to hold the lock across an await.
287    pub fn keys(&self) -> Vec<String> {
288        self.writes.lock().unwrap().keys().cloned().collect()
289    }
290
291    /// Snapshot of the bytes at `key`, or `None` if absent. Cloned so the
292    /// caller can hold the result across `await` points without retaining
293    /// the internal lock.
294    pub fn get(&self, key: &str) -> Option<Vec<u8>> {
295        self.writes.lock().unwrap().get(key).cloned()
296    }
297
298    /// Number of objects stored. Cheap snapshot.
299    pub fn len(&self) -> usize {
300        self.writes.lock().unwrap().len()
301    }
302
303    /// Returns true if the store is empty.
304    pub fn is_empty(&self) -> bool {
305        self.writes.lock().unwrap().is_empty()
306    }
307
308    /// Snapshot of every delete that's been requested, in arrival order.
309    pub fn deletes_seen(&self) -> Vec<String> {
310        self.deletes.lock().unwrap().clone()
311    }
312
313    /// Insert caller-selected bytes at one exact logical slot.
314    pub fn insert_exact_object(&self, logical_key: &str, bytes: Vec<u8>) -> ObjectSlot {
315        let slot =
316            ObjectSlot::logical(logical_key.to_string()).expect("test logical key is non-empty");
317        self.writes
318            .lock()
319            .unwrap()
320            .insert(logical_key.to_string(), bytes);
321        slot
322    }
323
324    /// Snapshot the bytes stored at one exact slot, or `None` if absent.
325    pub fn stored_exact_bytes(&self, slot: &ObjectSlot) -> Option<Vec<u8>> {
326        let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
327        self.writes.lock().unwrap().get(&key).cloned()
328    }
329
330    /// Re-insert bytes at one exact slot, restoring an object dropped by
331    /// [`remove_exact_object`](Self::remove_exact_object).
332    pub fn restore_exact_object(&self, slot: &ObjectSlot, bytes: Vec<u8>) {
333        let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
334        self.writes.lock().unwrap().insert(key, bytes);
335    }
336
337    /// Remove one exact object without recording a protocol delete.
338    pub fn remove_exact_object(&self, slot: &ObjectSlot) {
339        let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
340        self.writes.lock().unwrap().remove(&key);
341    }
342
343    /// Replace bytes at one exact slot without changing its locator.
344    pub fn replace_exact_object(&self, slot: &ObjectSlot, bytes: Vec<u8>) {
345        let previous = self.writes.lock().unwrap().insert(
346            Self::exact_storage_key(slot).expect("test exact slot is valid"),
347            bytes,
348        );
349        assert!(previous.is_some(), "exact slot exists");
350    }
351}
352
353impl Default for InMemoryCloudHome {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359/// A [`PartSink`] for the in-memory backend: accumulate the streamed parts in
360/// order and store the assembled object on `finish`, so a multipart upload
361/// round-trips exactly like a single `put_object`.
362struct InMemoryPartSink {
363    writes: Arc<Mutex<HashMap<String, Vec<u8>>>>,
364    key: String,
365    buf: Vec<u8>,
366}
367
368#[async_trait]
369impl PartSink for InMemoryPartSink {
370    fn part_size(&self) -> usize {
371        super::PROGRESS_CHUNK_SIZE
372    }
373
374    async fn send_part(
375        &mut self,
376        part: Bytes,
377        _offset: u64,
378        _is_last: bool,
379    ) -> Result<(), CloudHomeError> {
380        self.buf.extend_from_slice(&part);
381        Ok(())
382    }
383
384    async fn abort(&mut self) -> Result<(), CloudHomeError> {
385        Ok(())
386    }
387
388    async fn finish(self: Box<Self>) -> Result<(), CloudHomeError> {
389        self.writes.lock().unwrap().insert(self.key, self.buf);
390        Ok(())
391    }
392}
393
394impl InMemoryCloudHome {
395    async fn put_object(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError> {
396        if self.fail_writes.load(Ordering::SeqCst) {
397            return Err(CloudHomeError::Transport(
398                "InMemoryCloudHome: armed write failure".into(),
399            ));
400        }
401        self.writes.lock().unwrap().insert(key.to_string(), data);
402        Ok(())
403    }
404
405    async fn open_multipart<'a>(
406        &'a self,
407        key: &str,
408        _total_len: u64,
409    ) -> Result<BoxPartSink<'a>, CloudHomeError> {
410        // Gate multipart too, so `arm_write_failures` fails a write whatever its
411        // size — `write_blob` routes blobs above `multipart_threshold` here.
412        if self.fail_writes.load(Ordering::SeqCst) {
413            return Err(CloudHomeError::Transport(
414                "InMemoryCloudHome: armed write failure".into(),
415            ));
416        }
417        Ok(Box::new(InMemoryPartSink {
418            writes: self.writes.clone(),
419            key: key.to_string(),
420            buf: Vec::new(),
421        }))
422    }
423
424    fn multipart_threshold(&self) -> u64 {
425        // A small threshold so tests exercise the multipart driver path; the part
426        // size matches so a multi-part blob ticks progress several times.
427        super::PROGRESS_CHUNK_SIZE as u64
428    }
429    fn validate_exact_slot(slot: &ObjectSlot) -> Result<(), CloudHomeError> {
430        slot.validate()?;
431        Ok(())
432    }
433
434    fn exact_storage_key(slot: &ObjectSlot) -> Result<String, CloudHomeError> {
435        Self::validate_exact_slot(slot)?;
436        Ok(match slot.physical() {
437            super::PhysicalObjectLocator::LogicalKey => slot.logical_key().to_string(),
438            super::PhysicalObjectLocator::Opaque(provider_id) => {
439                format!("{}#exact#{provider_id}", slot.logical_key())
440            }
441        })
442    }
443
444    async fn create_at_slot(
445        &self,
446        slot: &ObjectSlot,
447        body: BlobBody,
448        progress: &UploadProgress<'_>,
449    ) -> Result<(), CloudHomeError> {
450        if self.fail_writes.load(Ordering::SeqCst) {
451            return Err(CloudHomeError::Transport(
452                "InMemoryCloudHome: armed write failure".into(),
453            ));
454        }
455        let key = Self::exact_storage_key(slot)?;
456        let call = self.exact_create_count.fetch_add(1, Ordering::SeqCst) + 1;
457        if self.fail_exact_create_before.load(Ordering::SeqCst) == call {
458            self.fail_exact_create_before.store(0, Ordering::SeqCst);
459            return Err(CloudHomeError::Transport(format!(
460                "InMemoryCloudHome: forced failure before exact create call {call}"
461            )));
462        }
463        let bytes = body.collect().await?;
464        progress(bytes.len() as u64);
465        {
466            let mut writes = self.writes.lock().unwrap();
467            if writes.contains_key(&key) {
468                return Err(CloudHomeError::AlreadyExists(key));
469            }
470            writes.insert(key.clone(), bytes);
471        }
472        if self.corrupt_exact_readback.load(Ordering::SeqCst) == call {
473            self.corrupt_exact_readback.store(0, Ordering::SeqCst);
474            self.writes
475                .lock()
476                .unwrap()
477                .insert(key.clone(), b"corrupt readback".to_vec());
478        }
479        let pause = self
480            .exact_create_pause
481            .lock()
482            .unwrap()
483            .clone()
484            .filter(|pause| pause.call == call);
485        if let Some(pause) = pause {
486            pause.reached.notify_one();
487            pause.release.notified().await;
488            self.exact_create_pause.lock().unwrap().take();
489        }
490        if self.fail_exact_create_after.load(Ordering::SeqCst) == call {
491            self.fail_exact_create_after.store(0, Ordering::SeqCst);
492            return Err(CloudHomeError::Transport(format!(
493                "InMemoryCloudHome: forced failure after exact create call {call}"
494            )));
495        }
496        Ok(())
497    }
498
499    async fn read_exact(&self, slot: &ObjectSlot) -> Result<Vec<u8>, CloudHomeError> {
500        self.exact_full_read_count.fetch_add(1, Ordering::SeqCst);
501        let key = Self::exact_storage_key(slot)?;
502        self.exact_reads.lock().unwrap().push(slot.clone());
503        self.writes
504            .lock()
505            .unwrap()
506            .get(&key)
507            .cloned()
508            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))
509    }
510
511    async fn read_exact_to_file(
512        &self,
513        slot: &ObjectSlot,
514        destination: &std::path::Path,
515    ) -> Result<(), CloudFileReadError> {
516        self.exact_stream_read_count.fetch_add(1, Ordering::SeqCst);
517        self.exact_reads.lock().unwrap().push(slot.clone());
518        let inflight = self
519            .exact_stream_read_inflight
520            .fetch_add(1, Ordering::SeqCst)
521            + 1;
522        self.exact_stream_read_max_inflight
523            .fetch_max(inflight, Ordering::SeqCst);
524        let _guard = ExactStreamReadGuard {
525            inflight: self.exact_stream_read_inflight.clone(),
526        };
527        let barrier = self.exact_stream_read_barrier.lock().unwrap().clone();
528        if let Some(barrier) = barrier {
529            barrier.wait().await;
530        }
531        let key = Self::exact_storage_key(slot)?;
532        let bytes = self
533            .writes
534            .lock()
535            .unwrap()
536            .get(&key)
537            .cloned()
538            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))?;
539        let stream = futures_util::stream::once(async move { Ok(bytes::Bytes::from(bytes)) });
540        super::write_cloud_object_stream(destination, Box::pin(stream)).await?;
541        Ok(())
542    }
543
544    async fn delete_exact(&self, slot: &ObjectSlot) -> Result<(), CloudHomeError> {
545        let key = Self::exact_storage_key(slot)?;
546        let call = self.exact_delete_count.fetch_add(1, Ordering::SeqCst) + 1;
547        if self.fail_exact_delete_on.load(Ordering::SeqCst) == call {
548            self.fail_exact_delete_on.store(0, Ordering::SeqCst);
549            return Err(CloudHomeError::Transport(format!(
550                "InMemoryCloudHome: forced exact delete failure on call {call}"
551            )));
552        }
553        {
554            let mut targeted = self.fail_exact_delete_of.lock().unwrap();
555            if let Some(failure) = targeted.as_mut() {
556                if failure.keys.contains(&key) {
557                    failure.countdown -= 1;
558                    if failure.countdown == 0 {
559                        *targeted = None;
560                        return Err(CloudHomeError::Transport(format!(
561                            "InMemoryCloudHome: forced exact delete failure of {key}"
562                        )));
563                    }
564                }
565            }
566        }
567        self.writes.lock().unwrap().remove(&key);
568        self.deletes.lock().unwrap().push(key);
569        Ok(())
570    }
571    async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError> {
572        self.writes
573            .lock()
574            .unwrap()
575            .get(key)
576            .cloned()
577            .ok_or_else(|| CloudHomeError::NotFound(key.to_string()))
578    }
579
580    async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError> {
581        // An armed range read fails before touching the store. `checked_sub`
582        // returns `None` at zero, so `fetch_update` only succeeds (and errors)
583        // while the countdown is positive.
584        if self
585            .fail_next_range_reads
586            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
587            .is_ok()
588        {
589            return Err(CloudHomeError::Transport(
590                "InMemoryCloudHome: armed range-read failure".into(),
591            ));
592        }
593        let data = self.read(key).await?;
594        let s = start as usize;
595        let e = (end as usize).min(data.len());
596        if s > data.len() {
597            return Err(CloudHomeError::NotFound(format!("range past end of {key}")));
598        }
599        Ok(data[s..e].to_vec())
600    }
601
602    async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError> {
603        let mut keys: Vec<String> = self
604            .writes
605            .lock()
606            .unwrap()
607            .keys()
608            .filter(|k| k.starts_with(prefix))
609            .cloned()
610            .collect();
611        if self.sort_listings.load(Ordering::SeqCst) {
612            keys.sort();
613        }
614        Ok(keys)
615    }
616
617    async fn delete(&self, key: &str) -> Result<(), CloudHomeError> {
618        self.writes.lock().unwrap().remove(key);
619        self.deletes.lock().unwrap().push(key.to_string());
620        Ok(())
621    }
622
623    async fn exists(&self, key: &str) -> Result<bool, CloudHomeError> {
624        Ok(self.writes.lock().unwrap().contains_key(key))
625    }
626
627    async fn set_access(
628        &self,
629        desired: super::CloudAccessState,
630    ) -> Result<super::CloudAccessOutcome, CloudHomeError> {
631        Ok(match desired {
632            super::CloudAccessState::Present { .. } => {
633                super::CloudAccessOutcome::Present(super::CloudHomeJoinInfo::S3 {
634                    bucket: "in-memory".to_string(),
635                    region: "test".to_string(),
636                    endpoint: Some("https://in-memory.invalid".to_string()),
637                    access_key: "in-memory".to_string(),
638                    secret_key: "in-memory".to_string(),
639                    key_prefix: None,
640                })
641            }
642            super::CloudAccessState::Absent { .. } => {
643                super::CloudAccessOutcome::Absent(super::RevokeOutcome::Unsupported)
644            }
645        })
646    }
647}
648
649#[async_trait]
650impl CloudHome for InMemoryCloudHome {
651    fn exact_slot_storage(self: Arc<Self>) -> Option<Arc<dyn ExactSlotStorage>> {
652        Some(self)
653    }
654
655    async fn probe(&self) -> Result<(), CloudHomeError> {
656        let pause = self.probe_pause.lock().unwrap().take();
657        if let Some(pause) = pause {
658            pause.reached.notify_one();
659            pause.release.notified().await;
660        }
661        Ok(())
662    }
663
664    async fn put_object(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError> {
665        InMemoryCloudHome::put_object(self, key, data).await
666    }
667
668    async fn open_multipart<'a>(
669        &'a self,
670        key: &str,
671        total_len: u64,
672    ) -> Result<BoxPartSink<'a>, CloudHomeError> {
673        InMemoryCloudHome::open_multipart(self, key, total_len).await
674    }
675
676    fn multipart_threshold(&self) -> u64 {
677        InMemoryCloudHome::multipart_threshold(self)
678    }
679
680    async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError> {
681        InMemoryCloudHome::read(self, key).await
682    }
683
684    async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError> {
685        InMemoryCloudHome::read_range(self, key, start, end).await
686    }
687
688    async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError> {
689        InMemoryCloudHome::list(self, prefix).await
690    }
691
692    async fn delete(&self, key: &str) -> Result<(), CloudHomeError> {
693        InMemoryCloudHome::delete(self, key).await
694    }
695
696    async fn exists(&self, key: &str) -> Result<bool, CloudHomeError> {
697        InMemoryCloudHome::exists(self, key).await
698    }
699
700    async fn set_access(
701        &self,
702        desired: CloudAccessState,
703    ) -> Result<CloudAccessOutcome, CloudHomeError> {
704        InMemoryCloudHome::set_access(self, desired).await
705    }
706}
707
708#[async_trait]
709impl ExactSlotStorage for InMemoryCloudHome {
710    async fn provider_binding(
711        &self,
712    ) -> Result<crate::sync::storage::ResolvedProviderBinding, CloudHomeError> {
713        Ok(self.provider_binding.clone())
714    }
715
716    async fn allocate_slot(&self, logical_key: &str) -> Result<ObjectSlot, CloudHomeError> {
717        match &self.provider_binding.store {
718            crate::sync::storage::StoreProviderBinding::GoogleDrive { .. } => {
719                let allocation = self.exact_slot_allocations.fetch_add(1, Ordering::SeqCst) + 1;
720                ObjectSlot::opaque(logical_key.to_string(), format!("in-memory-{allocation}"))
721            }
722            crate::sync::storage::StoreProviderBinding::S3 { .. }
723            | crate::sync::storage::StoreProviderBinding::Dropbox { .. }
724            | crate::sync::storage::StoreProviderBinding::OneDrive { .. }
725            | crate::sync::storage::StoreProviderBinding::CloudKit { .. } => {
726                ObjectSlot::logical(logical_key.to_string())
727            }
728        }
729    }
730
731    async fn create_at(
732        &self,
733        slot: &ObjectSlot,
734        body: BlobBody,
735        progress: &UploadProgress<'_>,
736    ) -> Result<(), CloudHomeError> {
737        InMemoryCloudHome::create_at_slot(self, slot, body, progress).await
738    }
739
740    async fn read_at(&self, slot: &ObjectSlot) -> Result<Vec<u8>, CloudHomeError> {
741        InMemoryCloudHome::read_exact(self, slot).await
742    }
743
744    async fn read_range_at(
745        &self,
746        slot: &ObjectSlot,
747        start: u64,
748        end: u64,
749    ) -> Result<Vec<u8>, CloudHomeError> {
750        let bytes = InMemoryCloudHome::read_exact(self, slot).await?;
751        let start = start as usize;
752        let end = (end as usize).min(bytes.len());
753        if start > bytes.len() {
754            return Err(CloudHomeError::NotFound(format!(
755                "range past end of {}",
756                slot.logical_key()
757            )));
758        }
759        Ok(bytes[start..end].to_vec())
760    }
761
762    async fn read_at_to_file(
763        &self,
764        slot: &ObjectSlot,
765        destination: &std::path::Path,
766    ) -> Result<(), CloudFileReadError> {
767        InMemoryCloudHome::read_exact_to_file(self, slot, destination).await
768    }
769
770    async fn delete_at(&self, slot: &ObjectSlot) -> Result<(), CloudHomeError> {
771        InMemoryCloudHome::delete_exact(self, slot).await
772    }
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778    use crate::storage::cloud::{no_progress, BlobBody};
779    use std::sync::atomic::{AtomicU64, Ordering};
780    use std::sync::Arc;
781
782    #[tokio::test]
783    async fn write_then_read_roundtrips() {
784        let h = InMemoryCloudHome::new();
785        h.write(
786            "foo",
787            BlobBody::from_bytes(b"hello".to_vec()),
788            &no_progress(),
789        )
790        .await
791        .unwrap();
792        assert_eq!(h.read("foo").await.unwrap(), b"hello");
793        assert!(h.exists("foo").await.unwrap());
794        assert!(!h.exists("bar").await.unwrap());
795    }
796
797    #[tokio::test]
798    async fn write_reports_progress_in_chunks_reaching_the_total() {
799        let h = InMemoryCloudHome::new();
800        // Two-and-a-bit chunks so progress fires more than once and the final
801        // value equals the total.
802        let len = super::super::PROGRESS_CHUNK_SIZE * 2 + 7;
803        let last = Arc::new(AtomicU64::new(0));
804        let ticks = Arc::new(AtomicU64::new(0));
805        let last2 = last.clone();
806        let ticks2 = ticks.clone();
807        let sink = move |n: u64| {
808            last2.store(n, Ordering::Relaxed);
809            ticks2.fetch_add(1, Ordering::Relaxed);
810        };
811        h.write("big", BlobBody::from_bytes(vec![0u8; len]), &sink)
812            .await
813            .unwrap();
814        assert_eq!(last.load(Ordering::Relaxed), len as u64);
815        assert_eq!(ticks.load(Ordering::Relaxed), 3);
816    }
817
818    #[tokio::test]
819    async fn read_range_returns_a_slice() {
820        let h = InMemoryCloudHome::new();
821        h.write(
822            "k",
823            BlobBody::from_bytes(b"0123456789".to_vec()),
824            &no_progress(),
825        )
826        .await
827        .unwrap();
828        assert_eq!(h.read_range("k", 2, 5).await.unwrap(), b"234");
829    }
830
831    #[tokio::test]
832    async fn list_filters_by_prefix() {
833        let h = InMemoryCloudHome::new();
834        h.write("a/x", BlobBody::from_bytes(vec![1]), &no_progress())
835            .await
836            .unwrap();
837        h.write("a/y", BlobBody::from_bytes(vec![2]), &no_progress())
838            .await
839            .unwrap();
840        h.write("b/x", BlobBody::from_bytes(vec![3]), &no_progress())
841            .await
842            .unwrap();
843        let mut got = h.list("a/").await.unwrap();
844        got.sort();
845        assert_eq!(got, vec!["a/x".to_string(), "a/y".to_string()]);
846    }
847
848    #[tokio::test]
849    async fn delete_removes_and_records() {
850        let h = InMemoryCloudHome::new();
851        h.write("k", BlobBody::from_bytes(vec![1]), &no_progress())
852            .await
853            .unwrap();
854        h.delete("k").await.unwrap();
855        assert!(matches!(
856            h.read("k").await,
857            Err(CloudHomeError::NotFound(_))
858        ));
859        assert_eq!(h.deletes_seen(), vec!["k".to_string()]);
860    }
861
862    #[tokio::test]
863    async fn arm_write_failures_fails_writes_after_arming() {
864        let h = InMemoryCloudHome::new();
865        // Writes land before arming.
866        h.write("before", BlobBody::from_bytes(vec![1]), &no_progress())
867            .await
868            .unwrap();
869
870        h.arm_write_failures();
871        let err = h
872            .write("after", BlobBody::from_bytes(vec![2]), &no_progress())
873            .await
874            .unwrap_err();
875        assert!(matches!(err, CloudHomeError::Transport(_)));
876        assert!(err.is_retryable());
877        // Nothing was stored for the failed write, and the earlier one survives.
878        assert!(h.get("after").is_none());
879        assert_eq!(h.get("before"), Some(vec![1]));
880    }
881
882    #[tokio::test]
883    async fn fail_next_range_reads_fails_the_next_n_then_recovers() {
884        let h = InMemoryCloudHome::new();
885        h.write(
886            "k",
887            BlobBody::from_bytes(b"0123456789".to_vec()),
888            &no_progress(),
889        )
890        .await
891        .unwrap();
892
893        h.fail_next_range_reads(2);
894        assert!(matches!(
895            h.read_range("k", 0, 4).await,
896            Err(CloudHomeError::Transport(_))
897        ));
898        assert!(matches!(
899            h.read_range("k", 0, 4).await,
900            Err(CloudHomeError::Transport(_))
901        ));
902        // The third serves real bytes — the countdown is spent.
903        assert_eq!(h.read_range("k", 0, 4).await.unwrap(), b"0123");
904    }
905
906    #[tokio::test]
907    async fn remove_drops_a_key_out_of_band() {
908        let h = InMemoryCloudHome::new();
909        h.write("k", BlobBody::from_bytes(vec![1]), &no_progress())
910            .await
911            .unwrap();
912
913        h.remove("k");
914        assert!(matches!(
915            h.read("k").await,
916            Err(CloudHomeError::NotFound(_))
917        ));
918        // Out-of-band removal is not a delete, so it leaves no delete record.
919        assert!(h.deletes_seen().is_empty());
920    }
921
922    #[tokio::test]
923    async fn exact_create_is_visible_before_a_lost_response() {
924        let h = InMemoryCloudHome::new();
925        let slot = h.allocate_slot("store-v1/test/one.json").await.unwrap();
926        let (reached, release) = h.pause_after_exact_create_call(1);
927        let writer = h.clone();
928        let writer_slot = slot.clone();
929        let task = tokio::spawn(async move {
930            writer
931                .create_at(
932                    &writer_slot,
933                    BlobBody::from_bytes(b"first".to_vec()),
934                    &no_progress(),
935                )
936                .await
937        });
938
939        reached.notified().await;
940        assert_eq!(h.exact_create_count(), 1);
941        assert_eq!(h.read_at(&slot).await.unwrap(), b"first");
942        release.notify_one();
943        task.await.unwrap().unwrap();
944    }
945
946    #[tokio::test]
947    async fn exact_create_never_overwrites() {
948        let h = InMemoryCloudHome::new();
949        let slot = h.allocate_slot("store-v1/test/one.json").await.unwrap();
950        h.create_at(
951            &slot,
952            BlobBody::from_bytes(b"winner".to_vec()),
953            &no_progress(),
954        )
955        .await
956        .unwrap();
957
958        assert!(matches!(
959            h.create_at(
960                &slot,
961                BlobBody::from_bytes(b"loser".to_vec()),
962                &no_progress(),
963            )
964            .await,
965            Err(CloudHomeError::AlreadyExists(_))
966        ));
967        assert_eq!(h.read_at(&slot).await.unwrap(), b"winner");
968    }
969
970    #[tokio::test]
971    async fn google_drive_exact_slots_with_one_logical_key_remain_independent() {
972        let h = InMemoryCloudHome::new().with_provider_binding(
973            crate::sync::storage::ResolvedProviderBinding {
974                store: crate::sync::storage::StoreProviderBinding::GoogleDrive {
975                    corpus: crate::sync::storage::GoogleDriveCorpus::SharedDrive {
976                        drive_id: "drive-id".to_string(),
977                        folder_id: "folder-id".to_string(),
978                    },
979                },
980                device: crate::sync::storage::ProviderDeviceBinding {
981                    principal: crate::sync::storage::ProviderPrincipalId::GoogleDrive {
982                        permission_id: "permission-id".to_string(),
983                    },
984                },
985            },
986        );
987        let first = h.allocate_slot("store-v1/test/one.json").await.unwrap();
988        let second = h.allocate_slot("store-v1/test/one.json").await.unwrap();
989        assert_ne!(first, second);
990        h.create_at(
991            &first,
992            BlobBody::from_bytes(b"first".to_vec()),
993            &no_progress(),
994        )
995        .await
996        .unwrap();
997        h.create_at(
998            &second,
999            BlobBody::from_bytes(b"second".to_vec()),
1000            &no_progress(),
1001        )
1002        .await
1003        .unwrap();
1004        assert_eq!(h.read_at(&first).await.unwrap(), b"first");
1005        assert_eq!(h.read_at(&second).await.unwrap(), b"second");
1006        h.delete_at(&first).await.unwrap();
1007        assert!(matches!(
1008            h.read_at(&first).await,
1009            Err(CloudHomeError::NotFound(_))
1010        ));
1011        assert_eq!(h.read_at(&second).await.unwrap(), b"second");
1012    }
1013
1014    #[tokio::test]
1015    async fn google_drive_exact_slots_with_different_logical_keys_have_distinct_file_ids() {
1016        let h = InMemoryCloudHome::new().with_provider_binding(
1017            crate::sync::storage::ResolvedProviderBinding {
1018                store: crate::sync::storage::StoreProviderBinding::GoogleDrive {
1019                    corpus: crate::sync::storage::GoogleDriveCorpus::SharedDrive {
1020                        drive_id: "drive-id".to_string(),
1021                        folder_id: "folder-id".to_string(),
1022                    },
1023                },
1024                device: crate::sync::storage::ProviderDeviceBinding {
1025                    principal: crate::sync::storage::ProviderPrincipalId::GoogleDrive {
1026                        permission_id: "permission-id".to_string(),
1027                    },
1028                },
1029            },
1030        );
1031
1032        let first = h.allocate_slot("store-v1/test/one.json").await.unwrap();
1033        let second = h.allocate_slot("store-v1/test/two.json").await.unwrap();
1034
1035        assert_ne!(first.physical(), second.physical());
1036    }
1037
1038    #[tokio::test]
1039    async fn access_matches_the_in_memory_s3_binding() {
1040        let h = InMemoryCloudHome::new();
1041        let desired = CloudAccessState::Present {
1042            member_pubkey: "member".to_string(),
1043            provider_account_email: None,
1044        };
1045
1046        let first = h.set_access(desired.clone()).await.unwrap();
1047        let second = h.set_access(desired).await.unwrap();
1048        let expected = CloudAccessOutcome::Present(super::super::CloudHomeJoinInfo::S3 {
1049            bucket: "in-memory".to_string(),
1050            region: "test".to_string(),
1051            endpoint: Some("https://in-memory.invalid".to_string()),
1052            access_key: "in-memory".to_string(),
1053            secret_key: "in-memory".to_string(),
1054            key_prefix: None,
1055        });
1056        assert_eq!(first, expected);
1057        assert_eq!(second, expected);
1058        assert_eq!(
1059            h.set_access(CloudAccessState::Absent {
1060                member_pubkey: "member".to_string(),
1061                provider_account_email: None,
1062            })
1063            .await
1064            .unwrap(),
1065            CloudAccessOutcome::Absent(super::super::RevokeOutcome::Unsupported)
1066        );
1067    }
1068}