diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 9603568a..1a47b46d 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -15,8 +15,9 @@ information see https://github.com/alexcrichton/wasm-bindgen. [dependencies] docopt = "1.0" failure = "0.1" +parity-wasm = "0.31" +rouille = { version = "2.1.0", default-features = false } serde = "1.0" serde_derive = "1.0" wasm-bindgen-cli-support = { path = "../cli-support", version = "=0.2.15" } wasm-bindgen-shared = { path = "../shared", version = "=0.2.15" } -parity-wasm = "0.31" diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/index.html b/crates/cli/src/bin/wasm-bindgen-test-runner/index.html new file mode 100644 index 00000000..88fad944 --- /dev/null +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/index.html @@ -0,0 +1,25 @@ + + + + + +
Loading scripts...
+ + + + diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs similarity index 50% rename from crates/cli/src/bin/wasm-bindgen-test-runner.rs rename to crates/cli/src/bin/wasm-bindgen-test-runner/main.rs index aa908d31..c316f475 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs @@ -1,17 +1,22 @@ #[macro_use] extern crate failure; -extern crate wasm_bindgen_cli_support; extern crate parity_wasm; +extern crate rouille; +extern crate wasm_bindgen_cli_support; use std::env; use std::fs; +use std::io::{self, Write}; use std::path::PathBuf; -use std::process::{self, Command}; +use std::process; use failure::{ResultExt, Error}; use parity_wasm::elements::{Module, Deserialize}; use wasm_bindgen_cli_support::Bindgen; +mod node; +mod server; + fn main() { let err = match rmain() { Ok(()) => return, @@ -53,52 +58,6 @@ fn rmain() -> Result<(), Error> { .and_then(|s| s.to_str()) .ok_or_else(|| format_err!("invalid filename passed in"))?; - let mut js_to_execute = format!(r#" - const {{ exit }} = require('process'); - - let cx = null; - - // override `console.log` and `console.error` before we import tests to - // ensure they're bound correctly in wasm. This'll allow us to intercept - // all these calls and capture the output of tests - const prev_log = console.log; - console.log = function() {{ - if (cx === null) {{ - prev_log.apply(null, arguments); - }} else {{ - cx.console_log(prev_log, arguments); - }} - }}; - const prev_error = console.error; - console.error = function() {{ - if (cx === null) {{ - prev_error.apply(null, arguments); - }} else {{ - cx.console_error(prev_error, arguments); - }} - }}; - - const support = require("./{0}"); - const wasm = require("./{0}_bg"); - - // Hack for now to support 0 tests in a binary. This should be done - // better... - if (support.Context === undefined) - process.exit(0); - - cx = new support.Context(); - - // Forward runtime arguments. These arguments are also arguments to the - // `wasm-bindgen-test-runner` which forwards them to node which we - // forward to the test harness. this is basically only used for test - // filters for now. - cx.args(process.argv.slice(2)); - - const tests = []; - "#, - module - ); - // 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. @@ -110,53 +69,41 @@ fn rmain() -> Result<(), Error> { .context("failed to read wasm file")?; let wasm = Module::deserialize(&mut &wasm[..]) .context("failed to deserialize wasm module")?; + let mut tests = Vec::new(); if let Some(exports) = wasm.export_section() { for export in exports.entries() { if !export.field().starts_with("__wbg_test") { continue } - js_to_execute.push_str(&format!("tests.push(wasm.{})\n", export.field())); + tests.push(export.field().to_string()); } } + if tests.len() == 0 { + println!("no tests to run!"); + return Ok(()) + } - // And as a final addendum, exit with a nonzero code if any tests fail. - js_to_execute.push_str("if (!cx.run(tests)) exit(1);\n"); + let node = true; + + print!("Executing bindgen ...\r"); + io::stdout().flush()?; // For now unconditionally generate wasm-bindgen code tailored for node.js, // but eventually we'll want more options here for browsers! let mut b = Bindgen::new(); b.debug(true) - .nodejs(true) - .input_module(module, wasm, |m| parity_wasm::serialize(m).unwrap()) + .nodejs(node) + .input_module(module, wasm, |w| parity_wasm::serialize(w).unwrap()) .keep_debug(false) .generate(&tmpdir) .context("executing `wasm-bindgen` over the wasm file")?; - let js_path = tmpdir.join("run.js"); - fs::write(&js_path, js_to_execute) - .context("failed to write JS file")?; + print!(" \r"); + io::stdout().flush()?; - // Last but not least, execute `node`! Add an entry to `NODE_PATH` for the - // project root to hopefully pick up `node_modules` and other local imports. - let path = env::var_os("NODE_PATH").unwrap_or_default(); - let mut paths = env::split_paths(&path).collect::>(); - paths.push(env::current_dir().unwrap()); - exec( - Command::new("node") - .env("NODE_PATH", env::join_paths(&paths).unwrap()) - .arg(&js_path) - .args(args) - ) -} + if node { + return node::execute(&module, &tmpdir, &args.collect::>(), &tests) + } -#[cfg(unix)] -fn exec(cmd: &mut Command) -> Result<(), Error> { - use std::os::unix::prelude::*; - Err(Error::from(cmd.exec()).context("failed to execute `node`").into()) -} - -#[cfg(windows)] -fn exec(cmd: &mut Command) -> Result<(), Error> { - let status = cmd.status()?; - process::exit(status.code().unwrap_or(3)); + server::spawn(&module, &tmpdir, &args.collect::>(), &tests) } diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs new file mode 100644 index 00000000..75c01d65 --- /dev/null +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs @@ -0,0 +1,100 @@ +use std::env; +use std::ffi::OsString; +use std::fs; +use std::path::Path; +use std::process::Command; + +use failure::{ResultExt, Error}; + +pub fn execute(module: &str, tmpdir: &Path, args: &[OsString], tests: &[String]) + -> Result<(), Error> +{ + let mut js_to_execute = format!(r#" + const {{ exit }} = require('process'); + + let cx = null; + + // override `console.log` and `console.error` before we import tests to + // ensure they're bound correctly in wasm. This'll allow us to intercept + // all these calls and capture the output of tests + const prev_log = console.log; + console.log = function() {{ + if (cx === null) {{ + prev_log.apply(null, arguments); + }} else {{ + cx.console_log(prev_log, arguments); + }} + }}; + const prev_error = console.error; + console.error = function() {{ + if (cx === null) {{ + prev_error.apply(null, arguments); + }} else {{ + cx.console_error(prev_error, arguments); + }} + }}; + + function main(tests) {{ + const support = require("./{0}"); + const wasm = require("./{0}_bg"); + + // Hack for now to support 0 tests in a binary. This should be done + // better... + if (support.Context === undefined) + process.exit(0); + + cx = new support.Context(); + + // Forward runtime arguments. These arguments are also arguments to the + // `wasm-bindgen-test-runner` which forwards them to node which we + // forward to the test harness. this is basically only used for test + // filters for now. + cx.args(process.argv.slice(2)); + + if (!cx.run(tests.map(n => wasm[n]))) + exit(1); + }} + + const tests = []; + "#, + module + ); + + // Note that we're collecting *JS objects* that represent the functions to + // execute, and then those objects are passed into wasm for it to execute + // when it sees fit. + for test in tests { + js_to_execute.push_str(&format!("tests.push('{}')\n", test)); + } + + // And as a final addendum, exit with a nonzero code if any tests fail. + js_to_execute.push_str(" + main(tests) + "); + + let js_path = tmpdir.join("run.js"); + fs::write(&js_path, js_to_execute) + .context("failed to write JS file")?; + + let path = env::var("NODE_PATH").unwrap_or_default(); + let mut path = env::split_paths(&path).collect::>(); + path.push(env::current_dir().unwrap()); + exec( + Command::new("node") + .env("NODE_PATH", env::join_paths(&path).unwrap()) + .arg(&js_path) + .args(args) + ) +} + +#[cfg(unix)] +fn exec(cmd: &mut Command) -> Result<(), Error> { + use std::os::unix::prelude::*; + Err(Error::from(cmd.exec()).context("failed to execute `node`").into()) +} + +#[cfg(windows)] +fn exec(cmd: &mut Command) -> Result<(), Error> { + let status = cmd.status()?; + process::exit(status.code().unwrap_or(3)); +} diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs new file mode 100644 index 00000000..419a3f7a --- /dev/null +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs @@ -0,0 +1,107 @@ +use std::ffi::OsString; +use std::path::Path; +use std::fs; + +use failure::{ResultExt, Error}; +use rouille::{self, Response, Request}; +use wasm_bindgen_cli_support::wasm2es6js::Config; + +pub fn spawn(module: &str, tmpdir: &Path, args: &[OsString], tests: &[String]) + -> Result<(), Error> +{ + let mut js_to_execute = format!(r#" + import {{ Context }} from './{0}'; + import * as wasm from './{0}_bg'; + + document.getElementById('output').innerHTML = "Loading wasm module..."; + + async function main(test) {{ + await wasm.booted; + const cx = Context.new(); + window.global_cx = cx; + + // Forward runtime arguments. These arguments are also arguments to the + // `wasm-bindgen-test-runner` which forwards them to node which we + // forward to the test harness. this is basically only used for test + // filters for now. + cx.args({1:?}); + + cx.run(test.map(s => wasm[s])); + }} + + const tests = []; + "#, + module, args, + ); + for test in tests { + js_to_execute.push_str(&format!("tests.push('{}');\n", test)); + } + js_to_execute.push_str("main(tests);\n"); + + let js_path = tmpdir.join("run.js"); + fs::write(&js_path, js_to_execute) + .context("failed to write JS file")?; + + // No browser today supports a wasm file as ES modules natively, so we need + // to shim it. Use `wasm2es6js` here to fetch an appropriate URL and look + // like an ES module with the wasm module under the hood. + let wasm_name = format!("{}_bg.wasm", module); + let wasm = fs::read(tmpdir.join(&wasm_name))?; + let output = Config::new() + .fetch(Some(format!("/{}", wasm_name))) + .generate(&wasm)?; + let js = output.js()?; + fs::write(tmpdir.join(format!("{}_bg.js", module)), js) + .context("failed to write JS file")?; + + // For now, always run forever on this port. We may update this later! + println!("Listening on port 8000"); + let tmpdir = tmpdir.to_path_buf(); + rouille::start_server("localhost:8000", move |request| { + // The root path gets our canned `index.html` + if request.url() == "/" { + return Response::from_data("text/html", include_str!("index.html")); + } + + // Otherwise we need to find the asset here. It may either be in our + // temporary directory (generated files) or in the main directory + // (relative import paths to JS). Try to find both locations. + 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"); + return response + }); + + 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 + } +}