Skip to main content

commonware_runtime/storage/
faulty.rs

1//! A storage wrapper that injects deterministic faults for testing crash recovery.
2
3use crate::{deterministic::BoxDynRng, Error, IoBufs, IoBufsMut};
4use bytes::Buf;
5use commonware_utils::sync::{Mutex, RwLock};
6use rand::Rng;
7use std::{
8    io::Error as IoError,
9    sync::{
10        atomic::{AtomicU64, Ordering},
11        Arc,
12    },
13};
14
15/// Operation types for fault injection.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17enum Op {
18    Open,
19    Read,
20    Write,
21    Sync,
22    Resize,
23    Remove,
24    Scan,
25}
26
27/// Configuration for deterministic storage fault injection.
28///
29/// Each rate is a probability from 0.0 (never fail) to 1.0 (always fail).
30#[derive(Clone, Debug, Default)]
31pub struct Config {
32    /// Failure rate for `open_versioned` operations.
33    pub open_rate: Option<f64>,
34
35    /// Failure rate for `read_at` operations.
36    pub read_rate: Option<f64>,
37
38    /// Failure rate for `write_at` operations.
39    pub write_rate: Option<f64>,
40
41    /// Probability that a write failure is a partial write (some bytes written
42    /// before failure) rather than a complete failure (no bytes written).
43    /// Only applies when `write_rate` triggers a failure.
44    /// Value from 0.0 (always complete failure) to 1.0 (always partial write).
45    pub partial_write_rate: Option<f64>,
46
47    /// Failure rate for `sync` operations.
48    pub sync_rate: Option<f64>,
49
50    /// Failure rate for `resize` operations.
51    pub resize_rate: Option<f64>,
52
53    /// Probability that a resize failure is partial (resized to an intermediate
54    /// size before failure) rather than a complete failure (size unchanged).
55    /// Only applies when `resize_rate` triggers a failure.
56    /// Value from 0.0 (always complete failure) to 1.0 (always partial resize).
57    pub partial_resize_rate: Option<f64>,
58
59    /// Failure rate for `remove` operations.
60    pub remove_rate: Option<f64>,
61
62    /// Failure rate for `scan` operations.
63    pub scan_rate: Option<f64>,
64}
65
66impl Config {
67    /// Get the failure rate for an operation type.
68    fn rate_for(&self, op: Op) -> f64 {
69        match op {
70            Op::Open => self.open_rate,
71            Op::Read => self.read_rate,
72            Op::Write => self.write_rate,
73            Op::Sync => self.sync_rate,
74            Op::Resize => self.resize_rate,
75            Op::Remove => self.remove_rate,
76            Op::Scan => self.scan_rate,
77        }
78        .unwrap_or(0.0)
79    }
80
81    /// Set the open failure rate.
82    pub const fn open(mut self, rate: f64) -> Self {
83        self.open_rate = Some(rate);
84        self
85    }
86
87    /// Set the read failure rate.
88    pub const fn read(mut self, rate: f64) -> Self {
89        self.read_rate = Some(rate);
90        self
91    }
92
93    /// Set the write failure rate.
94    pub const fn write(mut self, rate: f64) -> Self {
95        self.write_rate = Some(rate);
96        self
97    }
98
99    /// Set the partial write rate (probability of partial vs complete write failure).
100    pub const fn partial_write(mut self, rate: f64) -> Self {
101        self.partial_write_rate = Some(rate);
102        self
103    }
104
105    /// Set the sync failure rate.
106    pub const fn sync(mut self, rate: f64) -> Self {
107        self.sync_rate = Some(rate);
108        self
109    }
110
111    /// Set the resize failure rate.
112    pub const fn resize(mut self, rate: f64) -> Self {
113        self.resize_rate = Some(rate);
114        self
115    }
116
117    /// Set the partial resize rate (probability of partial vs complete resize failure).
118    pub const fn partial_resize(mut self, rate: f64) -> Self {
119        self.partial_resize_rate = Some(rate);
120        self
121    }
122
123    /// Set the remove failure rate.
124    pub const fn remove(mut self, rate: f64) -> Self {
125        self.remove_rate = Some(rate);
126        self
127    }
128
129    /// Set the scan failure rate.
130    pub const fn scan(mut self, rate: f64) -> Self {
131        self.scan_rate = Some(rate);
132        self
133    }
134}
135
136/// Shared fault injection context.
137#[derive(Clone)]
138struct Oracle {
139    rng: Arc<Mutex<BoxDynRng>>,
140    config: Arc<RwLock<Config>>,
141}
142
143impl Oracle {
144    /// Check if a fault should be injected for the given operation.
145    fn should_fail(&self, op: Op) -> bool {
146        self.roll(Some(self.config.read().rate_for(op)))
147    }
148
149    /// Check if a write fault should be injected. Returns (should_fail, partial_rate).
150    /// Reads config once to avoid nested lock acquisition.
151    fn check_write_fault(&self) -> (bool, Option<f64>) {
152        let config = self.config.read();
153        let fail = self.roll(Some(config.rate_for(Op::Write)));
154        (fail, config.partial_write_rate)
155    }
156
157    /// Check if a resize fault should be injected. Returns (should_fail, partial_rate).
158    /// Reads config once to avoid nested lock acquisition.
159    fn check_resize_fault(&self) -> (bool, Option<f64>) {
160        let config = self.config.read();
161        let fail = self.roll(Some(config.rate_for(Op::Resize)));
162        (fail, config.partial_resize_rate)
163    }
164
165    /// Check if an event should occur based on a probability rate.
166    fn roll(&self, rate: Option<f64>) -> bool {
167        let rate = rate.unwrap_or(0.0);
168        if rate <= 0.0 {
169            return false;
170        }
171        if rate >= 1.0 {
172            return true;
173        }
174        self.rng.lock().gen::<f64>() < rate
175    }
176
177    /// Generate a random value strictly between `from` and `to`, or None if not possible.
178    fn random_between(&self, from: u64, to: u64) -> Option<u64> {
179        if from == to {
180            return None;
181        }
182        let (min, max) = if from < to { (from, to) } else { (to, from) };
183        if max - min <= 1 {
184            return None;
185        }
186        Some(self.rng.lock().gen_range(min + 1..max))
187    }
188
189    /// Try to generate a partial operation target. Returns Some if both the rate
190    /// check passes and an intermediate value exists between `from` and `to`.
191    fn try_partial(&self, rate: Option<f64>, from: u64, to: u64) -> Option<u64> {
192        if self.roll(rate) {
193            self.random_between(from, to)
194        } else {
195            None
196        }
197    }
198}
199
200/// A storage wrapper that injects deterministic faults based on configuration.
201///
202/// Uses a shared RNG for determinism.
203#[derive(Clone)]
204pub struct Storage<S: crate::Storage> {
205    inner: S,
206    ctx: Oracle,
207}
208
209impl<S: crate::Storage> Storage<S> {
210    /// Create a new faulty storage wrapper.
211    pub fn new(inner: S, rng: Arc<Mutex<BoxDynRng>>, config: Arc<RwLock<Config>>) -> Self {
212        Self {
213            inner,
214            ctx: Oracle { rng, config },
215        }
216    }
217
218    /// Get a reference to the inner storage.
219    pub const fn inner(&self) -> &S {
220        &self.inner
221    }
222
223    /// Get access to the fault configuration for dynamic modification.
224    pub fn config(&self) -> Arc<RwLock<Config>> {
225        self.ctx.config.clone()
226    }
227}
228
229/// Create an IoError for injected faults.
230fn injected_io_error() -> IoError {
231    IoError::other("injected storage fault")
232}
233
234impl<S: crate::Storage> crate::Storage for Storage<S> {
235    type Blob = Blob<S::Blob>;
236
237    async fn open_versioned(
238        &self,
239        partition: &str,
240        name: &[u8],
241        versions: std::ops::RangeInclusive<u16>,
242    ) -> Result<(Self::Blob, u64, u16), Error> {
243        if self.ctx.should_fail(Op::Open) {
244            return Err(Error::Io(injected_io_error()));
245        }
246        self.inner
247            .open_versioned(partition, name, versions)
248            .await
249            .map(|(blob, len, blob_version)| {
250                (Blob::new(self.ctx.clone(), blob, len), len, blob_version)
251            })
252    }
253
254    async fn remove(&self, partition: &str, name: Option<&[u8]>) -> Result<(), Error> {
255        if self.ctx.should_fail(Op::Remove) {
256            return Err(Error::Io(injected_io_error()));
257        }
258        self.inner.remove(partition, name).await
259    }
260
261    async fn scan(&self, partition: &str) -> Result<Vec<Vec<u8>>, Error> {
262        if self.ctx.should_fail(Op::Scan) {
263            return Err(Error::Io(injected_io_error()));
264        }
265        self.inner.scan(partition).await
266    }
267}
268
269/// A blob wrapper that injects deterministic faults based on configuration.
270#[derive(Clone)]
271pub struct Blob<B: crate::Blob> {
272    inner: B,
273    ctx: Oracle,
274    /// Tracked size for partial resize support.
275    size: Arc<AtomicU64>,
276}
277
278impl<B: crate::Blob> Blob<B> {
279    fn new(ctx: Oracle, inner: B, size: u64) -> Self {
280        Self {
281            inner,
282            ctx,
283            size: Arc::new(AtomicU64::new(size)),
284        }
285    }
286}
287
288impl<B: crate::Blob> crate::Blob for Blob<B> {
289    async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufsMut, Error> {
290        if self.ctx.should_fail(Op::Read) {
291            return Err(Error::Io(injected_io_error()));
292        }
293        self.inner.read_at(offset, len).await
294    }
295
296    async fn read_at_buf(
297        &self,
298        offset: u64,
299        len: usize,
300        bufs: impl Into<IoBufsMut> + Send,
301    ) -> Result<IoBufsMut, Error> {
302        if self.ctx.should_fail(Op::Read) {
303            return Err(Error::Io(injected_io_error()));
304        }
305        self.inner.read_at_buf(offset, len, bufs.into()).await
306    }
307
308    async fn write_at(&self, offset: u64, bufs: impl Into<IoBufs> + Send) -> Result<(), Error> {
309        let bufs = bufs.into();
310        let total_bytes = bufs.remaining() as u64;
311
312        let (should_fail, partial_rate) = self.ctx.check_write_fault();
313        if should_fail {
314            if let Some(bytes) = self.ctx.try_partial(partial_rate, 0, total_bytes) {
315                // Partial write: write some bytes, sync, then fail
316                self.inner
317                    .write_at(offset, bufs.coalesce().slice(..bytes as usize))
318                    .await?;
319                self.inner.sync().await?;
320                self.size
321                    .fetch_max(offset.saturating_add(bytes), Ordering::Relaxed);
322                return Err(Error::Io(injected_io_error()));
323            }
324            return Err(Error::Io(injected_io_error()));
325        }
326
327        self.inner.write_at(offset, bufs).await?;
328        self.size
329            .fetch_max(offset.saturating_add(total_bytes), Ordering::Relaxed);
330        Ok(())
331    }
332
333    async fn write_at_sync(
334        &self,
335        offset: u64,
336        bufs: impl Into<IoBufs> + Send,
337    ) -> Result<(), Error> {
338        let bufs = bufs.into();
339        let total_bytes = bufs.remaining() as u64;
340        if total_bytes == 0 {
341            return Ok(());
342        }
343
344        let (should_fail, partial_rate) = self.ctx.check_write_fault();
345        if should_fail {
346            if let Some(bytes) = self.ctx.try_partial(partial_rate, 0, total_bytes) {
347                self.inner
348                    .write_at_sync(offset, bufs.coalesce().slice(..bytes as usize))
349                    .await?;
350                self.size
351                    .fetch_max(offset.saturating_add(bytes), Ordering::Relaxed);
352                return Err(Error::Io(injected_io_error()));
353            }
354            return Err(Error::Io(injected_io_error()));
355        }
356
357        if self.ctx.should_fail(Op::Sync) {
358            self.inner.write_at(offset, bufs).await?;
359            self.size
360                .fetch_max(offset.saturating_add(total_bytes), Ordering::Relaxed);
361            return Err(Error::Io(injected_io_error()));
362        }
363
364        self.inner.write_at_sync(offset, bufs).await?;
365        self.size
366            .fetch_max(offset.saturating_add(total_bytes), Ordering::Relaxed);
367        Ok(())
368    }
369
370    async fn resize(&self, len: u64) -> Result<(), Error> {
371        let (should_fail, partial_rate) = self.ctx.check_resize_fault();
372        if should_fail {
373            let current = self.size.load(Ordering::Relaxed);
374            if let Some(len) = self.ctx.try_partial(partial_rate, current, len) {
375                // Partial resize: resize to intermediate size, sync, then fail
376                self.inner.resize(len).await?;
377                self.inner.sync().await?;
378                self.size.store(len, Ordering::Relaxed);
379                return Err(Error::Io(injected_io_error()));
380            }
381            return Err(Error::Io(injected_io_error()));
382        }
383        self.inner.resize(len).await?;
384        self.size.store(len, Ordering::Relaxed);
385        Ok(())
386    }
387
388    async fn sync(&self) -> Result<(), Error> {
389        if self.ctx.should_fail(Op::Sync) {
390            return Err(Error::Io(injected_io_error()));
391        }
392        self.inner.sync().await
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use crate::{
400        storage::{memory::Storage as MemStorage, tests::run_storage_tests},
401        telemetry::metrics::Registry,
402        Blob as _, BufferPool, BufferPoolConfig, Storage as _,
403    };
404    use rand::{rngs::StdRng, SeedableRng};
405
406    fn test_pool() -> BufferPool {
407        let mut registry = Registry::default();
408        BufferPool::new(BufferPoolConfig::for_storage(), &mut registry)
409    }
410
411    /// Test harness with faulty storage wrapping memory storage.
412    struct Harness {
413        inner: MemStorage,
414        storage: Storage<MemStorage>,
415        config: Arc<RwLock<Config>>,
416    }
417
418    impl Harness {
419        fn new(config: Config) -> Self {
420            Self::with_seed(42, config)
421        }
422
423        fn with_seed(seed: u64, config: Config) -> Self {
424            let inner = MemStorage::new(test_pool());
425            let rng = Arc::new(Mutex::new(
426                Box::new(StdRng::seed_from_u64(seed)) as BoxDynRng
427            ));
428            let config = Arc::new(RwLock::new(config));
429            let storage = Storage::new(inner.clone(), rng, config.clone());
430            Self {
431                inner,
432                storage,
433                config,
434            }
435        }
436    }
437
438    #[tokio::test]
439    async fn test_faulty_storage_no_faults() {
440        let h = Harness::new(Config::default());
441        run_storage_tests(h.storage).await;
442    }
443
444    #[tokio::test]
445    async fn test_faulty_storage_sync_always_fails() {
446        let h = Harness::new(Config::default().sync(1.0));
447
448        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
449        blob.write_at(0, b"data".to_vec()).await.unwrap();
450
451        assert!(matches!(blob.sync().await, Err(Error::Io(_))));
452    }
453
454    #[tokio::test]
455    async fn test_faulty_storage_write_always_fails() {
456        let h = Harness::new(Config::default().write(1.0));
457
458        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
459
460        assert!(matches!(
461            blob.write_at(0, b"data".to_vec()).await,
462            Err(Error::Io(_))
463        ));
464    }
465
466    #[tokio::test]
467    async fn test_faulty_storage_write_at_sync_write_always_fails() {
468        let h = Harness::new(Config::default().write(1.0));
469
470        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
471
472        assert!(matches!(
473            blob.write_at_sync(0, b"data".to_vec()).await,
474            Err(Error::Io(_))
475        ));
476    }
477
478    #[tokio::test]
479    async fn test_faulty_storage_write_at_sync_sync_failure_is_not_durable() {
480        let h = Harness::new(Config::default().sync(1.0));
481
482        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
483
484        assert!(matches!(
485            blob.write_at_sync(0, b"data".to_vec()).await,
486            Err(Error::Io(_))
487        ));
488
489        let (_reopened, size) = h.inner.open("partition", b"test").await.unwrap();
490        assert_eq!(size, 0);
491    }
492
493    #[tokio::test]
494    async fn test_faulty_storage_empty_write_at_sync_does_not_sync_prior_write() {
495        let h = Harness::new(Config::default().sync(1.0));
496
497        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
498        blob.write_at(0, b"data".to_vec()).await.unwrap();
499
500        blob.write_at_sync(4, Vec::<u8>::new()).await.unwrap();
501
502        let (_reopened, size) = h.inner.open("partition", b"test").await.unwrap();
503        assert_eq!(size, 0);
504    }
505
506    #[tokio::test]
507    async fn test_faulty_storage_read_always_fails() {
508        let h = Harness::new(Config::default());
509
510        // Write some data first (no faults)
511        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
512        blob.write_at(0, b"data".to_vec()).await.unwrap();
513        blob.sync().await.unwrap();
514
515        // Enable read faults
516        h.config.write().read_rate = Some(1.0);
517
518        assert!(matches!(blob.read_at(0, 4).await, Err(Error::Io(_))));
519    }
520
521    #[tokio::test]
522    async fn test_faulty_storage_open_always_fails() {
523        let h = Harness::new(Config::default().open(1.0));
524
525        assert!(matches!(
526            h.storage.open("partition", b"test").await,
527            Err(Error::Io(_))
528        ));
529    }
530
531    #[tokio::test]
532    async fn test_faulty_storage_remove_always_fails() {
533        let h = Harness::new(Config::default());
534
535        // Create a blob first
536        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
537        blob.write_at(0, b"data".to_vec()).await.unwrap();
538        blob.sync().await.unwrap();
539        drop(blob);
540
541        // Enable remove faults
542        h.config.write().remove_rate = Some(1.0);
543
544        assert!(matches!(
545            h.storage.remove("partition", Some(b"test")).await,
546            Err(Error::Io(_))
547        ));
548    }
549
550    #[tokio::test]
551    async fn test_faulty_storage_scan_always_fails() {
552        let h = Harness::new(Config::default());
553
554        // Create some blobs first
555        for i in 0..3 {
556            let name = format!("blob{i}");
557            let (blob, _) = h.storage.open("partition", name.as_bytes()).await.unwrap();
558            blob.write_at(0, b"data".to_vec()).await.unwrap();
559            blob.sync().await.unwrap();
560        }
561
562        // Enable scan faults
563        h.config.write().scan_rate = Some(1.0);
564
565        assert!(matches!(
566            h.storage.scan("partition").await,
567            Err(Error::Io(_))
568        ));
569    }
570
571    #[tokio::test]
572    async fn test_faulty_storage_determinism() {
573        async fn run_ops(seed: u64, rate: f64) -> Vec<bool> {
574            let h = Harness::with_seed(seed, Config::default().open(rate));
575            let mut results = Vec::new();
576            for i in 0..20 {
577                let name = format!("blob{i}");
578                results.push(h.storage.open("partition", name.as_bytes()).await.is_ok());
579            }
580            results
581        }
582
583        let results1 = run_ops(42, 0.5).await;
584        let results2 = run_ops(42, 0.5).await;
585        assert_eq!(results1, results2, "Same seed should produce same results");
586
587        let results3 = run_ops(999, 0.5).await;
588        assert_ne!(
589            results1, results3,
590            "Different seeds should produce different results"
591        );
592    }
593
594    #[tokio::test]
595    async fn test_faulty_storage_rate_for() {
596        let config = Config::default().open(0.1).sync(0.9);
597
598        assert!((config.rate_for(Op::Open) - 0.1).abs() < f64::EPSILON);
599        assert!((config.rate_for(Op::Sync) - 0.9).abs() < f64::EPSILON);
600        assert!(config.rate_for(Op::Write).abs() < f64::EPSILON);
601    }
602
603    #[tokio::test]
604    async fn test_faulty_storage_dynamic_config() {
605        let h = Harness::new(Config::default());
606
607        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
608        blob.sync().await.unwrap();
609
610        h.config.write().sync_rate = Some(1.0);
611        assert!(matches!(blob.sync().await, Err(Error::Io(_))));
612
613        h.config.write().sync_rate = Some(0.0);
614        blob.sync().await.unwrap();
615    }
616
617    #[tokio::test]
618    async fn test_faulty_storage_partial_write() {
619        let h = Harness::new(Config::default().write(1.0).partial_write(1.0));
620
621        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
622        let data = b"hello world".to_vec();
623        let result = blob.write_at(0, data.clone()).await;
624
625        assert!(matches!(result, Err(Error::Io(_))));
626
627        let (inner_blob, size) = h.inner.open("partition", b"test").await.unwrap();
628        let bytes_written = size as usize;
629        assert!(
630            bytes_written > 0 && bytes_written < data.len(),
631            "Expected partial write: {bytes_written} bytes out of {}",
632            data.len()
633        );
634
635        let read_result = inner_blob.read_at(0, bytes_written).await.unwrap();
636        assert_eq!(read_result.coalesce().as_ref(), &data[..bytes_written]);
637    }
638
639    #[tokio::test]
640    async fn test_faulty_storage_partial_write_disabled() {
641        let h = Harness::new(Config::default().write(1.0).partial_write(0.0));
642
643        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
644        let result = blob.write_at(0, b"hello world".to_vec()).await;
645
646        assert!(matches!(result, Err(Error::Io(_))));
647
648        let (_, size) = h.inner.open("partition", b"test").await.unwrap();
649        assert_eq!(
650            size, 0,
651            "Expected no bytes written when partial_write_rate is 0"
652        );
653    }
654
655    #[tokio::test]
656    async fn test_faulty_storage_partial_write_single_byte() {
657        let h = Harness::new(Config::default().write(1.0).partial_write(1.0));
658
659        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
660        let result = blob.write_at(0, b"x".to_vec()).await;
661
662        assert!(matches!(result, Err(Error::Io(_))));
663
664        let (_, size) = h.inner.open("partition", b"test").await.unwrap();
665        assert_eq!(size, 0, "No partial write possible for single byte");
666    }
667
668    #[tokio::test]
669    async fn test_faulty_storage_partial_resize_grow() {
670        let h = Harness::new(Config::default().resize(1.0).partial_resize(1.0));
671
672        let (blob, initial_size) = h.storage.open("partition", b"test").await.unwrap();
673        assert_eq!(initial_size, 0);
674
675        let target_size = 100u64;
676        let result = blob.resize(target_size).await;
677
678        assert!(matches!(result, Err(Error::Io(_))));
679
680        let (_, actual_size) = h.inner.open("partition", b"test").await.unwrap();
681        assert!(
682            actual_size > 0 && actual_size < target_size,
683            "Expected partial resize: size {actual_size} should be between 0 and {target_size}"
684        );
685    }
686
687    #[tokio::test]
688    async fn test_faulty_storage_partial_resize_shrink() {
689        let h = Harness::new(Config::default());
690
691        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
692        blob.resize(100).await.unwrap();
693        blob.sync().await.unwrap();
694
695        {
696            let mut cfg = h.config.write();
697            cfg.resize_rate = Some(1.0);
698            cfg.partial_resize_rate = Some(1.0);
699        }
700
701        let target_size = 10u64;
702        let result = blob.resize(target_size).await;
703
704        assert!(matches!(result, Err(Error::Io(_))));
705
706        let (_, actual_size) = h.inner.open("partition", b"test").await.unwrap();
707        assert!(
708            actual_size > target_size && actual_size < 100,
709            "Expected partial shrink: size {actual_size} should be between {target_size} and 100"
710        );
711    }
712
713    #[tokio::test]
714    async fn test_faulty_storage_partial_resize_disabled() {
715        let h = Harness::new(Config::default().resize(1.0).partial_resize(0.0));
716
717        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
718        let result = blob.resize(100).await;
719
720        assert!(matches!(result, Err(Error::Io(_))));
721
722        let (_, size) = h.inner.open("partition", b"test").await.unwrap();
723        assert_eq!(size, 0, "Expected no resize when partial_resize_rate is 0");
724    }
725
726    #[tokio::test]
727    async fn test_faulty_storage_partial_resize_same_size() {
728        let h = Harness::new(Config::default().resize(1.0).partial_resize(1.0));
729
730        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
731        let result = blob.resize(0).await;
732
733        assert!(matches!(result, Err(Error::Io(_))));
734
735        let (_, size) = h.inner.open("partition", b"test").await.unwrap();
736        assert_eq!(size, 0);
737    }
738
739    #[tokio::test]
740    async fn test_faulty_storage_partial_resize_after_write_extends() {
741        let h = Harness::new(Config::default());
742
743        let (blob, initial_size) = h.storage.open("partition", b"test").await.unwrap();
744        assert_eq!(initial_size, 0);
745
746        blob.write_at(0, vec![0xABu8; 50]).await.unwrap();
747        blob.sync().await.unwrap();
748
749        let (_, size_after_write) = h.inner.open("partition", b"test").await.unwrap();
750        assert_eq!(size_after_write, 50);
751
752        {
753            let mut cfg = h.config.write();
754            cfg.resize_rate = Some(1.0);
755            cfg.partial_resize_rate = Some(1.0);
756        }
757
758        let target_size = 10u64;
759        let result = blob.resize(target_size).await;
760
761        assert!(matches!(result, Err(Error::Io(_))));
762
763        let (_, actual_size) = h.inner.open("partition", b"test").await.unwrap();
764        assert!(
765            actual_size > target_size && actual_size < 50,
766            "Expected partial shrink from 50: size {actual_size} should be between {target_size} and 50"
767        );
768    }
769
770    #[tokio::test]
771    async fn test_faulty_storage_partial_resize_one_byte_difference() {
772        let h = Harness::new(Config::default().resize(1.0).partial_resize(1.0));
773
774        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
775        let result = blob.resize(1).await;
776
777        assert!(matches!(result, Err(Error::Io(_))));
778
779        let (_, size) = h.inner.open("partition", b"test").await.unwrap();
780        assert_eq!(size, 0);
781    }
782}