Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ jobs:
- uses: actions/setup-node@v6
with:
node-version: '20'
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- run: cargo test
- run: cargo test -p wasm-bindgen-cli-support
- run: cargo test -p wasm-bindgen-cli
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG_FFI.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# doctest-support

Actually run doctests in wasm-bindgen-test-runner. Previously, doctests were silently skipped because they export a `main` function instead of `__wbgt_*` test exports. The runner would print "no tests to run!" and exit 0, causing the tooling to report the doctest as passed.

Now doctests are detected and executed properly. Supports:
- Node.js CommonJS (default, no configuration needed)
- Node.js ES modules (`wasm_bindgen_test_configure!(run_in_node_experimental)`)
- Deno (`WASM_BINDGEN_USE_DENO=1` environment variable)
- Browser main thread (`wasm_bindgen_test_configure!(run_in_browser)`)
- Dedicated worker (`wasm_bindgen_test_configure!(run_in_dedicated_worker)`)
- Shared worker (`wasm_bindgen_test_configure!(run_in_shared_worker)`)
- Service worker (`wasm_bindgen_test_configure!(run_in_service_worker)`)

# nodejs-threads

Add Node.js `worker_threads` support for atomics builds. Supports both CommonJS (`--target nodejs`) and ESM (`--target experimental-nodejs-module`) targets. When targeting Node.js with atomics enabled, wasm-bindgen now generates:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ wasm-bindgen-shared = { path = "crates/shared", version = "=0.2.106" }

[build-dependencies]
# In older MSRVs, dependencies and crate features can't have the same name.
rustversion-compat = { package = "rustversion", version = "1.0" }
rustversion-compat = { package = "rustversion", version = "1.0.6" }

