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_JS_STRING_BUILTINS").is_ok() {
350 b.js_string_builtins(true);
351 }
352 if std::env::var("WASM_BINDGEN_KEEP_LLD_EXPORTS").is_ok() {
353 b.keep_lld_exports(true);
354 }
355
356 let benchmark = if let Ok(path) = std::env::var("WASM_BINDGEN_BENCH_RESULT") {
358 PathBuf::from(path)
359 } else {
360 let path = env::current_dir()
362 .context("Failed to get current dir")?
363 .join("target");
364 if cli.bench {
366 fs::create_dir_all(&path)?;
367 }
368 path.join("wbg_benchmark.json")
369 };
370
371 b.debug(debug)
378 .export_wasm(true)
379 .input_module(module, wasm)
380 .emit_start(false)
381 .generate(&tmpdir_path)
382 .context("executing `wasm-bindgen` over the Wasm file")?;
383 shell.clear();
384
385 match test_mode {
386 TestMode::Node { no_modules } => {
387 node::execute(module, &tmpdir_path, cli, tests, !no_modules, benchmark)?
388 }
389 TestMode::Deno => deno::execute(module, &tmpdir_path, cli, tests)?,
390 TestMode::Emscripten => {
391 let srv = server::spawn_emscripten(
392 &if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
393 address.parse().unwrap()
394 } else if headless {
395 "127.0.0.1:0".parse().unwrap()
396 } else {
397 "127.0.0.1:8000".parse().unwrap()
398 },
399 &tmpdir_path,
400 std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
401 )
402 .context("failed to spawn server")?;
403 let addr = srv.server_addr();
404 if !headless {
405 println!("Interactive browsers tests are now available at http://{addr}");
406 println!();
407 println!("Note that interactive mode is enabled because `NO_HEADLESS`");
408 println!("is specified in the environment of this process. Once you're");
409 println!("done with testing you'll need to kill this server with");
410 println!("Ctrl-C.");
411 srv.run();
412 return Ok(());
413 }
414 println!("Tests are now available at http://{addr}");
415 thread::spawn(|| srv.run());
416 headless::run(&addr, &shell, &timeouts, cli.nocapture)?;
417 }
418 TestMode::Browser { .. }
419 | TestMode::DedicatedWorker { .. }
420 | TestMode::SharedWorker { .. }
421 | TestMode::ServiceWorker { .. } => {
422 let nocapture = cli.nocapture;
423 let srv = server::spawn(
424 &if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
425 address.parse().unwrap()
426 } else if headless {
427 "127.0.0.1:0".parse().unwrap()
428 } else {
429 "127.0.0.1:8000".parse().unwrap()
430 },
431 headless,
432 module,
433 &tmpdir_path,
434 cli,
435 tests,
436 test_mode,
437 std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
438 benchmark,
439 )
440 .context("failed to spawn server")?;
441 let addr = srv.server_addr();
442
443 if !headless {
446 println!("Interactive browsers tests are now available at http://{addr}");
447 println!();
448 println!("Note that interactive mode is enabled because `NO_HEADLESS`");
449 println!("is specified in the environment of this process. Once you're");
450 println!("done with testing you'll need to kill this server with");
451 println!("Ctrl-C.");
452 srv.run();
453 return Ok(());
454 }
455
456 thread::spawn(|| srv.run());
457 headless::run(&addr, &shell, &timeouts, nocapture)?;
458 }
459 }
460 Ok(())
461}
462
463fn timeout_from_env(name: &str, default_secs: u64) -> Duration {
464 let Ok(value) = env::var(name) else {
465 return Duration::from_secs(default_secs);
466 };
467 let secs = value
468 .parse()
469 .unwrap_or_else(|_| panic!("Could not parse '{name}'"));
470 println!("Set {name} to {secs} seconds...");
471 Duration::from_secs(secs)
472}
473
474fn module_uses_memory64(wasm: &[u8]) -> anyhow::Result<bool> {
475 for payload in WasmParser::new(0).parse_all(wasm) {
476 match payload.context("failed to inspect Wasm module")? {
477 Payload::ImportSection(imports) => {
478 for import in imports {
479 match import? {
480 Imports::Single(_, import) => {
481 if let TypeRef::Memory(memory) = import.ty {
482 if memory.memory64 {
483 return Ok(true);
484 }
485 }
486 }
487 Imports::Compact1 { items, .. } => {
488 for item in items {
489 if let TypeRef::Memory(memory) = item?.ty {
490 if memory.memory64 {
491 return Ok(true);
492 }
493 }
494 }
495 }
496 Imports::Compact2 { ty, .. } => {
497 if let TypeRef::Memory(memory) = ty {
498 if memory.memory64 {
499 return Ok(true);
500 }
501 }
502 }
503 }
504 }
505 }
506 Payload::MemorySection(memories) => {
507 for memory in memories {
508 if memory?.memory64 {
509 return Ok(true);
510 }
511 }
512 }
513 _ => {}
514 }
515 }
516
517 Ok(false)
518}
519
520#[derive(Debug, Copy, Clone, Eq, PartialEq)]
521enum TestMode {
522 Node { no_modules: bool },
523 Deno,
524 Browser { no_modules: bool },
525 DedicatedWorker { no_modules: bool },
526 SharedWorker { no_modules: bool },
527 ServiceWorker { no_modules: bool },
528 Emscripten,
529}
530
531impl TestMode {
532 fn is_worker(self) -> bool {
533 matches!(
534 self,
535 Self::DedicatedWorker { .. } | Self::SharedWorker { .. } | Self::ServiceWorker { .. }
536 )
537 }
538
539 fn no_modules(self) -> bool {
540 match self {
541 Self::Deno | Self::Emscripten => true,
542 Self::Browser { no_modules }
543 | Self::Node { no_modules }
544 | Self::DedicatedWorker { no_modules }
545 | Self::SharedWorker { no_modules }
546 | Self::ServiceWorker { no_modules } => no_modules,
547 }
548 }
549
550 fn env(self) -> &'static str {
551 match self {
552 TestMode::Node { .. } => "WASM_BINDGEN_USE_NODE_EXPERIMENTAL",
553 TestMode::Deno => "WASM_BINDGEN_USE_DENO",
554 TestMode::Browser { .. } => "WASM_BINDGEN_USE_BROWSER",
555 TestMode::DedicatedWorker { .. } => "WASM_BINDGEN_USE_DEDICATED_WORKER",
556 TestMode::SharedWorker { .. } => "WASM_BINDGEN_USE_SHARED_WORKER",
557 TestMode::ServiceWorker { .. } => "WASM_BINDGEN_USE_SERVICE_WORKER",
558 TestMode::Emscripten => "WASM_BINDGEN_USE_EMSCRIPTEN",
559 }
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::{Cli, Tests};
566 use std::path::PathBuf;
567
568 fn cli() -> Cli {
569 Cli {
570 file: PathBuf::from("test.wasm"),
571 bench: false,
572 include_ignored: true,
573 ignored: false,
574 exact: false,
575 skip: Vec::new(),
576 list: false,
577 nocapture: false,
578 format: None,
579 filter: None,
580 }
581 }
582
583 #[test]
584 fn runner_args_keep_filtered_count_on_number_abi() {
585 let cli = cli();
586 let tests = Tests {
587 tests: Vec::new(),
588 filtered: 3,
589 };
590
591 let args = cli.get_args(&tests);
592
593 assert!(args.contains("cx.include_ignored(true);"));
594 assert!(args.contains("cx.filtered_count(3);"));
595 assert!(!args.contains("3n"));
596 }
597}
598
599#[derive(Debug, Clone, Copy, ValueEnum)]
601enum FormatSetting {
602 Terse,
604}