1use std::future::Future;
2use std::time::Duration;
3
4use futures_core::future::BoxFuture;
5
6use base64::{engine::general_purpose::URL_SAFE, Engine as _};
7pub use fixtures::FixtureSnapshot;
8use sha2::{Digest, Sha512};
9
10use crate::connection::{ConnectOptions, Connection};
11use crate::database::Database;
12use crate::error::Error;
13use crate::executor::Executor;
14use crate::migrate::{Migrate, Migrator};
15use crate::pool::{Pool, PoolConnection, PoolOptions};
16
17mod fixtures;
18
19pub trait TestSupport: Database {
20 fn test_context(args: &TestArgs) -> BoxFuture<'_, Result<TestContext<Self>, Error>>;
30
31 fn cleanup_test(db_name: &str) -> BoxFuture<'_, Result<(), Error>>;
32
33 fn cleanup_test_dbs() -> BoxFuture<'static, Result<Option<usize>, Error>>;
40
41 fn snapshot(conn: &mut Self::Connection)
45 -> BoxFuture<'_, Result<FixtureSnapshot<Self>, Error>>;
46
47 fn db_name(args: &TestArgs) -> String {
49 let mut hasher = Sha512::new();
50 hasher.update(args.test_path.as_bytes());
51 let hash = hasher.finalize();
52 let hash = URL_SAFE.encode(&hash[..39]);
53 let db_name = format!("_sqlx_test_{}", hash).replace('-', "_");
54 debug_assert!(db_name.len() == 63);
55 db_name
56 }
57}
58
59pub struct TestFixture {
60 pub path: &'static str,
61 pub contents: &'static str,
62}
63
64pub struct TestArgs {
65 pub test_path: &'static str,
66 pub migrator: Option<&'static Migrator>,
67 pub fixtures: &'static [TestFixture],
68}
69
70pub trait TestFn {
71 type Output;
72
73 fn run_test(self, args: TestArgs) -> Self::Output;
74}
75
76pub trait TestTermination {
77 fn is_success(&self) -> bool;
78}
79
80pub struct TestContext<DB: Database> {
81 pub pool_opts: PoolOptions<DB>,
82 pub connect_opts: <DB::Connection as Connection>::Options,
83 pub db_name: String,
84}
85
86impl<DB, Fut> TestFn for fn(Pool<DB>) -> Fut
87where
88 DB: TestSupport + Database,
89 DB::Connection: Migrate,
90 for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
91 Fut: Future,
92 Fut::Output: TestTermination,
93{
94 type Output = Fut::Output;
95
96 fn run_test(self, args: TestArgs) -> Self::Output {
97 run_test_with_pool(args, self)
98 }
99}
100
101impl<DB, Fut> TestFn for fn(PoolConnection<DB>) -> Fut
102where
103 DB: TestSupport + Database,
104 DB::Connection: Migrate,
105 for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
106 Fut: Future,
107 Fut::Output: TestTermination,
108{
109 type Output = Fut::Output;
110
111 fn run_test(self, args: TestArgs) -> Self::Output {
112 run_test_with_pool(args, |pool| async move {
113 let conn = pool
114 .acquire()
115 .await
116 .expect("failed to acquire test pool connection");
117 let res = (self)(conn).await;
118 pool.close().await;
119 res
120 })
121 }
122}
123
124impl<DB, Fut> TestFn for fn(PoolOptions<DB>, <DB::Connection as Connection>::Options) -> Fut
125where
126 DB: Database + TestSupport,
127 DB::Connection: Migrate,
128 for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
129 Fut: Future,
130 Fut::Output: TestTermination,
131{
132 type Output = Fut::Output;
133
134 fn run_test(self, args: TestArgs) -> Self::Output {
135 run_test(args, self)
136 }
137}
138
139impl<Fut> TestFn for fn() -> Fut
140where
141 Fut: Future,
142{
143 type Output = Fut::Output;
144
145 fn run_test(self, args: TestArgs) -> Self::Output {
146 assert!(
147 args.fixtures.is_empty(),
148 "fixtures cannot be applied for a bare function"
149 );
150 crate::rt::test_block_on(self())
151 }
152}
153
154impl TestArgs {
155 pub fn new(test_path: &'static str) -> Self {
156 TestArgs {
157 test_path,
158 migrator: None,
159 fixtures: &[],
160 }
161 }
162
163 pub fn migrator(&mut self, migrator: &'static Migrator) {
164 self.migrator = Some(migrator);
165 }
166
167 pub fn fixtures(&mut self, fixtures: &'static [TestFixture]) {
168 self.fixtures = fixtures;
169 }
170}
171
172impl TestTermination for () {
173 fn is_success(&self) -> bool {
174 true
175 }
176}
177
178impl<T, E> TestTermination for Result<T, E> {
179 fn is_success(&self) -> bool {
180 self.is_ok()
181 }
182}
183
184fn run_test_with_pool<DB, F, Fut>(args: TestArgs, test_fn: F) -> Fut::Output
185where
186 DB: TestSupport,
187 DB::Connection: Migrate,
188 for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
189 F: FnOnce(Pool<DB>) -> Fut,
190 Fut: Future,
191 Fut::Output: TestTermination,
192{
193 let test_path = args.test_path;
194 run_test::<DB, _, _>(args, |pool_opts, connect_opts| async move {
195 let pool = pool_opts
196 .connect_with(connect_opts)
197 .await
198 .expect("failed to connect test pool");
199
200 let res = test_fn(pool.clone()).await;
201
202 let close_timed_out = crate::rt::timeout(Duration::from_secs(10), pool.close())
203 .await
204 .is_err();
205
206 if close_timed_out {
207 eprintln!("test {test_path} held onto Pool after exiting");
208 }
209
210 res
211 })
212}
213
214fn run_test<DB, F, Fut>(args: TestArgs, test_fn: F) -> Fut::Output
215where
216 DB: TestSupport,
217 DB::Connection: Migrate,
218 for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
219 F: FnOnce(PoolOptions<DB>, <DB::Connection as Connection>::Options) -> Fut,
220 Fut: Future,
221 Fut::Output: TestTermination,
222{
223 crate::rt::test_block_on(async move {
224 let test_context = DB::test_context(&args)
225 .await
226 .expect("failed to connect to setup test database");
227
228 setup_test_db::<DB>(&test_context.connect_opts, &args).await;
229
230 let res = test_fn(test_context.pool_opts, test_context.connect_opts).await;
231
232 if res.is_success() {
233 if let Err(e) = DB::cleanup_test(&DB::db_name(&args)).await {
234 eprintln!(
235 "failed to delete database {:?}: {}",
236 test_context.db_name, e
237 );
238 }
239 }
240
241 res
242 })
243}
244
245async fn setup_test_db<DB: Database>(
246 copts: &<DB::Connection as Connection>::Options,
247 args: &TestArgs,
248) where
249 DB::Connection: Migrate + Sized,
250 for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
251{
252 let mut conn = copts
253 .connect()
254 .await
255 .expect("failed to connect to test database");
256
257 if let Some(migrator) = args.migrator {
258 migrator
259 .run_direct(&mut conn)
260 .await
261 .expect("failed to apply migrations");
262 }
263
264 for fixture in args.fixtures {
265 (&mut conn)
266 .execute(fixture.contents)
267 .await
268 .unwrap_or_else(|e| panic!("failed to apply test fixture {:?}: {:?}", fixture.path, e));
269 }
270
271 conn.close()
272 .await
273 .expect("failed to close setup connection");
274}