[dev-dependencies]
once_cell = "1"
Expand Down
7 changes: 7 additions & 0 deletions crates/cli-support/src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ impl<'src> Decode<'src> for u32 {
impl<'src> Decode<'src> for &'src str {
fn decode(data: &mut &'src [u8]) -> &'src str {
let n = u32::decode(data);
if n as usize > data.len() {
panic!(
"String decode failed: length {n} but only {} bytes remaining. First 20 bytes: {:?}",
data.len(),
&data[..data.len().min(20)]
);
}
let (a, b) = data.split_at(n as usize);
*data = b;
let r = str::from_utf8(a).unwrap();
Expand Down
194 changes: 147 additions & 47 deletions crates/cli/src/wasm_bindgen_test_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use std::thread;
use wasm_bindgen_cli_support::Bindgen;

mod deno;
mod doctest;
mod headless;
mod node;
mod server;
Expand Down Expand Up @@ -222,10 +223,21 @@ fn rmain(cli: Cli) -> anyhow::Result<()> {

let module = "wasm-bindgen-test";

// Check if this is a doctest - doctests have a `main` export instead of
// `__wbgt_*` test exports, AND have a function named `__doctest_main_*`
// which is generated by rustdoc.
let has_main_export = wasm.exports.iter().any(|e| e.name == "main");
let has_doctest_main = wasm.funcs.iter().any(|f| {
f.name
.as_ref()
.is_some_and(|n| n.contains("__doctest_main"))
});
let is_doctest = tests.tests.is_empty() && has_main_export && has_doctest_main;

// Right now there's a bug where if no tests are present then the
// `wasm-bindgen-test` runtime support isn't linked in, so just bail out
// early saying everything is ok.
if tests.tests.is_empty() {
if tests.tests.is_empty() && !is_doctest {
println!("no tests to run!");
return Ok(());
}
Expand All @@ -237,13 +249,18 @@ fn rmain(cli: Cli) -> anyhow::Result<()> {

let custom_section = wasm.customs.remove_raw("__wasm_bindgen_test_unstable");
let no_modules = std::env::var("WASM_BINDGEN_USE_NO_MODULE").is_ok();
// Force no_modules for ServiceWorker because Firefox < 147 doesn't support
// ES module service workers. See https://bugzilla.mozilla.org/show_bug.cgi?id=1360870
let service_worker_no_modules = true;
let test_mode = match custom_section {
Some(section) if section.data.contains(&0x01) => TestMode::Browser { no_modules },
Some(section) if section.data.contains(&0x02) => TestMode::DedicatedWorker { no_modules },
Some(section) if section.data.contains(&0x03) => TestMode::SharedWorker { no_modules },
Some(section) if section.data.contains(&0x04) => TestMode::ServiceWorker { no_modules },
Some(section) if section.data.contains(&0x04) => TestMode::ServiceWorker {
no_modules: service_worker_no_modules,
},
Some(section) if section.data.contains(&0x05) => TestMode::Node { no_modules },
Some(_) => bail!("invalid __wasm_bingen_test_unstable value"),
Some(_) => bail!("invalid __wasm_bindgen_test_unstable value"),
None => {
let mut modes = Vec::new();
let mut add_mode =
Expand All @@ -252,7 +269,9 @@ fn rmain(cli: Cli) -> anyhow::Result<()> {
add_mode(TestMode::Browser { no_modules });
add_mode(TestMode::DedicatedWorker { no_modules });
add_mode(TestMode::SharedWorker { no_modules });
add_mode(TestMode::ServiceWorker { no_modules });
add_mode(TestMode::ServiceWorker {
no_modules: service_worker_no_modules,
});
add_mode(TestMode::Node { no_modules });

match modes.len() {
Expand Down Expand Up @@ -366,57 +385,138 @@ fn rmain(cli: Cli) -> anyhow::Result<()> {
// code.
//
// It has nothing to do with Rust.
b.debug(debug)
let bindgen_result = b
.debug(debug)
.input_module(module, wasm)
.emit_start(false)
.generate(&tmpdir_path)
.context("executing `wasm-bindgen` over the Wasm file")?;
.generate(&tmpdir_path);
shell.clear();

match test_mode {
TestMode::Node { no_modules } => {
node::execute(module, &tmpdir_path, cli, tests, !no_modules, benchmark)?
// For doctests, if wasm-bindgen fails, try a fallback that executes the raw wasm
// with stub imports. This handles doctests that use wasm-bindgen types but don't
// actually need the full wasm-bindgen runtime.
if is_doctest {
let use_fallback = bindgen_result.is_err();
if use_fallback {
log::info!(
"wasm-bindgen failed for doctest, using fallback execution: {:?}",
bindgen_result.as_ref().unwrap_err()
);
}
TestMode::Deno => deno::execute(module, &tmpdir_path, cli, tests)?,
TestMode::Browser { .. }
| TestMode::DedicatedWorker { .. }
| TestMode::SharedWorker { .. }
| TestMode::ServiceWorker { .. } => {
let srv = server::spawn(
&if headless {
"127.0.0.1:0".parse().unwrap()
} else if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
address.parse().unwrap()

match test_mode {
TestMode::Node { no_modules } => {
println!("running 1 doctest");
if use_fallback {
doctest::execute_node_fallback(&cli.file)?;
} else {
"127.0.0.1:8000".parse().unwrap()
},
headless,
module,
&tmpdir_path,
cli,
tests,
test_mode,
std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
benchmark,
)
.context("failed to spawn server")?;
let addr = srv.server_addr();

// TODO: eventually we should provide the ability to exit at some point
// (gracefully) here, but for now this just runs forever.
if !headless {
println!("Interactive browsers tests are now available at http://{addr}");
println!();
println!("Note that interactive mode is enabled because `NO_HEADLESS`");
println!("is specified in the environment of this process. Once you're");
println!("done with testing you'll need to kill this server with");
println!("Ctrl-C.");
srv.run();
return Ok(());
doctest::execute_node(module, &tmpdir_path, !no_modules)?;
}
}
TestMode::Deno => {
if use_fallback {
bail!(
"This doctest cannot be processed by wasm-bindgen. \
Deno fallback execution is not yet implemented. \
Consider adding `wasm_bindgen_test` imports to enable full support."
);
}
println!("running 1 doctest");
doctest::execute_deno(module, &tmpdir_path)?;
}
TestMode::Browser { .. }
| TestMode::DedicatedWorker { .. }
| TestMode::SharedWorker { .. }
| TestMode::ServiceWorker { .. } => {
// Browser fallback not yet implemented
if use_fallback {
bail!(
"This doctest cannot be processed by wasm-bindgen. \
Browser fallback execution is not yet implemented. \
Consider adding `wasm_bindgen_test` imports to enable full support."
);
}
println!("running 1 doctest");
let srv = server::spawn_doctest(
&if headless {
"127.0.0.1:0".parse().unwrap()
} else if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
address.parse().unwrap()
} else {
"127.0.0.1:8000".parse().unwrap()
},
headless,
module,
&tmpdir_path,
test_mode,
std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
)
.context("failed to spawn server")?;
let addr = srv.server_addr();

if !headless {
println!("Interactive doctest is now available at http://{addr}");
println!();
println!("Note that interactive mode is enabled because `NO_HEADLESS`");
println!("is specified in the environment of this process. Once you're");
println!("done with testing you'll need to kill this server with");
println!("Ctrl-C.");
srv.run();
return Ok(());
}

thread::spawn(|| srv.run());
headless::run(&addr, &shell, driver_timeout, browser_timeout)?;
thread::spawn(|| srv.run());
headless::run(&addr, &shell, driver_timeout, browser_timeout)?;
}
}
} else {
// For non-doctests, wasm-bindgen must succeed
bindgen_result.context("executing `wasm-bindgen` over the Wasm file")?;
match test_mode {
TestMode::Node { no_modules } => {
node::execute(module, &tmpdir_path, cli, tests, !no_modules, benchmark)?
}
TestMode::Deno => deno::execute(module, &tmpdir_path, cli, tests)?,
TestMode::Browser { .. }
| TestMode::DedicatedWorker { .. }
| TestMode::SharedWorker { .. }
| TestMode::ServiceWorker { .. } => {
let srv = server::spawn(
&if headless {
"127.0.0.1:0".parse().unwrap()
} else if let Ok(address) = std::env::var("WASM_BINDGEN_TEST_ADDRESS") {
address.parse().unwrap()
} else {
"127.0.0.1:8000".parse().unwrap()
},
headless,
module,
&tmpdir_path,
cli,
tests,
test_mode,
std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(),
benchmark,
)
.context("failed to spawn server")?;
let addr = srv.server_addr();

// TODO: eventually we should provide the ability to exit at some point
// (gracefully) here, but for now this just runs forever.
if !headless {
println!("Interactive browsers tests are now available at http://{addr}");
println!();
println!("Note that interactive mode is enabled because `NO_HEADLESS`");
println!("is specified in the environment of this process. Once you're");
println!("done with testing you'll need to kill this server with");
println!("Ctrl-C.");
srv.run();
return Ok(());
}

thread::spawn(|| srv.run());
headless::run(&addr, &shell, driver_timeout, browser_timeout)?;
}
}
}
Ok(())
Expand Down
Loading