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}