Skip to main content

dial9_core/
primitives.rs

1//! Cfg-gated concurrency primitives.
2//!
3//! Under normal compilation this re-exports from `std`. With `--cfg shuttle`
4//! it re-exports from `shuttle`, giving the shuttle scheduler control over all
5//! synchronization points so that tests can explore thread interleavings
6//! deterministically.
7
8// ── std path (production) ───────────────────────────────────────────────────
9
10#[cfg(not(shuttle))]
11pub mod sync {
12    pub use std::sync::atomic;
13    pub use std::sync::mpsc;
14    #[allow(unused_imports)]
15    pub use std::sync::{Arc, Barrier, Mutex, Weak};
16}
17
18#[cfg(not(shuttle))]
19pub mod thread {
20    #[allow(unused_imports)]
21    pub use std::thread::{JoinHandle, sleep, spawn};
22
23    /// Spawn a named thread. Uses `std::thread::Builder` in production,
24    /// falls back to plain `spawn` under shuttle (which has no Builder).
25    pub fn spawn_named<F, T>(name: &str, f: F) -> JoinHandle<T>
26    where
27        F: FnOnce() -> T + Send + 'static,
28        T: Send + 'static,
29    {
30        std::thread::Builder::new()
31            .name(name.into())
32            .spawn(f)
33            .expect("failed to spawn thread")
34    }
35}
36
37#[cfg(not(shuttle))]
38#[macro_export]
39macro_rules! define_thread_local {
40    ($($tt:tt)*) => { std::thread_local! { $($tt)* } };
41}
42#[cfg(not(shuttle))]
43pub use crate::define_thread_local as thread_local;
44
45#[cfg(all(not(shuttle), feature = "pipeline"))]
46pub mod time {
47    pub use tokio::time::error::Elapsed;
48    pub use tokio::time::{Instant, sleep, sleep_until, timeout};
49
50    pub fn now() -> Instant {
51        Instant::now()
52    }
53
54    pub fn elapsed_since(t: std::time::SystemTime) -> std::time::Duration {
55        t.elapsed().unwrap_or_default()
56    }
57}
58
59// ── shuttle path (deterministic testing) ────────────────────────────────────
60
61#[cfg(shuttle)]
62pub mod sync {
63    pub use shuttle::sync::atomic;
64    #[allow(unused_imports)]
65    pub use shuttle::sync::{Arc, Barrier, Mutex, Weak};
66
67    /// Shuttle's `recv_timeout` ignores the timeout and blocks forever,
68    /// which would stop the flush loop from ever looping. This wraps it to
69    /// randomly return `Timeout` instead, so shuttle can explore multiple
70    /// flush-loop cycles.
71    pub mod mpsc {
72        pub use shuttle::sync::mpsc::{RecvTimeoutError, SyncSender};
73
74        pub struct Receiver<T> {
75            inner: shuttle::sync::mpsc::Receiver<T>,
76        }
77
78        // SAFETY: shuttle's Receiver<T> is Send when T: Send, so the wrapper can be too.
79        unsafe impl<T: Send> Send for Receiver<T> {}
80
81        impl<T> Receiver<T> {
82            pub fn recv_timeout(
83                &self,
84                _timeout: std::time::Duration,
85            ) -> Result<T, RecvTimeoutError> {
86                // Randomly decide whether to simulate a timeout, giving
87                // the flush loop a chance to execute its body.
88                if shuttle::rand::thread_rng().gen_bool(0.8) {
89                    match self.inner.try_recv() {
90                        Ok(val) => Ok(val),
91                        Err(shuttle::sync::mpsc::TryRecvError::Empty) => {
92                            Err(RecvTimeoutError::Timeout)
93                        }
94                        Err(shuttle::sync::mpsc::TryRecvError::Disconnected) => {
95                            Err(RecvTimeoutError::Disconnected)
96                        }
97                    }
98                } else {
99                    // Delegate to shuttle's blocking recv to explore the
100                    // "flush loop blocks waiting for command" path.
101                    self.inner
102                        .recv()
103                        .map_err(|_| RecvTimeoutError::Disconnected)
104                }
105            }
106
107            pub fn recv(&self) -> Result<T, shuttle::sync::mpsc::RecvError> {
108                self.inner.recv()
109            }
110        }
111
112        use shuttle::rand::Rng;
113
114        /// Wraps shuttle's `sync_channel` to return our `Receiver` wrapper.
115        pub fn sync_channel<T>(bound: usize) -> (SyncSender<T>, Receiver<T>) {
116            let (tx, rx) = shuttle::sync::mpsc::sync_channel(bound);
117            (tx, Receiver { inner: rx })
118        }
119    }
120}
121
122#[cfg(shuttle)]
123pub mod thread {
124    #[allow(unused_imports)]
125    pub use shuttle::thread::{JoinHandle, sleep, spawn};
126
127    pub fn spawn_named<F, T>(_name: &str, f: F) -> JoinHandle<T>
128    where
129        F: FnOnce() -> T + Send + 'static,
130        T: Send + 'static,
131    {
132        spawn(f)
133    }
134}
135
136#[cfg(shuttle)]
137#[macro_export]
138macro_rules! define_thread_local {
139    ($($tt:tt)*) => { shuttle::thread_local! { $($tt)* } };
140}
141#[cfg(shuttle)]
142pub use crate::define_thread_local as thread_local;
143
144#[cfg(all(shuttle, feature = "pipeline"))]
145pub mod time {
146    use std::cell::Cell;
147    use std::future::Future;
148    use std::pin::Pin;
149    use std::sync::atomic::{AtomicUsize, Ordering};
150    use std::task::{Context, Poll};
151
152    pub use tokio::time::Instant;
153
154    // `Instant::now()` is nondeterministic under shuttle replay (real
155    // clock). `now()` reads a thread-local logical clock instead, advanced
156    // one tick per genuine `Yield` suspend, isolated per concurrently
157    // running `#[test]`.
158    std::thread_local! {
159        static LOGICAL_CLOCK: (Instant, Cell<u64>) = (Instant::now(), Cell::new(0));
160    }
161
162    // Comfortably clears this crate's current millisecond-scale test
163    // deadlines; not derived from anything principled.
164    const LOGICAL_TICK: std::time::Duration = std::time::Duration::from_millis(10);
165
166    pub fn now() -> Instant {
167        LOGICAL_CLOCK.with(|(base, nanos)| *base + std::time::Duration::from_nanos(nanos.get()))
168    }
169
170    /// Always zero: `SystemTime::elapsed()` needs a real clock read, which
171    /// is nondeterministic under shuttle replay. Matches `sleep`/
172    /// `sleep_until` below, which also ignore their requested duration
173    /// rather than fake one.
174    pub fn elapsed_since(_t: std::time::SystemTime) -> std::time::Duration {
175        std::time::Duration::ZERO
176    }
177
178    /// Shuttle has no virtual clock. Like `primitives::thread::sleep`, this
179    /// is a single scheduling point, not a real delay.
180    pub fn sleep(_duration: std::time::Duration) -> Yield {
181        Yield::default()
182    }
183
184    pub fn sleep_until(_deadline: Instant) -> Yield {
185        Yield::default()
186    }
187
188    #[derive(Debug, Default)]
189    pub struct Yield {
190        yielded: bool,
191    }
192
193    // Shuttle resets its own state every execution, but this must accumulate
194    // across a whole `check_pct` batch to answer "did any schedule in this batch
195    // actually suspend here."
196    static YIELD_PENDING_POLLS: AtomicUsize = AtomicUsize::new(0);
197
198    /// Count of `Yield::poll` calls that returned `Pending` since the last call.
199    /// Lets a scenario built around `sleep`/`sleep_until`
200    /// assert it actually suspended at least once across a batch.
201    ///
202    /// Test-integrity check: a failure here means the scenario stopped exercising
203    /// the code path it exists to cover.
204    pub fn take_yield_pending_polls() -> usize {
205        YIELD_PENDING_POLLS.swap(0, Ordering::Relaxed)
206    }
207
208    impl Future for Yield {
209        type Output = ();
210        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
211            if self.yielded {
212                Poll::Ready(())
213            } else {
214                self.yielded = true;
215                YIELD_PENDING_POLLS.fetch_add(1, Ordering::Relaxed);
216                LOGICAL_CLOCK
217                    .with(|(_, nanos)| nanos.set(nanos.get() + LOGICAL_TICK.as_nanos() as u64));
218                cx.waker().wake_by_ref();
219                Poll::Pending
220            }
221        }
222    }
223
224    /// No virtual clock to compare `_duration` against: each `Pending`
225    /// poll of `future` has a small chance of simulating the deadline
226    /// instead of waiting, so short futures rarely get cut off while
227    /// long ones accumulate rising odds across a `pct`/`determinism` batch.
228    pub fn timeout<F: Future + Unpin>(_duration: std::time::Duration, future: F) -> Timeout<F> {
229        Timeout { future }
230    }
231
232    #[derive(Debug)]
233    pub struct Elapsed(());
234
235    pub struct Timeout<F> {
236        future: F,
237    }
238
239    // Per-pending-poll odds of simulating the deadline. Low enough that a
240    // handful of polls (a typical drain) is very unlikely to get cut off.
241    const FIRE_PROBABILITY_PER_PENDING_POLL: f64 = 0.02;
242
243    impl<F: Future + Unpin> Future for Timeout<F> {
244        type Output = Result<F::Output, Elapsed>;
245        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
246            use shuttle::rand::Rng;
247            match Pin::new(&mut self.future).poll(cx) {
248                Poll::Ready(v) => Poll::Ready(Ok(v)),
249                Poll::Pending => {
250                    if shuttle::rand::thread_rng().gen_bool(FIRE_PROBABILITY_PER_PENDING_POLL) {
251                        Poll::Ready(Err(Elapsed(())))
252                    } else {
253                        Poll::Pending
254                    }
255                }
256            }
257        }
258    }
259}
260
261/// `tokio::select!` normally; `shuttle_tokio_impl_inner::select!` under
262/// `--cfg shuttle`, which patches `select!`'s branch tie-break to draw from
263/// `shuttle::rand::thread_rng()` instead of real OS entropy, so a replayed
264/// schedule picks the same branch every time.
265///
266/// Doesn't cover `sleep`/`sleep_until`: its `Sleep` resolves immediately on
267/// first poll instead of suspending, so `primitives::time` keeps its own
268/// `Yield` for those.
269#[cfg(all(shuttle, feature = "pipeline"))]
270#[macro_export]
271macro_rules! shuttle_select {
272    ($($arms:tt)*) => {
273        shuttle_tokio_impl_inner::select! { $($arms)* }
274    };
275}
276#[cfg(all(not(shuttle), feature = "pipeline"))]
277#[macro_export]
278macro_rules! shuttle_select {
279    ($($arms:tt)*) => {
280        tokio::select! { $($arms)* }
281    };
282}
283#[cfg(feature = "pipeline")]
284pub use crate::shuttle_select;
285
286/// Real `tokio::runtime::Builder` normally; under `--cfg shuttle`,
287/// `shuttle-tokio-impl-inner`'s stand-in, whose `block_on` is
288/// `shuttle::future::block_on`: a genuine `Runtime` would capture
289/// shuttle's one OS thread forever.
290#[cfg(all(not(shuttle), feature = "pipeline"))]
291pub mod runtime {
292    pub use tokio::runtime::Builder;
293}
294#[cfg(all(shuttle, feature = "pipeline"))]
295pub mod runtime {
296    pub use shuttle_tokio_impl_inner::runtime::Builder;
297}
298
299/// Coroutine stack size for a shuttle scenario whose call depth SIGBUSes
300/// on the bare-core 60KB default. Matches `shuttle-tokio`'s own default.
301/// Use via `shuttle_test!`'s `stack_size = $bytes` modifier.
302#[cfg(shuttle)]
303pub const SHUTTLE_TOKIO_STACK_SIZE: usize = 0x000F_0000;
304
305/// Pairs a shuttle scenario with `check_pct` and `check_uncontrolled_nondeterminism`
306/// Nests the scenario in its own module so `pct`/`determinism` can be fixed leaf names.
307///
308/// ```ignore
309/// shuttle_test! {
310///     num_iters = 5_000, depth = 3;
311///     fn my_scenario() { /* ... */ }
312/// }
313/// ```
314///
315/// Modifiers, added after `depth = $depth`:
316/// - `should_panic` -- document a known bug instead of asserting
317///   correctness. Add `expect_panic = "..."` to pin the panic message (default: any panic).
318///   Add `replay = "<schedule>"` to also check in a
319///   `replay_known_failure` test pinning one captured failing schedule (from
320///   a `pct` run's "failing schedule" output -- not `determinism`'s "failing
321///   seed", which `shuttle::replay` can't take).
322/// - `should_panic, flaky_sigabrt_determinism_only` -- same, but `#[ignore]`s
323///   `determinism` because it's confirmed to sometimes SIGABRT the process
324///   (see that arm's comment below). Confirm the crash first; don't use
325///   defensively.
326/// - `verify_faults_triggered` -- also asserts
327///   `primitives::fs::take_faults_triggered() > 0`, so fault injection can't
328///   silently stop exercising its error path.
329/// - `stack_size = $bytes`: build `shuttle::Runner` directly with a
330///   bumped coroutine stack, for a scenario whose call depth SIGBUSes on
331///   the hardcoded 60KB default. Pass [`SHUTTLE_TOKIO_STACK_SIZE`].
332///
333/// Use `num_iters = $num_iters, determinism_only;` instead of `num_iters =
334/// .., depth = ..` for a scenario with no real concurrency to explore
335/// (`check_pct` panics on those). Still needs shuttle's harness whenever the
336/// scenario touches a shuttle-swapped primitive.
337///
338/// Don't use this macro for a scenario touching real global `static` state --
339/// the generated tests run concurrently and would corrupt shuttle's own
340/// bookkeeping; write those by hand behind a real `std::sync::Mutex`.
341///
342/// `default` in place of `num_iters = .., depth = ..` picks up this
343/// codebase's established budget (5,000/3 plain, 100 `determinism_only`,
344/// 10,000/3 `verify_faults_triggered`). Not offered for `should_panic`: pick
345/// and justify an explicit number, since real scenarios there range
346/// 500-5,000 depending on how narrow the race is.
347#[cfg(shuttle)]
348#[macro_export]
349macro_rules! shuttle_test {
350    // Re-dispatches to the explicit-budget arms below, so budgets can't
351    // drift out of sync.
352    (default; $(#[$attr:meta])* fn $name:ident() $body:block) => {
353        $crate::shuttle_test! {
354            num_iters = 5_000, depth = 3;
355            $(#[$attr])* fn $name() $body
356        }
357    };
358    (default, determinism_only; $(#[$attr:meta])* fn $name:ident() $body:block) => {
359        $crate::shuttle_test! {
360            num_iters = 100, determinism_only;
361            $(#[$attr])* fn $name() $body
362        }
363    };
364    (default, verify_faults_triggered; $(#[$attr:meta])* fn $name:ident() $body:block) => {
365        $crate::shuttle_test! {
366            num_iters = 10_000, depth = 3, verify_faults_triggered;
367            $(#[$attr])* fn $name() $body
368        }
369    };
370    (num_iters = $num_iters:expr, depth = $depth:expr; $(#[$attr:meta])* fn $name:ident() $body:block) => {
371        mod $name {
372            use super::*;
373
374            $(#[$attr])*
375            fn $name() $body
376
377            #[test]
378            fn pct() {
379                shuttle::check_pct($name, $num_iters, $depth);
380            }
381
382            #[test]
383            fn determinism() {
384                shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
385            }
386        }
387    };
388    // No `pct`: it panics on a closure with no real concurrency to schedule
389    // (single-threaded, or a fork immediately joined with no
390    // overlapping-runnable window). Still needs shuttle's harness via
391    // `check_uncontrolled_nondeterminism` for any scenario touching a
392    // shuttle-swapped primitive.
393    (num_iters = $num_iters:expr, determinism_only; $(#[$attr:meta])* fn $name:ident() $body:block) => {
394        mod $name {
395            use super::*;
396
397            $(#[$attr])*
398            fn $name() $body
399
400            #[test]
401            fn determinism() {
402                shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
403            }
404        }
405    };
406    (num_iters = $num_iters:expr, depth = $depth:expr, should_panic $(, expect_panic = $msg:expr)? $(, replay = $schedule:expr)?; $(#[$attr:meta])* fn $name:ident() $body:block) => {
407        mod $name {
408            use super::*;
409
410            $(#[$attr])*
411            fn $name() $body
412
413            #[test]
414            #[should_panic $((expected = $msg))?]
415            fn pct() {
416                shuttle::check_pct($name, $num_iters, $depth);
417            }
418
419            #[test]
420            #[should_panic $((expected = $msg))?]
421            fn determinism() {
422                shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
423            }
424
425            $(
426                /// Replays a captured failing schedule so this exact
427                /// failure reproduces deterministically, without waiting on
428                /// `pct`/`determinism` exploration to find it again. No
429                /// `expect_panic` pin needed -- a fixed schedule can't surface
430                /// an unrelated panic.
431                #[test]
432                #[should_panic]
433                fn replay_known_failure() {
434                    shuttle::replay($name, $schedule);
435                }
436            )?
437        }
438    };
439    // Same as plain `should_panic`, but `#[ignore]`s only `determinism`
440    // (`pct` is unaffected) -- for a scenario confirmed to sometimes SIGABRT
441    // the process under shuttle.
442    //
443    // Root cause: `check_uncontrolled_nondeterminism` runs each schedule
444    // twice (record, then replay) to verify the same tasks stay runnable;
445    // `check_pct` doesn't. If a task still holds a shuttle-backed
446    // `ThreadLocalBuffer` when an uncaught panic unwinds through shuttle's
447    // `Execution::run`, its `Drop` runs after shuttle's `EXECUTION_STATE` is
448    // already torn down and panics again mid-unwind -- SIGABRT instead of a
449    // normal test failure.
450    //
451    // Confirm the crash first (run `determinism` alone, repeatedly) before
452    // using this -- don't use it defensively. Still runnable manually with
453    // `--ignored`.
454    //
455    // If also using `replay = $schedule`: capture it from `pct`'s failure
456    // output, not `determinism`'s. `determinism`'s schedules come from
457    // the same record-twice mechanism that SIGABRTs, so replaying one
458    // could reproduce the abort instead of a catchable panic.
459    (num_iters = $num_iters:expr, depth = $depth:expr, should_panic, flaky_sigabrt_determinism_only $(, expect_panic = $msg:expr)? $(, replay = $schedule:expr)?; $(#[$attr:meta])* fn $name:ident() $body:block) => {
460        mod $name {
461            use super::*;
462
463            $(#[$attr])*
464            fn $name() $body
465
466            #[test]
467            #[should_panic $((expected = $msg))?]
468            fn pct() {
469                shuttle::check_pct($name, $num_iters, $depth);
470            }
471
472            #[test]
473            #[should_panic $((expected = $msg))?]
474            #[ignore = "can SIGABRT the whole process under shuttle -- see shuttle_test!'s flaky_sigabrt_determinism_only arm; run manually with --ignored"]
475            fn determinism() {
476                shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
477            }
478
479            $(
480                /// Replays a captured failing schedule so this exact
481                /// failure reproduces deterministically, without waiting on
482                /// `pct`/`determinism` exploration to find it again. No
483                /// `expect_panic` pin needed -- a fixed schedule can't surface
484                /// an unrelated panic.
485                #[test]
486                #[should_panic]
487                fn replay_known_failure() {
488                    shuttle::replay($name, $schedule);
489                }
490            )?
491        }
492    };
493    // Same as the plain form, but also asserts
494    // `primitives::fs::take_faults_triggered() > 0` across the whole batch,
495    // so a broken fault-visibility thread-local can't silently stop fault
496    // injection without failing loudly. Checked inside the same
497    // `pct`/`determinism` runs, not separate tests, to avoid exploring twice.
498    (num_iters = $num_iters:expr, depth = $depth:expr, verify_faults_triggered; $(#[$attr:meta])* fn $name:ident() $body:block) => {
499        mod $name {
500            use super::*;
501
502            $(#[$attr])*
503            fn $name() $body
504
505            fn assert_faults_were_triggered() {
506                assert!(
507                    $crate::primitives::fs::take_faults_triggered() > 0,
508                    "no run across {} iterations triggered a single fault; fault injection is \
509                     not reaching the flush thread (e.g. a broken fault-visibility thread-local), \
510                     so this test is not exercising any error path.",
511                    $num_iters,
512                );
513            }
514
515            #[test]
516            fn pct() {
517                $crate::primitives::fs::take_faults_triggered(); // drain any count left over from an earlier test
518                shuttle::check_pct($name, $num_iters, $depth);
519                assert_faults_were_triggered();
520            }
521
522            #[test]
523            fn determinism() {
524                $crate::primitives::fs::take_faults_triggered(); // drain any count left over from an earlier test
525                shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
526                assert_faults_were_triggered();
527            }
528        }
529    };
530    // Same as the plain form, but builds `shuttle::Runner` directly with a
531    // bumped `stack_size`, for a scenario whose call depth SIGBUSes on
532    // the hardcoded 60KB default.
533    (num_iters = $num_iters:expr, depth = $depth:expr, stack_size = $stack_size:expr; $(#[$attr:meta])* fn $name:ident() $body:block) => {
534        mod $name {
535            use super::*;
536
537            $(#[$attr])*
538            fn $name() $body
539
540            fn config() -> shuttle::Config {
541                let mut config = shuttle::Config::new();
542                config.stack_size = $stack_size;
543                config
544            }
545
546            #[test]
547            fn pct() {
548                use shuttle::scheduler::PctScheduler;
549                let scheduler = PctScheduler::new($depth, $num_iters);
550                shuttle::Runner::new(scheduler, config()).run($name);
551            }
552
553            #[test]
554            fn determinism() {
555                use shuttle::scheduler::{RandomScheduler, UncontrolledNondeterminismCheckScheduler};
556                let scheduler =
557                    UncontrolledNondeterminismCheckScheduler::new(RandomScheduler::new($num_iters));
558                shuttle::Runner::new(scheduler, config()).run($name);
559            }
560        }
561    };
562}
563
564#[cfg(not(shuttle))]
565pub mod fs {
566    use std::io::{self, Write};
567    use std::path::Path;
568
569    pub fn create_dir_all(path: &Path) -> io::Result<()> {
570        std::fs::create_dir_all(path)
571    }
572    pub fn rename(from: &Path, to: &Path) -> io::Result<()> {
573        std::fs::rename(from, to)
574    }
575    pub fn remove_file(path: &Path) -> io::Result<()> {
576        std::fs::remove_file(path)
577    }
578    pub fn remove_dir(path: &Path) -> io::Result<()> {
579        std::fs::remove_dir(path)
580    }
581    pub fn read_dir(path: &Path) -> io::Result<std::fs::ReadDir> {
582        std::fs::read_dir(path)
583    }
584    pub fn metadata(path: &Path) -> io::Result<std::fs::Metadata> {
585        std::fs::metadata(path)
586    }
587    pub fn read(path: &Path) -> io::Result<Vec<u8>> {
588        std::fs::read(path)
589    }
590
591    /// Active-segment file handle.
592    #[derive(Debug)]
593    pub struct File(std::fs::File);
594
595    impl File {
596        pub fn create(path: &Path) -> io::Result<File> {
597            std::fs::File::create(path).map(File)
598        }
599    }
600
601    impl Write for File {
602        #[inline]
603        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
604            self.0.write(buf)
605        }
606        #[inline]
607        fn flush(&mut self) -> io::Result<()> {
608            self.0.flush()
609        }
610    }
611}
612
613#[cfg(shuttle)]
614pub mod fs {
615    use std::cell::Cell;
616    use std::io::{self, ErrorKind, Write};
617    use std::path::Path;
618
619    use shuttle::rand::Rng;
620
621    /// How to fail filesystem operations during a shuttle run.
622    #[derive(Clone, Copy, Debug)]
623    pub enum FaultPolicy {
624        /// Delegate to real `std::fs`, nothing fails.
625        None,
626        /// Every fallible op returns `PermissionDenied`.
627        FailAll,
628        /// Each op independently fails with this probability, drawn from
629        /// shuttle's RNG so the scheduler explores the fault schedule.
630        FailProb(f64),
631    }
632
633    // Shuttle's threads are coroutines on one real OS thread, so this
634    // stays visible to every spawned thread instead of being isolated per
635    // logical thread. Swapping to `shuttle::thread` would silently stop fault injection
636    // from reaching spawned threads. Pinned by `fs_fault_visible_across_threads`.
637    std::thread_local! {
638        static FAULT: Cell<FaultPolicy> = const { Cell::new(FaultPolicy::None) };
639    }
640
641    /// Arm `policy`: the returned guard restores the previous one on drop so a
642    /// fault can't leak into the next shuttle iteration.
643    #[must_use]
644    pub fn set_fault(policy: FaultPolicy) -> FaultGuard {
645        let prev = FAULT.with(|f| f.replace(policy));
646        FaultGuard { prev }
647    }
648
649    pub struct FaultGuard {
650        prev: FaultPolicy,
651    }
652
653    impl Drop for FaultGuard {
654        fn drop(&mut self) {
655            FAULT.with(|f| f.set(self.prev));
656        }
657    }
658
659    fn check() -> io::Result<()> {
660        let fail = match FAULT.with(|f| f.get()) {
661            FaultPolicy::None => false,
662            FaultPolicy::FailAll => true,
663            FaultPolicy::FailProb(p) => shuttle::rand::thread_rng().gen_bool(p),
664        };
665        if fail {
666            Err(io::Error::from(ErrorKind::PermissionDenied))
667        } else {
668            Ok(())
669        }
670    }
671
672    pub fn create_dir_all(path: &Path) -> io::Result<()> {
673        check()?;
674        std::fs::create_dir_all(path)
675    }
676    pub fn rename(from: &Path, to: &Path) -> io::Result<()> {
677        check()?;
678        std::fs::rename(from, to)
679    }
680    pub fn remove_file(path: &Path) -> io::Result<()> {
681        check()?;
682        std::fs::remove_file(path)
683    }
684    pub fn remove_dir(path: &Path) -> io::Result<()> {
685        check()?;
686        std::fs::remove_dir(path)
687    }
688    pub fn read_dir(path: &Path) -> io::Result<std::fs::ReadDir> {
689        check()?;
690        std::fs::read_dir(path)
691    }
692    pub fn metadata(path: &Path) -> io::Result<std::fs::Metadata> {
693        check()?;
694        std::fs::metadata(path)
695    }
696    pub fn read(path: &Path) -> io::Result<Vec<u8>> {
697        check()?;
698        std::fs::read(path)
699    }
700
701    /// Active-segment file handle whose `write`/`flush` honor the armed fault
702    /// policy. `create` is never faulted, so a writer can always be built.
703    #[derive(Debug)]
704    pub struct File(std::fs::File);
705
706    impl File {
707        pub fn create(path: &Path) -> io::Result<File> {
708            std::fs::File::create(path).map(File)
709        }
710    }
711
712    impl Write for File {
713        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
714            check()?;
715            self.0.write(buf)
716        }
717        fn flush(&mut self) -> io::Result<()> {
718            check()?;
719            self.0.flush()
720        }
721    }
722}
723
724// ── BoundedQueue ────────────────────────────────────────────────────────────
725
726/// A bounded MPMC queue. Production uses `crossbeam_queue::ArrayQueue`;
727/// under shuttle it uses a `Mutex<VecDeque>` so the scheduler can control
728/// access.
729#[cfg(not(shuttle))]
730pub struct BoundedQueue<T> {
731    inner: crossbeam_queue::ArrayQueue<T>,
732}
733
734#[cfg(not(shuttle))]
735impl<T> std::fmt::Debug for BoundedQueue<T> {
736    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
737        f.debug_struct("BoundedQueue")
738            .field("len", &self.inner.len())
739            .field("capacity", &self.inner.capacity())
740            .finish()
741    }
742}
743
744#[cfg(not(shuttle))]
745impl<T> BoundedQueue<T> {
746    pub fn new(capacity: usize) -> Self {
747        Self {
748            inner: crossbeam_queue::ArrayQueue::new(capacity),
749        }
750    }
751
752    /// Push a value, evicting the oldest if full. Returns the evicted value.
753    pub fn force_push(&self, value: T) -> Option<T> {
754        self.inner.force_push(value)
755    }
756
757    pub fn pop(&self) -> Option<T> {
758        self.inner.pop()
759    }
760}
761
762#[cfg(shuttle)]
763pub struct BoundedQueue<T> {
764    inner: shuttle::sync::Mutex<std::collections::VecDeque<T>>,
765    capacity: usize,
766}
767
768#[cfg(shuttle)]
769impl<T> std::fmt::Debug for BoundedQueue<T> {
770    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
771        f.debug_struct("BoundedQueue")
772            .field("capacity", &self.capacity)
773            .finish_non_exhaustive()
774    }
775}
776
777#[cfg(shuttle)]
778impl<T> BoundedQueue<T> {
779    pub fn new(capacity: usize) -> Self {
780        Self {
781            inner: shuttle::sync::Mutex::new(std::collections::VecDeque::with_capacity(capacity)),
782            capacity,
783        }
784    }
785
786    pub fn force_push(&self, value: T) -> Option<T> {
787        let mut q = self.inner.lock().unwrap();
788        let evicted = if q.len() >= self.capacity {
789            q.pop_front()
790        } else {
791            None
792        };
793        q.push_back(value);
794        evicted
795    }
796
797    pub fn pop(&self) -> Option<T> {
798        self.inner.lock().unwrap().pop_front()
799    }
800}