1use crate::build;
4use crate::cache;
5use crate::command::utils::get_crate_path;
6use crate::install::{self, InstallMode, Tool};
7use crate::lockfile::Lockfile;
8use crate::manifest;
9use crate::test::{self, webdriver};
10use anyhow::{bail, Result};
11use binary_install::Cache;
12use clap::Args;
13use console::style;
14use log::info;
15use std::path::PathBuf;
16use std::str::FromStr;
17use std::time::Instant;
18
19#[derive(Debug, Default, Args)]
20#[command(allow_hyphen_values = true, trailing_var_arg = true)]
21pub struct TestOptions {
23 #[clap(long = "node")]
24 pub node: bool,
26
27 #[clap(long = "firefox")]
28 pub firefox: bool,
33
34 #[clap(long = "geckodriver")]
35 pub geckodriver: Option<PathBuf>,
38
39 #[clap(long = "chrome")]
40 pub chrome: bool,
45
46 #[clap(long = "chromedriver")]
47 pub chromedriver: Option<PathBuf>,
50
51 #[clap(long = "safari")]
52 pub safari: bool,
57
58 #[clap(long = "safaridriver")]
59 pub safaridriver: Option<PathBuf>,
62
63 #[clap(long = "headless")]
64 pub headless: bool,
67
68 #[clap(long = "mode", short = 'm', default_value = "normal")]
69 pub mode: InstallMode,
71
72 #[clap(long = "release", short = 'r')]
73 pub release: bool,
75
76 #[clap(long = "panic-unwind")]
77 pub panic_unwind: bool,
82
83 pub path_and_extra_options: Vec<String>,
90}
91
92pub struct Test {
94 crate_path: PathBuf,
95 crate_data: manifest::CrateData,
96 cache: Cache,
97 node: bool,
98 mode: InstallMode,
99 firefox: bool,
100 geckodriver: Option<PathBuf>,
101 chrome: bool,
102 chromedriver: Option<PathBuf>,
103 safari: bool,
104 safaridriver: Option<PathBuf>,
105 headless: bool,
106 release: bool,
107 panic_unwind: bool,
108 test_runner_path: Option<PathBuf>,
109 extra_options: Vec<String>,
110 target_triple: String,
111}
112
113type TestStep = fn(&mut Test) -> Result<()>;
114
115impl Test {
116 pub fn try_from_opts(test_opts: TestOptions) -> Result<Self> {
118 let TestOptions {
119 node,
120 mode,
121 headless,
122 release,
123 panic_unwind,
124 chrome,
125 chromedriver,
126 firefox,
127 geckodriver,
128 safari,
129 safaridriver,
130 mut path_and_extra_options,
131 } = test_opts;
132
133 let first_arg_is_path = path_and_extra_options
134 .get(0)
135 .map(|first_arg| !first_arg.starts_with("-"))
136 .unwrap_or(false);
137
138 let (path, extra_options) = if first_arg_is_path {
139 let path = PathBuf::from_str(&path_and_extra_options.remove(0))?;
140 let extra_options = path_and_extra_options;
141
142 (Some(path), extra_options)
143 } else {
144 (None, path_and_extra_options)
145 };
146
147 let crate_path = get_crate_path(path)?;
148 let crate_data = manifest::CrateData::new(&crate_path, None)?;
149 let any_browser = chrome || firefox || safari;
150
151 let target_triple = {
154 let mut iter = extra_options.iter();
155 let from_args = iter
156 .by_ref()
157 .find(|o| o.as_str() == "--target")
158 .and_then(|_| iter.next())
159 .cloned();
160 from_args
161 .or_else(|| std::env::var("CARGO_BUILD_TARGET").ok())
162 .or_else(|| crate::command::build::read_cargo_build_target(&crate_path))
163 .unwrap_or_else(|| "wasm32-unknown-unknown".to_string())
164 };
165
166 if !node && !any_browser {
167 bail!("Must specify at least one of `--node`, `--chrome`, `--firefox`, or `--safari`")
168 }
169
170 if headless && !any_browser {
171 bail!(
172 "The `--headless` flag only applies to browser tests. Node does not provide a UI, \
173 so it doesn't make sense to talk about a headless version of Node tests."
174 )
175 }
176
177 Ok(Test {
178 cache: cache::get_wasm_pack_cache()?,
179 crate_path,
180 crate_data,
181 node,
182 mode,
183 chrome,
184 chromedriver,
185 firefox,
186 geckodriver,
187 safari,
188 safaridriver,
189 headless,
190 release,
191 panic_unwind,
192 test_runner_path: None,
193 target_triple,
194 extra_options,
195 })
196 }
197
198 pub fn set_cache(&mut self, cache: Cache) {
200 self.cache = cache;
201 }
202
203 pub fn run(mut self) -> Result<()> {
205 let process_steps = self.get_process_steps();
206
207 let started = Instant::now();
208 for (_, process_step) in process_steps {
209 process_step(&mut self)?;
210 }
211 let duration = crate::command::utils::elapsed(started.elapsed());
212 info!("Done in {}.", &duration);
213
214 Ok(())
215 }
216
217 fn get_process_steps(&self) -> Vec<(&'static str, TestStep)> {
218 macro_rules! steps {
219 ($($name:ident $(if $e:expr)* ),+) => {
220 {
221 let mut steps: Vec<(&'static str, TestStep)> = Vec::new();
222 $(
223 $(if $e)* {
224 steps.push((stringify!($name), Test::$name));
225 }
226 )*
227 steps
228 }
229 };
230 ($($name:ident $(if $e:expr)* ,)*) => (steps![$($name $(if $e)* ),*])
231 }
232 match self.mode {
233 InstallMode::Normal => steps![
234 step_check_rustc_version,
235 step_check_for_wasm_target,
236 step_build_tests,
237 step_install_wasm_bindgen,
238 step_test_node if self.node,
239 step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
240 step_test_chrome if self.chrome,
241 step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
242 step_test_firefox if self.firefox,
243 step_get_safaridriver if self.safari && self.safaridriver.is_none(),
244 step_test_safari if self.safari,
245 ],
246 InstallMode::Force => steps![
247 step_check_for_wasm_target,
248 step_build_tests,
249 step_install_wasm_bindgen,
250 step_test_node if self.node,
251 step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
252 step_test_chrome if self.chrome,
253 step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
254 step_test_firefox if self.firefox,
255 step_get_safaridriver if self.safari && self.safaridriver.is_none(),
256 step_test_safari if self.safari,
257 ],
258 InstallMode::Noinstall => steps![
259 step_build_tests,
260 step_install_wasm_bindgen,
261 step_test_node if self.node,
262 step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
263 step_test_chrome if self.chrome,
264 step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
265 step_test_firefox if self.firefox,
266 step_get_safaridriver if self.safari && self.safaridriver.is_none(),
267 step_test_safari if self.safari,
268 ],
269 }
270 }
271
272 fn step_check_rustc_version(&mut self) -> Result<()> {
273 if self.panic_unwind {
275 info!("Skipping rustc version check (using nightly via --panic-unwind).");
276 return Ok(());
277 }
278 info!("Checking rustc version...");
279 let _ = build::check_rustc_version()?;
280 info!("Rustc version is correct.");
281 Ok(())
282 }
283
284 fn step_check_for_wasm_target(&mut self) -> Result<()> {
285 if self.panic_unwind {
286 info!("Checking nightly toolchain prerequisites for panic=unwind...");
287 build::wasm_target::check_nightly_prerequisites()?;
288 info!("Nightly prerequisites check was successful.");
289 return Ok(());
290 }
291 info!("Adding wasm-target...");
292 build::wasm_target::check_for_wasm_target(&self.target_triple)?;
293 info!("Adding wasm-target was successful.");
294 Ok(())
295 }
296
297 fn step_build_tests(&mut self) -> Result<()> {
298 info!("Compiling tests to wasm...");
299
300 let extra_options =
303 if let Some(index) = self.extra_options.iter().position(|arg| arg == "--") {
304 &self.extra_options[..index]
305 } else {
306 &self.extra_options
307 };
308 build::cargo_build_wasm_tests(
309 &self.crate_path,
310 !self.release,
311 extra_options,
312 &self.target_triple,
313 self.panic_unwind,
314 )?;
315
316 info!("Finished compiling tests to wasm.");
317 Ok(())
318 }
319
320 fn step_install_wasm_bindgen(&mut self) -> Result<()> {
321 info!("Identifying wasm-bindgen dependency...");
322 let lockfile = Lockfile::new(&self.crate_data)?;
323 let bindgen_version = lockfile.require_wasm_bindgen()?;
324
325 if lockfile.wasm_bindgen_test_version().is_none() {
331 bail!(
332 "Ensure that you have \"{}\" as a dependency in your Cargo.toml file:\n\
333 [dev-dependencies]\n\
334 wasm-bindgen-test = \"0.2\"",
335 style("wasm-bindgen-test").bold().dim(),
336 )
337 }
338
339 let status = install::download_prebuilt_or_cargo_install(
340 Tool::WasmBindgen,
341 &self.cache,
342 &bindgen_version,
343 self.mode.install_permitted(),
344 )?;
345
346 self.test_runner_path = match status {
347 install::Status::Found(dl) => Some(dl.binary("wasm-bindgen-test-runner")?),
348 _ => bail!("Could not find 'wasm-bindgen-test-runner'."),
349 };
350
351 info!("Getting wasm-bindgen-cli was successful.");
352 Ok(())
353 }
354
355 fn step_test_node(&mut self) -> Result<()> {
356 assert!(self.node);
357 info!("Running tests in node...");
358 let runner_env = format!(
359 "CARGO_TARGET_{}_RUNNER",
360 self.target_triple.replace('-', "_").to_uppercase()
361 );
362 let runner_path = self
363 .test_runner_path
364 .as_ref()
365 .unwrap()
366 .to_str()
367 .unwrap()
368 .to_string();
369 test::cargo_test_wasm(
370 &self.crate_path,
371 self.release,
372 vec![
373 (runner_env, runner_path),
374 ("WASM_BINDGEN_TEST_ONLY_NODE".to_string(), "1".to_string()),
375 ],
376 &self.extra_options,
377 &self.target_triple,
378 )?;
379 info!("Finished running tests in node.");
380 Ok(())
381 }
382
383 fn step_get_chromedriver(&mut self) -> Result<()> {
384 assert!(self.chrome && self.chromedriver.is_none());
385
386 self.chromedriver = Some(webdriver::get_or_install_chromedriver(
387 &self.cache,
388 self.mode,
389 )?);
390 Ok(())
391 }
392
393 fn step_test_chrome(&mut self) -> Result<()> {
394 let chromedriver = self.chromedriver.as_ref().unwrap().display().to_string();
395 info!(
396 "Running tests in Chrome with chromedriver at {}",
397 chromedriver
398 );
399
400 let mut envs = self.webdriver_env();
401 envs.push(("CHROMEDRIVER".to_string(), chromedriver));
402
403 test::cargo_test_wasm(
404 &self.crate_path,
405 self.release,
406 envs,
407 &self.extra_options,
408 &self.target_triple,
409 )?;
410 Ok(())
411 }
412
413 fn step_get_geckodriver(&mut self) -> Result<()> {
414 assert!(self.firefox && self.geckodriver.is_none());
415
416 self.geckodriver = Some(webdriver::get_or_install_geckodriver(
417 &self.cache,
418 self.mode,
419 )?);
420 Ok(())
421 }
422
423 fn step_test_firefox(&mut self) -> Result<()> {
424 let geckodriver = self.geckodriver.as_ref().unwrap().display().to_string();
425 info!(
426 "Running tests in Firefox with geckodriver at {}",
427 geckodriver
428 );
429
430 let mut envs = self.webdriver_env();
431 envs.push(("GECKODRIVER".to_string(), geckodriver));
432
433 test::cargo_test_wasm(
434 &self.crate_path,
435 self.release,
436 envs,
437 &self.extra_options,
438 &self.target_triple,
439 )?;
440 Ok(())
441 }
442
443 fn step_get_safaridriver(&mut self) -> Result<()> {
444 assert!(self.safari && self.safaridriver.is_none());
445
446 self.safaridriver = Some(webdriver::get_safaridriver()?);
447 Ok(())
448 }
449
450 fn step_test_safari(&mut self) -> Result<()> {
451 let safaridriver = self.safaridriver.as_ref().unwrap().display().to_string();
452 info!(
453 "Running tests in Safari with safaridriver at {}",
454 safaridriver
455 );
456
457 let mut envs = self.webdriver_env();
458 envs.push(("SAFARIDRIVER".to_string(), safaridriver));
459
460 test::cargo_test_wasm(
461 &self.crate_path,
462 self.release,
463 envs,
464 &self.extra_options,
465 &self.target_triple,
466 )?;
467 Ok(())
468 }
469
470 fn webdriver_env(&self) -> Vec<(String, String)> {
471 let test_runner = self
472 .test_runner_path
473 .as_ref()
474 .unwrap()
475 .to_str()
476 .unwrap()
477 .to_string();
478 info!("Using wasm-bindgen test runner at {}", test_runner);
479 let runner_env = format!(
480 "CARGO_TARGET_{}_RUNNER",
481 self.target_triple.replace('-', "_").to_uppercase()
482 );
483 let mut envs = vec![
484 (runner_env, test_runner),
485 ("WASM_BINDGEN_TEST_ONLY_WEB".to_string(), "1".to_string()),
486 ];
487 if !self.headless {
488 envs.push(("NO_HEADLESS".to_string(), "1".to_string()));
489 }
490 envs
491 }
492}