Skip to content

Commit

Permalink
Add the necessary emscripten test mode.
Browse files Browse the repository at this point in the history
Because the generated .js file when targeting emscripten in wasm-bindgen is meant to be
consumed by emscripten rather than a standalone executable, we need some
custom testing logic for emscripten.
  • Loading branch information
google-yfyang committed Feb 26, 2025
1 parent c3a0e58 commit 8eb889c
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 30 deletions.
2 changes: 1 addition & 1 deletion crates/cli-support/src/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2566,7 +2566,7 @@ __wbg_set_wasm(wasm);"
real.original = state;
CLOSURE_DTORS.register(real, state, state);
return real;
}},\n
}}
",
));
}
Expand Down
55 changes: 55 additions & 0 deletions crates/cli/src/bin/wasm-bindgen-test-runner/emscripten_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
(function() {{
var elem = document.querySelector('#output');
window.extraLibraryFuncs = [];
window.addToLibrary = function(LibraryWbg) {
window.wasmExports = {__wbindgen_start:() => {}};
window.cachedTextEncoder = {encodeInto:() => {}};
window.Module = {};

try {
LibraryWbg.$initBindgen();
} catch (e) {
elem.innerText = "test setup failed: " + e;
}

function testExtraLibraryFuncs () {
['$initBindgen', '$addOnInit', '$CLOSURE_DTORS', '$getStringFromWasm0'].forEach((value) => {
if (!extraLibraryFuncs.includes(value)) {
return { status: false, e: `test result: ${value} not found`};
}
});
return {status: true, e: `test result: ok`};
}

function testLibraryWbg () {
if (typeof Module.hello !== "function") {
return {status: false, e:'test result: hello() is not found'};
}
if (typeof Module.Interval !== "function") {
return {status: false, e:'test result: Interval is not found'};
}

const keys = Object.keys(LibraryWbg);
const testNames = ['clearInterval', 'setInterval', 'log'];

for (const name of testNames) {
const regex = new RegExp(`^__wbg_${name}`);
const res = keys.find(key => regex.test(key));
if (!res) {
return {status: false, e:`test result: ${name} not found`};
}
}
return {status: true, e:'test result: ok'};
}

const tests = [testExtraLibraryFuncs(), testLibraryWbg()];
for (const res of tests) {
if (!res.status) {
elem.innerText = res.e;
return;
}
}
elem.innerText = 'test result: ok';

};
}}());
14 changes: 14 additions & 0 deletions crates/cli/src/bin/wasm-bindgen-test-runner/index-emscripten.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
</head>
<body>
<pre id="output">Loading scripts...</pre>
<pre id="console_log"></pre>
<pre id="console_info"></pre>
<pre id="console_warn"></pre>
<pre id="console_error"></pre>
<!-- {IMPORT_SCRIPTS} -->
</body>
</html>
36 changes: 33 additions & 3 deletions crates/cli/src/bin/wasm-bindgen-test-runner/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,27 @@ fn main() -> anyhow::Result<()> {

let shell = shell::Shell::new();

let file_name = cli
let mut file_name = cli
.file
.file_name()
.map(Path::new)
.context("file to test is not a valid file, can't extract file name")?;

let mut file_name_buf= PathBuf::from(cli.file.clone());

// Repoint the file to be read from "name.js" to "name.wasm" in the case of emscripten.
// Rustc generates a .js and a .wasm file when targeting emscripten. It lists the .js
// file as the primary executor which is inconsitent with what is expected here.
if file_name.extension().unwrap() == "js" {
file_name_buf.pop();
file_name_buf.push(file_name.file_stem().unwrap());
file_name_buf.set_extension("wasm");
file_name = Path::new(&file_name_buf);
}
// Collect all tests that the test harness is supposed to run. We assume
// that any exported function with the prefix `__wbg_test` is a test we need
// to execute.
let wasm = fs::read(&cli.file).context("failed to read Wasm file")?;
let wasm = fs::read(&file_name_buf).context("failed to read Wasm file")?;
let mut wasm =
walrus::Module::from_buffer(&wasm).context("failed to deserialize Wasm module")?;
let mut tests = Tests::new();
Expand Down Expand Up @@ -203,6 +214,7 @@ fn main() -> anyhow::Result<()> {
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(&0x05) => TestMode::Node { no_modules },
Some(section) if section.data.contains(&0x06) => TestMode::Emscripten {},
Some(_) => bail!("invalid __wasm_bingen_test_unstable value"),
None => {
let mut modes = Vec::new();
Expand Down Expand Up @@ -295,6 +307,9 @@ fn main() -> anyhow::Result<()> {
} else {
b.web(true)?
}
},
TestMode::Emscripten {} => {
b.emscripten(true)?
}
};

Expand All @@ -316,6 +331,19 @@ fn main() -> anyhow::Result<()> {
TestMode::Node { no_modules } => {
node::execute(module, tmpdir.path(), cli, tests, !no_modules, coverage)?
}
TestMode::Emscripten => {
let srv = server::spawn_emscripten(
&"127.0.0.1:0".parse().unwrap(),
tmpdir.path(),
std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err()).context("failed to spawn server")?;
let addr = srv.server_addr();
println!(
"Tests are now available at http://{}",
addr
);
thread::spawn(|| srv.run());
headless::run(&addr, &shell, driver_timeout, browser_timeout)?;
}
TestMode::Deno => deno::execute(module, tmpdir.path(), cli, tests)?,
TestMode::Browser { .. }
| TestMode::DedicatedWorker { .. }
Expand Down Expand Up @@ -372,6 +400,7 @@ enum TestMode {
DedicatedWorker { no_modules: bool },
SharedWorker { no_modules: bool },
ServiceWorker { no_modules: bool },
Emscripten,
}

impl TestMode {
Expand All @@ -384,7 +413,7 @@ impl TestMode {

fn no_modules(self) -> bool {
match self {
Self::Deno => true,
Self::Deno | Self::Emscripten => true,
Self::Browser { no_modules }
| Self::Node { no_modules }
| Self::DedicatedWorker { no_modules }
Expand All @@ -401,6 +430,7 @@ impl TestMode {
TestMode::DedicatedWorker { .. } => "WASM_BINDGEN_USE_DEDICATED_WORKER",
TestMode::SharedWorker { .. } => "WASM_BINDGEN_USE_SHARED_WORKER",
TestMode::ServiceWorker { .. } => "WASM_BINDGEN_USE_SERVICE_WORKER",
TestMode::Emscripten { .. } => "WASM_BINDGEN_USE_EMSCRIPTEN",
}
}
}
Expand Down
87 changes: 63 additions & 24 deletions crates/cli/src/bin/wasm-bindgen-test-runner/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,37 +353,76 @@ pub(crate) fn spawn(
response
})
.map_err(|e| anyhow!("{}", e))?;
return Ok(srv);
return Ok(srv);
}

pub(crate) fn spawn_emscripten(
addr: &SocketAddr,
tmpdir: &Path,
isolate_origin: bool,
) -> Result<Server<impl Fn(&Request) -> Response + Send + Sync>, Error> {
let js_path = tmpdir.join("run.js");
fs::write(js_path, include_str!("emscripten_test.js")).context("failed to write JS file")?;
let tmpdir = tmpdir.to_path_buf();
let srv = Server::new(addr, move |request| {
if request.url() == "/" {
let s =
include_str!("index-emscripten.html");
let s =
s.replace(
"<!-- {IMPORT_SCRIPTS} -->",
"<script src=\"run.js\"></script>\n <script src=\"library_bindgen.js\"></script>",
);

let response = Response::from_data("text/html", s);

fn try_asset(request: &Request, dir: &Path) -> Response {
let response = rouille::match_assets(request, dir);
if response.is_success() {
return response;
}

// When a browser is doing ES imports it's using the directives we
// write in the code that *don't* have file extensions (aka we say `from
// 'foo'` instead of `from 'foo.js'`. Fixup those paths here to see if a
// `js` file exists.
if let Some(part) = request.url().split('/').last() {
if !part.contains('.') {
let new_request = Request::fake_http(
request.method(),
format!("{}.js", request.url()),
request
.headers()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect(),
Vec::new(),
);
let response = rouille::match_assets(&new_request, dir);
if response.is_success() {
return response;
}
}
let mut response = try_asset(request, &tmpdir);
if !response.is_success() {
response = try_asset(request, ".".as_ref());
}
// Make sure browsers don't cache anything (Chrome appeared to with this
// header?)
response.headers.retain(|(k, _)| k != "Cache-Control");
if isolate_origin {
set_isolate_origin_headers(&mut response)
}
response
})
.map_err(|e| anyhow!("{}", e))?;
return Ok(srv);
}

fn try_asset(request: &Request, dir: &Path) -> Response {
let response = rouille::match_assets(request, dir);
if response.is_success() {
return response;
}

// When a browser is doing ES imports it's using the directives we
// write in the code that *don't* have file extensions (aka we say `from
// 'foo'` instead of `from 'foo.js'`. Fixup those paths here to see if a
// `js` file exists.
if let Some(part) = request.url().split('/').last() {
if !part.contains('.') {
let new_request = Request::fake_http(
request.method(),
format!("{}.js", request.url()),
request
.headers()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect(),
Vec::new(),
);
let response = rouille::match_assets(&new_request, dir);
if response.is_success() {
return response;
}
}
}
response
}

fn handle_coverage_dump(profraw_path: &Path, request: &Request) -> anyhow::Result<()> {
Expand Down
8 changes: 8 additions & 0 deletions crates/test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ macro_rules! wasm_bindgen_test_configure {
$crate::wasm_bindgen_test_configure!($($others)*);
};
);
(run_in_emscripten $($others:tt)*) => (
const _: () = {
#[link_section = "__wasm_bindgen_test_unstable"]
#[cfg(target_arch = "wasm32")]
pub static __WBG_TEST_run_in_emscripten: [u8; 1] = [0x06];
$crate::wasm_bindgen_test_configure!($($others)*);
};
);
() => ()
}

Expand Down
2 changes: 1 addition & 1 deletion tests/headless/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![cfg(target_arch = "wasm32")]
#![cfg(all(target_arch = "wasm32", target_os = "unknown"))]

extern crate wasm_bindgen;
extern crate wasm_bindgen_test;
Expand Down
2 changes: 1 addition & 1 deletion tests/wasm/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![cfg(target_arch = "wasm32")]
#![cfg(all(target_arch = "wasm32", target_os = "unknown"))]
#![allow(renamed_and_removed_lints)] // clippy::drop_ref will be renamed to drop_ref
#![allow(clippy::drop_ref, clippy::drop_non_drop)]

Expand Down
57 changes: 57 additions & 0 deletions tests/wasm32-emscripten/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#![cfg(all(target_arch = "wasm32", target_os = "emscripten"))]

extern crate wasm_bindgen;
extern crate wasm_bindgen_test;

use wasm_bindgen::prelude::*;
use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_emscripten);

#[wasm_bindgen]
extern "C" {
fn setInterval(closure: &Closure<dyn FnMut()>, millis: u32) -> f64;
fn clearInterval(token: f64);

#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}

#[wasm_bindgen]
pub struct Interval {
closure: Closure<dyn FnMut()>,
token: f64,
}

impl Interval {
pub fn new<F: 'static>(millis: u32, f: F) -> Interval
where
F: FnMut()
{
// Construct a new closure.
let closure = Closure::new(f);

// Pass the closure to JS, to run every n milliseconds.
let token = setInterval(&closure, millis);

Interval { closure, token }
}
}

// When the Interval is destroyed, clear its `setInterval` timer.
impl Drop for Interval {
fn drop(&mut self) {
clearInterval(self.token);
}
}

// Keep logging "hello" every second until the resulting `Interval` is dropped.
#[wasm_bindgen]
pub fn hello() -> Interval {
Interval::new(1_000, || log("hello"))
}

#[wasm_bindgen_test]
fn hello_test() {
hello();
}

0 comments on commit 8eb889c

Please sign in to comment.