use anyhow::{Context, Error, Result, bail};
use libtest_mimic::{Arguments, Trial};
use pretty_assertions::assert_eq;
use std::{borrow::Cow, fs, path::Path};
use wasm_encoder::{Encode, Section};
use wasm_metadata::{Metadata, Payload};
use wasmparser::{Parser, Validator, WasmFeatures};
use wit_component::{ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter};
use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup};
fn main() -> Result<()> {
drop(env_logger::try_init());
let mut trials = Vec::new();
for entry in fs::read_dir("tests/components")? {
let path = entry?.path();
if !path.is_dir() {
continue;
}
trials.push(Trial::test(path.to_str().unwrap().to_string(), move || {
run_test(&path).map_err(|e| format!("{e:?}").into())
}));
}
let mut args = Arguments::from_args();
if cfg!(target_family = "wasm") && !cfg!(target_feature = "atomics") {
args.test_threads = Some(1);
}
libtest_mimic::run(&args, trials).exit();
}
fn run_test(path: &Path) -> Result<()> {
let test_case = path.file_stem().unwrap().to_str().unwrap();
let mut resolve = Resolve::default();
let (pkg_id, _) = resolve.push_dir(&path)?;
let path = path.to_path_buf();
let module_path = path.join("module.wat");
let mut adapters = glob::glob(path.join("adapt-*.wat").to_str().unwrap())?;
let result = if module_path.is_file() {
let module = read_core_module(&module_path, &resolve, pkg_id)
.with_context(|| format!("failed to read core module at {module_path:?}"))?;
adapters
.try_fold(
ComponentEncoder::default()
.debug_names(true)
.module(&module)?,
|encoder, path| {
let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg_id)?;
Ok::<_, Error>(encoder.adapter(&name, &wasm)?)
},
)?
.encode()
} else {
let mut libs = glob::glob(path.join("lib-*.wat").to_str().unwrap())?
.map(|path| Ok(("lib-", path?, false)))
.chain(
glob::glob(path.join("dlopen-lib-*.wat").to_str().unwrap())?
.map(|path| Ok(("dlopen-lib-", path?, true))),
)
.collect::<Result<Vec<_>>>()?;
libs.sort_by(|(_, a, _), (_, b, _)| a.cmp(b));
let mut linker = Linker::default().validate(false).debug_names(true);
if path.join("stub-missing-functions").is_file() {
linker = linker.stub_missing_functions(true);
}
if path.join("use-built-in-libdl").is_file() {
linker = linker.use_built_in_libdl(true);
}
let linker = libs
.into_iter()
.try_fold(linker, |linker, (prefix, path, dl_openable)| {
let (name, wasm) = read_name_and_module(prefix, &path, &resolve, pkg_id)?;
Ok::<_, Error>(linker.library(&name, &wasm, dl_openable)?)
})?;
adapters
.try_fold(linker, |linker, path| {
let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg_id)?;
Ok::<_, Error>(linker.adapter(&name, &wasm)?)
})?
.encode()
};
let component_path = path.join("component.wat");
let component_wit_path = path.join("component.wit.print");
let error_path = path.join("error.txt");
let bytes = match result {
Ok(bytes) => {
if test_case.starts_with("error-") {
bail!("expected an error but got success");
}
bytes
}
Err(err) => {
if !test_case.starts_with("error-") {
return Err(err);
}
assert_output(&format!("{err:#}"), &error_path)?;
return Ok(());
}
};
Validator::new_with_features(WasmFeatures::all())
.validate_all(&bytes)
.context("failed to validate component output")?;
let wat = wasmprinter::print_bytes(&bytes).context("failed to print bytes")?;
assert_output(&wat, &component_path)?;
let mut parser = Parser::new(0);
parser.set_features(WasmFeatures::all());
let (pkg, resolve) = match wit_component::decode_reader(bytes.as_slice())
.context("failed to decode resolve")?
{
DecodedWasm::WitPackage(..) => unreachable!(),
DecodedWasm::Component(resolve, world) => (resolve.worlds[world].package.unwrap(), resolve),
};
let mut printer = WitPrinter::default();
printer
.print(&resolve, pkg, &[])
.context("failed to print WIT")?;
let wit = printer.output.to_string();
assert_output(&wit, &component_wit_path)?;
UnresolvedPackageGroup::parse(&component_wit_path, &wit)
.context("failed to parse printed WIT")?;
match Payload::from_binary(&bytes).unwrap() {
Payload::Component { children, .. } => match &children[0] {
Payload::Module(Metadata { producers, .. }) => {
let producers = producers.as_ref().expect("child module has producers");
let processed_by = producers
.get("processed-by")
.expect("child has processed-by section");
assert_eq!(
processed_by
.get("wit-component")
.expect("wit-component producer present"),
env!("CARGO_PKG_VERSION")
);
if module_path.is_file() {
assert_eq!(
processed_by
.get("my-fake-bindgen")
.expect("added bindgen field present"),
"123.45"
);
} else {
}
}
_ => panic!("expected child to be a module"),
},
_ => panic!("expected top level metadata of component"),
}
Ok(())
}
fn read_name_and_module(
prefix: &str,
path: &Path,
resolve: &Resolve,
pkg: PackageId,
) -> Result<(String, Vec<u8>)> {
let wasm = read_core_module(path, resolve, pkg)
.with_context(|| format!("failed to read core module at {path:?}"))?;
let stem = path.file_stem().unwrap().to_str().unwrap();
let name = if let Some(name) = fs::read_to_string(path)?
.lines()
.next()
.and_then(|line| line.strip_prefix(";; module name: "))
{
name.to_owned()
} else {
stem.trim_start_matches(prefix).to_owned()
};
Ok((name, wasm))
}
fn read_core_module(path: &Path, resolve: &Resolve, pkg: PackageId) -> Result<Vec<u8>> {
let mut wasm = wat::parse_file(path)?;
let name = path.file_stem().and_then(|s| s.to_str()).unwrap();
let world = resolve
.select_world(&[pkg], Some(name))
.context("failed to select a world")?;
let mut producers = wasm_metadata::Producers::empty();
producers.add("processed-by", "my-fake-bindgen", "123.45");
let encoded =
wit_component::metadata::encode(resolve, world, StringEncoding::UTF8, Some(&producers))?;
let section = wasm_encoder::CustomSection {
name: "component-type".into(),
data: Cow::Borrowed(&encoded),
};
wasm.push(section.id());
section.encode(&mut wasm);
Ok(wasm)
}
fn assert_output(contents: &str, path: &Path) -> Result<()> {
let contents = contents.replace("\r\n", "\n").replace(
concat!("\"", env!("CARGO_PKG_VERSION"), "\""),
"\"$CARGO_PKG_VERSION\"",
);
if std::env::var_os("BLESS").is_some() {
fs::write(path, contents)?;
} else {
match fs::read_to_string(path) {
Ok(expected) => {
assert_eq!(
expected.replace("\r\n", "\n").trim(),
contents.trim(),
"failed baseline comparison ({})",
path.display(),
);
}
Err(_) => {
panic!("expected {path:?} to contain\n{contents}");
}
}
}
Ok(())
}