xtask/tests/
mod.rs

1use crate::Backend;
2use cargo_metadata::{Metadata, MetadataCommand};
3use std::process::Command;
4use std::process::Stdio;
5
6#[derive(clap::Args, Debug)]
7pub(crate) struct TestArgs {
8    /// Run tests for a specific backend
9    #[clap(default_value_t = Backend::All)]
10    backend: Backend,
11    /// skip the unit/integration tests
12    #[clap(long = "no-integration-tests")]
13    no_integration_tests: bool,
14    /// skip the doc tests
15    #[clap(long = "no-doc-tests")]
16    no_doc_tests: bool,
17    // skip the checks for the example schema setup
18    #[clap(long = "no-example-schema-check")]
19    no_example_schema_check: bool,
20    /// do not abort running if we encounter an error
21    /// while running tests for all backends
22    #[clap(long = "keep-going")]
23    keep_going: bool,
24    // run wasm tests, currently only supports sqlite
25    #[clap(long = "wasm")]
26    wasm: bool,
27    /// additional flags passed to cargo nextest while running
28    /// unit/integration tests.
29    ///
30    /// This is useful for passing custom test filters/arguments
31    ///
32    /// See <https://nexte.st/docs/running/> for details
33    flags: Vec<String>,
34}
35
36impl TestArgs {
37    pub(crate) fn run(mut self) {
38        let metadata = MetadataCommand::default().exec().unwrap();
39        let success = if matches!(self.backend, Backend::All) {
40            let mut success = true;
41            for backend in Backend::ALL {
42                self.backend = *backend;
43                let result = self.run_tests(&metadata);
44                success = success && result;
45                if !result && !self.keep_going {
46                    break;
47                }
48            }
49            success
50        } else {
51            self.run_tests(&metadata)
52        };
53        if !success {
54            std::process::exit(1);
55        }
56    }
57
58    fn run_tests(&self, metadata: &Metadata) -> bool {
59        let backend_name = self.backend.to_string();
60        println!("Running tests for {backend_name}");
61        let exclude = crate::utils::get_exclude_for_backend(&backend_name, metadata);
62        if std::env::var("DATABASE_URL").is_err() {
63            match self.backend {
64                Backend::Postgres => {
65                    if std::env::var("PG_DATABASE_URL").is_err() {
66                        println!(
67                            "Remember to set `PG_DATABASE_URL` for running the postgres tests"
68                        );
69                    }
70                }
71                Backend::Sqlite => {
72                    if std::env::var("SQLITE_DATABASE_URL").is_err() {
73                        println!(
74                            "Remember to set `SQLITE_DATABASE_URL` for running the sqlite tests"
75                        );
76                    }
77                }
78                Backend::Mysql => {
79                    if std::env::var("MYSQL_DATABASE_URL").is_err()
80                        || std::env::var("MYSQL_UNIT_TEST_DATABASE_URL").is_err()
81                    {
82                        println!("Remember to set `MYSQL_DATABASE_URL` and `MYSQL_UNIT_TEST_DATABASE_URL` for running the mysql tests");
83                    }
84                }
85                Backend::All => unreachable!(),
86            }
87        }
88        let backend = &self.backend;
89        if self.wasm {
90            if matches!(backend, Backend::Sqlite) {
91                let mut command = Command::new("wasm-pack");
92                let out = command
93                    .args(["test", "--chrome", "--headless", "--features", "sqlite"])
94                    .current_dir(metadata.workspace_root.join("diesel"))
95                    .stderr(Stdio::inherit())
96                    .stdout(Stdio::inherit())
97                    .status()
98                    .unwrap();
99                if !out.success() {
100                    eprintln!("Failed to run wasm diesel unit tests");
101                    return false;
102                }
103                let mut command = Command::new("wasm-pack");
104                let out = command
105                    .args(["test", "--chrome", "--headless", "--features", "sqlite"])
106                    .current_dir(metadata.workspace_root.join("diesel_tests"))
107                    .stderr(Stdio::inherit())
108                    .stdout(Stdio::inherit())
109                    .status()
110                    .unwrap();
111                if !out.success() {
112                    eprintln!("Failed to run wasm integration tests");
113                    return false;
114                }
115                return true;
116            } else {
117                eprintln!(
118                    "Only the sqlite backend supports wasm for now, the current backend is {backend}"
119                );
120                return true;
121            }
122        }
123        let url = match backend {
124            Backend::Postgres => std::env::var("PG_DATABASE_URL"),
125            Backend::Sqlite => std::env::var("SQLITE_DATABASE_URL"),
126            Backend::Mysql => std::env::var("MYSQL_DATABASE_URL"),
127            Backend::All => unreachable!(),
128        };
129        let url = url
130            .or_else(|_| std::env::var("DATABASE_URL"))
131            .expect("DATABASE_URL is set for tests");
132
133        // run the migrations
134        let mut command = Command::new("cargo");
135        command
136            .args(["run", "-p", "diesel_cli", "--no-default-features", "-F"])
137            .arg(backend.to_string())
138            .args(["--", "migration", "run", "--migration-dir"])
139            .arg(
140                metadata
141                    .workspace_root
142                    .join("migrations")
143                    .join(backend.to_string()),
144            )
145            .arg("--database-url")
146            .arg(&url);
147        println!("Run database migration via `{command:?}`");
148        let status = command
149            .stdout(Stdio::inherit())
150            .stderr(Stdio::inherit())
151            .status()
152            .unwrap();
153        if !status.success() {
154            eprintln!("Failed to run migrations");
155            return false;
156        }
157
158        if !self.no_integration_tests {
159            // run the normal tests via nextest
160            let mut command = Command::new("cargo");
161            command
162                .args(["nextest", "run", "--workspace", "--no-default-features"])
163                .current_dir(&metadata.workspace_root)
164                .args(exclude)
165                .arg("-F")
166                .arg(format!("diesel/{backend}"))
167                .args(["-F", "diesel/extras"])
168                .arg("-F")
169                .arg(format!("diesel_derives/{backend}"))
170                .arg("-F")
171                .arg(format!("diesel_cli/{backend}"))
172                .arg("-F")
173                .arg(format!("migrations_macros/{backend}"))
174                .arg("-F")
175                .arg(format!("diesel_migrations/{backend}"))
176                .arg("-F")
177                .arg(format!("diesel_tests/{backend}"))
178                .arg("-F")
179                .arg(format!("diesel-dynamic-schema/{backend}"))
180                .args(&self.flags);
181
182            if matches!(self.backend, Backend::Mysql) {
183                // cannot run mysql tests in parallel
184                command.args(["-j", "1"]);
185            }
186            println!("Running tests via `{command:?}`: ");
187
188            let out = command
189                .stderr(Stdio::inherit())
190                .stdout(Stdio::inherit())
191                .status()
192                .unwrap();
193            if !out.success() {
194                eprintln!("Failed to run integration tests");
195                return false;
196            }
197        } else {
198            println!("Integration tests skipped because `--no-integration-tests` was passed");
199        }
200        if !self.no_doc_tests {
201            let mut command = Command::new("cargo");
202
203            command
204                .current_dir(&metadata.workspace_root)
205                .args([
206                    "test",
207                    "--doc",
208                    "--no-default-features",
209                    "-p",
210                    "diesel",
211                    "-p",
212                    "diesel_derives",
213                    "-p",
214                    "diesel_migrations",
215                    "-p",
216                    "diesel-dynamic-schema",
217                    "-p",
218                    "dsl_auto_type",
219                    "-p",
220                    "diesel_table_macro_syntax",
221                    "-F",
222                    "diesel/extras",
223                ])
224                .arg("-F")
225                .arg(format!("diesel/{backend}"))
226                .arg("-F")
227                .arg(format!("diesel_derives/{backend}"))
228                .arg("-F")
229                .arg(format!("diesel-dynamic-schema/{backend}"));
230            if matches!(backend, Backend::Mysql) {
231                // cannot run mysql tests in parallel
232                command.args(["-j", "1"]);
233            }
234            println!("Running tests via `{command:?}`: ");
235            let status = command
236                .stdout(Stdio::inherit())
237                .stderr(Stdio::inherit())
238                .status()
239                .unwrap();
240            if !status.success() {
241                eprintln!("Failed to run doc tests");
242                return false;
243            }
244        } else {
245            println!("Doc tests are skipped because `--no-doc-tests` was passed");
246        }
247
248        if !self.no_example_schema_check {
249            let examples = metadata
250                .workspace_root
251                .join("examples")
252                .join(backend.to_string());
253            let temp_dir = if matches!(backend, Backend::Sqlite) {
254                Some(tempfile::tempdir().unwrap())
255            } else {
256                None
257            };
258            let mut fail = false;
259            for p in metadata
260                .workspace_packages()
261                .into_iter()
262                .filter(|p| p.manifest_path.starts_with(&examples))
263            {
264                let example_root = p.manifest_path.parent().unwrap();
265                if example_root.join("migrations").exists() {
266                    let db_url = if matches!(backend, Backend::Sqlite) {
267                        temp_dir
268                            .as_ref()
269                            .unwrap()
270                            .path()
271                            .join(&p.name)
272                            .display()
273                            .to_string()
274                    } else {
275                        // it's a url with the structure postgres://[user:password@host:port/database?options
276                        // we parse it manually as we don't want to pull in the url crate with all
277                        // its features
278                        let (start, end) = url.rsplit_once('/').unwrap();
279                        let query = end.split_once('?').map(|(_, q)| q);
280
281                        let mut url = format!("{start}/{}", p.name);
282                        if let Some(query) = query {
283                            url.push('?');
284                            url.push_str(query);
285                        }
286                        url
287                    };
288
289                    let mut command = Command::new("cargo");
290                    command
291                        .current_dir(example_root)
292                        .args(["run", "-p", "diesel_cli", "--no-default-features", "-F"])
293                        .arg(backend.to_string())
294                        .args(["--", "database", "reset", "--locked-schema"])
295                        .env("DATABASE_URL", db_url);
296                    println!(
297                        "Check schema for example `{}` ({example_root}) with command `{command:?}`",
298                        p.name,
299                    );
300                    let status = command.status().unwrap();
301                    if !status.success() {
302                        fail = true;
303                        eprintln!("Failed to check example schema for `{}`", p.name);
304                        if !self.keep_going {
305                            return false;
306                        }
307                    }
308                }
309            }
310            if fail {
311                return false;
312            }
313        } else {
314            println!(
315                "Example schema check is skipped because `--no-example-schema-check` was passed"
316            );
317        }
318
319        true
320    }
321}