1use anyhow::{bail, Context};
15use clap::Parser;
16use clap::ValueEnum;
17use std::env;
18use std::ffi::OsString;
19use std::fs;
20use std::path::PathBuf;
21use std::thread;
22use std::time::Duration;
23use wasm_bindgen_cli_support::Bindgen;
24use wasmparser::{Imports, Parser as WasmParser, Payload, TypeRef};
25
26mod deno;
27mod headless;
28mod node;
29mod server;
30mod shell;
31
32#[derive(Parser)]
33#[command(name = "wasm-bindgen-test-runner", version, about, long_about = None)]
34struct Cli {
35 #[arg(
36 index = 1,
37 help = "The file to test. `cargo test` passes this argument for you."
38 )]
39 file: PathBuf,
40 #[arg(long, help = "Run benchmarks")]
41 bench: bool,
42 #[arg(long, conflicts_with = "ignored", help = "Run ignored tests")]
43 include_ignored: bool,
44 #[arg(long, conflicts_with = "include_ignored", help = "Run ignored tests")]
45 ignored: bool,
46 #[arg(long, help = "Exactly match filters rather than by substring")]
47 exact: bool,
48 #[arg(
49 long,
50 value_name = "FILTER",
51 help = "Skip tests whose names contain FILTER (this flag can be used multiple times)"
52 )]
53 skip: Vec<String>,
54 #[arg(long, help = "List all tests and benchmarks")]
55 list: bool,
56 #[arg(
57 long,
58 help = "don't capture `console.*()` of each task, allow printing directly"
59 )]
60 nocapture: bool,
61 #[arg(
62 long,
63 value_enum,
64 value_name = "terse",
65 help = "Configure formatting of output"
66 )]
67 format: Option<FormatSetting>,
68 #[arg(
69 index = 2,
70 value_name = "FILTER",
71 help = "The FILTER string is tested against the name of all tests, and only those tests \
72 whose names contain the filter are run."
73 )]
74 filter: Option<String>,
75}
76
77impl Cli {
78 fn get_args(&self, tests: &Tests) -> String {
79 let include_ignored = self.include_ignored;
80 let filtered = tests.filtered;
81
82 format!(
83 r#"
84 // Forward runtime arguments.
85 cx.include_ignored({include_ignored:?});
86 cx.filtered_count({filtered});
87 "#
88 )
89 }
90}
91
92struct Tests {
93 tests: Vec<Test>,
94 filtered: usize,
95}
96
97impl Tests {
98 fn new() -> Self {
99 Self {
100 tests: Vec::new(),
101 filtered: 0,
102 }
103 }
104}
105
106struct Test {
107 name: String,
109 export: String,
111 ignored: bool,
112}
113
114pub fn run_cli_with_args<I, T>(args: I) -> anyhow::Result<()>
115where
116 I: IntoIterator<Item = T>,
117 T: Into<OsString> + Clone,
118{
119 let cli = match Cli::try_parse_from(args) {
120 Ok(a) => a,
121 Err(e) => match e.kind() {
122 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
124 print!("{e}");
125 return Ok(());
126 }
127 _ => bail!(e),
128 },
129 };
130 rmain(cli)
131}
132
133fn rmain(cli: Cli) -> anyhow::Result<()> {
134 let mut file_name_buf = cli.file.clone();
135
136 if file_name_buf.extension().unwrap_or_default() == "js" {
140 file_name_buf.set_extension("wasm");
141 }
142 let wasm = fs::read(file_name_buf).context("failed to read Wasm file")?;
143 let uses_memory64 = module_uses_memory64(&wasm)?;
144 let mut wasm = walrus::ModuleConfig::new()
145 .generate_dwarf(!uses_memory64)
150 .parse(&wasm)
151 .context("failed to deserialize Wasm module")?;
152 let mut tests = Tests::new();
153
154 let prefix = if cli.bench { "__wbgb_" } else { "__wbgt_" };
156
157 'outer: for export in wasm.exports.iter() {
158 let Some(name) = export.name.strip_prefix(prefix) else {
159 continue;
160 };
161 let modifiers = name.split_once('_').expect("found invalid identifier").0;
162
163 let Some(name) = export.name.split_once("::").map(|s| s.1) else {
164 continue;
165 };
166
167 let test = Test {
168 name: name.into(),
169 export: export.name.clone(),
170 ignored: modifiers.contains('$'),
171 };
172
173 if let Some(filter) = &cli.filter {
174 let matches = if cli.exact {
175 name == *filter
176 } else {
177 name.contains(filter)
178 };
179
180 if !matches {
181 tests.filtered += 1;
182 continue;
183 }
184 }
185
186 for skip in &cli.skip {
187 let matches = if cli.exact {
188 name == *skip
189 } else {
190 name.contains(skip)
191 };
192
193 if matches {
194 tests.filtered += 1;
195 continue 'outer;
196 }
197 }
198
199 if !test.ignored && cli.ignored {
200 tests.filtered += 1;
201 } else {
202 tests.tests.push(test);
203 }
204 }
205
206 if cli.list {
207 for test in tests.tests {
208 if cli.bench {
209 println!("{}: benchmark", test.name);
210 } else {
211 println!("{}: test", test.name);
212 }
213 }
214
215 return Ok(());
216 }
217
218 let tmpdir = tempfile::tempdir()?;
219
220 let tmpdir_path = if env::var("WASM_BINDGEN_KEEP_TEST_BUILD").is_ok() {
222 let path = tmpdir.keep();
223 println!(
224 "Retaining temporary build output folder: {}",
225 path.to_string_lossy()
226 );
227 path
228 } else {
229 tmpdir.path().to_path_buf()
230 };
231
232 let module = "wasm-bindgen-test";
233
234 if tests.tests.is_empty() {
238 println!("no tests to run!");
239 return Ok(());
240 }
241
242 let custom_section = wasm.customs.remove_raw("__wasm_bindgen_test_unstable");
248 let no_modules = std::env::var("WASM_BINDGEN_USE_NO_MODULE").is_ok();
249 let test_mode = match custom_section {
250 Some(section) if section.data.contains(&0x01) => TestMode::Browser { no_modules },
251 Some(section) if section.data.contains(&0x02) => TestMode::DedicatedWorker { no_modules },
252 Some(section) if section.data.contains(&0x03) => TestMode::SharedWorker { no_modules },
253 Some(section) if section.data.contains(&0x04) => TestMode::ServiceWorker { no_modules },
254 Some(section) if section.data.contains(&0x05) => TestMode::Node { no_modules },
255 Some(section) if section.data.contains(&0x06) => TestMode::Emscripten {},
256 Some(_) => bail!("invalid __wasm_bingen_test_unstable value"),
257 None => {
258 let mut modes = Vec::new();
259 let mut add_mode =
260 |mode: TestMode| std::env::var(mode.env()).is_ok().then(|| modes.push(mode));
261 add_mode(TestMode::Deno);
262 add_mode(TestMode::Browser { no_modules });
263 add_mode(TestMode::DedicatedWorker { no_modules });
264 add_mode(TestMode::SharedWorker { no_modules });
265 add_mode(TestMode::ServiceWorker { no_modules });
266 add_mode(TestMode::Node { no_modules });
267
268 match modes.len() {
269 0 => TestMode::Node { no_modules: true },
270 1 => modes[0],
271 _ => {
272 bail!(
273 "only one test mode must be set, found: `{}`",
274 modes
275 .into_iter()
276 .map(TestMode::env)
277 .collect::<Vec<_>>()
278 .join("`, `")
279 )
280 }
281 }
282 }
283 };
284
285 let headless = env::var("NO_HEADLESS").is_err();
286 let debug = env::var("WASM_BINDGEN_NO_DEBUG").is_err();
287
288 let node = matches!(test_mode, TestMode::Node { .. });
290
291 if env::var_os("WASM_BINDGEN_TEST_ONLY_NODE").is_some() && !node {
292 println!(
293 "this test suite is only configured to run in a browser, \
294 but we're only testing node.js tests so skipping"
295 );
296 return Ok(());
297 }
298 if env::var_os("WASM_BINDGEN_TEST_ONLY_WEB").is_some() && node {
299 println!(
300 "\
301 This test suite is only configured to run in node.js, but we're only running
302 browser tests so skipping. If you'd like to run the tests in a browser
303 include this in your crate when testing:
304
305 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
306
307 You'll likely want to put that in a `#[cfg(test)]` module or at the top of an
308 integration test.\
309 "
310 );
311 return Ok(());
312 }
313
314 let test = timeout_from_env("WASM_BINDGEN_TEST_TIMEOUT", 20);
315 let timeouts = headless::Timeouts {
318 driver: timeout_from_env("WASM_BINDGEN_TEST_DRIVER_TIMEOUT", 5),
319 startup: Duration::from_secs(60).max(test),
320 page_load: Duration::from_secs(300).max(test),
321 test,
322 };
323
324 let shell = shell::Shell::new();
325
326 shell.status("Executing bindgen...");
328 let mut b = Bindgen::new();
329 match test_mode {
330 TestMode::Node { no_modules: true } => b.nodejs(true)?,
331 TestMode::Node { no_modules: false } => b.nodejs_module(true)?,
332 TestMode::Deno => b.deno(true)?,
333 TestMode::Browser { .. }
334 | TestMode::DedicatedWorker { .. }
335 | TestMode::SharedWorker { .. }
336 | TestMode::ServiceWorker { .. }
337 | TestMode::Emscripten => {
338 if test_mode.no_modules() {
339 b.no_modules(true)?
340 } else {
341 b.web(true)?
342 }
343 }
344 };
345
346 if std::env::var("WASM_BINDGEN_SPLIT_LINKED_MODULES").is_ok() {
347 b.split_linked_modules(true);
348 }
349 if std::env::var("WASM_BINDGEN_KEEP_LLD_EXPORTS").is_ok() {
350 b.keep_lld_exports(true);
351 }
352
353 let benchmark = if let Ok(path) = std::env::var("WASM_BINDGEN_BENCH_RESULT") {
355 PathBuf::from(path)
356 } else {
357 let path = env::current_dir()
359 .context("Failed to get current dir")?
360 .join("target");
361 if cli.bench {
363 fs::create_dir_all(&path)?;
364 }
365 path.join("wbg_benchmark.json")
366 };
367
368 b.debug(debug)
373 .input_module(module, wasm)
374 .emit_start(false)
375 .generate(&tmpdir_path)
376 .context("executing `wasm-bindgen` over the Wasm file")?;
377 shell.clear();
378
379 match test_mode {
380 TestMode::Node { no_modules } => {
381 node::execute(module, &tmpdir_path, cli, tests, !no_modules, benchmark)?
382 }
383 TestMode::Deno => deno::execute(module, &tmpdir_path, cli, tests)?,
384 TestMode::Emscripten => {
385 let srv = server::spawn_emscripten(
386 &if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
387 address.parse().unwrap()
388 } else if headless {
389 "127.0.0.1:0".parse().unwrap()
390 } else {
391 "127.0.0.1:8000".parse().unwrap()
392 },
393 &tmpdir_path,
394 std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
395 )
396 .context("failed to spawn server")?;
397 let addr = srv.server_addr();
398 if !headless {
399 println!("Interactive browsers tests are now available at http://{addr}");
400 println!();
401 println!("Note that interactive mode is enabled because `NO_HEADLESS`");
402 println!("is specified in the environment of this process. Once you're");
403 println!("done with testing you'll need to kill this server with");
404 println!("Ctrl-C.");
405 srv.run();
406 return Ok(());
407 }
408 println!("Tests are now available at http://{addr}");
409 thread::spawn(|| srv.run());
410 headless::run(&addr, &shell, &timeouts, cli.nocapture)?;
411 }
412 TestMode::Browser { .. }
413 | TestMode::DedicatedWorker { .. }
414 | TestMode::SharedWorker { .. }
415 | TestMode::ServiceWorker { .. } => {
416 let nocapture = cli.nocapture;
417 let srv = server::spawn(
418 &if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
419 address.parse().unwrap()
420 } else if headless {
421 "127.0.0.1:0".parse().unwrap()
422 } else {
423 "127.0.0.1:8000".parse().unwrap()
424 },
425 headless,
426 module,
427 &tmpdir_path,
428 cli,
429 tests,
430 test_mode,
431 std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
432 benchmark,
433 )
434 .context("failed to spawn server")?;
435 let addr = srv.server_addr();
436
437 if !headless {
440 println!("Interactive browsers tests are now available at http://{addr}");
441 println!();
442 println!("Note that interactive mode is enabled because `NO_HEADLESS`");
443 println!("is specified in the environment of this process. Once you're");
444 println!("done with testing you'll need to kill this server with");
445 println!("Ctrl-C.");
446 srv.run();
447 return Ok(());
448 }
449
450 thread::spawn(|| srv.run());
451 headless::run(&addr, &shell, &timeouts, nocapture)?;
452 }
453 }
454 Ok(())
455}
456
457fn timeout_from_env(name: &str, default_secs: u64) -> Duration {
458 let Ok(value) = env::var(name) else {
459 return Duration::from_secs(default_secs);
460 };
461 let secs = value
462 .parse()
463 .unwrap_or_else(|_| panic!("Could not parse '{name}'"));
464 println!("Set {name} to {secs} seconds...");
465 Duration::from_secs(secs)
466}
467
468fn module_uses_memory64(wasm: &[u8]) -> anyhow::Result<bool> {
469 for payload in WasmParser::new(0).parse_all(wasm) {
470 match payload.context("failed to inspect Wasm module")? {
471 Payload::ImportSection(imports) => {
472 for import in imports {
473 match import? {
474 Imports::Single(_, import) => {
475 if let TypeRef::Memory(memory) = import.ty {
476 if memory.memory64 {
477 return Ok(true);
478 }
479 }
480 }
481 Imports::Compact1 { items, .. } => {
482 for item in items {
483 if let TypeRef::Memory(memory) = item?.ty {
484 if memory.memory64 {
485 return Ok(true);
486 }
487 }
488 }
489 }
490 Imports::Compact2 { ty, .. } => {
491 if let TypeRef::Memory(memory) = ty {
492 if memory.memory64 {
493 return Ok(true);
494 }
495 }
496 }
497 }
498 }
499 }
500 Payload::MemorySection(memories) => {
501 for memory in memories {
502 if memory?.memory64 {
503 return Ok(true);
504 }
505 }
506 }
507 _ => {}
508 }
509 }
510
511 Ok(false)
512}
513
514#[derive(Debug, Copy, Clone, Eq, PartialEq)]
515enum TestMode {
516 Node { no_modules: bool },
517 Deno,
518 Browser { no_modules: bool },
519 DedicatedWorker { no_modules: bool },
520 SharedWorker { no_modules: bool },
521 ServiceWorker { no_modules: bool },
522 Emscripten,
523}
524
525impl TestMode {
526 fn is_worker(self) -> bool {
527 matches!(
528 self,
529 Self::DedicatedWorker { .. } | Self::SharedWorker { .. } | Self::ServiceWorker { .. }
530 )
531 }
532
533 fn no_modules(self) -> bool {
534 match self {
535 Self::Deno | Self::Emscripten => true,
536 Self::Browser { no_modules }
537 | Self::Node { no_modules }
538 | Self::DedicatedWorker { no_modules }
539 | Self::SharedWorker { no_modules }
540 | Self::ServiceWorker { no_modules } => no_modules,
541 }
542 }
543
544 fn env(self) -> &'static str {
545 match self {
546 TestMode::Node { .. } => "WASM_BINDGEN_USE_NODE_EXPERIMENTAL",
547 TestMode::Deno => "WASM_BINDGEN_USE_DENO",
548 TestMode::Browser { .. } => "WASM_BINDGEN_USE_BROWSER",
549 TestMode::DedicatedWorker { .. } => "WASM_BINDGEN_USE_DEDICATED_WORKER",
550 TestMode::SharedWorker { .. } => "WASM_BINDGEN_USE_SHARED_WORKER",
551 TestMode::ServiceWorker { .. } => "WASM_BINDGEN_USE_SERVICE_WORKER",
552 TestMode::Emscripten => "WASM_BINDGEN_USE_EMSCRIPTEN",
553 }
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::{Cli, Tests};
560 use std::path::PathBuf;
561
562 fn cli() -> Cli {
563 Cli {
564 file: PathBuf::from("test.wasm"),
565 bench: false,
566 include_ignored: true,
567 ignored: false,
568 exact: false,
569 skip: Vec::new(),
570 list: false,
571 nocapture: false,
572 format: None,
573 filter: None,
574 }
575 }
576
577 #[test]
578 fn runner_args_keep_filtered_count_on_number_abi() {
579 let cli = cli();
580 let tests = Tests {
581 tests: Vec::new(),
582 filtered: 3,
583 };
584
585 let args = cli.get_args(&tests);
586
587 assert!(args.contains("cx.include_ignored(true);"));
588 assert!(args.contains("cx.filtered_count(3);"));
589 assert!(!args.contains("3n"));
590 }
591}
592
593#[derive(Debug, Clone, Copy, ValueEnum)]
595enum FormatSetting {
596 Terse,
598}