Skip to content

Commit

Permalink
Support reporting failing seeds found by RandomScheduler and replay…
Browse files Browse the repository at this point in the history
…ing from seeds
  • Loading branch information
funemy committed Sep 10, 2024
1 parent fa9499c commit 312a54c
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 3 deletions.
61 changes: 61 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,21 @@ where
runner.run(f);
}

/// Run the given function under a random seed for one iteration.
/// This effectively makes generating the random seed for each execution independent from
/// `RandomScheduler`. Therefore, this can be used with a library (like proptest) that takes care of
/// generating the random seeds.
pub fn check_random_with_seed<F>(f: F, seed: u64)
where
F: Fn() + Send + Sync + 'static,
{
use crate::scheduler::RandomScheduler;

let scheduler = RandomScheduler::new_from_seed(seed, 1);
let runner = Runner::new(scheduler, Default::default());
runner.run(f);
}

/// Run the given function under a PCT concurrency scheduler for some number of iterations at the
/// given depth. Each iteration will run a (potentially) different randomized schedule.
pub fn check_pct<F>(f: F, iterations: usize, depth: usize)
Expand Down Expand Up @@ -414,6 +429,52 @@ where
runner.run(f);
}

/// Run the given function using random scheduler according to a seed.
///
/// This function allows deterministic replay of a failing schedule, as long as `f` contains no
/// uncontrolled non-determinism. Uncontrolled non-determinism can be checked using
/// `check_uncontrolled_nondeterminism`.
///
/// This is a convenience function for constructing a [`Runner`] that uses
/// [`RandomScheduler::new_from_seed`](scheduler::RandomScheduler::new_from_seed) for one
/// iteration.
pub fn replay_random_from_seed<F>(f: F, seed: u64)
where
F: Fn() + Send + Sync + 'static,
{
use crate::scheduler::RandomScheduler;

let scheduler = RandomScheduler::new_from_seed(seed, 1);
let runner = Runner::new(scheduler, Default::default());
runner.run(f);
}

/// Run the given function using random scheduler according to a seed, passed as an environment
/// variable.
///
/// This function allows deterministic replay of a failing schedule, as long as `f` contains no
/// uncontrolled non-determinism. Uncontrolled non-determinism can be checked using
/// `check_uncontrolled_nondeterminism`.
///
/// This is an ergonomic version of `replay_random_from_seed`.
pub fn replay_random<F>(f: F)
where
F: Fn() + Send + Sync + 'static,
{
let seed_env = std::env::var("SHUTTLE_REPLAY_SEED");

match seed_env {
Ok(s) => {
let seed = s.as_str().parse::<u64>().unwrap();
replay_random_from_seed(f, seed)
}
Err(e) => panic!(
"The environment variable SHUTTLE_REPLAY_SEED for replaying is not properly set: {}",
e
),
}
}

/// Declare a new thread local storage key of type [`LocalKey`](crate::thread::LocalKey).
#[macro_export]
macro_rules! thread_local {
Expand Down
34 changes: 33 additions & 1 deletion src/scheduler/random.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,33 @@ pub struct RandomScheduler {
rng: Pcg64Mcg,
iterations: usize,
data_source: RandomDataSource,
current_seed: CurrentSeedDropGuard,
}

#[derive(Debug, Default)]
struct CurrentSeedDropGuard {
inner: Option<u64>,
}

impl CurrentSeedDropGuard {
fn wipe(&mut self) {
self.inner = None
}

fn update(&mut self, seed: u64) {
self.inner = Some(seed)
}
}

impl Drop for CurrentSeedDropGuard {
fn drop(&mut self) {
if let Some(s) = self.inner {
eprintln!(
"failing seed:\n\"\n{}\n\"\npass the seed to `shuttle::replay_from_seed` to replay the failure.",
s
)
}
}
}

impl RandomScheduler {
Expand All @@ -37,17 +64,22 @@ impl RandomScheduler {
rng,
iterations: 0,
data_source: RandomDataSource::initialize(seed),
current_seed: CurrentSeedDropGuard::default(),
}
}
}

