sqlx_core_oldapi/testing/
mod.rs

1use 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    /// Get parameters to construct a `Pool` suitable for testing.
20    ///
21    /// This `Pool` instance will behave somewhat specially:
22    /// * all handles share a single global semaphore to avoid exceeding the connection limit
23    ///   on the database server.
24    /// * each invocation results in a different temporary database.
25    ///
26    /// The implementation may require `DATABASE_URL` to be set in order to manage databases.
27    /// The user credentials it contains must have the privilege to create and drop databases.
28    fn test_context(args: &TestArgs) -> BoxFuture<'_, Result<TestContext<Self>, Error>>;
29
30    fn cleanup_test(db_name: &str) -> BoxFuture<'_, Result<(), Error>>;
31
32    /// Cleanup any test databases that are no longer in-use.
33    ///
34    /// Returns a count of the databases deleted, if possible.
35    ///
36    /// The implementation may require `DATABASE_URL` to be set in order to manage databases.
37    /// The user credentials it contains must have the privilege to create and drop databases.
38    fn cleanup_test_dbs() -> BoxFuture<'static, Result<Option<usize>, Error>>;
39
40    /// Take a snapshot of the current state of the database (data only).
41    ///
42    /// This snapshot can then be used to generate test fixtures.
43    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}