mirror of
https://github.com/fluencelabs/wasmer
synced 2025-04-25 10:22:19 +00:00
Improved test generation
This commit is contained in:
parent
c2306bd39e
commit
a7dba54b7f
40
Cargo.lock
generated
40
Cargo.lock
generated
@ -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"
|
||||
|
@ -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
|
||||
|
8
Makefile
8
Makefile
@ -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
102
build.rs
@ -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(())
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -1,2 +1,2 @@
|
||||
pub mod utils;
|
||||
mod emtests;
|
||||
pub mod utils;
|
||||
|
1572
tests/spectest.rs
1572
tests/spectest.rs
File diff suppressed because it is too large
Load Diff
8
tests/test-generator/Cargo.toml
Normal file
8
tests/test-generator/Cargo.toml
Normal 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"
|
162
tests/test-generator/src/lib.rs
Normal file
162
tests/test-generator/src/lib.rs
Normal 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
17
tests/wast/Cargo.toml
Normal 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
7
tests/wast/README.md
Normal 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
50
tests/wast/src/errors.rs
Normal 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
34
tests/wast/src/lib.rs
Normal 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");
|
56
tests/wast/src/spectest.rs
Normal file
56
tests/wast/src/spectest.rs
Normal 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
506
tests/wast/src/wast.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user