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