Add tests for the wasm-gc crate

This commit adds a test harness and the beginnings of a test suite for
the crate that performs GC over a wasm module. This crate historically
has had zero tests because it was thought that it would no longer be
used once LLD landed with `--gc-sections`, but `wasm-bindgen` has come
to rely more and more on `wasm-gc` for various purposes.

The last release of `wasm-bindgen` was also released with a bug in the
recently refactored support in the `wasm-gc` crate, providing a perfect
time and motivation to start writing some tests!

All tests added here are `*.wat` files which contain the expected output
after the gc pass is executed. Tests are automatically updated with
`BLESS_TESTS=1` in the environment, which is the expected way to
generate the output for each test.
This commit is contained in:
Alex Crichton 2018-10-29 15:15:03 -07:00
parent 6dfbb4be89
commit 91d68a0012
34 changed files with 650 additions and 13 deletions

View File

@ -167,6 +167,16 @@ matrix:
script: cargo test -p ui-tests script: cargo test -p ui-tests
if: branch = master if: branch = master
# wasm-gc tests work alright
- name: "test wasm-bindgen-gc crate"
install:
- git clone https://github.com/WebAssembly/wabt
- mkdir -p wabt/build
- (cd wabt/wabt && cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=sccache -DCMAKE_CXX_COMPILER_ARG1=c++ && cmake --build . -- -j4)
- export PATH=$PATH:`pwd`/wabt/build
script: cargo test -p wasm-bindgen-gc
if: branch = master
# Dist linux binary # Dist linux binary
- name: "dist: Linux (x86_64-unknown-linux-musl)" - name: "dist: Linux (x86_64-unknown-linux-musl)"
env: JOB=dist-linux TARGET=x86_64-unknown-linux-musl env: JOB=dist-linux TARGET=x86_64-unknown-linux-musl

View File

@ -14,3 +14,14 @@ Support for removing unused items from a wasm executable
parity-wasm = "0.35.1" parity-wasm = "0.35.1"
log = "0.4" log = "0.4"
rustc-demangle = "0.1.9" rustc-demangle = "0.1.9"
[dev-dependencies]
rayon = "1.0.2"
tempfile = "3.0.4"
[lib]
doctest = false
[[test]]
name = 'all'
harness = false

View File

