Skip to main content

wasm_pack/command/
test.rs

1//! Implementation of the `wasm-pack test` command.
2
3use 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)]
21/// Everything required to configure the `wasm-pack test` command.
22pub struct TestOptions {
23    #[clap(long = "node")]
24    /// Run the tests in Node.js.
25    pub node: bool,
26
27    #[clap(long = "firefox")]
28    /// Run the tests in Firefox. This machine must have a Firefox installation.
29    /// If the `geckodriver` WebDriver client is not on the `$PATH`, and not
30    /// specified with `--geckodriver`, then `wasm-pack` will download a local
31    /// copy.
32    pub firefox: bool,
33
34    #[clap(long = "geckodriver")]
35    /// The path to the `geckodriver` WebDriver client for testing in
36    /// Firefox. Implies `--firefox`.
37    pub geckodriver: Option<PathBuf>,
38
39    #[clap(long = "chrome")]
40    /// Run the tests in Chrome. This machine must have a Chrome installation.
41    /// If the `chromedriver` WebDriver client is not on the `$PATH`, and not
42    /// specified with `--chromedriver`, then `wasm-pack` will download a local
43    /// copy.
44    pub chrome: bool,
45
46    #[clap(long = "chromedriver")]
47    /// The path to the `chromedriver` WebDriver client for testing in
48    /// Chrome. Implies `--chrome`.
49    pub chromedriver: Option<PathBuf>,
50
51    #[clap(long = "safari")]
52    /// Run the tests in Safari. This machine must have a Safari installation,
53    /// and the `safaridriver` WebDriver client must either be on the `$PATH` or
54    /// specified explicitly with the `--safaridriver` flag. `wasm-pack` cannot
55    /// download the `safaridriver` WebDriver client for you.
56    pub safari: bool,
57
58    #[clap(long = "safaridriver")]
59    /// The path to the `safaridriver` WebDriver client for testing in
60    /// Safari. Implies `--safari`.
61    pub safaridriver: Option<PathBuf>,
62
63    #[clap(long = "headless")]
64    /// When running browser tests, run the browser in headless mode without any
65    /// UI or windows.
66    pub headless: bool,
67
68    #[clap(long = "mode", short = 'm', default_value = "normal")]
69    /// Sets steps to be run. [possible values: no-install, normal]
70    pub mode: InstallMode,
71
72    #[clap(long = "release", short = 'r')]
73    /// Build with the release profile.
74    pub release: bool,
75
76    #[clap(long = "panic-unwind")]
77    /// Build tests with panic=unwind. Requires the nightly Rust toolchain;
78    /// uses `-Z build-std` to rebuild `std` with `-Cpanic=unwind`. The nightly
79    /// toolchain, `rust-src` component, and nightly `wasm32-unknown-unknown`
80    /// target will be installed via `rustup` if not already present.
81    pub panic_unwind: bool,
82
83    /// Path to the Rust crate, and extra options to pass to `cargo test`.
84    ///
85    /// If the path is not provided, this command searches up the path from the current directory.
86    ///
87    /// This is a workaround to allow wasm pack to provide the same command line interface as `cargo`.
88    /// See <https://github.com/wasm-bindgen/wasm-pack/pull/851> for more information.
89    pub path_and_extra_options: Vec<String>,
90}
91
92/// A configured `wasm-pack test` command.
93pub 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    /// Construct a test command from the given options.
117    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 = {
152            let mut iter = extra_options.iter();
153            if iter.by_ref().any(|option| option == "--target") {
154                iter.next().map(|s| s.as_str())
155            } else {
156                None
157            }
158            .unwrap_or("wasm32-unknown-unknown")
159            .to_owned()
160        };
161
162        if !node && !any_browser {
163            bail!("Must specify at least one of `--node`, `--chrome`, `--firefox`, or `--safari`")
164        }
165
166        if headless && !any_browser {
167            bail!(
168                "The `--headless` flag only applies to browser tests. Node does not provide a UI, \
169                 so it doesn't make sense to talk about a headless version of Node tests."
170            )
171        }
172
173        Ok(Test {
174            cache: cache::get_wasm_pack_cache()?,
175            crate_path,
176            crate_data,
177            node,
178            mode,
179            chrome,
180            chromedriver,
181            firefox,
182            geckodriver,
183            safari,
184            safaridriver,
185            headless,
186            release,
187            panic_unwind,
188            test_runner_path: None,
189            target_triple,
190            extra_options,
191        })
192    }
193
194    /// Configures the cache that this test command uses
195    pub fn set_cache(&mut self, cache: Cache) {
196        self.cache = cache;
197    }
198
199    /// Execute this test command.
200    pub fn run(mut self) -> Result<()> {
201        let process_steps = self.get_process_steps();
202
203        let started = Instant::now();
204        for (_, process_step) in process_steps {
205            process_step(&mut self)?;
206        }
207        let duration = crate::command::utils::elapsed(started.elapsed());
208        info!("Done in {}.", &duration);
209
210        Ok(())
211    }
212
213    fn get_process_steps(&self) -> Vec<(&'static str, TestStep)> {
214        macro_rules! steps {
215            ($($name:ident $(if $e:expr)* ),+) => {
216                {
217                    let mut steps: Vec<(&'static str, TestStep)> = Vec::new();
218                    $(
219                        $(if $e)* {
220                            steps.push((stringify!($name), Test::$name));
221                        }
222                    )*
223                    steps
224                }
225            };
226            ($($name:ident $(if $e:expr)* ,)*) => (steps![$($name $(if $e)* ),*])
227        }
228        match self.mode {
229            InstallMode::Normal => steps![
230                step_check_rustc_version,
231                step_check_for_wasm_target,
232                step_build_tests,
233                step_install_wasm_bindgen,
234                step_test_node if self.node,
235                step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
236                step_test_chrome if self.chrome,
237                step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
238                step_test_firefox if self.firefox,
239                step_get_safaridriver if self.safari && self.safaridriver.is_none(),
240                step_test_safari if self.safari,
241            ],
242            InstallMode::Force => steps![
243                step_check_for_wasm_target,
244                step_build_tests,
245                step_install_wasm_bindgen,
246                step_test_node if self.node,
247                step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
248                step_test_chrome if self.chrome,
249                step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
250                step_test_firefox if self.firefox,
251                step_get_safaridriver if self.safari && self.safaridriver.is_none(),
252                step_test_safari if self.safari,
253            ],
254            InstallMode::Noinstall => steps![
255                step_build_tests,
256                step_install_wasm_bindgen,
257                step_test_node if self.node,
258                step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
259                step_test_chrome if self.chrome,
260                step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
261                step_test_firefox if self.firefox,
262                step_get_safaridriver if self.safari && self.safaridriver.is_none(),
263                step_test_safari if self.safari,
264            ],
265        }
266    }
267
268    fn step_check_rustc_version(&mut self) -> Result<()> {
269        // Stable rustc version is irrelevant when --panic-unwind is set.
270        if self.panic_unwind {
271            info!("Skipping rustc version check (using nightly via --panic-unwind).");
272            return Ok(());
273        }
274        info!("Checking rustc version...");
275        let _ = build::check_rustc_version()?;
276        info!("Rustc version is correct.");
277        Ok(())
278    }
279
280    fn step_check_for_wasm_target(&mut self) -> Result<()> {
281        if self.panic_unwind {
282            info!("Checking nightly toolchain prerequisites for panic=unwind...");
283            build::wasm_target::check_nightly_prerequisites()?;
284            info!("Nightly prerequisites check was successful.");
285            return Ok(());
286        }
287        info!("Adding wasm-target...");
288        build::wasm_target::check_for_wasm_target(&self.target_triple)?;
289        info!("Adding wasm-target was successful.");
290        Ok(())
291    }
292
293    fn step_build_tests(&mut self) -> Result<()> {
294        info!("Compiling tests to wasm...");
295
296        // If the user has run `wasm-pack test -- --features "f1" -- test_name`, then we want to only pass through
297        // `--features "f1"` to `cargo build`
298        let extra_options =
299            if let Some(index) = self.extra_options.iter().position(|arg| arg == "--") {
300                &self.extra_options[..index]
301            } else {
302                &self.extra_options
303            };
304        build::cargo_build_wasm_tests(
305            &self.crate_path,
306            !self.release,
307            extra_options,
308            &self.target_triple,
309            self.panic_unwind,
310        )?;
311
312        info!("Finished compiling tests to wasm.");
313        Ok(())
314    }
315
316    fn step_install_wasm_bindgen(&mut self) -> Result<()> {
317        info!("Identifying wasm-bindgen dependency...");
318        let lockfile = Lockfile::new(&self.crate_data)?;
319        let bindgen_version = lockfile.require_wasm_bindgen()?;
320
321        // Unlike `wasm-bindgen` and `wasm-bindgen-cli`, `wasm-bindgen-test`
322        // will work with any semver compatible `wasm-bindgen-cli`, so just make
323        // sure that it is depended upon, so we can run tests on
324        // `wasm32-unkown-unknown`. Don't enforce that it is the same version as
325        // `wasm-bindgen`.
326        if lockfile.wasm_bindgen_test_version().is_none() {
327            bail!(
328                "Ensure that you have \"{}\" as a dependency in your Cargo.toml file:\n\
329                 [dev-dependencies]\n\
330                 wasm-bindgen-test = \"0.2\"",
331                style("wasm-bindgen-test").bold().dim(),
332            )
333        }
334
335        let status = install::download_prebuilt_or_cargo_install(
336            Tool::WasmBindgen,
337            &self.cache,
338            &bindgen_version,
339            self.mode.install_permitted(),
340        )?;
341
342        self.test_runner_path = match status {
343            install::Status::Found(dl) => Some(dl.binary("wasm-bindgen-test-runner")?),
344            _ => bail!("Could not find 'wasm-bindgen-test-runner'."),
345        };
346
347        info!("Getting wasm-bindgen-cli was successful.");
348        Ok(())
349    }
350
351    fn step_test_node(&mut self) -> Result<()> {
352        assert!(self.node);
353        info!("Running tests in node...");
354        let runner_env = format!(
355            "CARGO_TARGET_{}_RUNNER",
356            self.target_triple.replace('-', "_").to_uppercase()
357        );
358        let runner_path = self
359            .test_runner_path
360            .as_ref()
361            .unwrap()
362            .to_str()
363            .unwrap()
364            .to_string();
365        test::cargo_test_wasm(
366            &self.crate_path,
367            self.release,
368            vec![
369                (runner_env, runner_path),
370                ("WASM_BINDGEN_TEST_ONLY_NODE".to_string(), "1".to_string()),
371            ],
372            &self.extra_options,
373            &self.target_triple,
374        )?;
375        info!("Finished running tests in node.");
376        Ok(())
377    }
378
379    fn step_get_chromedriver(&mut self) -> Result<()> {
380        assert!(self.chrome && self.chromedriver.is_none());
381
382        self.chromedriver = Some(webdriver::get_or_install_chromedriver(
383            &self.cache,
384            self.mode,
385        )?);
386        Ok(())
387    }
388
389    fn step_test_chrome(&mut self) -> Result<()> {
390        let chromedriver = self.chromedriver.as_ref().unwrap().display().to_string();
391        info!(
392            "Running tests in Chrome with chromedriver at {}",
393            chromedriver
394        );
395
396        let mut envs = self.webdriver_env();
397        envs.push(("CHROMEDRIVER".to_string(), chromedriver));
398
399        test::cargo_test_wasm(
400            &self.crate_path,
401            self.release,
402            envs,
403            &self.extra_options,
404            &self.target_triple,
405        )?;
406        Ok(())
407    }
408
409    fn step_get_geckodriver(&mut self) -> Result<()> {
410        assert!(self.firefox && self.geckodriver.is_none());
411
412        self.geckodriver = Some(webdriver::get_or_install_geckodriver(
413            &self.cache,
414            self.mode,
415        )?);
416        Ok(())
417    }
418
419    fn step_test_firefox(&mut self) -> Result<()> {
420        let geckodriver = self.geckodriver.as_ref().unwrap().display().to_string();
421        info!(
422            "Running tests in Firefox with geckodriver at {}",
423            geckodriver
424        );
425
426        let mut envs = self.webdriver_env();
427        envs.push(("GECKODRIVER".to_string(), geckodriver));
428
429        test::cargo_test_wasm(
430            &self.crate_path,
431            self.release,
432            envs,
433            &self.extra_options,
434            &self.target_triple,
435        )?;
436        Ok(())
437    }
438
439    fn step_get_safaridriver(&mut self) -> Result<()> {
440        assert!(self.safari && self.safaridriver.is_none());
441
442        self.safaridriver = Some(webdriver::get_safaridriver()?);
443        Ok(())
444    }
445
446    fn step_test_safari(&mut self) -> Result<()> {
447        let safaridriver = self.safaridriver.as_ref().unwrap().display().to_string();
448        info!(
449            "Running tests in Safari with safaridriver at {}",
450            safaridriver
451        );
452
453        let mut envs = self.webdriver_env();
454        envs.push(("SAFARIDRIVER".to_string(), safaridriver));
455
456        test::cargo_test_wasm(
457            &self.crate_path,
458            self.release,
459            envs,
460            &self.extra_options,
461            &self.target_triple,
462        )?;
463        Ok(())
464    }
465
466    fn webdriver_env(&self) -> Vec<(String, String)> {
467        let test_runner = self
468            .test_runner_path
469            .as_ref()
470            .unwrap()
471            .to_str()
472            .unwrap()
473            .to_string();
474        info!("Using wasm-bindgen test runner at {}", test_runner);
475        let runner_env = format!(
476            "CARGO_TARGET_{}_RUNNER",
477            self.target_triple.replace('-', "_").to_uppercase()
478        );
479        let mut envs = vec![
480            (runner_env, test_runner),
481            ("WASM_BINDGEN_TEST_ONLY_WEB".to_string(), "1".to_string()),
482        ];
483        if !self.headless {
484            envs.push(("NO_HEADLESS".to_string(), "1".to_string()));
485        }
486        envs
487    }
488}