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:
22
winch/filetests/Cargo.toml
Normal file
22
winch/filetests/Cargo.toml
Normal 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 }
|
||||
12
winch/filetests/filetests/x64/simple.wat
Normal file
12
winch/filetests/filetests/x64/simple.wat
Normal 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
|
||||
59
winch/filetests/src/disasm.rs
Normal file
59
winch/filetests/src/disasm.rs
Normal 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
165
winch/filetests/src/lib.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user