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}