Start supporting in-browser testing

This commit starts to add support for in-browser testing with
`wasm-bindgen-test-runner`. The current idea here is that somehow it'll be
configured and it'll spawn a little HTTP server serving up files from the
filesystem. This has been tested in various ways but isn't hooked up just yet,
wanted to make sure this was somewhat standalone! Future support for actually
running these tests will be coming in later commits.
This commit is contained in:
Alex Crichton 2018-07-24 11:27:06 -07:00
parent 7e16690f10
commit 0770f830e7
5 changed files with 259 additions and 79 deletions

View File

@ -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"

View File

@ -0,0 +1,25 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
</head>
<body>
<pre id='output'>Loading scripts...</pre>
<script>
const orig_console_log = console.log;
const orig_console_error = console.error;
console.log = function() {
if (window.global_cx)
window.global_cx.console_log(orig_console_log, arguments);
else
orig_console_log.apply(this, arguments);
};
console.error = function() {
if (window.global_cx)
window.global_cx.console_error(orig_console_error, arguments);
else
orig_console_error.apply(this, arguments);
};
</script>
<script src='run.js' type=module></script>
</body>
</html>

View File

@ -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::<Vec<_>>();
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::<Vec<_>>(), &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::<Vec<_>>(), &tests)
}

View File

@ -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::<Vec<_>>();
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));
}

View File

@ -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
}
}