1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]

use std::{future::Future, time::Duration};

#[cfg(any(test, feature = "test"))]
mod real;

#[cfg(all(test, not(feature = "test")))]
const _: () = panic!("Trying to test `reord` without its `test` feature");

// TODO: implement RwLocks

/// Configuration for a `reord`-based test
#[derive(Debug)]
#[non_exhaustive]
pub struct Config {
    /// The random seed used to choose which task to run
    ///
    /// Changing this seed will give other task orderings, but leaving it the same should (if
    /// lock tracking is correctly implemented by the application using `Lock`) keep execution
    /// reproducible.
    pub seed: u64,

    /// Timeout after which a [`maybe_lock`] will be considered as having blocked on the lock
    pub maybe_lock_timeout: Duration,

    /// If set to `Some`, will allow two tasks to voluntarily collide on a named lock to validate
    /// that locking is implemented correctly. It will then wait for the time indicated by this
    /// setting, and if the next `reord::point` has not been reached by then, assume the locking
    /// worked properly and continue testing.
    pub check_named_locks_work_for: Option<Duration>,

    /// If set to `Some`, will allow two tasks to voluntarily collide on an addressed lock to
    /// validate that locking is implemented correctly. It will then wait for the time indicated
    /// by this setting, and if the next `reord::point` has not been reached by then, assume the
    /// locking worked properly and continue testing.
    pub check_addressed_locks_work_for: Option<Duration>,
}

impl Config {
    /// Generate a configuration with the default parameters and a random seed
    ///
    /// If you are running under a fuzzer, you should have the fuzzer generate your seed and pass
    /// it to [`Config::from_seed`].
    pub fn with_random_seed() -> Config {
        use rand::Rng;
        let seed = rand::thread_rng().gen();
        eprintln!("Running `reord` test with random seed {:?}", seed);
        Config {
            seed,
            maybe_lock_timeout: Duration::from_millis(100),
            check_addressed_locks_work_for: None,
            check_named_locks_work_for: None,
        }
    }

    /// Generate a configuration with the default parameters from a given seed
    pub fn from_seed(seed: u64) -> Config {
        Config {
            seed,
            maybe_lock_timeout: Duration::from_millis(100),
            check_addressed_locks_work_for: None,
            check_named_locks_work_for: None,
        }
    }
}

/// Start a test
///
/// Note that this relies on global variables for convenience, and thus should only ever be used with
/// `cargo-nextest`.
#[inline]
#[allow(unused_variables)]
pub async fn init_test(config: Config) {
    #[cfg(feature = "test")]
    real::init_test(config).await
}

/// Add a task to the `reord` framework
///
/// This should be used around all the futures spawned by the test
#[inline]
pub async fn new_task<T>(f: impl Future<Output = T>) -> T {
    #[cfg(feature = "test")]
    let res = real::new_task(f).await;
    #[cfg(not(feature = "test"))]
    let res = f.await;
    res
}

/// Start the test once `tasks` tasks are ready for execution
///
/// This should be called after at least `tasks` tasks have been spawned on the executor,
/// wrapped by `new_task`.
///
/// This will start executing the tasks in a random but reproducible order, and then return
/// as soon as the `tasks` tasks have started executing.
///
/// This returns a `JoinHandle`, that you should join if you want to catch panics related to
/// lock handling.
#[inline]
#[allow(unused_variables)]
pub async fn start(tasks: usize) -> tokio::task::JoinHandle<()> {
    #[cfg(not(feature = "test"))]
    panic!("Trying to start a `reord` test, but the `test` feature is not set");
    #[cfg(feature = "test")]
    real::start(tasks).await
}

/// Execution order randomization point
///
/// Reaching this point makes `reord` able to switch the execution to another thread.
#[inline]
pub async fn point() {
    #[cfg(feature = "test")]
    real::point().await
}

/// Potential lock-taking activity happening, that is impossible to encode with [`Lock`]
///
/// This should not be used if you can exactly define the locking behavior with [`Lock`], as it incurs
/// a heavy performance penalty: each time `reord` will hit this point, it will continue assuming there
/// is no lock, and then if nothing happens during the time configured in [`Config`], it will assume
/// that a lock was actually taken and start running another task.
///
/// For these reasons, if using this, you should:
/// - Make sure this is just before the potentially-lock-taking operation
/// - Make sure you have a [`point()`] call just after the potentially-lock-taking operation
/// - Make sure the lock-taking operation itself cannot be a source of non-determinism, as `reord`
///   will run multiple of them in parallel when the lock is released, until they reach the next [`point`]
/// - Make sure you do not have a panic or error that'd make code escape between the [`maybe_lock`]
///   and its associated [`point`], to stay reproducible
/// - Use this as sparsely as possible
///
/// Unfortunately, there are circumstances that force the use of `maybe_lock`. For example, postgresql
/// database access take locks that live for the lifetime of the transaction, that are basically impossible
/// to guess or otherwise encode in a clean locking format.
#[inline]
pub async fn maybe_lock() {
    #[cfg(feature = "test")]
    real::maybe_lock().await
}

/// Lock information
///
/// This can be either a user-defined name, or an address. When using `Addressed`, you should usually take
/// the address of the `Mutex`, `RwLock` or equivalent, and cast it to `usize`.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum LockInfo {
    /// A user-defined name
    Named(String),

    /// An address, usually the address of a `Mutex` or similar cast to `usize`
    Addressed(usize),
}

/// Lock handling
///
/// This records, for `reord`, which locks are currently taken by the program. The lock should be acquired
/// as close as possible before the real lock acquiring, and released as soon as possible after the real
/// lock release.
///
/// In addition, there should be a `reord::point().await` just after the real lock managed to be acquired,
/// and one ideally just after the real lock was released, though this may be harder due to early returns
/// and the current absence of `async Drop`.
///
/// If too long passes with `reord` unaware of the state of your locks, you could end up with
/// non-reproducible behavior, due to two execution threads actually running in parallel on your executor.
#[derive(Debug)]
pub struct Lock {
    #[cfg(feature = "test")]
    _data: real::Lock,
    #[cfg(not(feature = "test"))]
    _unused: (),
}

impl Lock {
    /// Take a lock with a given name
    #[inline]
    #[allow(unused_variables)]
    pub async fn take_named(name: String) -> Lock {
        #[cfg(feature = "test")]
        let res = Lock {
            _data: real::Lock::take_named(name).await,
        };
        #[cfg(not(feature = "test"))]
        let res = Lock { _unused: () };
        res
    }

    /// Take a lock at a given address
    #[inline]
    #[allow(unused_variables)]
    pub async fn take_addressed(address: usize) -> Lock {
        #[cfg(feature = "test")]
        let res = Lock {
            _data: real::Lock::take_addressed(address).await,
        };
        #[cfg(not(feature = "test"))]
        let res = Lock { _unused: () };
        res
    }

    /// Take multiple locks, atomically
    ///
    /// If you try to take multiple `reord::Lock`s one after the other before locking them atomically, then
    /// `reord` will think that your first lock failed to actually lock. In order to avoid this, you should
    /// use `reord::Lock::take_atomic` to take multiple locks.
    #[inline]
    #[allow(unused_variables)]
    pub async fn take_atomic(l: Vec<LockInfo>) -> Lock {
        #[cfg(feature = "test")]
        let res = Lock {
            _data: real::Lock::take_atomic(l).await,
        };
        #[cfg(not(feature = "test"))]
        let res = Lock { _unused: () };
        res
    }
}

impl Drop for Lock {
    #[inline]
    fn drop(&mut self) {}
}

#[cfg(test)]
mod tests;