impl Scheduler for RandomScheduler {
fn new_execution(&mut self) -> Option<Schedule> {
if self.iterations >= self.max_iterations {
self.current_seed.wipe();
None
} else {
self.iterations += 1;
Some(Schedule::new(self.data_source.reinitialize()))
let seed = self.data_source.reinitialize();
self.rng = Pcg64Mcg::seed_from_u64(seed);
self.current_seed.update(seed);
Some(Schedule::new(seed))
}
}

Expand Down
38 changes: 37 additions & 1 deletion tests/data/random.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::check_replay_roundtrip;
use crate::{check_replay_from_seed_match_schedule, check_replay_roundtrip};
use shuttle::rand::{thread_rng, Rng};
use shuttle::scheduler::RandomScheduler;
use shuttle::sync::Mutex;
Expand Down Expand Up @@ -172,6 +172,42 @@ fn dfs_threads_decorrelated_enabled() {
runner.run(thread_rng_decorrelated);
}

#[test]
fn replay_from_seed_match_schedule0() {
check_replay_from_seed_match_schedule(
broken_atomic_counter_stress,
15603830570056246250,
"91022ceac7d5bcb1a7fcc5d801a8050ea528954032492693491200000000",
);
}

#[test]
fn replay_from_seed_match_schedule1() {
check_replay_from_seed_match_schedule(
broken_atomic_counter_stress,
2185777353610950419,
"91023c93eecb80c29ddcaa1ef81a1c5251494a2c92928a2a954a25a904000000000000",
);
}

#[test]
fn replay_from_seed_match_schedule2() {
check_replay_from_seed_match_schedule(
broken_atomic_counter_stress,
14231716651102207764,
"91024b94fed5e7c2dccdc0c501185a9c0a889e169b64ca455b2d954a52492a49a59204000000\n00000000",
);
}

#[test]
fn replay_from_seed_match_schedule3() {
check_replay_from_seed_match_schedule(
broken_atomic_counter_stress,
14271799263003420363,
"910278cbcd808888bae787c601081eda4f904cb34937e96cb72db9da965c65d2969b29956dab\ne81625a54432c83469d24c020000000000000000",
);
}

// The DFS scheduler uses the same stream of randomness on each execution to ensure determinism
#[test]
fn dfs_does_not_reseed_across_executions() {
Expand Down
21 changes: 20 additions & 1 deletion tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod demo;
mod future;

use shuttle::scheduler::{ReplayScheduler, Scheduler};
use shuttle::{replay_from_file, Config, FailurePersistence, Runner};
use shuttle::{replay_from_file, replay_random_from_seed, Config, FailurePersistence, Runner};
use std::panic::{self, RefUnwindSafe, UnwindSafe};
use std::sync::Arc;

Expand Down Expand Up @@ -94,6 +94,25 @@ where
// `FailurePersistence`s for each test
}

/// Validates that replay-from-seed works by running a failing seed found by a random scheduler for one
/// iteration, expecting it to fail, comparing the new failing schedule against the previously collected one,
/// and checking the two schedules being identical.
fn check_replay_from_seed_match_schedule<F>(test_func: F, seed: u64, schedule: &str)
where
F: Fn() + Send + Sync + UnwindSafe + 'static,
{
let result = {
panic::catch_unwind(move || {
replay_random_from_seed(test_func, seed);
})
.expect_err("replay should panic")
};
let new_output = result.downcast::<String>().unwrap();
let new_schedule = parse_schedule::from_stdout(&new_output).expect("output should contain a schedule");

assert_eq!(new_schedule, schedule);
}

/// Helpers to parse schedules from different types of output (as determined by [`FailurePersistence`])
mod parse_schedule {
use regex::Regex;
Expand Down

0 comments on commit 312a54c

Please sign in to comment.