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, 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 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 let prefix = if cli.bench { "__wbgb_" } else { "__wbgt_" };
154
155 'outer: for export in wasm.exports.iter() {
156 let Some(name) = export.name.strip_prefix(prefix) else {
157 continue;
158 };
159 let modifiers = name.split_once('_').expect("found invalid identifier").0;
160
161 let Some(name) = export.name.split_once("::").map(|s| s.1) else {
162 continue;
163 };
164
165 let test = Test {
166 name: name.into(),
167 export: export.name.clone(),
168 ignored: modifiers.contains('$'),
169 };
170
171 if let Some(filter) = &cli.filter {
172 let matches = if cli.exact {
173 name == *filter
174 } else {
175 name.contains(filter)
176 };
177
178 if !matches {
179 tests.filtered += 1;
180 continue;
181 }
182 }
183
184 for skip in &cli.skip {
185 let matches = if cli.exact {
186 name == *skip
187 } else {
188 name.contains(skip)
189 };
190
191 if matches {
192 tests.filtered += 1;
193 continue 'outer;
194 }
195 }
196
197 if !test.ignored && cli.ignored {
198 tests.filtered += 1;
199 } else {
200 tests.tests.push(test);
201 }
202 }
203
204 if cli.list {
205 for test in tests.tests {
206 if cli.bench {
207 println!("{}: benchmark", test.name);
208 } else {
209 println!("{}: test", test.name);
210 }
211 }
212
213 return Ok(());
214 }
215
216 let tmpdir = tempfile::tempdir()?;
217
218 let tmpdir_path = if env::var("WASM_BINDGEN_KEEP_TEST_BUILD").is_ok() {
220 let path = tmpdir.keep();
221 println!(
222 "Retaining temporary build output folder: {}",
223 path.to_string_lossy()
224 );
225 path
226 } else {
227 tmpdir.path().to_path_buf()
228 };
229
230 let module = "wasm-bindgen-test";
231
232 if tests.tests.is_empty() {
236 println!("no tests to run!");
237 return Ok(());
238 }
239
240 let custom_section = wasm.customs.remove_raw("__wasm_bindgen_test_unstable");
246 let no_modules = std::env::var("WASM_BINDGEN_USE_NO_MODULE").is_ok();
247 let test_mode = match custom_section {
248 Some(section) if section.data.contains(&0x01) => TestMode::Browser { no_modules },
249 Some(section) if section.data.contains(&0x02) => TestMode::DedicatedWorker { no_modules },
250 Some(section) if section.data.contains(&0x03) => TestMode::SharedWorker { no_modules },
251 Some(section) if section.data.contains(&0x04) => TestMode::ServiceWorker { no_modules },
252 Some(section) if section.data.contains(&0x05) => TestMode::Node { no_modules },
253 Some(_) => bail!("invalid __wasm_bingen_test_unstable value"),
254 None => {
255 let mut modes = Vec::new();
256 let mut add_mode =
257 |mode: TestMode| std::env::var(mode.env()).is_ok().then(|| modes.push(mode));
258 add_mode(TestMode::Deno);
259 add_mode(TestMode::Browser { no_modules });
260 add_mode(TestMode::DedicatedWorker { no_modules });
261 add_mode(TestMode::SharedWorker { no_modules });
262 add_mode(TestMode::ServiceWorker { no_modules });
263 add_mode(TestMode::Node { no_modules });
264
265 match modes.len() {
266 0 => TestMode::Node { no_modules: true },
267 1 => modes[0],
268 _ => {
269 bail!(
270 "only one test mode must be set, found: `{}`",
271 modes
272 .into_iter()
273 .map(TestMode::env)
274 .collect::<Vec<_>>()
275 .join("`, `")
276 )
277 }
278 }
279 }
280 };
281
282 let headless = env::var("NO_HEADLESS").is_err();
283 let debug = env::var("WASM_BINDGEN_NO_DEBUG").is_err();
284
285 let node = matches!(test_mode, TestMode::Node { .. });
287
288 if env::var_os("WASM_BINDGEN_TEST_ONLY_NODE").is_some() && !node {
289 println!(
290 "this test suite is only configured to run in a browser, \
291 but we're only testing node.js tests so skipping"
292 );
293 return Ok(());
294 }
295 if env::var_os("WASM_BINDGEN_TEST_ONLY_WEB").is_some() && node {
296 println!(
297 "\
298 This test suite is only configured to run in node.js, but we're only running
299 browser tests so skipping. If you'd like to run the tests in a browser
300 include this in your crate when testing:
301
302 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
303
304 You'll likely want to put that in a `#[cfg(test)]` module or at the top of an
305 integration test.\
306 "
307 );
308 return Ok(());
309 }
310
311 let driver_timeout = env::var("WASM_BINDGEN_TEST_DRIVER_TIMEOUT")
312 .map(|timeout| {
313 timeout
314 .parse()
315 .expect("Could not parse 'WASM_BINDGEN_TEST_DRIVER_TIMEOUT'")
316 })
317 .unwrap_or(5);
318
319 let browser_timeout = env::var("WASM_BINDGEN_TEST_TIMEOUT")
320 .map(|timeout| {
321 let timeout = timeout
322 .parse()
323 .expect("Could not parse 'WASM_BINDGEN_TEST_TIMEOUT'");
324 println!("Set timeout to {timeout} seconds...");
325 timeout
326 })
327 .unwrap_or(20);
328
329 let shell = shell::Shell::new();
330
331 shell.status("Executing bindgen...");
333 let mut b = Bindgen::new();
334 match test_mode {
335 TestMode::Node { no_modules: true } => b.nodejs(true)?,
336 TestMode::Node { no_modules: false } => b.nodejs_module(true)?,
337 TestMode::Deno => b.deno(true)?,
338 TestMode::Browser { .. }
339 | TestMode::DedicatedWorker { .. }
340 | TestMode::SharedWorker { .. }
341 | TestMode::ServiceWorker { .. } => {
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 coverage = coverage_args(file_name);
358
359 let benchmark = if let Ok(path) = std::env::var("WASM_BINDGEN_BENCH_RESULT") {
361 PathBuf::from(path)
362 } else {
363 let path = env::current_dir()
365 .context("Failed to get current dir")?
366 .join("target");
367 if cli.bench {
369 fs::create_dir_all(&path)?;
370 }
371 path.join("wbg_benchmark.json")
372 };
373
374 b.debug(debug)
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 } => node::execute(
387 module,
388 &tmpdir_path,
389 cli,
390 tests,
391 !no_modules,
392 coverage,
393 benchmark,
394 )?,
395 TestMode::Deno => deno::execute(module, &tmpdir_path, cli, tests)?,
396 TestMode::Browser { .. }
397 | TestMode::DedicatedWorker { .. }
398 | TestMode::SharedWorker { .. }
399 | TestMode::ServiceWorker { .. } => {
400 let srv = server::spawn(
401 &if headless {
402 "127.0.0.1:0".parse().unwrap()
403 } else if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
404 address.parse().unwrap()
405 } else {
406 "127.0.0.1:8000".parse().unwrap()
407 },
408 headless,
409 module,
410 &tmpdir_path,
411 cli,
412 tests,
413 test_mode,
414 std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
415 coverage,
416 benchmark,
417 )
418 .context("failed to spawn server")?;
419 let addr = srv.server_addr();
420
421 if !headless {
424 println!("Interactive browsers tests are now available at http://{addr}");
425 println!();
426 println!("Note that interactive mode is enabled because `NO_HEADLESS`");
427 println!("is specified in the environment of this process. Once you're");
428 println!("done with testing you'll need to kill this server with");
429 println!("Ctrl-C.");
430 srv.run();
431 return Ok(());
432 }
433
434 thread::spawn(|| srv.run());
435 headless::run(&addr, &shell, driver_timeout, browser_timeout)?;
436 }
437 }
438 Ok(())
439}
440
441#[derive(Debug, Copy, Clone, Eq, PartialEq)]
442enum TestMode {
443 Node { no_modules: bool },
444 Deno,
445 Browser { no_modules: bool },
446 DedicatedWorker { no_modules: bool },
447 SharedWorker { no_modules: bool },
448 ServiceWorker { no_modules: bool },
449}
450
451impl TestMode {
452 fn is_worker(self) -> bool {
453 matches!(
454 self,
455 Self::DedicatedWorker { .. } | Self::SharedWorker { .. } | Self::ServiceWorker { .. }
456 )
457 }
458
459 fn no_modules(self) -> bool {
460 match self {
461 Self::Deno => true,
462 Self::Browser { no_modules }
463 | Self::Node { no_modules }
464 | Self::DedicatedWorker { no_modules }
465 | Self::SharedWorker { no_modules }
466 | Self::ServiceWorker { no_modules } => no_modules,
467 }
468 }
469
470 fn env(self) -> &'static str {
471 match self {
472 TestMode::Node { .. } => "WASM_BINDGEN_USE_NODE_EXPERIMENTAL",
473 TestMode::Deno => "WASM_BINDGEN_USE_DENO",
474 TestMode::Browser { .. } => "WASM_BINDGEN_USE_BROWSER",
475 TestMode::DedicatedWorker { .. } => "WASM_BINDGEN_USE_DEDICATED_WORKER",
476 TestMode::SharedWorker { .. } => "WASM_BINDGEN_USE_SHARED_WORKER",
477 TestMode::ServiceWorker { .. } => "WASM_BINDGEN_USE_SERVICE_WORKER",
478 }
479 }
480}
481
482fn coverage_args(file_name: &Path) -> PathBuf {
483 fn generated(file_name: &Path, prefix: &str) -> String {
484 let res = format!("{prefix}{}.profraw", file_name.display());
485 res
486 }
487
488 let prefix = env::var_os("WASM_BINDGEN_UNSTABLE_TEST_PROFRAW_PREFIX")
489 .map(|s| s.to_str().unwrap().to_string())
490 .unwrap_or_default();
491
492 match env::var_os("WASM_BINDGEN_UNSTABLE_TEST_PROFRAW_OUT") {
493 Some(s) => {
494 let mut buf = PathBuf::from(s);
495 if buf.is_dir() {
496 buf.push(generated(file_name, &prefix));
497 }
498 buf
499 }
500 None => PathBuf::from(generated(file_name, &prefix)),
501 }
502}
503
504#[derive(Debug, Clone, Copy, ValueEnum)]
506enum FormatSetting {
507 Terse,
509}