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 the continuation function when a task panics.
59/// Modelled as a non-exhaustive enum because there are a couple of unimplemented behaviors, such as
60/// returning the continuation function, or sending the function to a "sacrificial" thread to be dropped
61pub enum ContinuationFunctionBehavior {
62    /// Drop the continuation function when a task panics.
63    Drop,
64    /// Leak the continuation function when a task panics.
65    Leak,
66}
67
68impl ContinuationFunctionBehavior {
69    /// Create a new default `ContinuationFunctionBehavior`
70    pub const fn new() -> Self {
71        // This is the default because most Shuttle tests are not written in a "collect" mode, meaning
72        // the volume of leaks is low, and because we already default to leaking the continuation itself (via
73        // `force_reset`), which is a much bigger memory leak.
74        Self::Leak
75    }
76}
77
78impl Default for ContinuationFunctionBehavior {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84#[derive(Copy, Clone, Debug)]
85#[non_exhaustive]
86/// The config to define how to handle ungraceful shutdowns, ie. when the test panics.
87pub struct UngracefulShutdownConfig {
88    /// By default (when this is `false`) when a task panics we will serialize the schedule, then
89    /// continue scheduling until the panicking task has fully unwound its stack, and only then return.
90    /// This is somewhat wasteful, and also exposes us to more chances of having the entire test abort,
91    /// as we are running test code with `std::thread::panicking` (thus a second panic will be an abort).
92    /// Setting this to `true` will cause scheduling to stop as soon as a task panics. Note that the chance of
93    /// an abort (after serializing the schedule) is still present, as we will resume the unwind, and may panic
94    /// while calling drop handlers.
95    pub immediately_return_on_panic: bool,
96
97    /// What to do with the continuation function when it is dropped after a panic.
98    pub continuation_function_behavior: ContinuationFunctionBehavior,
99}
100
101impl UngracefulShutdownConfig {
102    /// Create a new default `UngracefulShutdownConfig`
103    pub const fn new() -> Self {
104        Self {
105            immediately_return_on_panic: false,
106            continuation_function_behavior: ContinuationFunctionBehavior::new(),
107        }
108    }
109}
110
111impl Default for UngracefulShutdownConfig {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl Config {
118    /// Create a new default configuration
119    pub fn new() -> Self {
120        Self {
121            stack_size: 0xf000,
122            failure_persistence: FailurePersistence::Print,
123            max_steps: MaxSteps::FailAfter(1_000_000),
124            max_time: None,
125            silence_warnings: false,
126            record_steps_in_span: false,
127            ungraceful_shutdown_config: UngracefulShutdownConfig::default(),
128        }
129    }
130}
131
132impl Default for Config {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138/// Specifies how to persist schedules when a Shuttle test fails
139///
140/// By default, schedules are printed to stdout/stderr, and can be replayed using `replay`.
141/// Optionally, they can instead be persisted to a file and replayed using `replay_from_file`,
142/// which can be useful if the schedule is too large to conveniently include in a call to
143/// `replay`.
144#[derive(Debug, Clone, PartialEq, Eq)]
145#[non_exhaustive]
146pub enum FailurePersistence {
147    /// Do not persist failing schedules
148    None,
149    /// Print failing schedules to stdout/stderr
150    Print,
151    /// Persist schedules as files in the given directory, or the current directory if None.
152    File(Option<std::path::PathBuf>),
153}
154
155/// Specifies an upper bound on the number of steps a single iteration of a Shuttle test can take,
156/// and how to react when the bound is reached.
157///
158/// A "step" is an atomic region (all the code between two yieldpoints). For example, all the
159/// (non-concurrency-operation) code between acquiring and releasing a `Mutex` is a single step.
160/// Shuttle can bound the maximum number of steps a single test iteration can take to prevent
161/// infinite loops. If the bound is hit, the test can either fail (`FailAfter`) or continue to the
162/// next iteration (`ContinueAfter`).
163///
164/// The steps bound can be used to protect against livelock and fairness issues. For example, if a
165/// thread is waiting for another thread to make progress, but the chosen `Scheduler` never
166/// schedules that thread, a livelock occurs and the test will not terminate without a step bound.
167///
168/// By default, Shuttle fails a test after 1,000,000 steps.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170#[non_exhaustive]
171pub enum MaxSteps {
172    /// Do not enforce any bound on the maximum number of steps
173    None,
174    /// Fail the test (by panicking) after the given number of steps
175    FailAfter(usize),
176    /// When the given number of steps is reached, stop the current iteration of the test and
177    /// begin a new iteration
178    ContinueAfter(usize),
179}