wasm-bindgen/crates/cli/tests/reference.rs
Alex Crichton 203d86f343
Add tests for the interface types output of wasm-bindgen (#1898)
* Add tests for the interface types output of wasm-bindgen

This commit expands the test suite with assertions about the output of
the interface types pass in wasm-bindgen. The goal here is to actually
assert that we produce the right output and have a suite of reference
files to show how the interface types output is changing over time.

The `reference` test suite added in the previous PR has been updated to
work for interface types as well, generating `*.wit` file assertions
which are printed via the `wit-printer` crate on crates.io.

Along the way a number of bugs were fixed with the interface types
output, such as:

* Non-determinism in output caused by iteration of a `HashMap`

* Avoiding JS generation entirely in interface types mode, ensuring that
  we don't export extraneous intrinsics that aren't otherwise needed.

* Fixing location of the stack pointer for modules where it's GC'd out.
  It's now rooted in the aux section of wasm-bindgen so it's available
  to later passes, like the multi-value pass.

* Interface types emission now works in debug mode, meaning the
  `--release` flag is no longer required. This previously did not work
  because the `__wbindgen_throw` intrinsic was required in debug mode.
  This comes about because of the `malloc_failure` and `internal_error`
  functions in the anyref pass. The purpose of these functions is to
  signal fatal runtime errors, if any, in a way that's usable to the
  user. For wasm interface types though we can replace calls to these
  functions with `unreachable` to avoid needing to import the
  intrinsic. This has the accidental side effect of making
  `wasm_bindgen::throw_str` "just work" with wasm interface types by
  aborting the program, but that's not actually entirely intended. It's
  hoped that a split of a `wasm-bindgen-core` crate would solve this
  issue for the future.

* Run the wasm interface types validator in tests

* Add more gc roots for adapter gc

* Improve stack pointer detection

The stack pointer is never initialized to zero, but some other mutable
globals are (TLS, thread ID, etc), so let's filter those out.
2019-12-04 15:19:48 -06:00

236 lines
6.5 KiB
Rust

//! A test suite to check the reference JS and wasm output of the `wasm-bindgen`
//! library.
//!
//! This is intended as an end-to-end integration test where we can track
//! changes to the JS and wasm output.
//!
//! Tests are located in `reference/*.rs` files and are accompanied with sibling
//! `*.js` files and `*.wat` files with the expected output of the `*.rs`
//! compilation. Use `BLESS=1` in the environment to automatically update all
//! tests.
use anyhow::{bail, Result};
use assert_cmd::prelude::*;
use rayon::prelude::*;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() -> Result<()> {
let filter = env::args().nth(1);
let mut tests = Vec::new();
let dir = env::current_dir()?.join("tests/reference");
for entry in dir.read_dir()? {
let path = entry?.path();
if path.extension().and_then(|s| s.to_str()) != Some("rs") {
continue;
}
if let Some(filter) = &filter {
if !path.display().to_string().contains(filter) {
continue;
}
}
tests.push(path);
}
tests.sort();
let errs = tests
.par_iter()
.filter_map(|t| runtest(t).err().map(|e| (t, e)))
.collect::<Vec<_>>();
if errs.len() == 0 {
println!("{} tests passed", tests.len());
return Ok(());
}
eprintln!("failed tests:\n");
for (test, err) in errs {
eprintln!("{} failure\n{}", test.display(), tab(&format!("{:?}", err)));
}
bail!("tests failed");
}
fn runtest(test: &Path) -> Result<()> {
let contents = fs::read_to_string(test)?;
let td = tempfile::TempDir::new()?;
let manifest = format!(
"
[package]
name = \"reference-test\"
authors = []
version = \"1.0.0\"
edition = '2018'
[dependencies]
wasm-bindgen = {{ path = '{}' }}
[lib]
crate-type = ['cdylib']
path = '{}'
",
repo_root().display(),
test.display(),
);
let interface_types = contents.contains("// interface-types");
fs::write(td.path().join("Cargo.toml"), manifest)?;
let target_dir = target_dir();
let mut cargo = Command::new("cargo");
cargo
.current_dir(td.path())
.arg("build")
.arg("--target")
.arg("wasm32-unknown-unknown")
.env("CARGO_TARGET_DIR", &target_dir);
exec(&mut cargo)?;
let wasm = target_dir
.join("wasm32-unknown-unknown")
.join("debug")
.join("reference_test.wasm");
let mut bindgen = Command::cargo_bin("wasm-bindgen")?;
bindgen
.arg("--out-dir")
.arg(td.path())
.arg(&wasm)
.arg("--no-typescript");
if contents.contains("// enable-anyref") {
bindgen.env("WASM_BINDGEN_ANYREF", "1");
}
if interface_types {
bindgen.env("WASM_INTERFACE_TYPES", "1");
}
exec(&mut bindgen)?;
if interface_types {
let wasm = td.path().join("reference_test.wasm");
wit_validator::validate(&fs::read(&wasm)?)?;
let wit = sanitize_wasm(&wasm)?;
assert_same(&wit, &test.with_extension("wit"))?;
} else {
let js = fs::read_to_string(td.path().join("reference_test.js"))?;
assert_same(&js, &test.with_extension("js"))?;
let wat = sanitize_wasm(&td.path().join("reference_test_bg.wasm"))?;
assert_same(&wat, &test.with_extension("wat"))?;
}
Ok(())
}
fn assert_same(output: &str, expected: &Path) -> Result<()> {
if env::var("BLESS").is_ok() {
fs::write(expected, output)?;
} else {
let expected = fs::read_to_string(&expected)?;
diff(&expected, output)?;
}
Ok(())
}
fn sanitize_wasm(wasm: &Path) -> Result<String> {
// Clean up the wasm module by removing all function
// implementations/instructions, data sections, etc. This'll help us largely
// only deal with exports/imports which is all we're really interested in.
let mut module = walrus::ModuleConfig::new()
.on_parse(wit_walrus::on_parse)
.parse_file(wasm)?;
for func in module.funcs.iter_mut() {
let local = match &mut func.kind {
walrus::FunctionKind::Local(l) => l,
_ => continue,
};
local.block_mut(local.entry_block()).instrs.truncate(0);
}
let ids = module.data.iter().map(|d| d.id()).collect::<Vec<_>>();
for id in ids {
module.data.delete(id);
}
for mem in module.memories.iter_mut() {
mem.data_segments.drain();
}
let ids = module
.exports
.iter()
.filter(|e| match e.item {
walrus::ExportItem::Global(_) => true,
_ => false,
})
.map(|d| d.id())
.collect::<Vec<_>>();
for id in ids {
module.exports.delete(id);
}
walrus::passes::gc::run(&mut module);
let mut wat = wit_printer::print_bytes(&module.emit_wasm())?;
wat.push_str("\n");
Ok(wat)
}
fn diff(a: &str, b: &str) -> Result<()> {
if a == b {
return Ok(());
}
let mut s = String::new();
for result in diff::lines(a, b) {
match result {
diff::Result::Both(l, _) => {
s.push_str(" ");
s.push_str(l);
}
diff::Result::Left(l) => {
s.push_str("-");
s.push_str(l);
}
diff::Result::Right(l) => {
s.push_str("+");
s.push_str(l);
}
}
s.push_str("\n");
}
bail!("found a difference:\n\n{}", s);
}
fn target_dir() -> PathBuf {
let mut dir = PathBuf::from(env::current_exe().unwrap());
dir.pop(); // current exe
if dir.ends_with("deps") {
dir.pop();
}
dir.pop(); // debug and/or release
return dir;
}
fn repo_root() -> PathBuf {
let mut repo_root = env::current_dir().unwrap();
repo_root.pop(); // remove 'cli'
repo_root.pop(); // remove 'crates'
repo_root
}
fn exec(cmd: &mut Command) -> Result<()> {
let output = cmd.output()?;
if output.status.success() {
return Ok(());
}
let mut err = format!("command failed {:?}", cmd);
err.push_str(&format!("\nstatus: {}", output.status));
err.push_str(&format!(
"\nstderr:\n{}",
tab(&String::from_utf8_lossy(&output.stderr))
));
err.push_str(&format!(
"\nstdout:\n{}",
tab(&String::from_utf8_lossy(&output.stdout))
));
bail!("{}", err);
}
fn tab(s: &str) -> String {
format!(" {}", s.replace("\n", "\n "))
}