Improved test generation

This commit is contained in:
Syrus 2020-04-14 11:41:12 -07:00
parent c2306bd39e
commit a7dba54b7f
15 changed files with 1018 additions and 1558 deletions

40
Cargo.lock generated
View File

@ -2258,6 +2258,14 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "test-generator"
version = "0.1.0"
dependencies = [
"anyhow",
"target-lexicon",
]
[[package]]
name = "textwrap"
version = "0.11.0"
@ -2269,18 +2277,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.11"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee14bf8e6767ab4c687c9e8bc003879e042a96fd67a3ba5934eadb6536bef4db"
checksum = "54b3d3d2ff68104100ab257bb6bb0cb26c901abe4bd4ba15961f3bf867924012"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.11"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7b51e1fbc44b5a0840be594fbc0f960be09050f2617e61e6aa43bef97cd3ef4"
checksum = "ca972988113b7715266f91250ddb98070d033c62a011fa0fcc57434a649310dd"
dependencies = [
"proc-macro2 1.0.9",
"quote 1.0.2",
@ -2746,6 +2754,7 @@ dependencies = [
name = "wasmer-bin"
version = "0.16.2"
dependencies = [
"anyhow",
"atty",
"byteorder",
"criterion",
@ -2759,6 +2768,7 @@ dependencies = [
"rustc_version",
"serde",
"structopt",
"test-generator",
"typetag",
"wabt",
"wasmer",
@ -2772,6 +2782,7 @@ dependencies = [
"wasmer-singlepass-backend",
"wasmer-wasi",
"wasmer-wasi-experimental-io-devices",
"wasmer-wast",
]
[[package]]
@ -2844,7 +2855,7 @@ version = "0.16.2"
dependencies = [
"nom",
"serde",
"wast",
"wast 8.0.0",
]
[[package]]
@ -2987,6 +2998,16 @@ dependencies = [
"wasmer-wasi",
]
[[package]]
name = "wasmer-wast"
version = "0.16.2"
dependencies = [
"anyhow",
"thiserror",
"wasmer",
"wast 9.0.0",
]
[[package]]
name = "wasmer-win-exception-handler"
version = "0.16.2"
@ -3012,6 +3033,15 @@ dependencies = [
"leb128",
]
[[package]]
name = "wast"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee7b16105405ca2aa2376ba522d8d4b1a11604941dd3bb7df9fd2ece60f8d16a"
dependencies = [
"leb128",
]
[[package]]
name = "web-sys"
version = "0.3.36"

View File

@ -59,18 +59,23 @@ members = [
"examples/parallel",
"examples/plugin-for-example",
"examples/parallel-guest",
"tests/test-generator",
"tests/generate-wasi-tests",
"tests/generate-emscripten-tests",
"tests/wast",
]
[build-dependencies]
wabt = "0.9.1"
anyhow = "1.0.19"
generate-emscripten-tests = { path = "tests/generate-emscripten-tests" }
generate-wasi-tests = { path = "tests/generate-wasi-tests" }
test-generator = { path = "tests/test-generator" }
glob = "0.3"
rustc_version = "0.2"
[dev-dependencies]
anyhow = "1.0.19"
wasmer-wast = { path = "tests/wast" }
criterion = "0.3"
glob = "0.3"
libc = "0.2.60" # for `tests/dev-utils`'s Stdout capturing

View File

@ -35,17 +35,17 @@ generate: generate-emtests generate-wasitests
# Spectests
spectests-singlepass:
WASMER_TEST_SINGLEPASS=1 cargo test test_run_spectests --release --no-default-features --features "wasi backend-singlepass" -- --nocapture --test-threads 1
WASMER_TEST_SINGLEPASS=1 cargo test singlepass::spec --release --no-default-features --features "wasi backend-singlepass"
spectests-cranelift:
WASMER_TEST_CRANELFIT=1 cargo test test_run_spectests --release --no-default-features --features "wasi backend-cranelift" -- --nocapture
WASMER_TEST_CRANELFIT=1 cargo test cranelift::spec --release --no-default-features --features "wasi backend-cranelift"
spectests-llvm:
WASMER_TEST_LLVM=1 cargo test test_run_spectests --release --no-default-features --features "wasi backend-llvm wasmer-llvm-backend/test" -- --nocapture
WASMER_TEST_LLVM=1 cargo test llvm::spec --release --no-default-features --features "wasi backend-llvm wasmer-llvm-backend/test"
spectests-all:
WASMER_TEST_CRANELIFT=1 WASMER_TEST_LLVM=1 WASMER_TEST_SINGLEPASS=1 \
cargo test test_run_spectests --release --no-default-features --features "wasi backend-cranelift backend-singlepass backend-llvm wasmer-llvm-backend/test" -- --nocapture --test-threads 1
cargo test spec --release --no-default-features --features "wasi backend-cranelift backend-singlepass backend-llvm wasmer-llvm-backend/test"
spectests: spectests-singlepass spectests-cranelift spectests-llvm

102
build.rs
View File

@ -4,8 +4,108 @@
use generate_emscripten_tests;
use generate_wasi_tests;
use std::env;
use std::fmt::Write;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use test_generator::{
build_ignores_from_textfile, extract_name, test_directory, test_directory_module,
with_test_module, Test, Testsuite,
};
/// Given a Testsuite and a path, process the path in case is a wast
/// file.
fn wast_processor(out: &mut Testsuite, p: PathBuf) -> Option<Test> {
let ext = p.extension()?;
// Only look at wast files.
if ext != "wast" {
return None;
}
// Ignore files starting with `.`, which could be editor temporary files
if p.file_stem()?.to_str()?.starts_with(".") {
return None;
}
let testname = extract_name(&p);
let body = format!(
"crate::run_wast(r#\"{}\"#, \"{}\")",
p.display(),
out.path.get(0).unwrap()
);
Some(Test {
name: testname.to_string(),
body: body.to_string(),
})
}
fn main() -> anyhow::Result<()> {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=test/ignores.txt");
fn main() {
generate_wasi_tests::build();
generate_emscripten_tests::build();
let out_dir = PathBuf::from(
env::var_os("OUT_DIR").expect("The OUT_DIR environment variable must be set"),
);
let ignores = build_ignores_from_textfile("tests/ignores.txt".into())?;
let mut out = Testsuite {
buffer: String::new(),
path: vec![],
ignores: ignores,
};
for compiler in &["singlepass", "cranelift", "llvm"] {
writeln!(out.buffer, "#[cfg(feature=\"backend-{}\")]", compiler);
writeln!(out.buffer, "#[cfg(test)]")?;
writeln!(out.buffer, "#[allow(non_snake_case)]")?;
with_test_module(&mut out, compiler, |mut out| {
with_test_module(&mut out, "spec", |out| {
let spec_tests = test_directory(out, "tests/spectests", wast_processor)?;
// Skip running spec_testsuite tests if the submodule isn't checked
// out.
// if spec_tests > 0 {
// test_directory_module(
// out,
// "tests/spec_testsuite/proposals/simd",
// wast_processor,
// )?;
// test_directory_module(
// out,
// "tests/spec_testsuite/proposals/multi-value",
// wast_processor,
// )?;
// test_directory_module(
// out,
// "tests/spec_testsuite/proposals/reference-types",
// wast_processor,
// )?;
// test_directory_module(
// out,
// "tests/spec_testsuite/proposals/bulk-memory-operations",
// wast_processor,
// )?;
// } else {
// println!(
// "cargo:warning=The spec testsuite is disabled. To enable, run `git submodule \
// update --remote`."
// );
// }
Ok(())
})?;
Ok(())
})?;
}
// println!("{}", out.buffer);
// std::process::exit(1);
// Write out our auto-generated tests and opportunistically format them with
// `rustfmt` if it's installed.
let output = out_dir.join("generated_tests.rs");
fs::write(&output, out.buffer)?;
drop(Command::new("rustfmt").arg(&output).status());
Ok(())
}

View File

@ -178,6 +178,11 @@ impl ImportObject {
}
out
}
/// Returns true if the ImportObject contains namespace with the provided name.
pub fn contains_namespace(&self, name: &str) -> bool {
self.map.lock().unwrap().borrow().contains_key(name)
}
}
/// Iterator for an `ImportObject`'s exports.

View File

@ -1,2 +1,2 @@
pub mod utils;
mod emtests;
pub mod utils;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
[package]
name = "test-generator"
version = "0.1.0"
edition = "2018"
[dependencies]
anyhow = "1.0.19"
target-lexicon = "0.10.0"

View File

@ -0,0 +1,162 @@
//! Build library to generate a program which runs all the testsuites.
//!
//! By generating a separate `#[test]` test for each file, we allow cargo test
//! to automatically run the files in parallel.
//!
//! > This program is inspired/forked from:
//! > https://github.com/bytecodealliance/wasmtime/blob/master/build.rs
use anyhow::Context;
use std::collections::HashSet;
use std::fmt::Write;
use std::fs::{DirEntry, File};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use target_lexicon::Triple;
pub type Ignores = HashSet<String>;
pub struct Testsuite {
pub buffer: String,
pub path: Vec<String>,
pub ignores: Ignores,
}
impl Testsuite {
fn ignore_current(&self) -> bool {
let full = self.path.join("::");
self.ignores.contains(&full)
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct Test {
pub name: String,
pub body: String,
}
pub type ProcessorType = fn(&mut Testsuite, PathBuf) -> Option<Test>;
/// Generates an Ignores struct from a text file
pub fn build_ignores_from_textfile(path: PathBuf) -> anyhow::Result<Ignores> {
let mut ignores = HashSet::new();
let file = File::open(path)?;
let reader = BufReader::new(file);
let host = Triple::host().to_string();
for line in reader.lines() {
let line = line.unwrap();
// If the line has a `#` we discard all the content that comes after
let line = if line.contains("#") {
let l: Vec<&str> = line.split('#').collect();
l.get(0).unwrap().to_string()
} else {
line
};
// If the lines contains ` on ` it means the test should be ignored
// on that platform
let (line, target) = if line.contains(" on ") {
let l: Vec<&str> = line.split(" on ").collect();
(
l.get(0).unwrap().to_string(),
Some(l.get(1).unwrap().to_string()),
)
} else {
(line, None)
};
let line = line.trim().to_string();
if line.len() == 0 {
continue;
}
match target {
Some(t) => {
// We skip the ignore if doesn't apply to the current
// host target
if !host.contains(&t) {
continue;
}
}
None => {}
}
ignores.insert(line);
}
Ok(ignores)
}
pub fn test_directory_module(
out: &mut Testsuite,
path: impl AsRef<Path>,
processor: ProcessorType,
) -> anyhow::Result<usize> {
let path = path.as_ref();
let testsuite = &extract_name(path);
with_test_module(out, testsuite, |out| test_directory(out, path, processor))
}
fn write_test(out: &mut Testsuite, testname: &str, body: &str) -> anyhow::Result<()> {
writeln!(out.buffer, "#[test]")?;
if out.ignore_current() {
writeln!(out.buffer, "#[ignore]")?;
}
writeln!(out.buffer, "fn r#{}() -> anyhow::Result<()> {{", &testname)?;
writeln!(out.buffer, "{}", body)?;
writeln!(out.buffer, "}}")?;
writeln!(out.buffer)?;
Ok(())
}
pub fn test_directory(
out: &mut Testsuite,
path: impl AsRef<Path>,
processor: ProcessorType,
) -> anyhow::Result<usize> {
let path = path.as_ref();
let mut dir_entries: Vec<_> = path
.read_dir()
.context(format!("failed to read {:?}", path))?
.map(|r| r.expect("reading testsuite directory entry"))
.filter_map(|dir_entry| processor(out, dir_entry.path()))
.collect();
dir_entries.sort();
for Test {
name: testname,
body,
} in dir_entries.iter()
{
out.path.push(testname.to_string());
write_test(out, &testname, &body).unwrap();
out.path.pop().unwrap();
}
Ok(dir_entries.len())
}
/// Extract a valid Rust identifier from the stem of a path.
pub fn extract_name(path: impl AsRef<Path>) -> String {
path.as_ref()
.file_stem()
.expect("filename should have a stem")
.to_str()
.expect("filename should be representable as a string")
.replace("-", "_")
.replace("/", "_")
}
pub fn with_test_module<T>(
out: &mut Testsuite,
testsuite: &str,
f: impl FnOnce(&mut Testsuite) -> anyhow::Result<T>,
) -> anyhow::Result<T> {
out.path.push(testsuite.to_string());
out.buffer.push_str("mod ");
out.buffer.push_str(testsuite);
out.buffer.push_str(" {\n");
let result = f(out)?;
out.buffer.push_str("}\n");
out.path.pop().unwrap();
Ok(result)
}

17
tests/wast/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "wasmer-wast"
version = "0.16.2"
authors = ["Wasmer Engineering Team <engineering@wasmer.io>"]
description = "wast testing support for wasmer"
license = "MIT OR (Apache-2.0 WITH LLVM-exception)"
categories = ["wasm"]
keywords = ["webassembly", "wasm"]
repository = "https://github.com/wasmerio/wasmer"
readme = "README.md"
edition = "2018"
[dependencies]
anyhow = "1.0.19"
wasmer = { path = "../../lib/api", version = "0.16.2" }
wast = "9.0.0"
thiserror = "1.0.15"

7
tests/wast/README.md Normal file
View File

@ -0,0 +1,7 @@
This is the `wasmer-wast` crate, which contains an implementation of WebAssembly's
"wast" test scripting language, which is used in the
[WebAssembly spec testsuite], using wasmer for execution.
[WebAssembly spec testsuite]: https://github.com/WebAssembly/testsuite
> Note: this project started as a fork of [this crate](https://crates.io/crates/wasmtime-wast).

50
tests/wast/src/errors.rs Normal file
View File

@ -0,0 +1,50 @@
use std::fmt;
use thiserror::Error;
/// A call Error
#[derive(Error, Debug)]
pub struct CallError {
/// The failing message received when running the directive
pub message: String,
}
impl fmt::Display for CallError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{}", self.message)
}
}
/// A Directive Error
#[derive(Debug)]
pub struct DirectiveError {
/// The line where the directive is defined
pub line: usize,
/// The column where the directive is defined
pub col: usize,
/// The failing message received when running the directive
pub message: String,
}
/// A structure holding the list of all executed directives
#[derive(Error, Debug)]
pub struct DirectiveErrors {
/// The filename where the error occured
pub filename: String,
/// The list of errors
pub errors: Vec<DirectiveError>,
}
impl fmt::Display for DirectiveErrors {
// This trait requires `fmt` with this exact signature.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Write strictly the first element into the supplied output
// stream: `f`. Returns `fmt::Result` which indicates whether the
// operation succeeded or failed. Note that `write!` uses syntax which
// is very similar to `println!`.
writeln!(f, "Failed directives on {}:", self.filename)?;
for error in self.errors.iter() {
writeln!(f, " • {} ({}:{})", error.message, error.line, error.col)?;
}
Ok(())
}
}

34
tests/wast/src/lib.rs Normal file
View File

@ -0,0 +1,34 @@
//! Implementation of the WAST text format for wasmer.
#![deny(missing_docs, trivial_numeric_casts, unused_extern_crates)]
#![warn(unused_import_braces)]
#![deny(unstable_features)]
#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))]
#![cfg_attr(
feature = "cargo-clippy",
allow(clippy::new_without_default, clippy::new_without_default_derive)
)]
#![cfg_attr(
feature = "cargo-clippy",
warn(
clippy::float_arithmetic,
clippy::mut_mut,
clippy::nonminimal_bool,
clippy::option_map_unwrap_or,
clippy::option_map_unwrap_or_else,
clippy::print_stdout,
clippy::unicode_not_nfc,
clippy::use_self
)
)]
mod errors;
mod spectest;
mod wast;
pub use crate::errors::{DirectiveError, DirectiveErrors};
pub use crate::spectest::spectest_importobject;
pub use crate::wast::Wast;
/// Version number of this crate.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

