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        // Same precedence cargo uses, so wasm-pack and cargo agree on the
152        // target. See `command::build::read_cargo_build_target`.
153        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    /// Configures the cache that this test command uses
199    pub fn set_cache(&mut self, cache: Cache) {
200        self.cache = cache;
201    }
202
203    /// Execute this test command.
204    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        // Stable rustc version is irrelevant when --panic-unwind is set.
274        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        // If the user has run `wasm-pack test -- --features "f1" -- test_name`, then we want to only pass through
301        // `--features "f1"` to `cargo build`
302        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        // Unlike `wasm-bindgen` and `wasm-bindgen-cli`, `wasm-bindgen-test`
326        // will work with any semver compatible `wasm-bindgen-cli`, so just make
327        // sure that it is depended upon, so we can run tests on
328        // `wasm32-unkown-unknown`. Don't enforce that it is the same version as
329        // `wasm-bindgen`.
330        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}