xtask/tests/
mod.rs

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