#![allow(dead_code)]
use crate::api::instrument::{Config, LibraryLinkStrategy};
use crate::api::utils::wasm2wat_on_file;
use crate::common::instr::{run, try_path};
use crate::common::metrics::Metrics;
use crate::parser::yml_processor::pull_all_yml_files;
use log::{debug, error};
use std::fs::{remove_dir_all, File};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use wirm::Module;
const CORE_WASM_PATH: &str = "tests/libs/whamm_core.wasm";
const DEFS_PATH: &str = "./";
const TEST_DEBUG_DIR: &str = "output/tests/debug_me/";
const OUTPUT_DIR: &str = "output/tests/wast_suite";
const OUTPUT_WHAMMED_WAST: &str = "output/tests/wast_suite/should_pass";
const OUTPUT_UNINSTR_WAST: &str = "output/tests/wast_suite/should_fail";
pub fn run_all() -> Result<(), std::io::Error> {
clean();
let wast_tests = find_wast_tests();
setup_and_run_tests(&wast_tests)?;
Ok(())
}
pub(crate) fn clean() {
remove_dir_all(Path::new(OUTPUT_DIR)).ok();
}
pub fn setup_and_run_tests(wast_tests: &Vec<PathBuf>) -> Result<(), std::io::Error> {
let (all_wast_should_fail, all_wast_should_pass) = setup(wast_tests)?;
run_wast_tests(all_wast_should_fail, all_wast_should_pass);
Ok(())
}
fn setup(wast_tests: &Vec<PathBuf>) -> Result<(Vec<String>, Vec<String>), std::io::Error> {
let mut all_wast_should_pass = vec![];
let mut all_wast_should_fail = vec![];
for test in wast_tests {
let f = File::open(test.clone())?;
let mut reader = BufReader::new(f);
let test_setup = get_test_setup(&mut reader, test)?;
let test_cases = get_test_cases(reader);
for test_case in test_cases.iter() {
test_case.print();
}
match generate_should_fail_bin_wast(&test_setup, &test_cases, test) {
Err(e) => {
panic!(
"Unable to write UN-instrumented wast file due to error: {:?}",
e
);
}
Ok(mut files) => {
all_wast_should_fail.append(&mut files);
}
};
match generate_instrumented_bin_wast(&test_setup, &test_cases, test) {
Err(e) => {
panic!(
"Unable to write instrumented wast file due to error: {:?}",
e
);
}
Ok(mut files) => all_wast_should_pass.append(&mut files),
};
}
Ok((all_wast_should_fail, all_wast_should_pass))
}
fn run_wast_tests(wast_should_fail: Vec<String>, wast_should_pass: Vec<String>) {
let inters = get_available_interpreters();
assert!(!inters.is_empty(), "No supported interpreters are configured, fail!\n\
To fix, add an executable binary under {INT_PATH} for one of the following interpreter options:\n\
1. the wizeng interpreter, named '{WIZENG_SPEC_INT}'. https://github.com/titzer/wizard-engine/tree/master\n\
2. the Wasm reference interpreter, named '{WASM_REF_INT}'. https://github.com/WebAssembly/spec/tree/main/interpreter\n");
println!("\n>>> Running wast on the following available interpreters:");
for (i, (inter, _args)) in inters.iter().enumerate() {
println!("{i}. {inter}");
}
println!();
run_wast_tests_that_should_fail(&inters, wast_should_fail);
run_wast_tests_that_should_pass(&inters, wast_should_pass);
}
fn run_wast_tests_that_should_fail(inters: &[(String, Vec<String>)], wast_files: Vec<String>) {
debug!("Running wast tests that should fail.");
for (inter, args) in inters.iter() {
for wast in wast_files.iter() {
let res = run_wast_test(inter, args, wast);
if res.status.success() {
error!("The following command should have FAILED (ran un-instrumented): '{inter} {wast}'");
}
assert!(!res.status.success());
}
}
}
fn run_wast_tests_that_should_pass(inters: &[(String, Vec<String>)], wast_files: Vec<String>) {
debug!("Running wast tests that should pass.");
for (inter, args) in inters.iter() {
for wast in wast_files.iter() {
let res = run_wast_test(inter, args, wast);
if !res.status.success() {
error!(
"The following command should have PASSED: '{inter} {wast}'\n{}\n{}",
String::from_utf8(res.stdout).unwrap(),
String::from_utf8(res.stderr).unwrap()
);
}
assert!(res.status.success());
}
}
}
fn run_wast_test(inter: &String, args: &[String], wast_file_name: &String) -> Output {
let mut command = &mut Command::new(inter);
for arg in args.iter() {
command = command.arg(arg);
}
command
.arg(wast_file_name)
.output()
.expect("failed to execute process")
}
const INT_PATH: &str = "./output/tests/engines";
const WIZENG_SPEC_INT: &str = "wizard-spectest";
const WASM_REF_INT: &str = "wasm";
fn get_available_interpreters() -> Vec<(String, Vec<String>)> {
let supported_interpreters = [
(WASM_REF_INT, vec![]),
(WIZENG_SPEC_INT, vec!["-ext:multi-memory".to_string()]),
];
let mut available_interpreters = Vec::new();
for (interpreter, args) in supported_interpreters.iter() {
let int_path = format!("{INT_PATH}/{interpreter}");
match Command::new(&int_path).arg("-help").output() {
Err(..) => {
}
Ok(res) => {
if res.status.success() {
available_interpreters.push((int_path, args.clone()));
}
}
}
}
available_interpreters
}
fn generate_should_fail_bin_wast(
test_setup: &WastTestSetup,
test_cases: &[WastTestCase],
wast_path: &Path,
) -> Result<Vec<String>, std::io::Error> {
let mut created_wast_files = vec![];
for (test_idx, test_case) in test_cases.iter().enumerate() {
for (assertion_idx, assertion) in test_case.assertions.iter().enumerate() {
if assertion.passes_uninstrumented {
continue;
}
let new_file_path = new_wast_path(
wast_path,
test_idx,
Some(assertion_idx),
OUTPUT_UNINSTR_WAST,
);
write_bin_wast_file(
&new_file_path,
&test_setup.support_modules_wat,
&test_setup.support_stmts,
&test_setup.target_module_wat,
&"None".to_string(),
std::slice::from_ref(assertion),
)?;
created_wast_files.push(new_file_path);
}
}
Ok(created_wast_files)
}
fn generate_instrumented_bin_wast(
test_setup: &WastTestSetup,
test_cases: &[WastTestCase],
wast_path: &Path,
) -> Result<Vec<String>, std::io::Error> {
let mut created_wast_files = vec![];
for (idx, test_case) in test_cases.iter().enumerate() {
let cloned_module = test_setup.target_module_wat.clone();
let buff = wat::parse_bytes(cloned_module.as_slice())
.expect("couldn't convert the input wat to Wasm");
let mut module_to_instrument = Module::parse(&buff, false, true).unwrap();
let debug_file_path = format!(
"{TEST_DEBUG_DIR}/{}.wasm",
wast_path.file_name().unwrap().to_str().unwrap()
);
let wast_path_str = wast_path.to_str().unwrap().replace("\"", "");
let core_lib = std::fs::read(CORE_WASM_PATH).unwrap_or_else(|_| {
panic!(
"Could not read the core wasm module expected to be at location: {}",
CORE_WASM_PATH
)
});
let def_yamls = pull_all_yml_files(DEFS_PATH);
let mut metrics = Metrics::default();
if let Err(mut err) = run(
&core_lib,
&def_yamls,
&mut module_to_instrument,
&test_case.whamm_script,
&wast_path_str,
vec![],
0,
&mut metrics,
Config {
as_monitor_module: false,
enable_wei_alt: false,
metrics: false,
no_bundle: false,
no_body: false,
no_pred: false,
no_report: false,
testing: true,
library_strategy: LibraryLinkStrategy::Imported,
},
) {
err.report();
unreachable!("Shouldn't have had errors!")
}
let instrumented_module_wasm = module_to_instrument.encode();
try_path(&debug_file_path);
if let Err(e) = std::fs::write(&debug_file_path, instrumented_module_wasm.clone()) {
unreachable!(
"Failed to dump instrumented wasm to {} from error: {}",
&debug_file_path, e
)
}
wasm2wat_on_file(debug_file_path.as_str());
let new_file_path = new_wast_path(wast_path, idx, None, OUTPUT_WHAMMED_WAST);
write_bin_wast_file(
&new_file_path,
&test_setup.support_modules_wat,
&test_setup.support_stmts,
&instrumented_module_wasm,
&test_case.whamm_script,
&test_case.assertions,
)?;
created_wast_files.push(new_file_path);
}
Ok(created_wast_files)
}
fn write_bin_wast_file(
file_path: &String,
support_modules_wat: &Vec<Vec<u8>>,
support_stmts: &Vec<String>,
target_module: &Vec<u8>,
whamm_script: &String,
assertions: &[Assertion],
) -> Result<(), std::io::Error> {
let mut wast_file = File::create(file_path)?;
for module in support_modules_wat {
let module_wasm = wat::parse_bytes(module).expect("couldn't convert the input wat to Wasm");
wast_file.write_all("(module binary ".as_bytes())?;
wast_file.write_all(vec_as_hex(module_wasm.as_ref()).as_bytes())?;
wast_file.write_all(")\n\n".as_bytes())?;
}
for stmt in support_stmts {
wast_file.write_all(stmt.as_bytes())?;
wast_file.write_all(b"\n")?;
}
wast_file.write_all("(module binary ".as_bytes())?;
wast_file.write_all(vec_as_hex(target_module.as_slice()).as_bytes())?;
wast_file.write_all(")\n\n".as_bytes())?;
wast_file.write_all(format!("{} {}\n", WHAMM_PREFIX_PATTERN, whamm_script).as_bytes())?;
for assert in assertions.iter() {
wast_file.write_all(assert.str.as_bytes())?;
wast_file.write_all(b"\n")?;
}
wast_file.write_all(b"\n")?;
wast_file
.flush()
.expect("Failed to flush out the wast file");
Ok(())
}
const WAST_SUITE_DIR: &str = "tests/wast_suite";
const MODULE_PREFIX_PATTERN: &str = "(module";
const ASSERT_PREFIX_PATTERN: &str = "(assert";
const WHAMM_PREFIX_PATTERN: &str = ";; WHAMM --> ";
const PASSES_UNINSTR_PATTERN: &str = ";; @passes_uninstr";
const TO_INSTR_PATTERN: &str = ";; @instrument";
pub(crate) fn find_wast_tests() -> Vec<PathBuf> {
let mut wast_tests = Vec::new();
let suite_path = Path::new(WAST_SUITE_DIR);
find_tests(suite_path, &mut wast_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("wast") => {} Some("wasm") => panic!(
"use `*.wat` or `*.wast` instead of binaries: {:?}",
f.path()
),
_ => continue,
}
tests.push(f.path());
}
}
wast_tests
}
#[derive(Default)]
struct WastTestSetup {
target_module_wat: Vec<u8>,
support_modules_wat: Vec<Vec<u8>>,
support_stmts: Vec<String>,
}
fn get_test_setup(
reader: &mut BufReader<File>,
file_path: &Path,
) -> Result<WastTestSetup, std::io::Error> {
let mut mod_to_instr = false;
let mut setup = WastTestSetup::default();
let mut line = String::new();
while reader.read_line(&mut line)? > 0 {
if line.starts_with(TO_INSTR_PATTERN) {
mod_to_instr = true;
} else if line.starts_with(MODULE_PREFIX_PATTERN) {
let module = get_wasm_module(&line, reader)?;
if mod_to_instr {
if module.is_empty() {
panic!(
"Could not find the Wasm module-to-instrument in the wast file: {:?}",
file_path
);
}
debug!("{module}\n");
setup.target_module_wat = Vec::from(module.as_bytes());
break;
} else {
setup.support_modules_wat.push(Vec::from(module.as_bytes()));
}
mod_to_instr = false;
} else if line.starts_with('(') {
setup.support_stmts.push(line.clone());
}
line.clear();
}
Ok(setup)
}
fn get_wasm_module(
start_line: &str,
reader: &mut BufReader<File>,
) -> Result<String, std::io::Error> {
let mut module: String = start_line.to_string();
let mut num_left_parens = count_matched_chars(&module, &'(');
let mut num_right_parens = count_matched_chars(&module, &')');
let mut line = String::new();
while reader.read_line(&mut line)? > 0 {
module += &line;
num_left_parens += count_matched_chars(&line, &'(');
num_right_parens += count_matched_chars(&line, &')');
if num_left_parens == num_right_parens {
break;
}
line.clear();
}
fn count_matched_chars(s: &str, c: &char) -> usize {
s.chars().filter(|ch| *ch == *c).count()
}
Ok(module)
}
#[derive(Default)]
struct WastTestCase {
whamm_script: String,
assertions: Vec<Assertion>,
}
impl WastTestCase {
fn print(&self) {
debug!(">>> TEST CASE <<<");
debug!("{}", self.whamm_script);
for assertion in &self.assertions {
if assertion.passes_uninstrumented {
debug!("PASS un-instrumented: '{}'", assertion.str);
} else {
debug!("FAIL un-instrumented: '{}'", assertion.str);
}
}
}
}
#[derive(Clone)]
struct Assertion {
str: String,
passes_uninstrumented: bool,
}
fn get_test_cases(reader: BufReader<File>) -> Vec<WastTestCase> {
let mut test_cases = Vec::new();
let mut first = true;
let mut matched = false;
let mut passes_uninstr = false;
let mut curr_test = WastTestCase::default();
for line in reader.lines().map_while(Result::ok) {
if let Some(whamm) = line.strip_prefix(WHAMM_PREFIX_PATTERN) {
if !first {
test_cases.push(curr_test);
curr_test = WastTestCase::default();
}
first = false;
matched = true;
curr_test.whamm_script = whamm.to_string();
} else if line.starts_with(MODULE_PREFIX_PATTERN) {
panic!("Only one module per wast file!!")
} else if line.starts_with(ASSERT_PREFIX_PATTERN) {
curr_test.assertions.push(Assertion {
str: line,
passes_uninstrumented: passes_uninstr,
});
passes_uninstr = false;
} else if line.starts_with(PASSES_UNINSTR_PATTERN) {
passes_uninstr = true;
}
}
if matched {
test_cases.push(curr_test);
}
test_cases
}
fn new_wast_path(
wast_path: &Path,
idx: usize,
idx2: Option<usize>,
target_parent_dir: &str,
) -> String {
let file_name = wast_path.file_name().unwrap().to_str().unwrap().to_string();
let file_ext = wast_path.extension().unwrap().to_str().unwrap();
let file_name_stripped = file_name.strip_suffix(file_ext).unwrap();
let new_name = if let Some(idx2) = idx2 {
format!("{file_name_stripped}whamm{idx}.assertion{idx2}.bin.wast")
} else {
format!("{file_name_stripped}whamm{idx}.bin.wast")
};
let new_sub_path = match wast_path.strip_prefix(WAST_SUITE_DIR) {
Ok(p) => p.to_str().unwrap(),
Err(e) => panic!(
"Could not strip prefix from path '{:?}' due to error: {:?}",
wast_path, e
),
};
let new_path = format!("{target_parent_dir}/{}/{new_name}", new_sub_path);
try_path(&new_path);
new_path
}
pub fn vec_as_hex(vec: &[u8]) -> String {
let mut res = "\"".to_string();
for &byte in vec {
res += format!("\\{:02x}", byte).as_str();
}
res += "\"";
res
}