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
255
256
257
258
259
260
261
262
263
264
265
266
267
//! 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 {
//! // Inserts here are visible to assertions inside the
//! // closure, but rolled back when it returns.
//! insert_tx(tx, &article_q("First")).await?;
//! insert_tx(tx, &article_q("Second")).await?;
//!
//! let count = count_tx::<Article>(tx).await?;
//! assert_eq!(count, 2);
//! Ok(())
//! })).await.unwrap();
//!
//! // The two articles are gone — rollback happened on return.
//! }
//! ```
//!
//! ## 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.
//!
//! ## 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 act as savepoints. An outer rollback throws away
//! inner work even if the inner call committed.
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.
///
/// 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(tx, &q).await?;
/// // ... assertions ...
/// Ok(())
/// }).await
/// ```
/// Run `f`, then clear `tables` whatever the result. 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.
///
/// Like `manage flush`, the method depends on the dialect:
/// - **Postgres**: one `TRUNCATE TABLE … RESTART IDENTITY CASCADE`.
/// - **MySQL / SQLite**: `DELETE FROM "<table>"` per table, in the
/// given order. Sequences are not reset.
///
/// 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`. Public so fixtures and manual
/// teardown can reuse it outside [`with_truncate_after`].
///
/// Postgres gets one `TRUNCATE`; MySQL and SQLite get a
/// `DELETE FROM` per table. The per-table path does not stop at the
/// first failure, so the rest are still cleared.
///
/// An empty slice does nothing.
///
/// # Errors
/// - The first driver error. Later ones are dropped.
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
/// ```