You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
172 lines
5.5 KiB
172 lines
5.5 KiB
//! Run the tests in `wasi_testsuite` using Wasmtime's CLI binary and checking
|
|
//! the results with a [wasi-testsuite] spec.
|
|
//!
|
|
//! [wasi-testsuite]: https://github.com/WebAssembly/wasi-testsuite
|
|
|
|
#![cfg(not(miri))]
|
|
|
|
use crate::cli_tests::get_wasmtime_command;
|
|
use anyhow::{anyhow, Result};
|
|
use serde_derive::Deserialize;
|
|
use std::collections::HashMap;
|
|
use std::ffi::OsStr;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Output};
|
|
use walkdir::{DirEntry, WalkDir};
|
|
|
|
#[test]
|
|
#[cfg_attr(target_os = "windows", ignore)] // TODO: https://github.com/WebAssembly/WASI/issues/524
|
|
fn wasi_testsuite() -> Result<()> {
|
|
// Currently, Wasmtime's implementation in wasi-common does not line up
|
|
// exactly with the expectations in wasi-testsuite. This could be for one of
|
|
// various reasons:
|
|
// - wasi-common has a bug
|
|
// - wasi-testsuite overspecifies (or incorrectly specifies) a test
|
|
// - this test runner incorrectly configures Wasmtime's CLI execution.
|
|
//
|
|
// This list is expected to shrink as the failures are resolved. The easiest
|
|
// way to resolve one of these is to remove the file from the list and run
|
|
// `cargo test wasi_testsuite -- --nocapture`. The printed output will show
|
|
// the expected result, the actual result, and a command to replicate the
|
|
// failure from the command line.
|
|
const WASI_COMMON_IGNORE_LIST: &[&str] = &["fd_advise.wasm"];
|
|
run_all(
|
|
"tests/wasi_testsuite/wasi-common",
|
|
&[],
|
|
WASI_COMMON_IGNORE_LIST,
|
|
)?;
|
|
run_all(
|
|
"tests/wasi_testsuite/wasi-threads",
|
|
&["-Sthreads", "-Wthreads"],
|
|
&[],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn run_all(testsuite_dir: &str, extra_flags: &[&str], ignore: &[&str]) -> Result<()> {
|
|
// In case the previous run ended in failure, we clean up any created files
|
|
// that would otherwise be cleaned up at the end of this function.
|
|
clean_garbage(testsuite_dir)?;
|
|
|
|
// Execute and check each WebAssembly test case.
|
|
for module in list_files(testsuite_dir, is_wasm) {
|
|
if should_ignore(&module, ignore) {
|
|
println!("Ignoring {}", module.display());
|
|
} else {
|
|
println!("Testing {}", module.display());
|
|
let spec = if let Ok(contents) = fs::read_to_string(&module.with_extension("json")) {
|
|
serde_json::from_str(&contents)?
|
|
} else {
|
|
Spec::default()
|
|
};
|
|
let mut cmd = build_command(module, extra_flags, &spec)?;
|
|
let result = cmd.output()?;
|
|
if spec != result {
|
|
println!(" command: {cmd:?}");
|
|
println!(" spec: {spec:#?}");
|
|
println!(" result: {result:#?}");
|
|
panic!("FAILED! The result does not match the specification");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up any created files to avoid making the Git repository dirty.
|
|
clean_garbage(testsuite_dir)
|
|
}
|
|
|
|
fn list_files<F>(testsuite_dir: &str, filter: F) -> impl Iterator<Item = PathBuf>
|
|
where
|
|
F: FnMut(&DirEntry) -> bool,
|
|
{
|
|
WalkDir::new(testsuite_dir)
|
|
.into_iter()
|
|
.filter_map(Result::ok)
|
|
.filter(filter)
|
|
.map(|e| e.path().to_path_buf())
|
|
}
|
|
|
|
fn is_wasm(entry: &DirEntry) -> bool {
|
|
let path = entry.path();
|
|
let ext = path.extension().map(OsStr::to_str).flatten();
|
|
path.is_file() && (ext == Some("wat") || ext == Some("wasm"))
|
|
}
|
|
|
|
fn should_ignore<P: AsRef<Path>>(path: P, ignore_list: &[&str]) -> bool {
|
|
let file_name = path.as_ref().file_name().unwrap().to_str().unwrap();
|
|
ignore_list.contains(&file_name)
|
|
}
|
|
|
|
fn build_command<P: AsRef<Path>>(module: P, extra_flags: &[&str], spec: &Spec) -> Result<Command> {
|
|
let mut cmd = get_wasmtime_command()?;
|
|
let parent_dir = module
|
|
.as_ref()
|
|
.parent()
|
|
.ok_or(anyhow!("module has no parent?"))?;
|
|
|
|
// Add arguments.
|
|
cmd.args(["run", "-Ccache=n"]);
|
|
cmd.args(extra_flags);
|
|
if let Some(dirs) = &spec.dirs {
|
|
for dir in dirs {
|
|
cmd.arg("--dir");
|
|
cmd.arg(format!("{}::{}", parent_dir.join(dir).display(), dir));
|
|
}
|
|
}
|
|
// Add environment variables as CLI arguments.
|
|
if let Some(env) = &spec.env {
|
|
for env_pair in env {
|
|
cmd.arg("--env");
|
|
cmd.arg(format!("{}={}", env_pair.0, env_pair.1));
|
|
}
|
|
cmd.envs(env);
|
|
}
|
|
cmd.arg(module.as_ref().to_str().unwrap());
|
|
if let Some(spec_args) = &spec.args {
|
|
cmd.args(spec_args);
|
|
}
|
|
|
|
Ok(cmd)
|
|
}
|
|
|
|
fn clean_garbage(testsuite_dir: &str) -> Result<()> {
|
|
for path in list_files(testsuite_dir, is_garbage) {
|
|
println!("Removing {}", path.display());
|
|
if path.is_dir() {
|
|
fs::remove_dir(path)?;
|
|
} else {
|
|
fs::remove_file(path)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn is_garbage(entry: &DirEntry) -> bool {
|
|
let path = entry.path();
|
|
let ext = path.extension().map(OsStr::to_str).flatten();
|
|
ext == Some("cleanup")
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize)]
|
|
struct Spec {
|
|
args: Option<Vec<String>>,
|
|
dirs: Option<Vec<String>>,
|
|
env: Option<HashMap<String, String>>,
|
|
exit_code: Option<i32>,
|
|
stderr: Option<String>,
|
|
stdout: Option<String>,
|
|
}
|
|
|
|
impl PartialEq<Output> for Spec {
|
|
fn eq(&self, other: &Output) -> bool {
|
|
self.exit_code.unwrap_or(0) == other.status.code().unwrap()
|
|
&& matches_or_missing(&self.stdout, &other.stdout)
|
|
&& matches_or_missing(&self.stderr, &other.stderr)
|
|
}
|
|
}
|
|
|
|
fn matches_or_missing(a: &Option<String>, b: &[u8]) -> bool {
|
|
a.as_ref()
|
|
.map(|s| s == &String::from_utf8_lossy(b))
|
|
.unwrap_or(true)
|
|
}
|
|
|