#[cfg(not(shuttle))]
pub mod sync {
pub use std::sync::atomic;
pub use std::sync::mpsc;
#[allow(unused_imports)]
pub use std::sync::{Arc, Barrier, Mutex, Weak};
}
#[cfg(not(shuttle))]
pub mod thread {
#[allow(unused_imports)]
pub use std::thread::{JoinHandle, sleep, spawn};
pub fn spawn_named<F, T>(name: &str, f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
std::thread::Builder::new()
.name(name.into())
.spawn(f)
.expect("failed to spawn thread")
}
}
#[cfg(not(shuttle))]
#[macro_export]
macro_rules! define_thread_local {
($($tt:tt)*) => { std::thread_local! { $($tt)* } };
}
#[cfg(not(shuttle))]
pub use crate::define_thread_local as thread_local;
#[cfg(all(not(shuttle), feature = "pipeline"))]
pub mod time {
pub use tokio::time::error::Elapsed;
pub use tokio::time::{Instant, sleep, sleep_until, timeout};
pub fn now() -> Instant {
Instant::now()
}
pub fn elapsed_since(t: std::time::SystemTime) -> std::time::Duration {
t.elapsed().unwrap_or_default()
}
}
#[cfg(shuttle)]
pub mod sync {
pub use shuttle::sync::atomic;
#[allow(unused_imports)]
pub use shuttle::sync::{Arc, Barrier, Mutex, Weak};
pub mod mpsc {
pub use shuttle::sync::mpsc::{RecvTimeoutError, SyncSender};
pub struct Receiver<T> {
inner: shuttle::sync::mpsc::Receiver<T>,
}
unsafe impl<T: Send> Send for Receiver<T> {}
impl<T> Receiver<T> {
pub fn recv_timeout(
&self,
_timeout: std::time::Duration,
) -> Result<T, RecvTimeoutError> {
if shuttle::rand::thread_rng().gen_bool(0.8) {
match self.inner.try_recv() {
Ok(val) => Ok(val),
Err(shuttle::sync::mpsc::TryRecvError::Empty) => {
Err(RecvTimeoutError::Timeout)
}
Err(shuttle::sync::mpsc::TryRecvError::Disconnected) => {
Err(RecvTimeoutError::Disconnected)
}
}
} else {
self.inner
.recv()
.map_err(|_| RecvTimeoutError::Disconnected)
}
}
pub fn recv(&self) -> Result<T, shuttle::sync::mpsc::RecvError> {
self.inner.recv()
}
}
use shuttle::rand::Rng;
pub fn sync_channel<T>(bound: usize) -> (SyncSender<T>, Receiver<T>) {
let (tx, rx) = shuttle::sync::mpsc::sync_channel(bound);
(tx, Receiver { inner: rx })
}
}
}
#[cfg(shuttle)]
pub mod thread {
#[allow(unused_imports)]
pub use shuttle::thread::{JoinHandle, sleep, spawn};
pub fn spawn_named<F, T>(_name: &str, f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
spawn(f)
}
}
#[cfg(shuttle)]
#[macro_export]
macro_rules! define_thread_local {
($($tt:tt)*) => { shuttle::thread_local! { $($tt)* } };
}
#[cfg(shuttle)]
pub use crate::define_thread_local as thread_local;
#[cfg(all(shuttle, feature = "pipeline"))]
pub mod time {
use std::cell::Cell;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
pub use tokio::time::Instant;
std::thread_local! {
static LOGICAL_CLOCK: (Instant, Cell<u64>) = (Instant::now(), Cell::new(0));
}
const LOGICAL_TICK: std::time::Duration = std::time::Duration::from_millis(10);
pub fn now() -> Instant {
LOGICAL_CLOCK.with(|(base, nanos)| *base + std::time::Duration::from_nanos(nanos.get()))
}
pub fn elapsed_since(_t: std::time::SystemTime) -> std::time::Duration {
std::time::Duration::ZERO
}
pub fn sleep(_duration: std::time::Duration) -> Yield {
Yield::default()
}
pub fn sleep_until(_deadline: Instant) -> Yield {
Yield::default()
}
#[derive(Debug, Default)]
pub struct Yield {
yielded: bool,
}
static YIELD_PENDING_POLLS: AtomicUsize = AtomicUsize::new(0);
pub fn take_yield_pending_polls() -> usize {
YIELD_PENDING_POLLS.swap(0, Ordering::Relaxed)
}
impl Future for Yield {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.yielded {
Poll::Ready(())
} else {
self.yielded = true;
YIELD_PENDING_POLLS.fetch_add(1, Ordering::Relaxed);
LOGICAL_CLOCK
.with(|(_, nanos)| nanos.set(nanos.get() + LOGICAL_TICK.as_nanos() as u64));
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
pub fn timeout<F: Future + Unpin>(_duration: std::time::Duration, future: F) -> Timeout<F> {
Timeout { future }
}
#[derive(Debug)]
pub struct Elapsed(());
pub struct Timeout<F> {
future: F,
}
const FIRE_PROBABILITY_PER_PENDING_POLL: f64 = 0.02;
impl<F: Future + Unpin> Future for Timeout<F> {
type Output = Result<F::Output, Elapsed>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
use shuttle::rand::Rng;
match Pin::new(&mut self.future).poll(cx) {
Poll::Ready(v) => Poll::Ready(Ok(v)),
Poll::Pending => {
if shuttle::rand::thread_rng().gen_bool(FIRE_PROBABILITY_PER_PENDING_POLL) {
Poll::Ready(Err(Elapsed(())))
} else {
Poll::Pending
}
}
}
}
}
}
#[cfg(all(shuttle, feature = "pipeline"))]
#[macro_export]
macro_rules! shuttle_select {
($($arms:tt)*) => {
shuttle_tokio_impl_inner::select! { $($arms)* }
};
}
#[cfg(all(not(shuttle), feature = "pipeline"))]
#[macro_export]
macro_rules! shuttle_select {
($($arms:tt)*) => {
tokio::select! { $($arms)* }
};
}
#[cfg(feature = "pipeline")]
pub use crate::shuttle_select;
#[cfg(all(not(shuttle), feature = "pipeline"))]
pub mod runtime {
pub use tokio::runtime::Builder;
}
#[cfg(all(shuttle, feature = "pipeline"))]
pub mod runtime {
pub use shuttle_tokio_impl_inner::runtime::Builder;
}
#[cfg(shuttle)]
pub const SHUTTLE_TOKIO_STACK_SIZE: usize = 0x000F_0000;
#[cfg(shuttle)]
#[macro_export]
macro_rules! shuttle_test {
(default; $(#[$attr:meta])* fn $name:ident() $body:block) => {
$crate::shuttle_test! {
num_iters = 5_000, depth = 3;
$(#[$attr])* fn $name() $body
}
};
(default, determinism_only; $(#[$attr:meta])* fn $name:ident() $body:block) => {
$crate::shuttle_test! {
num_iters = 100, determinism_only;
$(#[$attr])* fn $name() $body
}
};
(default, verify_faults_triggered; $(#[$attr:meta])* fn $name:ident() $body:block) => {
$crate::shuttle_test! {
num_iters = 10_000, depth = 3, verify_faults_triggered;
$(#[$attr])* fn $name() $body
}
};
(num_iters = $num_iters:expr, depth = $depth:expr; $(#[$attr:meta])* fn $name:ident() $body:block) => {
mod $name {
use super::*;
$(#[$attr])*
fn $name() $body
#[test]
fn pct() {
shuttle::check_pct($name, $num_iters, $depth);
}
#[test]
fn determinism() {
shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
}
}
};
(num_iters = $num_iters:expr, determinism_only; $(#[$attr:meta])* fn $name:ident() $body:block) => {
mod $name {
use super::*;
$(#[$attr])*
fn $name() $body
#[test]
fn determinism() {
shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
}
}
};
(num_iters = $num_iters:expr, depth = $depth:expr, should_panic $(, expect_panic = $msg:expr)? $(, replay = $schedule:expr)?; $(#[$attr:meta])* fn $name:ident() $body:block) => {
mod $name {
use super::*;
$(#[$attr])*
fn $name() $body
#[test]
#[should_panic $((expected = $msg))?]
fn pct() {
shuttle::check_pct($name, $num_iters, $depth);
}
#[test]
#[should_panic $((expected = $msg))?]
fn determinism() {
shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
}
$(
#[test]
#[should_panic]
fn replay_known_failure() {
shuttle::replay($name, $schedule);
}
)?
}
};
(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) => {
mod $name {
use super::*;
$(#[$attr])*
fn $name() $body
#[test]
#[should_panic $((expected = $msg))?]
fn pct() {
shuttle::check_pct($name, $num_iters, $depth);
}
#[test]
#[should_panic $((expected = $msg))?]
#[ignore = "can SIGABRT the whole process under shuttle -- see shuttle_test!'s flaky_sigabrt_determinism_only arm; run manually with --ignored"]
fn determinism() {
shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
}
$(
#[test]
#[should_panic]
fn replay_known_failure() {
shuttle::replay($name, $schedule);
}
)?
}
};
(num_iters = $num_iters:expr, depth = $depth:expr, verify_faults_triggered; $(#[$attr:meta])* fn $name:ident() $body:block) => {
mod $name {
use super::*;
$(#[$attr])*
fn $name() $body
fn assert_faults_were_triggered() {
assert!(
$crate::primitives::fs::take_faults_triggered() > 0,
"no run across {} iterations triggered a single fault; fault injection is \
not reaching the flush thread (e.g. a broken fault-visibility thread-local), \
so this test is not exercising any error path.",
$num_iters,
);
}
#[test]
fn pct() {
$crate::primitives::fs::take_faults_triggered(); shuttle::check_pct($name, $num_iters, $depth);
assert_faults_were_triggered();
}
#[test]
fn determinism() {
$crate::primitives::fs::take_faults_triggered(); shuttle::check_uncontrolled_nondeterminism($name, $num_iters);
assert_faults_were_triggered();
}
}
};
(num_iters = $num_iters:expr, depth = $depth:expr, stack_size = $stack_size:expr; $(#[$attr:meta])* fn $name:ident() $body:block) => {
mod $name {
use super::*;
$(#[$attr])*
fn $name() $body
fn config() -> shuttle::Config {
let mut config = shuttle::Config::new();
config.stack_size = $stack_size;
config
}
#[test]
fn pct() {
use shuttle::scheduler::PctScheduler;
let scheduler = PctScheduler::new($depth, $num_iters);
shuttle::Runner::new(scheduler, config()).run($name);
}
#[test]
fn determinism() {
use shuttle::scheduler::{RandomScheduler, UncontrolledNondeterminismCheckScheduler};
let scheduler =
UncontrolledNondeterminismCheckScheduler::new(RandomScheduler::new($num_iters));
shuttle::Runner::new(scheduler, config()).run($name);
}
}
};
}
#[cfg(not(shuttle))]
pub mod fs {
use std::io::{self, Write};
use std::path::Path;
pub fn create_dir_all(path: &Path) -> io::Result<()> {
std::fs::create_dir_all(path)
}
pub fn rename(from: &Path, to: &Path) -> io::Result<()> {
std::fs::rename(from, to)
}
pub fn remove_file(path: &Path) -> io::Result<()> {
std::fs::remove_file(path)
}
pub fn remove_dir(path: &Path) -> io::Result<()> {
std::fs::remove_dir(path)
}
pub fn read_dir(path: &Path) -> io::Result<std::fs::ReadDir> {
std::fs::read_dir(path)
}
pub fn metadata(path: &Path) -> io::Result<std::fs::Metadata> {
std::fs::metadata(path)
}
pub fn read(path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(path)
}
#[derive(Debug)]
pub struct File(std::fs::File);
impl File {
pub fn create(path: &Path) -> io::Result<File> {
std::fs::File::create(path).map(File)
}
}
impl Write for File {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}
}
#[cfg(shuttle)]
pub mod fs {
use std::cell::Cell;
use std::io::{self, ErrorKind, Write};
use std::path::Path;
use shuttle::rand::Rng;
#[derive(Clone, Copy, Debug)]
pub enum FaultPolicy {
None,
FailAll,
FailProb(f64),
}
std::thread_local! {
static FAULT: Cell<FaultPolicy> = const { Cell::new(FaultPolicy::None) };
}
#[must_use]
pub fn set_fault(policy: FaultPolicy) -> FaultGuard {
let prev = FAULT.with(|f| f.replace(policy));
FaultGuard { prev }
}
pub struct FaultGuard {
prev: FaultPolicy,
}
impl Drop for FaultGuard {
fn drop(&mut self) {
FAULT.with(|f| f.set(self.prev));
}
}
fn check() -> io::Result<()> {
let fail = match FAULT.with(|f| f.get()) {
FaultPolicy::None => false,
FaultPolicy::FailAll => true,
FaultPolicy::FailProb(p) => shuttle::rand::thread_rng().gen_bool(p),
};
if fail {
Err(io::Error::from(ErrorKind::PermissionDenied))
} else {
Ok(())
}
}
pub fn create_dir_all(path: &Path) -> io::Result<()> {
check()?;
std::fs::create_dir_all(path)
}
pub fn rename(from: &Path, to: &Path) -> io::Result<()> {
check()?;
std::fs::rename(from, to)
}
pub fn remove_file(path: &Path) -> io::Result<()> {
check()?;
std::fs::remove_file(path)
}
pub fn remove_dir(path: &Path) -> io::Result<()> {
check()?;
std::fs::remove_dir(path)
}
pub fn read_dir(path: &Path) -> io::Result<std::fs::ReadDir> {
check()?;
std::fs::read_dir(path)
}
pub fn metadata(path: &Path) -> io::Result<std::fs::Metadata> {
check()?;
std::fs::metadata(path)
}
pub fn read(path: &Path) -> io::Result<Vec<u8>> {
check()?;
std::fs::read(path)
}
#[derive(Debug)]
pub struct File(std::fs::File);
impl File {
pub fn create(path: &Path) -> io::Result<File> {
std::fs::File::create(path).map(File)
}
}
impl Write for File {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
check()?;
self.0.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
check()?;
self.0.flush()
}
}
}
#[cfg(not(shuttle))]
pub struct BoundedQueue<T> {
inner: crossbeam_queue::ArrayQueue<T>,
}
#[cfg(not(shuttle))]
impl<T> std::fmt::Debug for BoundedQueue<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BoundedQueue")
.field("len", &self.inner.len())
.field("capacity", &self.inner.capacity())
.finish()
}
}
#[cfg(not(shuttle))]
impl<T> BoundedQueue<T> {
pub fn new(capacity: usize) -> Self {
Self {
inner: crossbeam_queue::ArrayQueue::new(capacity),
}
}
pub fn force_push(&self, value: T) -> Option<T> {
self.inner.force_push(value)
}
pub fn pop(&self) -> Option<T> {
self.inner.pop()
}
}
#[cfg(shuttle)]
pub struct BoundedQueue<T> {
inner: shuttle::sync::Mutex<std::collections::VecDeque<T>>,
capacity: usize,
}
#[cfg(shuttle)]
impl<T> std::fmt::Debug for BoundedQueue<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BoundedQueue")
.field("capacity", &self.capacity)
.finish_non_exhaustive()
}
}
#[cfg(shuttle)]
impl<T> BoundedQueue<T> {
pub fn new(capacity: usize) -> Self {
Self {
inner: shuttle::sync::Mutex::new(std::collections::VecDeque::with_capacity(capacity)),
capacity,
}
}
pub fn force_push(&self, value: T) -> Option<T> {
let mut q = self.inner.lock().unwrap();
let evicted = if q.len() >= self.capacity {
q.pop_front()
} else {
None
};
q.push_back(value);
evicted
}
pub fn pop(&self) -> Option<T> {
self.inner.lock().unwrap().pop_front()
}
}