View File

@ -0,0 +1,56 @@
use wasmer::import::ImportObject;
use wasmer::types::ElementType;
use wasmer::units::Pages;
use wasmer::wasm::{Func, Global, Memory, MemoryDescriptor, Table, TableDescriptor, Value};
use wasmer::*;
/// Return an instance implementing the "spectest" interface used in the
/// spec testsuite.
pub fn spectest_importobject() -> ImportObject {
let print = Func::new(|| {});
let print_i32 = Func::new(|val: i32| println!("{}: i32", val));
let print_i64 = Func::new(|val: i64| println!("{}: i64", val));
let print_f32 = Func::new(|val: f32| println!("{}: f32", val));
let print_f64 = Func::new(|val: f64| println!("{}: f64", val));
let print_i32_f32 = Func::new(|i: i32, f: f32| {
println!("{}: i32", i);
println!("{}: f32", f);
});
let print_f64_f64 = Func::new(|f1: f64, f2: f64| {
println!("{}: f64", f1);
println!("{}: f64", f2);
});
let global_i32 = Global::new(Value::I32(666));
let global_i64 = Global::new(Value::I64(666));
let global_f32 = Global::new(Value::F32(f32::from_bits(0x4426_8000)));
let global_f64 = Global::new(Value::F64(f64::from_bits(0x4084_d000_0000_0000)));
let memory_desc = MemoryDescriptor::new(Pages(1), Some(Pages(2)), false).unwrap();
let memory = Memory::new(memory_desc).unwrap();
let table = Table::new(TableDescriptor {
element: ElementType::Anyfunc,
minimum: 10,
maximum: Some(20),
})
.unwrap();
imports! {
"spectest" => {
"print" => print,
"print_i32" => print_i32,
"print_i64" => print_i64,
"print_f32" => print_f32,
"print_f64" => print_f64,
"print_i32_f32" => print_i32_f32,
"print_f64_f64" => print_f64_f64,
"global_i32" => global_i32,
"global_i64" => global_i64,
"global_f32" => global_f32,
"global_f64" => global_f64,
"table" => table,
"memory" => memory,
},
}
}

