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