1use anyhow::{bail, Context};
15use clap::Parser;
16use clap::ValueEnum;
17use std::env;
18use std::ffi::OsString;
19use std::fs;
20use std::path::Path;
21use std::path::PathBuf;
22use std::thread;
23use wasm_bindgen_cli_support::Bindgen;
24
25mod deno;
26mod headless;
27mod node;
28mod server;
29mod shell;
30
31#[derive(Parser)]
32#[command(name = "wasm-bindgen-test-runner", version, about, long_about = None)]
33struct Cli {
34 #[arg(
35 index = 1,
36 help = "The file to test. `cargo test` passes this argument for you."
37 )]
38 file: PathBuf,
39 #[arg(long, conflicts_with = "ignored", help = "Run ignored tests")]
40 include_ignored: bool,
41 #[arg(long, conflicts_with = "include_ignored", help = "Run ignored tests")]
42 ignored: bool,
43 #[arg(long, help = "Exactly match filters rather than by substring")]
44 exact: bool,
45 #[arg(
46 long,
47 value_name = "FILTER",
48 help = "Skip tests whose names contain FILTER (this flag can be used multiple times)"
49 )]
50 skip: Vec<String>,
51 #[arg(long, help = "List all tests and benchmarks")]
52 list: bool,
53 #[arg(
54 long,
55 help = "don't capture `console.*()` of each task, allow printing directly"
56 )]
57 nocapture: bool,
58 #[arg(
59 long,
60 value_enum,
61 value_name = "terse",
62 help = "Configure formatting of output"
63 )]
64 format: Option<FormatSetting>,
65 #[arg(
66 index = 2,
67 value_name = "FILTER",
68 help = "The FILTER string is tested against the name of all tests, and only those tests \
69 whose names contain the filter are run."
70 )]
71 filter: Option<String>,
72}
73
74impl Cli {
75 fn into_args(self, tests: &Tests) -> String {
76 let include_ignored = self.include_ignored;
77 let filtered = tests.filtered;
78
79 format!(
80 r#"
81 // Forward runtime arguments.
82 cx.include_ignored({include_ignored:?});
83 cx.filtered_count({filtered});
84 "#
85 )
86 }
87}
88
89struct Tests {
90 tests: Vec<Test>,
91 filtered: usize,
92}
93
94impl Tests {
95 fn new() -> Self {
96 Self {
97 tests: Vec::new(),
98 filtered: 0,
99 }
100 }
101}
102
103struct Test {
104 name: String,
106 export: String,
108 ignored: bool,
109}
110
111pub fn run_cli_with_args<I, T>(args: I) -> anyhow::Result<()>
112where
113 I: IntoIterator<Item = T>,
114 T: Into<OsString> + Clone,
115{
116 let cli = match Cli::try_parse_from(args) {
117 Ok(a) => a,
118 Err(e) => match e.kind() {
119 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
121 print!("{e}");
122 return Ok(());
123 }
124 _ => bail!(e),
125 },
126 };
127 rmain(cli)
128}
129
130fn rmain(cli: Cli) -> anyhow::Result<()> {
131 let shell = shell::Shell::new();
132
133 let file_name = cli
134 .file
135 .file_name()
136 .map(Path::new)
137 .context("file to test is not a valid file, can't extract file name")?;
138
139 let wasm = fs::read(&cli.file).context("failed to read Wasm file")?;
143 let mut wasm = walrus::ModuleConfig::new()
144 .generate_dwarf(true)
148 .parse(&wasm)
149 .context("failed to deserialize Wasm module")?;
150 let mut tests = Tests::new();
151
152 'outer: for export in wasm.exports.iter() {
153 let Some(name) = export.name.strip_prefix("__wbgt_") else {
154 continue;
155 };
156 let modifiers = name.split_once('_').expect("found invalid identifier").0;
157
158 let Some(name) = export.name.split_once("::").map(|s| s.1) else {
159 continue;
160 };
161
162 let test = Test {
163 name: name.into(),
164 export: export.name.clone(),
165 ignored: modifiers.contains('$'),
166 };
167
168 if let Some(filter) = &cli.filter {
169 let matches = if cli.exact {
170 name == *filter
171 } else {
172 name.contains(filter)
173 };
174
175 if !matches {
176 tests.filtered += 1;
177 continue;
178 }
179 }
180
181 for skip in &cli.skip {
182 let matches = if cli.exact {
183 name == *skip
184 } else {
185 name.contains(skip)
186 };
187
188 if matches {
189 tests.filtered += 1;
190 continue 'outer;
191 }
192 }
193
194 if !test.ignored && cli.ignored {
195 tests.filtered += 1;
196 } else {
197 tests.tests.push(test);
198 }
199 }
200
201 if cli.list {
202 for test in tests.tests {
203 println!("{}: test", test.name);
204 }
205
206 return Ok(());
207 }
208
209 let tmpdir = tempfile::tempdir()?;
210
211 let tmpdir_path = if env::var("WASM_BINDGEN_KEEP_TEST_BUILD").is_ok() {
213 let path = tmpdir.keep();
214 println!(
215 "Retaining temporary build output folder: {}",
216 path.to_string_lossy()
217 );
218 path
219 } else {
220 tmpdir.path().to_path_buf()
221 };
222
223 let module = "wasm-bindgen-test";
224
225 if tests.tests.is_empty() {
229 println!("no tests to run!");
230 return Ok(());
231 }
232
233 let custom_section = wasm.customs.remove_raw("__wasm_bindgen_test_unstable");
239 let no_modules = std::env::var("WASM_BINDGEN_USE_NO_MODULE").is_ok();
240 let test_mode = match custom_section {
241 Some(section) if section.data.contains(&0x01) => TestMode::Browser { no_modules },
242 Some(section) if section.data.contains(&0x02) => TestMode::DedicatedWorker { no_modules },
243 Some(section) if section.data.contains(&0x03) => TestMode::SharedWorker { no_modules },
244 Some(section) if section.data.contains(&0x04) => TestMode::ServiceWorker { no_modules },
245 Some(section) if section.data.contains(&0x05) => TestMode::Node { no_modules },
246 Some(_) => bail!("invalid __wasm_bingen_test_unstable value"),
247 None => {
248 let mut modes = Vec::new();
249 let mut add_mode =
250 |mode: TestMode| std::env::var(mode.env()).is_ok().then(|| modes.push(mode));
251 add_mode(TestMode::Deno);
252 add_mode(TestMode::Browser { no_modules });
253 add_mode(TestMode::DedicatedWorker { no_modules });
254 add_mode(TestMode::SharedWorker { no_modules });
255 add_mode(TestMode::ServiceWorker { no_modules });
256 add_mode(TestMode::Node { no_modules });
257
258 match modes.len() {
259 0 => TestMode::Node { no_modules: true },
260 1 => modes[0],
261 _ => {
262 bail!(
263 "only one test mode must be set, found: `{}`",
264 modes
265 .into_iter()
266 .map(TestMode::env)
267 .collect::<Vec<_>>()
268 .join("`, `")
269 )
270 }
271 }
272 }
273 };
274
275 let headless = env::var("NO_HEADLESS").is_err();
276 let debug = env::var("WASM_BINDGEN_NO_DEBUG").is_err();
277
278 let node = matches!(test_mode, TestMode::Node { .. });
280
281 if env::var_os("WASM_BINDGEN_TEST_ONLY_NODE").is_some() && !node {
282 println!(
283 "this test suite is only configured to run in a browser, \
284 but we're only testing node.js tests so skipping"
285 );
286 return Ok(());
287 }
288 if env::var_os("WASM_BINDGEN_TEST_ONLY_WEB").is_some() && node {
289 println!(
290 "\
291 This test suite is only configured to run in node.js, but we're only running
292 browser tests so skipping. If you'd like to run the tests in a browser
293 include this in your crate when testing:
294
295 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
296
297 You'll likely want to put that in a `#[cfg(test)]` module or at the top of an
298 integration test.\
299 "
300 );
301 return Ok(());
302 }
303
304 let driver_timeout = env::var("WASM_BINDGEN_TEST_DRIVER_TIMEOUT")
305 .map(|timeout| {
306 timeout
307 .parse()
308 .expect("Could not parse 'WASM_BINDGEN_TEST_DRIVER_TIMEOUT'")
309 })
310 .unwrap_or(5);
311
312 let browser_timeout = env::var("WASM_BINDGEN_TEST_TIMEOUT")
313 .map(|timeout| {
314 let timeout = timeout
315 .parse()
316 .expect("Could not parse 'WASM_BINDGEN_TEST_TIMEOUT'");
317 println!("Set timeout to {timeout} seconds...");
318 timeout
319 })
320 .unwrap_or(20);
321
322 shell.status("Executing bindgen...");
324 let mut b = Bindgen::new();
325 match test_mode {
326 TestMode::Node { no_modules: true } => b.nodejs(true)?,
327 TestMode::Node { no_modules: false } => b.nodejs_module(true)?,
328 TestMode::Deno => b.deno(true)?,
329 TestMode::Browser { .. }
330 | TestMode::DedicatedWorker { .. }
331 | TestMode::SharedWorker { .. }
332 | TestMode::ServiceWorker { .. } => {
333 if test_mode.no_modules() {
334 b.no_modules(true)?
335 } else {
336 b.web(true)?
337 }
338 }
339 };
340
341 if std::env::var("WASM_BINDGEN_SPLIT_LINKED_MODULES").is_ok() {
342 b.split_linked_modules(true);
343 }
344 if std::env::var("WASM_BINDGEN_KEEP_LLD_EXPORTS").is_ok() {
345 b.keep_lld_exports(true);
346 }
347
348 let coverage = coverage_args(file_name);
349
350 b.debug(debug)
355 .input_module(module, wasm)
356 .emit_start(false)
357 .generate(&tmpdir_path)
358 .context("executing `wasm-bindgen` over the Wasm file")?;
359 shell.clear();
360
361 match test_mode {
362 TestMode::Node { no_modules } => {
363 node::execute(module, &tmpdir_path, cli, tests, !no_modules, coverage)?
364 }
365 TestMode::Deno => deno::execute(module, &tmpdir_path, cli, tests)?,
366 TestMode::Browser { .. }
367 | TestMode::DedicatedWorker { .. }
368 | TestMode::SharedWorker { .. }
369 | TestMode::ServiceWorker { .. } => {
370 let srv = server::spawn(
371 &if headless {
372 "127.0.0.1:0".parse().unwrap()
373 } else if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
374 address.parse().unwrap()
375 } else {
376 "127.0.0.1:8000".parse().unwrap()
377 },
378 headless,
379 module,
380 &tmpdir_path,
381 cli,
382 tests,
383 test_mode,
384 std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
385 coverage,
386 )
387 .context("failed to spawn server")?;
388 let addr = srv.server_addr();
389
390 if !headless {
393 println!("Interactive browsers tests are now available at http://{addr}");
394 println!();
395 println!("Note that interactive mode is enabled because `NO_HEADLESS`");
396 println!("is specified in the environment of this process. Once you're");
397 println!("done with testing you'll need to kill this server with");
398 println!("Ctrl-C.");
399 srv.run();
400 return Ok(());
401 }
402
403 thread::spawn(|| srv.run());
404 headless::run(&addr, &shell, driver_timeout, browser_timeout)?;
405 }
406 }
407 Ok(())
408}
409
410#[derive(Debug, Copy, Clone, Eq, PartialEq)]
411enum TestMode {
412 Node { no_modules: bool },
413 Deno,
414 Browser { no_modules: bool },
415 DedicatedWorker { no_modules: bool },
416 SharedWorker { no_modules: bool },
417 ServiceWorker { no_modules: bool },
418}
419
420impl TestMode {
421 fn is_worker(self) -> bool {
422 matches!(
423 self,
424 Self::DedicatedWorker { .. } | Self::SharedWorker { .. } | Self::ServiceWorker { .. }
425 )
426 }
427
428 fn no_modules(self) -> bool {
429 match self {
430 Self::Deno => true,
431 Self::Browser { no_modules }
432 | Self::Node { no_modules }
433 | Self::DedicatedWorker { no_modules }
434 | Self::SharedWorker { no_modules }
435 | Self::ServiceWorker { no_modules } => no_modules,
436 }
437 }
438
439 fn env(self) -> &'static str {
440 match self {
441 TestMode::Node { .. } => "WASM_BINDGEN_USE_NODE_EXPERIMENTAL",
442 TestMode::Deno => "WASM_BINDGEN_USE_DENO",
443 TestMode::Browser { .. } => "WASM_BINDGEN_USE_BROWSER",
444 TestMode::DedicatedWorker { .. } => "WASM_BINDGEN_USE_DEDICATED_WORKER",
445 TestMode::SharedWorker { .. } => "WASM_BINDGEN_USE_SHARED_WORKER",
446 TestMode::ServiceWorker { .. } => "WASM_BINDGEN_USE_SERVICE_WORKER",
447 }
448 }
449}
450
451fn coverage_args(file_name: &Path) -> PathBuf {
452 fn generated(file_name: &Path, prefix: &str) -> String {
453 let res = format!("{prefix}{}.profraw", file_name.display());
454 res
455 }
456
457 let prefix = env::var_os("WASM_BINDGEN_UNSTABLE_TEST_PROFRAW_PREFIX")
458 .map(|s| s.to_str().unwrap().to_string())
459 .unwrap_or_default();
460
461 match env::var_os("WASM_BINDGEN_UNSTABLE_TEST_PROFRAW_OUT") {
462 Some(s) => {
463 let mut buf = PathBuf::from(s);
464 if buf.is_dir() {
465 buf.push(generated(file_name, &prefix));
466 }
467 buf
468 }
469 None => PathBuf::from(generated(file_name, &prefix)),
470 }
471}
472
473#[derive(Debug, Clone, Copy, ValueEnum)]
475enum FormatSetting {
476 Terse,
478}