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    /// Path to the Rust crate, and extra options to pass to `cargo test`.
77    ///
78    /// If the path is not provided, this command searches up the path from the current directory.
79    ///
80    /// This is a workaround to allow wasm pack to provide the same command line interface as `cargo`.
81    /// See <https://github.com/rustwasm/wasm-pack/pull/851> for more information.
82    pub path_and_extra_options: Vec<String>,
83}
84
85/// A configured `wasm-pack test` command.
86pub struct Test {
87    crate_path: PathBuf,
88    crate_data: manifest::CrateData,
89    cache: Cache,
90    node: bool,
91    mode: InstallMode,
92    firefox: bool,
93    geckodriver: Option<PathBuf>,
94    chrome: bool,
95    chromedriver: Option<PathBuf>,
96    safari: bool,
97    safaridriver: Option<PathBuf>,
98    headless: bool,
99    release: bool,
100    test_runner_path: Option<PathBuf>,
101    extra_options: Vec<String>,
102}
103
104type TestStep = fn(&mut Test) -> Result<()>;
105
106impl Test {
107    /// Construct a test command from the given options.
108    pub fn try_from_opts(test_opts: TestOptions) -> Result<Self> {
109        let TestOptions {
110            node,
111            mode,
112            headless,
113            release,
114            chrome,
115            chromedriver,
116            firefox,
117            geckodriver,
118            safari,
119            safaridriver,
120            mut path_and_extra_options,
121        } = test_opts;
122
123        let first_arg_is_path = path_and_extra_options
124            .get(0)
125            .map(|first_arg| !first_arg.starts_with("-"))
126            .unwrap_or(false);
127
128        let (path, extra_options) = if first_arg_is_path {
129            let path = PathBuf::from_str(&path_and_extra_options.remove(0))?;
130            let extra_options = path_and_extra_options;
131
132            (Some(path), extra_options)
133        } else {
134            (None, path_and_extra_options)
135        };
136
137        let crate_path = get_crate_path(path)?;
138        let crate_data = manifest::CrateData::new(&crate_path, None)?;
139        let any_browser = chrome || firefox || safari;
140
141        if !node && !any_browser {
142            bail!("Must specify at least one of `--node`, `--chrome`, `--firefox`, or `--safari`")
143        }
144
145        if headless && !any_browser {
146            bail!(
147                "The `--headless` flag only applies to browser tests. Node does not provide a UI, \
148                 so it doesn't make sense to talk about a headless version of Node tests."
149            )
150        }
151
152        Ok(Test {
153            cache: cache::get_wasm_pack_cache()?,
154            crate_path,
155            crate_data,
156            node,
157            mode,
158            chrome,
159            chromedriver,
160            firefox,
161            geckodriver,
162            safari,
163            safaridriver,
164            headless,
165            release,
166            test_runner_path: None,
167            extra_options,
168        })
169    }
170
171    /// Configures the cache that this test command uses
172    pub fn set_cache(&mut self, cache: Cache) {
173        self.cache = cache;
174    }
175
176    /// Execute this test command.
177    pub fn run(mut self) -> Result<()> {
178        let process_steps = self.get_process_steps();
179
180        let started = Instant::now();
181        for (_, process_step) in process_steps {
182            process_step(&mut self)?;
183        }
184        let duration = crate::command::utils::elapsed(started.elapsed());
185        info!("Done in {}.", &duration);
186
187        Ok(())
188    }
189
190    fn get_process_steps(&self) -> Vec<(&'static str, TestStep)> {
191        macro_rules! steps {
192            ($($name:ident $(if $e:expr)* ),+) => {
193                {
194                    let mut steps: Vec<(&'static str, TestStep)> = Vec::new();
195                    $(
196                        $(if $e)* {
197                            steps.push((stringify!($name), Test::$name));
198                        }
199                    )*
200                    steps
201                }
202            };
203            ($($name:ident $(if $e:expr)* ,)*) => (steps![$($name $(if $e)* ),*])
204        }
205        match self.mode {
206            InstallMode::Normal => steps![
207                step_check_rustc_version,
208                step_check_for_wasm_target,
209                step_build_tests,
210                step_install_wasm_bindgen,
211                step_test_node if self.node,
212                step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
213                step_test_chrome if self.chrome,
214                step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
215                step_test_firefox if self.firefox,
216                step_get_safaridriver if self.safari && self.safaridriver.is_none(),
217                step_test_safari if self.safari,
218            ],
219            InstallMode::Force => steps![
220                step_check_for_wasm_target,
221                step_build_tests,
222                step_install_wasm_bindgen,
223                step_test_node if self.node,
224                step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
225                step_test_chrome if self.chrome,
226                step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
227                step_test_firefox if self.firefox,
228                step_get_safaridriver if self.safari && self.safaridriver.is_none(),
229                step_test_safari if self.safari,
230            ],
231            InstallMode::Noinstall => steps![
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        }
243    }
244
245    fn step_check_rustc_version(&mut self) -> Result<()> {
246        info!("Checking rustc version...");
247        let _ = build::check_rustc_version()?;
248        info!("Rustc version is correct.");
249        Ok(())
250    }
251
252    fn step_check_for_wasm_target(&mut self) -> Result<()> {
253        info!("Adding wasm-target...");
254        build::wasm_target::check_for_wasm32_target()?;
255        info!("Adding wasm-target was successful.");
256        Ok(())
257    }
258
259    fn step_build_tests(&mut self) -> Result<()> {
260        info!("Compiling tests to wasm...");
261
262        // If the user has run `wasm-pack test -- --features "f1" -- test_name`, then we want to only pass through
263        // `--features "f1"` to `cargo build`
264        let extra_options =
265            if let Some(index) = self.extra_options.iter().position(|arg| arg == "--") {
266                &self.extra_options[..index]
267            } else {
268                &self.extra_options
269            };
270        build::cargo_build_wasm_tests(&self.crate_path, !self.release, extra_options)?;
271
272        info!("Finished compiling tests to wasm.");
273        Ok(())
274    }
275
276    fn step_install_wasm_bindgen(&mut self) -> Result<()> {
277        info!("Identifying wasm-bindgen dependency...");
278        let lockfile = Lockfile::new(&self.crate_data)?;
279        let bindgen_version = lockfile.require_wasm_bindgen()?;
280
281        // Unlike `wasm-bindgen` and `wasm-bindgen-cli`, `wasm-bindgen-test`
282        // will work with any semver compatible `wasm-bindgen-cli`, so just make
283        // sure that it is depended upon, so we can run tests on
284        // `wasm32-unkown-unknown`. Don't enforce that it is the same version as
285        // `wasm-bindgen`.
286        if lockfile.wasm_bindgen_test_version().is_none() {
287            bail!(
288                "Ensure that you have \"{}\" as a dependency in your Cargo.toml file:\n\
289                 [dev-dependencies]\n\
290                 wasm-bindgen-test = \"0.2\"",
291                style("wasm-bindgen-test").bold().dim(),
292            )
293        }
294
295        let status = install::download_prebuilt_or_cargo_install(
296            Tool::WasmBindgen,
297            &self.cache,
298            &bindgen_version,
299            self.mode.install_permitted(),
300        )?;
301
302        self.test_runner_path = match status {
303            install::Status::Found(dl) => Some(dl.binary("wasm-bindgen-test-runner")?),
304            _ => bail!("Could not find 'wasm-bindgen-test-runner'."),
305        };
306
307        info!("Getting wasm-bindgen-cli was successful.");
308        Ok(())
309    }
310
311    fn step_test_node(&mut self) -> Result<()> {
312        assert!(self.node);
313        info!("Running tests in node...");
314        test::cargo_test_wasm(
315            &self.crate_path,
316            self.release,
317            vec![
318                (
319                    "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER",
320                    &**self.test_runner_path.as_ref().unwrap(),
321                ),
322                ("WASM_BINDGEN_TEST_ONLY_NODE", "1".as_ref()),
323            ],
324            &self.extra_options,
325        )?;
326        info!("Finished running tests in node.");
327        Ok(())
328    }
329
330    fn step_get_chromedriver(&mut self) -> Result<()> {
331        assert!(self.chrome && self.chromedriver.is_none());
332
333        self.chromedriver = Some(webdriver::get_or_install_chromedriver(
334            &self.cache,
335            self.mode,
336        )?);
337        Ok(())
338    }
339
340    fn step_test_chrome(&mut self) -> Result<()> {
341        let chromedriver = self.chromedriver.as_ref().unwrap().display().to_string();
342        let chromedriver = chromedriver.as_str();
343        info!(
344            "Running tests in Chrome with chromedriver at {}",
345            chromedriver
346        );
347
348        let mut envs = self.webdriver_env();
349        envs.push(("CHROMEDRIVER", chromedriver));
350
351        test::cargo_test_wasm(&self.crate_path, self.release, envs, &self.extra_options)?;
352        Ok(())
353    }
354
355    fn step_get_geckodriver(&mut self) -> Result<()> {
356        assert!(self.firefox && self.geckodriver.is_none());
357
358        self.geckodriver = Some(webdriver::get_or_install_geckodriver(
359            &self.cache,
360            self.mode,
361        )?);
362        Ok(())
363    }
364
365    fn step_test_firefox(&mut self) -> Result<()> {
366        let geckodriver = self.geckodriver.as_ref().unwrap().display().to_string();
367        let geckodriver = geckodriver.as_str();
368        info!(
369            "Running tests in Firefox with geckodriver at {}",
370            geckodriver
371        );
372
373        let mut envs = self.webdriver_env();
374        envs.push(("GECKODRIVER", geckodriver));
375
376        test::cargo_test_wasm(&self.crate_path, self.release, envs, &self.extra_options)?;
377        Ok(())
378    }
379
380    fn step_get_safaridriver(&mut self) -> Result<()> {
381        assert!(self.safari && self.safaridriver.is_none());
382
383        self.safaridriver = Some(webdriver::get_safaridriver()?);
384        Ok(())
385    }
386
387    fn step_test_safari(&mut self) -> Result<()> {
388        let safaridriver = self.safaridriver.as_ref().unwrap().display().to_string();
389        let safaridriver = safaridriver.as_str();
390        info!(
391            "Running tests in Safari with safaridriver at {}",
392            safaridriver
393        );
394
395        let mut envs = self.webdriver_env();
396        envs.push(("SAFARIDRIVER", safaridriver));
397
398        test::cargo_test_wasm(&self.crate_path, self.release, envs, &self.extra_options)?;
399        Ok(())
400    }
401
402    fn webdriver_env(&self) -> Vec<(&'static str, &str)> {
403        let test_runner = self.test_runner_path.as_ref().unwrap().to_str().unwrap();
404        info!("Using wasm-bindgen test runner at {}", test_runner);
405        let mut envs = vec![
406            ("CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER", test_runner),
407            ("WASM_BINDGEN_TEST_ONLY_WEB", "1"),
408        ];
409        if !self.headless {
410            envs.push(("NO_HEADLESS", "1"));
411        }
412        envs
413    }
414}