use anyhow::{bail, Context};
use rayon::prelude::*;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use wast::parser::ParseBuffer;
use wast::*;
fn main() {
let tests = find_tests();
let filter = std::env::args().nth(1);
let mut tests = tests
.par_iter()
.filter_map(|test| {
if let Some(filter) = &filter {
if let Some(s) = test.file_name().and_then(|s| s.to_str()) {
if !s.contains(filter) {
return None;
}
}
}
let contents = std::fs::read_to_string(test).unwrap();
if skip_test(&test, &contents) {
None
} else {
Some((test, contents))
}
})
.collect::<Vec<_>>();
tests.sort_by_key(|p| p.1.len());
tests.reverse();
println!("running {} tests\n", tests.len());
let errors = tests
.par_iter()
.filter_map(|(test, contents)| run_test(test, contents).err())
.collect::<Vec<_>>();
if !errors.is_empty() {
for msg in errors.iter() {
eprintln!("{:?}", msg);
}
panic!("{} tests failed", errors.len())
}
println!("test result: ok. {} passed\n", tests.len());
}
fn find_tests() -> Vec<PathBuf> {
let mut tests = Vec::new();
if !Path::new("tests/wabt").exists() {
panic!("submodules need to be checked out");
}
find_tests("tests/wabt/test/desugar".as_ref(), &mut tests);
find_tests("tests/wabt/test/dump".as_ref(), &mut tests);
find_tests("tests/wabt/test/interp".as_ref(), &mut tests);
find_tests("tests/wabt/test/parse".as_ref(), &mut tests);
find_tests("tests/wabt/test/roundtrip".as_ref(), &mut tests);
find_tests("tests/wabt/test/spec".as_ref(), &mut tests);
find_tests("tests/wabt/test/typecheck".as_ref(), &mut tests);
find_tests("tests/wabt/third_party/testsuite".as_ref(), &mut tests);
find_tests("tests/regression".as_ref(), &mut tests);
tests.sort();
return tests;
fn find_tests(path: &Path, tests: &mut Vec<PathBuf>) {
for f in path.read_dir().unwrap() {
let f = f.unwrap();
if f.file_type().unwrap().is_dir() {
find_tests(&f.path(), tests);
continue;
}
match f.path().extension().and_then(|s| s.to_str()) {
Some("txt") | Some("wast") | Some("wat") => {}
_ => continue,
}
tests.push(f.path());
}
}
}
fn skip_test(test: &Path, contents: &str) -> bool {
if contents.contains(";; ERROR") {
return true;
}
if contents.contains("STDIN_FILE") {
return true;
}
if contents.contains("--enable-exceptions") || test.ends_with("all-features.txt") {
return true;
}
if contents.contains("--enable-tail-call") {
return true;
}
if contents.contains("--enable-annotations") {
return true;
}
if contents.contains("run-objdump") && contents.contains("(event") {
return true;
}
if test.ends_with("threads/atomic.wast") {
return true;
}
if test.ends_with("interp/simd-load-store.txt") {
return true;
}
if test.ends_with("logging-all-opcodes.txt") {
return true;
}
if test.ends_with("simd/simd_lane.wast")
|| test.ends_with("simd/simd_load.wast")
|| test.ends_with("simd/simd_conversions.wast")
{
return true;
}
if test.iter().any(|t| t == "tail-call") {
return true;
}
false
}
fn skip_wabt_compare(test: &Path, line: usize) -> bool {
if (test.ends_with("reference-types/select.wast") && line == 1)
|| (test.ends_with("reference-types/table_get.wast") && line == 1)
|| (test.ends_with("reference-types/table_set.wast") && line == 1)
|| (test.ends_with("reference-types/ref_is_null.wast") && line == 1)
|| test.ends_with("dump/table-multi.txt")
|| test.ends_with("dump/reference-types.txt")
{
return true;
}
if (test.ends_with("bulk-memory-operations/binary.wast") && line == 776)
|| (test.ends_with("reference-types/binary.wast") && line == 1083)
{
return true;
}
if test.ends_with("reference-types/linking.wast") {
return true;
}
if test.iter().any(|t| t == "simd") {
return true;
}
if let Some(name) = test.file_name().and_then(|s| s.to_str()) {
if name.starts_with("invalid-elem-segment") || name.starts_with("invalid-data-segment") {
return true;
}
}
false
}
fn run_test(test: &Path, contents: &str) -> anyhow::Result<()> {
let wast = contents.contains("TOOL: wast2json")
|| contents.contains("TOOL: run-objdump-spec")
|| test.display().to_string().ends_with(".wast");
if wast {
test_wast(test, contents)
} else {
let binary = wat::parse_file(test)?;
test_binary(test, 0, &binary)
}
}
fn test_wast(test: &Path, contents: &str) -> anyhow::Result<()> {
macro_rules! adjust {
($e:expr) => {{
let mut e = wast::Error::from($e);
e.set_path(test);
e.set_text(contents);
e
}};
}
let buf = ParseBuffer::new(contents).map_err(|e| adjust!(e))?;
let wast = parser::parse::<Wast>(&buf).map_err(|e| adjust!(e))?;
let results = wast
.directives
.into_par_iter()
.map(|directive| -> anyhow::Result<()> {
match directive {
WastDirective::Module(mut module)
| WastDirective::AssertUnlinkable { mut module, .. } => {
let binary = module.encode().map_err(|e| adjust!(e))?;
let (line, col) = module.span.linecol_in(contents);
let context = format!(
"failed for module at {}:{}:{}",
test.display(),
line + 1,
col + 1,
);
test_binary(test, line + 1, &binary).context(context)?;
}
_ => {}
}
Ok(())
})
.collect::<Vec<_>>();
let errors = results
.into_iter()
.filter_map(|e| e.err())
.collect::<Vec<_>>();
if errors.is_empty() {
return Ok(());
}
let mut s = format!("{} test failures in {}:", errors.len(), test.display());
for error in errors {
s.push_str("\n\t");
s.push_str(&format!("{:?}", error).replace("\n", "\n\t"));
}
bail!("{}", s)
}
fn test_binary(path: &Path, line: usize, contents: &[u8]) -> anyhow::Result<()> {
let actual = wasmprinter::print_bytes(contents)
.context(format!("rust failed to print `{}`", path.display()))?;
if skip_wabt_compare(path, line) {
return Ok(());
}
let expected =
wasm2wat(contents).context(format!("failed to run `wasm2wat` on `{}`", path.display()))?;
let actual = normalize(&actual);
let mut expected = normalize(&expected);
let needle = "ref.func\n";
while let Some(i) = expected.find(needle) {
let len = expected[i + needle.len()..]
.chars()
.take_while(|c| c.is_whitespace())
.count();
expected.drain(i + needle.len() - 1..i + needle.len() - 1 + len);
}
let expected = expected
.lines()
.map(|l| l.trim_end())
.collect::<Vec<_>>()
.join("\n")
.replace(" )", ")");
fn normalize(s: &str) -> String {
let mut s = s.trim().to_string();
while let Some(i) = s.find(" (;=") {
let end = s[i..].find(";)").unwrap();
s.drain(i..end + i + 2);
}
return s;
}
let mut bad = false;
let mut result = String::new();
for diff in diff::lines(&expected, &actual) {
match diff {
diff::Result::Left(s) => {
bad = true;
result.push_str("-");
result.push_str(s);
}
diff::Result::Right(s) => {
bad = true;
result.push_str("+");
result.push_str(s);
}
diff::Result::Both(s, _) => {
result.push_str(" ");
result.push_str(s);
}
}
result.push_str("\n");
}
if bad {
bail!(
"expected != actual for test `{}`\n\n{}",
path.display(),
result
);
} else {
Ok(())
}
}
fn wasm2wat(contents: &[u8]) -> anyhow::Result<String> {
let f = tempfile::TempDir::new().unwrap();
let wasm = f.path().join("wasm");
let wat = f.path().join("wat");
fs::write(&wasm, contents).context("failed to write wasm file")?;
let result = Command::new("wasm2wat")
.arg(&wasm)
.arg("--enable-all")
.arg("--no-check")
.arg("-o")
.arg(&wat)
.output()
.expect("failed to spawn `wasm2wat`");
if result.status.success() {
fs::read_to_string(&wat).context("failed to read wat file")
} else {
bail!(
"failed to run wasm2wat: {}\n\n {}",
result.status,
String::from_utf8_lossy(&result.stderr).replace("\n", "\n "),
)
}
}