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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
//! Database isolation for tests, in four tiers.
//!
//! There are no test base classes here. Each tier is a helper you
//! wrap around the test body, so pick the cheapest one that works:
//!
//! | Tier | Helper | Use when … |
//! |------------------------|----------------------------------------|---------------------------------------------------------|
//! | no database | plain `#[tokio::test]` | no DB access — fastest. |
//! | rolled-back writes | [`with_rollback`] | reads / writes that should be rolled back at end. |
//! | committed writes | [`with_truncate_after`] | code under test commits (signals, on_commit hooks). |
//! | real socket | [`crate::test_server::LiveServer`] | needs a real listening socket (browser, websockets). |
//!
//! The two DB helpers differ in what happens between tests:
//!
//! - `with_rollback` runs the body in a transaction that always
//! rolls back. It is the fastest, but nothing that waits for a
//! real commit runs: no signals, no `on_commit` hooks, no commit
//! triggers.
//! - `with_truncate_after` lets the body commit, then clears the
//! listed tables. The body sees committed state and the next test
//! starts clean, at the cost of the truncate.
//!
//! The common case:
//!
//! ```ignore
//! use rustango::test_db::with_rollback;
//!
//! #[tokio::test]
//! async fn create_and_count() {
//! let pool = test_pool().await;
//! with_rollback(&pool, |tx| Box::pin(async move {
//! // Rolled back when the closure returns.
//! let mut tx = tx.lock().await?;
//! insert_tx(&mut tx, &article_q("First")).await?;
//! insert_tx(&mut tx, &article_q("Second")).await?;
//! Ok(())
//! })).await.unwrap();
//!
//! // The two articles are gone — rollback happened on return.
//! assert_eq!(Article::objects().count(&pool).await.unwrap(), 0);
//! }
//! ```
//!
//! ## Why not `atomic()`?
//!
//! [`crate::sql::atomic`] commits on `Ok`. A test wants the rollback
//! every time, so [`with_rollback`] rolls back instead and still
//! hands back the closure's value. It is an `atomic` block otherwise:
//! the closure gets the same [`AtomicTx`], and an `atomic()` on the same
//! pool inside it runs in a savepoint that rolls back with it.
//!
//! ## Limits
//!
//! - The rollback covers one closure. Tests share the pool, so it
//! only undoes what this test wrote.
//! - Parallel tests can still race on rows they did not insert. Take
//! a suite-wide `tokio::Mutex` when a test touches global state.
//! - `on_commit` callbacks never fire under `with_rollback`, by
//! design, and any the closure registered are cleared.
//! - Nested calls and nested `atomic()` blocks on the same pool act as
//! savepoints. The outer rollback throws away their work too.
//! - Drop the `tx.lock()` guard before a nested `atomic()` or a
//! `bulk_insert_pool`: while it is held they fail with `NestedAtomic`.
use Future;
use Pin;
use crate;
/// Run `f` in a transaction that ALWAYS rolls back when the closure
/// returns, on `Ok` and on `Err` alike. The rollback happens after
/// the closure's value is captured, so you get that value back.
///
/// The closure gets an [`AtomicTx`]: lock it per statement. An `atomic()`
/// on `pool` inside it is a savepoint of this transaction (#1761).
///
/// An `Err` from the closure passes straight through. An `Ok` turns
/// into an error only if the rollback itself fails.
///
/// # Errors
/// - The closure's own error.
/// - `BEGIN` or `ROLLBACK` driver errors.
pub async
/// [`with_rollback`] with the `Box::pin(async move { … })` wrapper
/// written for you. Behaves the same:
///
/// ```ignore
/// rustango::with_rollback!(&pool, |tx| {
/// insert_tx(&mut *tx.lock().await?, &q).await?;
/// // ... assertions ...
/// Ok(())
/// }).await
/// ```
/// Run `f`, then clear `tables` whatever the result, even when `f`
/// panics (the panic is re-raised after the clear). This is the
/// `TransactionTestCase` shape: the closure's writes commit, so
/// signals, `on_commit` hooks and commit triggers all fire, and the
/// teardown leaves the tables clean for the next test.
///
/// The clear is [`truncate_tables`]: one transaction, any table order.
///
/// The closure's value comes back unchanged. A failed truncate
/// becomes the error only when the closure succeeded, so a noisy
/// teardown cannot hide the real failure.
///
/// **Concurrency**: this commits real rows, so tests using the same
/// tables must run one at a time behind a suite-wide
/// `tokio::sync::Mutex<()>`. Otherwise one worker's truncate wipes
/// another's setup.
///
/// List only the tables the test touches. Clearing every model's
/// table would tie each test to every other app. For a full wipe,
/// use `manage flush --yes`.
///
/// ```ignore
/// use rustango::test_db::with_truncate_after;
///
/// #[tokio::test]
/// async fn create_article_fires_post_save_signal() {
/// let pool = test_pool().await;
/// let _g = SUITE_MUTEX.lock().await;
/// with_truncate_after(&pool, &["articles"], || async move {
/// create_article("First").await?; // COMMITS — post_save fires
/// assert_signal_fired();
/// Ok(())
/// }).await.unwrap();
/// // The article is gone — truncate happened on return.
/// }
/// ```
///
/// # Errors
/// - The closure's own error (transitively).
/// - Truncate driver errors (only when the closure succeeded).
pub async
/// Clear every table in `tables`, in one transaction, in any order.
/// Public so fixtures and manual teardown can reuse it outside
/// [`with_truncate_after`]. The statements come from
/// [`crate::sql::Dialect::clear_tables_sql`]:
/// - **Postgres**: `TRUNCATE … RESTART IDENTITY CASCADE`; ids restart
/// and referencing tables are cleared too.
/// - **MySQL**: `DELETE` with FK checks off for that statement; ids
/// keep counting and rows in unlisted tables that point here stay.
/// - **SQLite**: `DELETE` with FK checks deferred to `COMMIT`, which
/// fails if an unlisted table still points at a cleared row.
///
/// An empty slice does nothing.
///
/// # Errors
/// The first driver error; the whole clear is then rolled back.
pub async
/// [`with_truncate_after`] in the same macro shape as
/// [`with_rollback!`](crate::with_rollback):
///
/// ```ignore
/// rustango::with_truncate_after!(&pool, &["articles", "comments"], {
/// create_article("First").await?;
/// create_comment("…").await?;
/// Ok(())
/// }).await
/// ```