506
tests/wast/src/wast.rs Normal file
View File

@ -0,0 +1,506 @@
use crate::errors::{CallError, DirectiveError, DirectiveErrors};
use crate::spectest::spectest_importobject;
use anyhow::{anyhow, bail, Context as _, Result};
use std::borrow::Borrow;
use std::collections::HashMap;
use std::path::Path;
use std::str;
use std::sync::{Arc, Mutex};
use wasmer::compiler::{compile_with_config_with, compiler_for_backend, Backend, CompilerConfig};
use wasmer::import::ImportObject;
use wasmer::wasm::Value;
use wasmer::wasm::{DynFunc, Features, Global};
use wasmer::*;
/// The wast test script language allows modules to be defined and actions
/// to be performed on them.
pub struct Wast {
/// Wast files have a concept of a "current" module, which is the most
/// recently defined.
current: Option<Arc<Mutex<Instance>>>,
/// The Import Object that all wast tests will have
import_object: ImportObject,
/// The instances in the test
instances: HashMap<String, Arc<Mutex<Instance>>>,
/// The Wasmer backend used
backend: Backend,
/// A flag indicating if Wast tests should stop as soon as one test fails.
pub fail_fast: bool,
}
impl Wast {
/// Construct a new instance of `Wast` with a given imports.
pub fn new(import_object: ImportObject, backend: Backend) -> Self {
Self {
current: None,
backend,
import_object,
instances: HashMap::new(),
fail_fast: false,
}
}
/// Construcet a new instance of `Wast` with the spectests imports.
pub fn new_with_spectest(backend: Backend) -> Self {
let import_object = spectest_importobject();
Self::new(import_object, backend)
}
fn get_instance(&self, instance_name: Option<&str>) -> Result<Arc<Mutex<Instance>>> {
match instance_name {
Some(name) => self
.instances
.get(name)
.as_ref()
.map(|x| Arc::clone(x))
.ok_or_else(|| anyhow!("failed to find instance named `{}`", name)),
None => self
.current
.as_ref()
.map(|x| Arc::clone(x))
.ok_or_else(|| anyhow!("no previous instance found")),
}
}
/// Perform the action portion of a command.
fn perform_execute(&mut self, exec: wast::WastExecute<'_>) -> Result<Vec<Value>> {
match exec {
wast::WastExecute::Invoke(invoke) => self.perform_invoke(invoke),
wast::WastExecute::Module(mut module) => {
let binary = module.encode()?;
let result = self.instantiate(&binary);
match result {
Ok(_) => Ok(Vec::new()),
Err(e) => Err(e),
}
}
wast::WastExecute::Get { module, global } => self.get(module.map(|s| s.name()), global),
}
}
fn perform_invoke(&mut self, exec: wast::WastInvoke<'_>) -> Result<Vec<Value>> {
let values = exec
.args
.iter()
.map(Self::runtime_value)
.collect::<Result<Vec<_>>>()?;
self.invoke(exec.module.map(|i| i.name()), exec.name, &values)
}
fn assert_return(
&self,
result: Result<Vec<Value>>,
results: &[wast::AssertExpression],
) -> Result<()> {
let values = result?;
for (v, e) in values.iter().zip(results) {
if val_matches(v, e)? {
continue;
}
bail!("expected {:?}, got {:?}", e, v)
}
Ok(())
}
fn assert_trap(&self, result: Result<Vec<Value>>, expected: &str) -> Result<()> {
let actual = match result {
Ok(values) => bail!("expected trap, got {:?}", values),
Err(t) => format!("{}", t),
};
if Self::matches_message_assert_trap(expected, &actual) {
return Ok(());
}
bail!("expected '{}', got '{}'", expected, actual)
}
fn run_directive(&mut self, directive: wast::WastDirective) -> Result<()> {
use wast::WastDirective::*;
match directive {
Module(mut module) => {
let binary = module.encode()?;
self.module(module.id.map(|s| s.name()), &binary)?;
}
Register {
span: _,
name,
module,
} => {
self.register(module.map(|s| s.name()), name)?;
}
Invoke(i) => {
self.perform_invoke(i)?;
}
AssertReturn {
span: _,
exec,
results,
} => {
let result = self.perform_execute(exec);
self.assert_return(result, &results)?;
}
AssertTrap {
span: _,
exec,
message,
} => {
let result = self.perform_execute(exec);
self.assert_trap(result, message)?;
}
AssertExhaustion {
span: _,
call,
message,
} => {
let result = self.perform_invoke(call);
self.assert_trap(result, message)?;
}
AssertInvalid {
span: _,
mut module,
message,
} => {
let bytes = module.encode()?;
let err = match self.module(None, &bytes) {
Ok(()) => bail!("expected module to fail to build"),
Err(e) => e,
};
let error_message = format!("{:?}", err);
if !Self::matches_message_assert_invalid(&message, &error_message) {
bail!(
"assert_invalid: expected \"{}\", got \"{}\"",
message,
error_message
)
}
}
AssertMalformed {
module,
span: _,
message: _,
} => {
let mut module = match module {
wast::QuoteModule::Module(m) => m,
// This is a `*.wat` parser test which we're not
// interested in.
wast::QuoteModule::Quote(_) => return Ok(()),
};
let bytes = module.encode()?;
if let Ok(_) = self.module(None, &bytes) {
bail!("expected malformed module to fail to instantiate");
}
}
AssertUnlinkable {
span: _,
mut module,
message,
} => {
let bytes = module.encode()?;
let err = match self.module(None, &bytes) {
Ok(()) => bail!("expected module to fail to link"),
Err(e) => e,
};
let error_message = format!("{:?}", err);
if !Self::matches_message_assert_unlinkable(&message, &error_message) {
bail!(
"assert_unlinkable: expected {}, got {}",
message,
error_message
)
}
}
}
Ok(())
}
/// Run a wast script from a byte buffer.
pub fn run_buffer(&mut self, filename: &str, wast: &[u8]) -> Result<()> {
let wast = str::from_utf8(wast)?;
let adjust_wast = |mut err: wast::Error| {
err.set_path(filename.as_ref());
err.set_text(wast);
err
};
let buf = wast::parser::ParseBuffer::new(wast).map_err(adjust_wast)?;
let ast = wast::parser::parse::<wast::Wast>(&buf).map_err(adjust_wast)?;
let mut errors = Vec::with_capacity(ast.directives.len());
for directive in ast.directives {
let sp = directive.span();
match self.run_directive(directive) {
Err(e) => {
let (line, col) = sp.linecol_in(wast);
errors.push(DirectiveError {
line: line + 1,
col,
message: format!("{}", e),
});
if self.fail_fast {
break;
}
}
Ok(_) => {}
}
}
if !errors.is_empty() {
return Err(DirectiveErrors {
filename: filename.to_string(),
errors,
}
.into());
}
Ok(())
}
/// Run a wast script from a file.
pub fn run_file(&mut self, path: &Path) -> Result<()> {
let bytes =
std::fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))?;
self.run_buffer(path.to_str().unwrap(), &bytes)
}
}
// This is the implementation specific to the Runtime
impl Wast {
/// Define a module and register it.
fn module(&mut self, instance_name: Option<&str>, module: &[u8]) -> Result<()> {
let instance = match self.instantiate(module) {
Ok(i) => i,
Err(e) => bail!("instantiation failed with: {}", e),
};
let instance = Arc::new(Mutex::new(instance));
if let Some(name) = instance_name {
self.instances.insert(name.to_string(), instance.clone());
}
self.current = Some(instance.clone());
Ok(())
}
fn instantiate(&self, module: &[u8]) -> Result<Instance> {
// let module = Module::new(module)?;
let config = CompilerConfig {
features: Features {
simd: true,
threads: true,
},
nan_canonicalization: true,
enable_verification: true,
..Default::default()
};
let compiler = compiler_for_backend(self.backend).expect("backend not found");
let module = compile_with_config_with(module, config, &*compiler)?;
let mut imports = self.import_object.clone_ref();
for import in module.imports() {
let module_name = import.namespace;
if imports.contains_namespace(&module_name) {
continue;
}
let instance = self
.instances
.get(&module_name)
.ok_or_else(|| anyhow!("no module named `{}`", module_name))?;
imports.register(module_name, Arc::clone(instance));
}
let instance = module
.instantiate(&imports)
.map_err(|e| anyhow!("Instantiate error: {}", e))?;
Ok(instance)
}
/// Register an instance to make it available for performing actions.
fn register(&mut self, name: Option<&str>, as_name: &str) -> Result<()> {
let instance = self.get_instance(name)?;
self.instances.insert(as_name.to_string(), instance);
Ok(())
}
/// Invoke an exported function from an instance.
fn invoke(
&mut self,
instance_name: Option<&str>,
field: &str,
args: &[Value],
) -> Result<Vec<Value>> {
let clonable_instance = self.get_instance(instance_name)?;
let instance = clonable_instance.lock().unwrap();
let func: DynFunc = instance.borrow().exports.get(field)?;
match func.call(args) {
Ok(result) => Ok(result.into()),
Err(e) => Err(CallError {
message: format!("{}", e),
}
.into()),
}
}
/// Get the value of an exported global from an instance.
fn get(&mut self, instance_name: Option<&str>, field: &str) -> Result<Vec<Value>> {
let clonable_instance = self.get_instance(instance_name)?;
let instance = clonable_instance.lock().unwrap();
let global: Global = instance.borrow().exports.get(field)?;
Ok(vec![global.get()])
}
/// Translate from a `script::Value` to a `Val`.
fn runtime_value(v: &wast::Expression<'_>) -> Result<Value> {
use wast::Instruction::*;
if v.instrs.len() != 1 {
bail!("too many instructions in {:?}", v);
}
Ok(match &v.instrs[0] {
I32Const(x) => Value::I32(*x),
I64Const(x) => Value::I64(*x),
F32Const(x) => Value::F32(f32::from_bits(x.bits)),
F64Const(x) => Value::F64(f64::from_bits(x.bits)),
V128Const(x) => Value::V128(u128::from_le_bytes(x.to_le_bytes())),
other => bail!("couldn't convert {:?} to a runtime value", other),
})
}
// Checks if the `assert_unlinkable` message matches the expected one
fn matches_message_assert_unlinkable(_expected: &str, _actual: &str) -> bool {
// We skip message matching for now
true
// actual.contains(&expected)
}
// Checks if the `assert_invalid` message matches the expected one
fn matches_message_assert_invalid(_expected: &str, _actual: &str) -> bool {
// We skip message matching for now
true
// actual.contains(expected)
// // Waiting on https://github.com/WebAssembly/bulk-memory-operations/pull/137
// // to propagate to WebAssembly/testsuite.
// || (expected.contains("unknown table") && actual.contains("unknown elem"))
// // `elem.wast` and `proposals/bulk-memory-operations/elem.wast` disagree
// // on the expected error message for the same error.
// || (expected.contains("out of bounds") && actual.contains("does not fit"))
}
// Checks if the `assert_trap` message matches the expected one
fn matches_message_assert_trap(_expected: &str, _actual: &str) -> bool {
// We skip message matching for now
true
// actual.contains(expected)
// // `bulk-memory-operations/bulk.wast` checks for a message that
// // specifies which element is uninitialized, but our traps don't
// // shepherd that information out.
// || (expected.contains("uninitialized element 2") && actual.contains("uninitialized element"))
}
}
fn extract_lane_as_i8(bytes: u128, lane: usize) -> i8 {
(bytes >> (lane * 8)) as i8
}
fn extract_lane_as_i16(bytes: u128, lane: usize) -> i16 {
(bytes >> (lane * 16)) as i16
}
fn extract_lane_as_i32(bytes: u128, lane: usize) -> i32 {
(bytes >> (lane * 32)) as i32
}
fn extract_lane_as_i64(bytes: u128, lane: usize) -> i64 {
(bytes >> (lane * 64)) as i64
}
fn val_matches(actual: &Value, expected: &wast::AssertExpression) -> Result<bool> {
Ok(match (actual, expected) {
(Value::I32(a), wast::AssertExpression::I32(b)) => a == b,
(Value::I64(a), wast::AssertExpression::I64(b)) => a == b,
// Note that these float comparisons are comparing bits, not float
// values, so we're testing for bit-for-bit equivalence
(Value::F32(a), wast::AssertExpression::F32(b)) => f32_matches(a.to_bits(), &b),
(Value::F64(a), wast::AssertExpression::F64(b)) => f64_matches(a.to_bits(), &b),
(Value::V128(a), wast::AssertExpression::V128(b)) => v128_matches(*a, &b),
// Legacy comparators
(Value::F32(a), wast::AssertExpression::LegacyCanonicalNaN) => a.is_canonical_nan(),
(Value::F32(a), wast::AssertExpression::LegacyArithmeticNaN) => a.is_arithmetic_nan(),
(Value::F64(a), wast::AssertExpression::LegacyCanonicalNaN) => a.is_canonical_nan(),
(Value::F64(a), wast::AssertExpression::LegacyArithmeticNaN) => a.is_arithmetic_nan(),
_ => bail!(
"don't know how to compare {:?} and {:?} yet",
actual,
expected
),
})
}
fn f32_matches(actual: u32, expected: &wast::NanPattern<wast::Float32>) -> bool {
match expected {
wast::NanPattern::CanonicalNan => f32::from_bits(actual).is_canonical_nan(),
wast::NanPattern::ArithmeticNan => f32::from_bits(actual).is_arithmetic_nan(),
wast::NanPattern::Value(expected_value) => actual == expected_value.bits,
}
}
fn f64_matches(actual: u64, expected: &wast::NanPattern<wast::Float64>) -> bool {
match expected {
wast::NanPattern::CanonicalNan => f64::from_bits(actual).is_canonical_nan(),
wast::NanPattern::ArithmeticNan => f64::from_bits(actual).is_arithmetic_nan(),
wast::NanPattern::Value(expected_value) => actual == expected_value.bits,
}
}
fn v128_matches(actual: u128, expected: &wast::V128Pattern) -> bool {
match expected {
wast::V128Pattern::I8x16(b) => b
.iter()
.enumerate()
.all(|(i, b)| *b == extract_lane_as_i8(actual, i)),
wast::V128Pattern::I16x8(b) => b
.iter()
.enumerate()
.all(|(i, b)| *b == extract_lane_as_i16(actual, i)),
wast::V128Pattern::I32x4(b) => b
.iter()
.enumerate()
.all(|(i, b)| *b == extract_lane_as_i32(actual, i)),
wast::V128Pattern::I64x2(b) => b
.iter()
.enumerate()
.all(|(i, b)| *b == extract_lane_as_i64(actual, i)),
wast::V128Pattern::F32x4(b) => b.iter().enumerate().all(|(i, b)| {
let a = extract_lane_as_i32(actual, i) as u32;
f32_matches(a, b)
}),
wast::V128Pattern::F64x2(b) => b.iter().enumerate().all(|(i, b)| {
let a = extract_lane_as_i64(actual, i) as u64;
f64_matches(a, b)
}),
}
}
pub trait NaNCheck {
fn is_arithmetic_nan(&self) -> bool;
fn is_canonical_nan(&self) -> bool;
}
impl NaNCheck for f32 {
fn is_arithmetic_nan(&self) -> bool {
const AF32_NAN: u32 = 0x0040_0000;
(self.to_bits() & AF32_NAN) == AF32_NAN
}
fn is_canonical_nan(&self) -> bool {
return (self.to_bits() & 0x7fff_ffff) == 0x7fc0_0000;
}
}
impl NaNCheck for f64 {
fn is_arithmetic_nan(&self) -> bool {
const AF64_NAN: u64 = 0x0008_0000_0000_0000;
(self.to_bits() & AF64_NAN) == AF64_NAN
}
fn is_canonical_nan(&self) -> bool {
(self.to_bits() & 0x7fff_ffff_ffff_ffff) == 0x7ff8_0000_0000_0000
}
}