@ -71,18 +71,20 @@ fn run(config: &mut Config, module: &mut Module) {
} }
} }
// Pessimistically assume all data and table segments are // Pessimistically assume all passive data and table segments are
// required // required
//
// TODO: this shouldn't assume this!
if let Some(section) = module.data_section() { if let Some(section) = module.data_section() {
for entry in section.entries() { for entry in section.entries() {
cx.add_data_segment(entry); if entry.passive() {
cx.add_data_segment(entry);
}
} }
} }
if let Some(elements) = module.elements_section() { if let Some(elements) = module.elements_section() {
for seg in elements.entries() { for (i, seg) in elements.entries().iter().enumerate() {
cx.add_element_segment(seg); if seg.passive() {
cx.add_element_segment(i as u32, seg);
}
} }
} }
@ -144,6 +146,7 @@ struct Analysis {
imports: BitSet, imports: BitSet,
exports: BitSet, exports: BitSet,
functions: BitSet, functions: BitSet,
elements: BitSet,
imported_functions: u32, imported_functions: u32,
imported_globals: u32, imported_globals: u32,
imported_memories: u32, imported_memories: u32,
@ -235,9 +238,13 @@ impl<'a> LiveContext<'a> {
// Add all element segments that initialize this table // Add all element segments that initialize this table
if let Some(elements) = self.element_section { if let Some(elements) = self.element_section {
for entry in elements.entries().iter().filter(|d| !d.passive()) { let iter = elements.entries()
.iter()
.enumerate()
.filter(|(_, d)| !d.passive());
for (i, entry) in iter {
if entry.index() == idx { if entry.index() == idx {
self.add_element_segment(entry); self.add_element_segment(i as u32, entry);
} }
} }
} }
@ -438,7 +445,10 @@ impl<'a> LiveContext<'a> {
} }
} }
fn add_element_segment(&mut self, seg: &ElementSegment) { fn add_element_segment(&mut self, idx: u32, seg: &ElementSegment) {
if !self.analysis.elements.insert(idx) {
return
}
for member in seg.members() { for member in seg.members() {
self.add_function(*member); self.add_function(*member);
} }
@ -530,8 +540,7 @@ impl<'a> RemapContext<'a> {
} }
if let Some(s) = m.table_section() { if let Some(s) = m.table_section() {
for i in 0..(s.entries().len() as u32) { for i in 0..(s.entries().len() as u32) {
// TODO: should remove `|| true` here when this is better tested if analysis.tables.contains(&(i + analysis.imported_tables)) {
if analysis.tables.contains(&(i + analysis.imported_tables)) || true {
tables.push(ntables); tables.push(ntables);
ntables += 1; ntables += 1;
} else { } else {
@ -542,8 +551,7 @@ impl<'a> RemapContext<'a> {
} }
if let Some(s) = m.memory_section() { if let Some(s) = m.memory_section() {
for i in 0..(s.entries().len() as u32) { for i in 0..(s.entries().len() as u32) {
// TODO: should remove `|| true` here when this is better tested if analysis.memories.contains(&(i + analysis.imported_memories)) {
if analysis.memories.contains(&(i + analysis.imported_memories)) || true {
memories.push(nmemories); memories.push(nmemories);
nmemories += 1; nmemories += 1;
} else { } else {
@ -713,6 +721,7 @@ impl<'a> RemapContext<'a> {
} }
fn remap_element_section(&self, s: &mut ElementSection) -> bool { fn remap_element_section(&self, s: &mut ElementSection) -> bool {
self.retain(&self.analysis.elements, s.entries_mut(), "element", 0);
for s in s.entries_mut() { for s in s.entries_mut() {
self.remap_element_segment(s); self.remap_element_segment(s);
} }

146
crates/gc/tests/all.rs Normal file
View File

@ -0,0 +1,146 @@
extern crate parity_wasm;
extern crate rayon;
extern crate tempfile;
extern crate wasm_bindgen_gc;
use std::env;
use std::error::Error;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use rayon::prelude::*;
use parity_wasm::elements::Module;
use tempfile::NamedTempFile;
struct Test {
input: PathBuf,
}
fn main() {
let mut tests = Vec::new();
find_tests(&mut tests, "tests/wat".as_ref());
run_tests(&tests);
}
fn find_tests(tests: &mut Vec<Test>, path: &Path) {
for entry in path.read_dir().unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if entry.file_type().unwrap().is_dir() {
find_tests(tests, &path);
continue
}
if path.extension().and_then(|s| s.to_str()) == Some("wat") {
tests.push(Test {
input: path,
});
}
}
}
fn run_tests(tests: &[Test]) {
println!("");
let results = tests.par_iter()
.map(|test| {
run_test(test).map_err(|e| (test, e.to_string()))
})
.collect::<Vec<_>>();
let mut bad = false;
for result in results {
let (test, err) = match result {
Ok(()) => continue,
Err(p) => p,
};
println!("fail: {} - {}", test.input.display(), err);
bad = true;
}
if bad {
std::process::exit(2);
}
println!("\nall good!");
}
fn run_test(test: &Test) -> Result<(), Box<Error>> {
println!("test {}", test.input.display());
let f = NamedTempFile::new()?;
let input = fs::read_to_string(&test.input)?;
let expected = extract_expected(&input);
let status = Command::new("wat2wasm")
.arg("--debug-names")
.arg(&test.input)
.arg("-o")
.arg(f.path())
.status()?;
if !status.success() {
return Err(io::Error::new(io::ErrorKind::Other, "failed to run wat2wasm").into())
}
let wasm = fs::read(f.path())?;
let mut module: Module = parity_wasm::deserialize_buffer(&wasm)?;
module = match module.parse_names() {
Ok(m) => m,
Err((_, m)) => m,
};
wasm_bindgen_gc::Config::new().run(&mut module);
let wasm = parity_wasm::serialize(module)?;
fs::write(f.path(), wasm)?;
let status = Command::new("wasm2wat")
.arg(&f.path())
.stderr(Stdio::inherit())
.output()?;
if !status.status.success() {
return Err(io::Error::new(io::ErrorKind::Other, "failed to run wasm2wat").into())
}
let actual = String::from_utf8(status.stdout)?;
let actual = actual.trim();
if env::var("BLESS_TESTS").is_ok() {
fs::write(&test.input, generate_blesssed(&input, &actual))?;
} else {
if actual != expected {
println!("{:?} {:?}", actual, expected);
return Err(io::Error::new(io::ErrorKind::Other,
"test failed").into())
}
}
Ok(())
}
fn extract_expected(input: &str) -> String {
input.lines()
.filter(|l| l.starts_with(";; "))
.skip_while(|l| !l.contains("STDOUT"))
.skip(1)
.take_while(|l| !l.contains("STDOUT"))
.map(|l| &l[3..])
.collect::<Vec<_>>()
.join("\n")
}
fn generate_blesssed(input: &str, actual: &str) -> String {
let mut input = input.lines()
.filter(|l| !l.starts_with(";;"))
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string();
input.push_str("\n\n");
input.push_str(";; STDOUT (update this section with `BLESS_TESTS=1` while running tests)\n");
for line in actual.lines() {
input.push_str(";; ");
input.push_str(line);
input.push_str("\n");
}
input.push_str(";; STDOUT\n");
return input
}

View File

@ -0,0 +1,11 @@
(module
(func $foo)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func $foo (type 0))
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,16 @@
(module
(func $foo
call $bar
)
(func $bar)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func $foo (type 0)
;; call $bar)
;; (func $bar (type 0))
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,21 @@
(module
(import "" "" (func (param i32)))
(func $foo
i32.const 0
call 0
)
(start $foo)
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func (param i32)))
;; (type (;1;) (func))
;; (import "" "" (func (;0;) (type 0)))
;; (func $foo (type 1)
;; i32.const 0
;; call 0)
;; (start 1))
;; STDOUT

View File

@ -0,0 +1,8 @@
(module
(import "" "a" (memory 0))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (import "" "a" (memory (;0;) 0)))
;; STDOUT

View File

@ -0,0 +1,10 @@
(module
(memory 0 1)
(data (i32.const 0) "foo")
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (memory (;0;) 0 1)
;; (data (;0;) (i32.const 0) "foo"))
;; STDOUT

View File

@ -0,0 +1,11 @@
(module
(global i32 (i32.const 0))
(export "foo" (global 0))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (global (;0;) i32 (i32.const 0))
;; (export "foo" (global 0)))
;; STDOUT

View File

@ -0,0 +1,8 @@
(module
(memory 0 17)
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (memory (;0;) 0 17))
;; STDOUT

View File

@ -0,0 +1,23 @@
(module
(global (mut i32) (i32.const 0))
(start $foo)
(func $bar)
(func $foo
i32.const 1
set_global 0
)
(func $baz)
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func $foo (type 0)
;; i32.const 1
;; set_global 0)
;; (global (;0;) (mut i32) (i32.const 0))
;; (start 0))
;; STDOUT

View File

@ -0,0 +1,14 @@
(module
(start $foo)
(func $bar)
(func $foo)
(func $baz)
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func $foo (type 0))
;; (start 0))
;; STDOUT

View File

@ -0,0 +1,10 @@
(module
(table 0 17 anyfunc)
(export "foo" (table 0))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (table (;0;) 0 17 anyfunc)
;; (export "foo" (table 0)))
;; STDOUT

View File

@ -0,0 +1,17 @@
(module
(table 0 17 anyfunc)
(func $foo
i32.const 0
call_indirect)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func $foo (type 0)
;; i32.const 0
;; call_indirect (type 0))
;; (table (;0;) 0 17 anyfunc)
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,39 @@
(module
(func $foo
(local i32 f32 i32 f64 i64 i32 f32 i64 i32 f32 f64)
get_local 0
get_local 1
get_local 2
get_local 3
get_local 4
get_local 5
get_local 6
get_local 7
get_local 8
get_local 9
get_local 10
unreachable
)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func $foo (type 0)
;; (local i32 i32 i32 i32 f32 f32 f32 f64 f64 i64 i64)
;; get_local 0
;; get_local 4
;; get_local 1
;; get_local 7
;; get_local 9
;; get_local 2
;; get_local 5
;; get_local 10
;; get_local 3
;; get_local 6
;; get_local 8
;; unreachable)
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,16 @@
(module
(func $foo (result i32)
(local i32)
get_local 0
)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func (result i32)))
;; (func $foo (type 0) (result i32)
;; (local i32)
;; get_local 0)
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,18 @@
(module
(func $foo (param i32)
(local i32)
get_local 0
set_local 1
)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func (param i32)))
;; (func $foo (type 0) (param i32)
;; (local i32)
;; get_local 0
;; set_local 1)
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,18 @@
(module
(func $foo (param i32) (result i32)
(local i32)
get_local 0
tee_local 1
)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func (param i32) (result i32)))
;; (func $foo (type 0) (param i32) (result i32)
;; (local i32)
;; get_local 0
;; tee_local 1)
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,7 @@
(module
(func $foo)
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module)
;; STDOUT

View File

@ -0,0 +1,9 @@
(module
(global i32 (i32.const 0))
(export "__heap_base" (global 0))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module)
;; STDOUT

View File

@ -0,0 +1,26 @@
(module
(import "" "a" (func $i1))
(import "" "b" (func $i2))
(import "" "c" (func $i3))
(func $bar)
(func $foo
call $i1
call $i3)
(func $baz)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (import "" "a" (func $i1 (type 0)))
;; (import "" "c" (func $i3 (type 0)))
;; (func $foo (type 0)
;; call $i1
;; call $i3)
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,7 @@
(module
(import "" "" (global i32))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module)
;; STDOUT

View File

@ -0,0 +1,35 @@
(module
(import "" "a" (global i32))
(import "" "b" (global i32))
(import "" "c" (global i32))
(global i32 (i32.const 1))
(global i32 (i32.const 2))
(func $foo
get_global 0
drop
get_global 2
drop
get_global 4
drop
)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (import "" "a" (global (;0;) i32))
;; (import "" "c" (global (;1;) i32))
;; (func $foo (type 0)
;; get_global 0
;; drop
;; get_global 1
;; drop
;; get_global 2
;; drop)
;; (global (;2;) i32 (i32.const 2))
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,11 @@
(module
(import "" "" (table 0 1 anyfunc))
(func $foo)
(elem (i32.const 1) $foo)
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module)
;; STDOUT

View File

@ -0,0 +1,13 @@
(module
(func $foo
(local i32)
)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func $foo (type 0))
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,7 @@
(module
(table 0 17 anyfunc)
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module)
;; STDOUT

View File

@ -0,0 +1,16 @@
(module
(func
call 2)
(func)
(func)
(export "foo" (func 0))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func (;0;) (type 0)
;; call 1)
;; (func (;1;) (type 0))
;; (export "foo" (func 0)))
;; STDOUT

View File

@ -0,0 +1,16 @@
(module
(global i32 (i32.const 0))
(global i32 (i32.const 0))
(func (result i32)
get_global 1)
(export "foo" (func 0))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func (result i32)))
;; (func (;0;) (type 0) (result i32)
;; get_global 0)
;; (global (;0;) i32 (i32.const 0))
;; (export "foo" (func 0)))
;; STDOUT

View File

@ -0,0 +1,16 @@
(module
(func $foo (result i32)
(local i32 i32)
get_local 1
)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func (result i32)))
;; (func $foo (type 0) (result i32)
;; (local i32)
;; get_local 0)
;; (export "foo" (func $foo)))
;; STDOUT

View File

@ -0,0 +1,13 @@
(module
(type (func (result i32)))
(type (func))
(func (type 1))
(export "foo" (func 0))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func (;0;) (type 0))
;; (export "foo" (func 0)))
;; STDOUT

View File

@ -0,0 +1,5 @@
(module)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module)
;; STDOUT

View File

@ -0,0 +1,15 @@
(module
(func $foo
i32.const 0
call_indirect
)
(func $bar)
(table 0 10 anyfunc)
(elem (i32.const 0) $bar)
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module)
;; STDOUT

View File

@ -0,0 +1,25 @@
(module
(func $foo
i32.const 0
call_indirect
)
(func $bar)
(table 0 10 anyfunc)
(elem (i32.const 0) $bar)
(export "foo" (func $foo))
)
;; STDOUT (update this section with `BLESS_TESTS=1` while running tests)
;; (module
;; (type (;0;) (func))
;; (func $foo (type 0)
;; i32.const 0
;; call_indirect (type 0))
;; (func $bar (type 0))
;; (table (;0;) 0 10 anyfunc)
;; (export "foo" (func $foo))
;; (elem (;0;) (i32.const 0) $bar))
;; STDOUT