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 wasm = fs::read(&cli.file).context("failed to read Wasm file")?;
136 let mut wasm = walrus::ModuleConfig::new()
137 .generate_dwarf(true)
141 .parse(&wasm)
142 .context("failed to deserialize Wasm module")?;
143 let mut tests = Tests::new();
144
145 let prefix = if cli.bench { "__wbgb_" } else { "__wbgt_" };
147
148 'outer: for export in wasm.exports.iter() {
149 let Some(name) = export.name.strip_prefix(prefix) else {
150 continue;
151 };
152 let modifiers = name.split_once('_').expect("found invalid identifier").0;
153
154 let Some(name) = export.name.split_once("::").map(|s| s.1) else {
155 continue;
156 };
157
158 let test = Test {
159 name: name.into(),
160 export: export.name.clone(),
161 ignored: modifiers.contains('$'),
162 };
163
164 if let Some(filter) = &cli.filter {
165 let matches = if cli.exact {
166 name == *filter
167 } else {
168 name.contains(filter)
169 };
170
171 if !matches {
172 tests.filtered += 1;
173 continue;
174 }
175 }
176
177 for skip in &cli.skip {
178 let matches = if cli.exact {
179 name == *skip
180 } else {
181 name.contains(skip)
182 };
183
184 if matches {
185 tests.filtered += 1;
186 continue 'outer;
187 }
188 }
189
190 if !test.ignored && cli.ignored {
191 tests.filtered += 1;
192 } else {
193 tests.tests.push(test);
194 }
195 }
196
197 if cli.list {
198 for test in tests.tests {
199 if cli.bench {
200 println!("{}: benchmark", test.name);
201 } else {
202 println!("{}: test", test.name);
203 }
204 }
205
206 return Ok(());
207 }
208
209 let tmpdir = tempfile::tempdir()?;
210
211 let tmpdir_path = if env::var("WASM_BINDGEN_KEEP_TEST_BUILD").is_ok() {
213 let path = tmpdir.keep();
214 println!(
215 "Retaining temporary build output folder: {}",
216 path.to_string_lossy()
217 );
218 path
219 } else {
220 tmpdir.path().to_path_buf()
221 };
222
223 let module = "wasm-bindgen-test";
224
225 if tests.tests.is_empty() {
229 println!("no tests to run!");
230 return Ok(());
231 }
232
233 let custom_section = wasm.customs.remove_raw("__wasm_bindgen_test_unstable");
239 let no_modules = std::env::var("WASM_BINDGEN_USE_NO_MODULE").is_ok();
240 let test_mode = match custom_section {
241 Some(section) if section.data.contains(&0x01) => TestMode::Browser { no_modules },
242 Some(section) if section.data.contains(&0x02) => TestMode::DedicatedWorker { no_modules },
243 Some(section) if section.data.contains(&0x03) => TestMode::SharedWorker { no_modules },
244 Some(section) if section.data.contains(&0x04) => TestMode::ServiceWorker { no_modules },
245 Some(section) if section.data.contains(&0x05) => TestMode::Node { no_modules },
246 Some(_) => bail!("invalid __wasm_bingen_test_unstable value"),
247 None => {
248 let mut modes = Vec::new();
249 let mut add_mode =
250 |mode: TestMode| std::env::var(mode.env()).is_ok().then(|| modes.push(mode));
251 add_mode(TestMode::Deno);
252 add_mode(TestMode::Browser { no_modules });
253 add_mode(TestMode::DedicatedWorker { no_modules });
254 add_mode(TestMode::SharedWorker { no_modules });
255 add_mode(TestMode::ServiceWorker { no_modules });
256 add_mode(TestMode::Node { no_modules });
257
258 match modes.len() {
259 0 => TestMode::Node { no_modules: true },
260 1 => modes[0],
261 _ => {
262 bail!(
263 "only one test mode must be set, found: `{}`",
264 modes
265 .into_iter()
266 .map(TestMode::env)
267 .collect::<Vec<_>>()
268 .join("`, `")
269 )
270 }
271 }
272 }
273 };
274
275 let headless = env::var("NO_HEADLESS").is_err();
276 let debug = env::var("WASM_BINDGEN_NO_DEBUG").is_err();
277
278 let node = matches!(test_mode, TestMode::Node { .. });
280
281 if env::var_os("WASM_BINDGEN_TEST_ONLY_NODE").is_some() && !node {
282 println!(
283 "this test suite is only configured to run in a browser, \
284 but we're only testing node.js tests so skipping"
285 );
286 return Ok(());
287 }
288 if env::var_os("WASM_BINDGEN_TEST_ONLY_WEB").is_some() && node {
289 println!(
290 "\
291 This test suite is only configured to run in node.js, but we're only running
292 browser tests so skipping. If you'd like to run the tests in a browser
293 include this in your crate when testing:
294
295 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
296
297 You'll likely want to put that in a `#[cfg(test)]` module or at the top of an
298 integration test.\
299 "
300 );
301 return Ok(());
302 }
303
304 let driver_timeout = env::var("WASM_BINDGEN_TEST_DRIVER_TIMEOUT")
305 .map(|timeout| {
306 timeout
307 .parse()
308 .expect("Could not parse 'WASM_BINDGEN_TEST_DRIVER_TIMEOUT'")
309 })
310 .unwrap_or(5);
311
312 let browser_timeout = env::var("WASM_BINDGEN_TEST_TIMEOUT")
313 .map(|timeout| {
314 let timeout = timeout
315 .parse()
316 .expect("Could not parse 'WASM_BINDGEN_TEST_TIMEOUT'");
317 println!("Set timeout to {timeout} seconds...");
318 timeout
319 })
320 .unwrap_or(20);
321
322 let shell = shell::Shell::new();
323
324 shell.status("Executing bindgen...");
326 let mut b = Bindgen::new();
327 match test_mode {
328 TestMode::Node { no_modules: true } => b.nodejs(true)?,
329 TestMode::Node { no_modules: false } => b.nodejs_module(true)?,
330 TestMode::Deno => b.deno(true)?,
331 TestMode::Browser { .. }
332 | TestMode::DedicatedWorker { .. }
333 | TestMode::SharedWorker { .. }
334 | TestMode::ServiceWorker { .. } => {
335 if test_mode.no_modules() {
336 b.no_modules(true)?
337 } else {
338 b.web(true)?
339 }
340 }
341 };
342
343 if std::env::var("WASM_BINDGEN_SPLIT_LINKED_MODULES").is_ok() {
344 b.split_linked_modules(true);
345 }
346 if std::env::var("WASM_BINDGEN_KEEP_LLD_EXPORTS").is_ok() {
347 b.keep_lld_exports(true);
348 }
349
350 let benchmark = if let Ok(path) = std::env::var("WASM_BINDGEN_BENCH_RESULT") {
352 PathBuf::from(path)
353 } else {
354 let path = env::current_dir()
356 .context("Failed to get current dir")?
357 .join("target");
358 if cli.bench {
360 fs::create_dir_all(&path)?;
361 }
362 path.join("wbg_benchmark.json")
363 };
364
365 b.debug(debug)
370 .input_module(module, wasm)
371 .emit_start(false)
372 .generate(&tmpdir_path)
373 .context("executing `wasm-bindgen` over the Wasm file")?;
374 shell.clear();
375
376 match test_mode {
377 TestMode::Node { no_modules } => {
378 node::execute(module, &tmpdir_path, cli, tests, !no_modules, benchmark)?
379 }
380 TestMode::Deno => deno::execute(module, &tmpdir_path, cli, tests)?,
381 TestMode::Browser { .. }
382 | TestMode::DedicatedWorker { .. }
383 | TestMode::SharedWorker { .. }
384 | TestMode::ServiceWorker { .. } => {
385 let srv = server::spawn(
386 &if headless {
387 "127.0.0.1:0".parse().unwrap()
388 } else if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
389 address.parse().unwrap()
390 } else {
391 "127.0.0.1:8000".parse().unwrap()
392 },
393 headless,
394 module,
395 &tmpdir_path,
396 cli,
397 tests,
398 test_mode,
399 std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
400 benchmark,
401 )
402 .context("failed to spawn server")?;
403 let addr = srv.server_addr();
404
405 if !headless {
408 println!("Interactive browsers tests are now available at http://{addr}");
409 println!();
410 println!("Note that interactive mode is enabled because `NO_HEADLESS`");
411 println!("is specified in the environment of this process. Once you're");
412 println!("done with testing you'll need to kill this server with");
413 println!("Ctrl-C.");
414 srv.run();
415 return Ok(());
416 }
417
418 thread::spawn(|| srv.run());
419 headless::run(&addr, &shell, driver_timeout, browser_timeout)?;
420 }
421 }
422 Ok(())
423}
424
425#[derive(Debug, Copy, Clone, Eq, PartialEq)]
426enum TestMode {
427 Node { no_modules: bool },
428 Deno,
429 Browser { no_modules: bool },
430 DedicatedWorker { no_modules: bool },
431 SharedWorker { no_modules: bool },
432 ServiceWorker { no_modules: bool },
433}
434
435impl TestMode {
436 fn is_worker(self) -> bool {
437 matches!(
438 self,
439 Self::DedicatedWorker { .. } | Self::SharedWorker { .. } | Self::ServiceWorker { .. }
440 )
441 }
442
443 fn no_modules(self) -> bool {
444 match self {
445 Self::Deno => true,
446 Self::Browser { no_modules }
447 | Self::Node { no_modules }
448 | Self::DedicatedWorker { no_modules }
449 | Self::SharedWorker { no_modules }
450 | Self::ServiceWorker { no_modules } => no_modules,
451 }
452 }
453
454 fn env(self) -> &'static str {
455 match self {
456 TestMode::Node { .. } => "WASM_BINDGEN_USE_NODE_EXPERIMENTAL",
457 TestMode::Deno => "WASM_BINDGEN_USE_DENO",
458 TestMode::Browser { .. } => "WASM_BINDGEN_USE_BROWSER",
459 TestMode::DedicatedWorker { .. } => "WASM_BINDGEN_USE_DEDICATED_WORKER",
460 TestMode::SharedWorker { .. } => "WASM_BINDGEN_USE_SHARED_WORKER",
461 TestMode::ServiceWorker { .. } => "WASM_BINDGEN_USE_SERVICE_WORKER",
462 }
463 }
464}
465
466#[derive(Debug, Clone, Copy, ValueEnum)]
468enum FormatSetting {
469 Terse,
471}