Skip to main content

shuttle_engine/
config.rs

1use std::cell::Cell;
2
3/// Configuration parameters for Shuttle
4#[derive(Clone, Debug)]
5#[non_exhaustive]
6pub struct Config {
7    /// Stack size allocated for each thread
8    pub stack_size: usize,
9
10    /// How to persist schedules when a test fails
11    pub failure_persistence: FailurePersistence,
12
13    /// Maximum number of steps a single iteration of a test can take, and how to react when the
14    /// limit is reached
15    pub max_steps: MaxSteps,
16
17    /// Time limit for an entire test. If set, calls to [`crate::runtime::runner::Runner::run`] will return when the time
18    /// limit is exceeded or the [`Scheduler`](crate::scheduler::Scheduler) chooses to stop (e.g.,
19    /// by hitting its maximum number of iterations), whichever comes first. This time limit will
20    /// not abort a currently running test iteration; the limit is only checked between iterations.
21    pub max_time: Option<std::time::Duration>,
22
23    /// Whether to silence warnings about Shuttle behaviors that may miss bugs or introduce false
24    /// positives:
25    /// 1. Unsound implementation of `atomic` may miss bugs
26    /// 2. `lazy_static` values are dropped at the end of an execution
27    pub silence_warnings: bool,
28
29    /// Whether to call the `Span::record()` method to update the step count (`i`) of the `Span`
30    /// containing the `TaskId` and the current step count for the given `TaskId`.
31    /// If `false`, this `Span` will look like this: `step{task=1}`, and if `true`, this `Span`
32    /// will look something like this: `step{task=1 i=3 i=9 i=12}`, or, if a `Subscriber` which
33    /// overwrites on calls to `span.record()` is used, something like this:
34    /// ```text
35    /// step{task=1 i=3}
36    /// step{task=1 i=9}
37    /// step{task=1 i=12}
38    /// ```
39    /// The reason this is a config option is that the most popular tracing `Subscriber`s, ie
40    /// `tracing_subscriber::fmt`, appends to the span on calls to `record()` (instead of
41    /// overwriting), which results in traces which are hard to read if the task is scheduled more
42    /// than a few times.
43    /// Thus: set `record_steps_in_span` to `true` if you want "append behavior", or if you are using
44    /// a `Subscriber` which overwrites on calls to `record()` and want to display the current step
45    /// count.
46    pub record_steps_in_span: bool,
47
48    /// The config to define how to handle ungraceful shutdowns, ie. when the test panics.
49    pub ungraceful_shutdown_config: UngracefulShutdownConfig,
50}
51
52std::thread_local! {
53    pub static UNGRACEFUL_SHUTDOWN_CONFIG: Cell<UngracefulShutdownConfig> = const { Cell::new(UngracefulShutdownConfig::new()) };
54}
55
56#[derive(Copy, Clone, Debug)]
57#[non_exhaustive]
58/// What to do with what the unfinished tasks of a failed execution leave behind: the functions of
59/// the tasks that never ran, and the futures of the future tasks that are waiting to be polled.
60/// The stacks of a failed execution's other unfinished tasks are always leaked, and its task-local
61/// values and statics are always dropped. Panics while dropping any of them are ignored, so that
62/// the failure is what gets reported.
63///
64/// Modelled as a non-exhaustive enum because there are a couple of unimplemented behaviors, such as
65/// returning the continuation function, or sending the function to a "sacrificial" thread to be dropped
66pub enum ContinuationFunctionBehavior {
67    /// Drop them, each on its task's own stack and as that task.
68    Drop,
69    /// Leak them.
70    Leak,
71}
72
73impl ContinuationFunctionBehavior {
74    /// Create a new default `ContinuationFunctionBehavior`
75    pub const fn new() -> Self {
76        // This is the default because most Shuttle tests are not written in a "collect" mode, meaning
77        // the volume of leaks is low, and because we already default to leaking the continuation itself (via
78        // `force_reset`), which is a much bigger memory leak.
79        Self::Leak
80    }
81
82    /// Whether this leaks what it applies to, rather than dropping it.
83    pub(crate) const fn leaks(self) -> bool {
84        match self {
85            Self::Drop => false,
86            Self::Leak => true,
87        }
88    }
89}
90
91impl Default for ContinuationFunctionBehavior {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97#[derive(Copy, Clone, Debug)]
98#[non_exhaustive]
99/// The config to define how to handle ungraceful shutdowns, ie. when the test panics.
100pub struct UngracefulShutdownConfig {
101    /// By default (when this is `false`) when a task panics we will serialize the schedule, then
102    /// continue scheduling until the panicking task has fully unwound its stack, and only then return.
103    /// This is somewhat wasteful, and also exposes us to more chances of having the entire test abort,
104    /// as we are running test code with `std::thread::panicking` (thus a second panic will be an abort).
105    /// Setting this to `true` will cause scheduling to stop as soon as a task panics. Note that the chance of
106    /// an abort (after serializing the schedule) is still present, as we will resume the unwind, and may panic
107    /// while calling drop handlers.
108    pub immediately_return_on_panic: bool,
109
110    /// What to do with the functions of a failed execution's tasks that never ran, and with the
111    /// futures of its future tasks that are waiting to be polled (see
112    /// [`ContinuationFunctionBehavior`]).
113    pub continuation_function_behavior: ContinuationFunctionBehavior,
114}
115
116impl UngracefulShutdownConfig {
117    /// Create a new default `UngracefulShutdownConfig`
118    pub const fn new() -> Self {
119        Self {
120            immediately_return_on_panic: false,
121            continuation_function_behavior: ContinuationFunctionBehavior::new(),
122        }
123    }
124}
125
126impl Default for UngracefulShutdownConfig {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132/// The step bound of the default configuration.
133pub(crate) const DEFAULT_MAX_STEPS: usize = 1_000_000;
134
135impl Config {
136    /// Create a new default configuration
137    pub fn new() -> Self {
138        Self {
139            stack_size: 0xf000,
140            failure_persistence: FailurePersistence::Print,
141            max_steps: MaxSteps::FailAfter(DEFAULT_MAX_STEPS),
142            max_time: None,
143            silence_warnings: false,
144            record_steps_in_span: false,
145            ungraceful_shutdown_config: UngracefulShutdownConfig::default(),
146        }
147    }
148}
149
150impl Default for Config {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156/// Specifies how to persist schedules when a Shuttle test fails
157///
158/// By default, schedules are printed to stdout/stderr, and can be replayed using `replay`.
159/// Optionally, they can instead be persisted to a file and replayed using `replay_from_file`,
160/// which can be useful if the schedule is too large to conveniently include in a call to
161/// `replay`.
162#[derive(Debug, Clone, PartialEq, Eq)]
163#[non_exhaustive]
164pub enum FailurePersistence {
165    /// Do not persist failing schedules
166    None,
167    /// Print failing schedules to stdout/stderr
168    Print,
169    /// Persist schedules as files in the given directory, or the current directory if None.
170    File(Option<std::path::PathBuf>),
171}
172
173/// Specifies an upper bound on the number of steps a single iteration of a Shuttle test can take,
174/// and how to react when the bound is reached.
175///
176/// A "step" is an atomic region (all the code between two yieldpoints). For example, all the
177/// (non-concurrency-operation) code between acquiring and releasing a `Mutex` is a single step.
178/// Shuttle can bound the maximum number of steps a single test iteration can take to prevent
179/// infinite loops. If the bound is hit, the test can either fail (`FailAfter`) or continue to the
180/// next iteration (`ContinueAfter`).
181///
182/// The steps bound can be used to protect against livelock and fairness issues. For example, if a
183/// thread is waiting for another thread to make progress, but the chosen `Scheduler` never
184/// schedules that thread, a livelock occurs and the test will not terminate without a step bound.
185///
186/// By default, Shuttle fails a test after 1,000,000 steps.
187///
188/// The bound applies to the destructors that run when an execution is torn down too, whose
189/// scheduling points count as steps: a destructor that spins fails the test. Under `ContinueAfter`,
190/// they get at least the default 1,000,000 steps, because teardown cannot stop and continue.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192#[non_exhaustive]
193pub enum MaxSteps {
194    /// Do not enforce any bound on the maximum number of steps
195    None,
196    /// Fail the test (by panicking) after the given number of steps
197    FailAfter(usize),
198    /// When the given number of steps is reached, stop the current iteration of the test and
199    /// begin a new iteration
200    ContinueAfter(usize),
201}