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                    .env("RUSTFLAGS", "--cfg getrandom_backend=\"wasm_js\"")
96                    .stderr(Stdio::inherit())
97                    .stdout(Stdio::inherit())
98                    .status()
99                    .unwrap();
100                if !out.success() {
101                    eprintln!("Failed to run wasm diesel unit tests");
102                    return false;
103                }
104                let mut command = Command::new("wasm-pack");
105                let out = command
106                    .args(["test", "--chrome", "--headless", "--features", "sqlite"])
107                    .current_dir(metadata.workspace_root.join("diesel_tests"))
108                    .env("RUSTFLAGS", "--cfg getrandom_backend=\"wasm_js\"")
109                    .stderr(Stdio::inherit())
110                    .stdout(Stdio::inherit())
111                    .status()
112                    .unwrap();
113                if !out.success() {
114                    eprintln!("Failed to run wasm integration tests");
115                    return false;
116                }
117                return true;
118            } else {
119                eprintln!(
120                    "Only the sqlite backend supports wasm for now, the current backend is {backend}"
121                );
122                return true;
123            }
124        }
125        let url = match backend {
126            Backend::Postgres => std::env::var("PG_DATABASE_URL"),
127            Backend::Sqlite => std::env::var("SQLITE_DATABASE_URL"),
128            Backend::Mysql => std::env::var("MYSQL_DATABASE_URL"),
129            Backend::All => unreachable!(),
130        };
131        let url = url
132            .or_else(|_| std::env::var("DATABASE_URL"))
133            .expect("DATABASE_URL is set for tests");
134
135        // run the migrations
136        let mut command = Command::new("cargo");
137        command
138            .args(["run", "-p", "diesel_cli", "--no-default-features", "-F"])
139            .arg(backend.to_string())
140            .args(["--", "migration", "run", "--migration-dir"])
141            .arg(
142                metadata
143                    .workspace_root
144                    .join("migrations")
145                    .join(backend.to_string()),
146            )
147            .arg("--database-url")
148            .arg(&url);
149        println!("Run database migration via `{command:?}`");
150        let status = command
151            .stdout(Stdio::inherit())
152            .stderr(Stdio::inherit())
153            .status()
154            .unwrap();
155        if !status.success() {
156            eprintln!("Failed to run migrations");
157            return false;
158        }
159
160        if !self.no_integration_tests {
161            // run the normal tests via nextest
162            let mut command = Command::new("cargo");
163            command
164                .args(["nextest", "run", "--workspace", "--no-default-features"])
165                .current_dir(&metadata.workspace_root)
166                .args(exclude)
167                .arg("-F")
168                .arg(format!("diesel/{backend}"))
169                .args(["-F", "diesel/extras"])
170                .arg("-F")
171                .arg(format!("diesel_derives/{backend}"))
172                .arg("-F")
173                .arg(format!("diesel_cli/{backend}"))
174                .arg("-F")
175                .arg(format!("migrations_macros/{backend}"))
176                .arg("-F")
177                .arg(format!("diesel_migrations/{backend}"))
178                .arg("-F")
179                .arg(format!("diesel_tests/{backend}"))
180                .arg("-F")
181                .arg(format!("diesel-dynamic-schema/{backend}"))
182                .args(&self.flags);
183
184            if matches!(self.backend, Backend::Mysql) {
185                // cannot run mysql tests in parallel
186                command.args(["-j", "1"]);
187            }
188            println!("Running tests via `{command:?}`: ");
189
190            let out = command
191                .stderr(Stdio::inherit())
192                .stdout(Stdio::inherit())
193                .status()
194                .unwrap();
195            if !out.success() {
196                eprintln!("Failed to run integration tests");
197                return false;
198            }
199        } else {
200            println!("Integration tests skipped because `--no-integration-tests` was passed");
201        }
202        if !self.no_doc_tests {
203            let mut command = Command::new("cargo");
204
205            command
206                .current_dir(&metadata.workspace_root)
207                .args([
208                    "test",
209                    "--doc",
210                    "--no-default-features",
211                    "-p",
212                    "diesel",
213                    "-p",
214                    "diesel_derives",
215                    "-p",
216                    "diesel_migrations",
217                    "-p",
218                    "diesel-dynamic-schema",
219                    "-p",
220                    "dsl_auto_type",
221                    "-p",
222                    "diesel_table_macro_syntax",
223                    "-F",
224                    "diesel/extras",
225                ])
226                .arg("-F")
227                .arg(format!("diesel/{backend}"))
228                .arg("-F")
229                .arg(format!("diesel_derives/{backend}"))
230                .arg("-F")
231                .arg(format!("diesel-dynamic-schema/{backend}"));
232            if matches!(backend, Backend::Mysql) {
233                // cannot run mysql tests in parallel
234                command.args(["-j", "1"]);
235            }
236            println!("Running tests via `{command:?}`: ");
237            let status = command
238                .stdout(Stdio::inherit())
239                .stderr(Stdio::inherit())
240                .status()
241                .unwrap();
242            if !status.success() {
243                eprintln!("Failed to run doc tests");
244                return false;
245            }
246        } else {
247            println!("Doc tests are skipped because `--no-doc-tests` was passed");
248        }
249
250        if !self.no_example_schema_check {
251            let examples = metadata
252                .workspace_root
253                .join("examples")
254                .join(backend.to_string());
255            let temp_dir = if matches!(backend, Backend::Sqlite) {
256                Some(tempfile::tempdir().unwrap())
257            } else {
258                None
259            };
260            let mut fail = false;
261            for p in metadata
262                .workspace_packages()
263                .into_iter()
264                .filter(|p| p.manifest_path.starts_with(&examples))
265            {
266                let example_root = p.manifest_path.parent().unwrap();
267                if example_root.join("migrations").exists() {
268                    let db_url = if matches!(backend, Backend::Sqlite) {
269                        temp_dir
270                            .as_ref()
271                            .unwrap()
272                            .path()
273                            .join(&p.name)
274                            .display()
275                            .to_string()
276                    } else {
277                        // it's a url with the structure postgres://[user:password@host:port/database?options
278                        // we parse it manually as we don't want to pull in the url crate with all
279                        // its features
280                        let (start, end) = url.rsplit_once('/').unwrap();
281                        let query = end.split_once('?').map(|(_, q)| q);
282
283                        let mut url = format!("{start}/{}", p.name);
284                        if let Some(query) = query {
285                            url.push('?');
286                            url.push_str(query);
287                        }
288                        url
289                    };
290
291                    let mut command = Command::new("cargo");
292                    command
293                        .current_dir(example_root)
294                        .args(["run", "-p", "diesel_cli", "--no-default-features", "-F"])
295                        .arg(backend.to_string())
296                        .args(["--", "database", "reset", "--locked-schema"])
297                        .env("DATABASE_URL", db_url);
298                    println!(
299                        "Check schema for example `{}` ({example_root}) with command `{command:?}`",
300                        p.name,
301                    );
302                    let status = command.status().unwrap();
303                    if !status.success() {
304                        fail = true;
305                        eprintln!("Failed to check example schema for `{}`", p.name);
306                        if !self.keep_going {
307                            return false;
308                        }
309                    }
310                }
311            }
312            if fail {
313                return false;
314            }
315        } else {
316            println!(
317                "Example schema check is skipped because `--no-example-schema-check` was passed"
318            );
319        }
320
321        true
322    }
323}