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