winch: Adding support for integration tests (#5588)

* Adding in the foundations for Winch `filetests`

This commit adds two new crates into the Winch workspace:
`filetests` and `test-macros`. The intent is to mimic the
structure of Cranelift `filetests`, but in a simpler way.

* Updates to documentation

This commits adds a high level document to outline how to test Winch
through the `winch-tools` utility. It also updates some inline
documentation which gets propagated to the CLI.

* Updating test-macro to use a glob instead of only a flat directory
This commit is contained in:
Kevin Rizzo
2023-01-19 07:34:48 -05:00
committed by GitHub
parent 7cea73a81d
commit da03ff47f1
16 changed files with 586 additions and 135 deletions

View File

@@ -0,0 +1,22 @@
[package]
authors = ["The Winch Project Developers"]
name = "winch-filetests"
description = "Tests for the Winch compiler based on a set of known valid files"
license = "Apache-2.0 WITH LLVM-exception"
repository = "https://github.com/bytecodealliance/wasmtime"
version = "0.0.0"
publish = false
edition.workspace = true
[dependencies]
winch-test-macros = {workspace = true}
target-lexicon = { workspace = true }
winch-codegen = { workspace = true, features = ['all-arch'] }
wasmtime-environ = { workspace = true }
anyhow = { workspace = true }
wat = { workspace = true }
similar = { workspace = true }
toml = { workspace = true }
serde = { workspace = true }
cranelift-codegen = { workspace = true }
capstone = { workspace = true }

View File

@@ -0,0 +1,12 @@
;;! target = "x86_64"
(module
(func (result i32)
(i32.const 42)
)
)
;; 0: 55 push rbp
;; 1: 4889e5 mov rbp, rsp
;; 4: 48c7c02a000000 mov rax, 0x2a
;; b: 5d pop rbp
;; c: c3 ret

View File

@@ -0,0 +1,59 @@
//! Disassembly utilities.
use anyhow::{bail, Result};
use capstone::prelude::*;
use std::fmt::Write;
use target_lexicon::Architecture;
use winch_codegen::TargetIsa;
/// Disassemble and print a machine code buffer.
pub fn disasm(bytes: &[u8], isa: &dyn TargetIsa) -> Result<Vec<String>> {
let dis = disassembler_for(isa)?;
let insts = dis.disasm_all(bytes, 0x0).unwrap();
let disassembled_lines = insts
.iter()
.map(|i| {
let mut line = String::new();
write!(&mut line, "{:4x}:\t ", i.address()).unwrap();
let mut bytes_str = String::new();
let mut len = 0;
for b in i.bytes() {
write!(&mut bytes_str, "{:02x}", b).unwrap();
len += 1;
}
write!(&mut line, "{:21}\t", bytes_str).unwrap();
if len > 8 {
write!(&mut line, "\n\t\t\t\t").unwrap();
}
if let Some(s) = i.mnemonic() {
write!(&mut line, "{}\t", s).unwrap();
}
if let Some(s) = i.op_str() {
write!(&mut line, "{}", s).unwrap();
}
line
})
.collect();
Ok(disassembled_lines)
}
fn disassembler_for(isa: &dyn TargetIsa) -> Result<Capstone> {
let disasm = match isa.triple().architecture {
Architecture::X86_64 => Capstone::new()
.x86()
.mode(arch::x86::ArchMode::Mode64)
.build()
.map_err(|e| anyhow::format_err!("{}", e))?,
_ => bail!("Unsupported ISA"),
};
Ok(disasm)
}

165
winch/filetests/src/lib.rs Normal file
View File

@@ -0,0 +1,165 @@
pub mod disasm;
#[cfg(test)]
mod test {
use super::disasm::disasm;
use anyhow::Context;
use cranelift_codegen::settings;
use serde::{Deserialize, Serialize};
use similar::TextDiff;
use std::str::FromStr;
use target_lexicon::Triple;
use wasmtime_environ::{
wasmparser::{types::Types, Parser as WasmParser, Validator},
DefinedFuncIndex, FunctionBodyData, Module, ModuleEnvironment, Tunables,
};
use winch_codegen::isa::TargetIsa;
use winch_codegen::lookup;
use winch_test_macros::generate_file_tests;
#[derive(Clone, Debug, Serialize, Deserialize)]
struct TestConfig {
target: String,
}
/// A helper function to parse the test configuration from the top of the file.
fn parse_config(wat: &str) -> TestConfig {
let config_lines: Vec<_> = wat
.lines()
.take_while(|l| l.starts_with(";;!"))
.map(|l| &l[3..])
.collect();
let config_text = config_lines.join("\n");
toml::from_str(&config_text)
.context("failed to parse the test configuration")
.unwrap()
}
/// A helper function to parse the expected result from the bottom of the file.
fn parse_expected_result(wat: &str) -> String {
let mut expected_lines: Vec<_> = wat
.lines()
.rev()
.take_while(|l| l.starts_with(";;"))
.map(|l| {
if l.starts_with(";; ") {
&l[3..]
} else {
&l[2..]
}
})
.collect();
expected_lines.reverse();
expected_lines.join("\n")
}
/// A helper function to rewrite the expected result in the file.
fn rewrite_expected(wat: &str, actual: &str) -> String {
let old_expectation_line_count = wat
.lines()
.rev()
.take_while(|l| l.starts_with(";;"))
.count();
let old_wat_line_count = wat.lines().count();
let new_wat_lines: Vec<_> = wat
.lines()
.take(old_wat_line_count - old_expectation_line_count)
.map(|l| l.to_string())
.chain(actual.lines().map(|l| {
if l.is_empty() {
";;".to_string()
} else {
format!(";; {l}")
}
}))
.collect();
let mut new_wat = new_wat_lines.join("\n");
new_wat.push('\n');
new_wat
}
#[generate_file_tests]
fn run_test(test_path: &str) {
let binding = std::fs::read_to_string(test_path).unwrap();
let wat = binding.as_str();
let config = parse_config(wat);
let wasm = wat::parse_str(&wat).unwrap();
let triple = Triple::from_str(&config.target).unwrap();
let binding = parse_expected_result(wat);
let expected = binding.as_str();
let shared_flags = settings::Flags::new(settings::builder());
let isa_builder = lookup(triple).unwrap();
let isa = isa_builder.build(shared_flags).unwrap();
let mut validator = Validator::new();
let parser = WasmParser::new(0);
let mut types = Default::default();
let tunables = Tunables::default();
let mut translation = ModuleEnvironment::new(&tunables, &mut validator, &mut types)
.translate(parser, &wasm)
.context("Failed to translate WebAssembly module")
.unwrap();
let _ = types.finish();
let body_inputs = std::mem::take(&mut translation.function_body_inputs);
let module = &translation.module;
let types = translation.get_types();
let binding = body_inputs
.into_iter()
.flat_map(|func| compile(&*isa, module, types, func))
.collect::<Vec<String>>()
.join("\n");
let actual = binding.as_str();
if std::env::var("WINCH_TEST_BLESS").unwrap_or_default() == "1" {
let new_wat = rewrite_expected(wat, actual);
std::fs::write(test_path, new_wat)
.with_context(|| format!("failed to write file: {}", test_path))
.unwrap();
return;
}
if expected.trim() != actual.trim() {
eprintln!(
"\n{}",
TextDiff::from_lines(expected, actual)
.unified_diff()
.header("expected", "actual")
);
eprintln!(
"note: You can re-run with the `WINCH_TEST_BLESS=1` environment variable set to update test expectations.\n"
);
panic!("Did not get the expected translation");
}
}
fn compile(
isa: &dyn TargetIsa,
module: &Module,
types: &Types,
f: (DefinedFuncIndex, FunctionBodyData<'_>),
) -> Vec<String> {
let index = module.func_index(f.0);
let sig = types
.func_type_at(index.as_u32())
.expect(&format!("function type at index {:?}", index.as_u32()));
let FunctionBodyData { body, validator } = f.1;
let validator = validator.into_validator(Default::default());
let buffer = isa
.compile_function(&sig, &body, validator)
.expect("Couldn't compile function");
disasm(buffer.data(), isa).unwrap()
}
}