1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
|
use anyhow::{bail, Context, Error, Result};
use libtest_mimic::{Arguments, Trial};
use pretty_assertions::assert_eq;
use std::{borrow::Cow, fs, path::Path};
use wasm_encoder::{Encode, Section};
use wit_component::{ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter};
use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup};
/// Tests the encoding of components.
///
/// This test looks in the `components/` directory for test cases.
///
/// The expected input files for a test case are:
///
/// * [required] `module.wat` *or* some combination of `lib-$name.wat` and
/// `dlopen-lib-$name.wat` - contains the core module definition(s) to be
/// encoded as a component. If one or more `lib-$name.wat` and/or
/// `dlopen-lib-$name.wat` files exist, they will be linked using `Linker`
/// such that the `lib-` ones are not `dlopen`-able but the `dlopen-lib-` ones
/// are.
/// * [required] `module.wit` *or* `lib-$name.wat` and `dlopen-lib-$name.wat`
/// corresponding to the WAT files above - WIT package(s) describing the
/// interfaces of the `module.wat` or `lib-$name.wat` and
/// `dlopen-lib-$name.wat` files. Must have a `default world`
/// * [optional] `adapt-$name.wat` - optional adapter for the module name
/// `$name`, can be specified for multiple `$name`s. Alternatively, if $name
/// doesn't work as part of a filename (e.g. contains forward slashes), it may
/// be specified on the first line of the file with the prefix `;; module name:
/// `, e.g. `;; module name: wasi:cli/environment@0.2.0`.
/// * [optional] `adapt-$name.wit` - required for each `*.wat` adapter to
/// describe imports/exports of the adapter.
/// * [optional] `stub-missing-functions` - if linking libraries and this file
/// exists, `Linker::stub_missing_functions` will be set to `true`. The
/// contents of the file are ignored.
/// * [optional] `use-built-in-libdl` - if linking libraries and this file
/// exists, `Linker::use_built_in_libdl` will be set to `true`. The contents
/// of the file are ignored.
///
/// And the output files are one of the following:
///
/// * `component.wat` - the expected encoded component in text format if the
/// encoding is expected to succeed.
/// * `component.wit` - if `component.wat` exists this is the inferred interface
/// of the component.
/// * `error.txt` - the expected error message if the encoding is expected to
/// fail.
///
/// The test encodes a component based on the input files. If the encoding
/// succeeds, it expects the output to match `component.wat`. If the encoding
/// fails, it expects the output to match `error.txt`.
///
/// Run the test with the environment variable `BLESS` set to update
/// either `component.wat` or `error.txt` depending on the outcome of the encoding.
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)?;
// If this test case contained multiple packages, create separate sub-directories for
// each.
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().module(&module)?.validate(true),
|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<_>>>()?;
// Sort list to ensure deterministic order, which determines priority in cases of duplicate symbols:
libs.sort_by(|(_, a, _), (_, b, _)| a.cmp(b));
let mut linker = Linker::default().validate(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(());
}
};
let wat = wasmprinter::print_bytes(&bytes).context("failed to print bytes")?;
assert_output(&wat, &component_path)?;
let (pkg, resolve) = match wit_component::decode(&bytes).context("failed to decode resolve")? {
DecodedWasm::WitPackage(..) => unreachable!(),
DecodedWasm::Component(resolve, world) => (resolve.worlds[world].package.unwrap(), resolve),
};
let wit = WitPrinter::default()
.print(&resolve, pkg, &[])
.context("failed to print WIT")?;
assert_output(&wit, &component_wit_path)?;
UnresolvedPackageGroup::parse(&component_wit_path, &wit)
.context("failed to parse printed WIT")?;
// Check that the producer data got piped through properly
let metadata = wasm_metadata::Metadata::from_binary(&bytes)?;
match metadata {
// Depends on the ComponentEncoder always putting the first module as the 0th child:
wasm_metadata::Metadata::Component { children, .. } => match children[0].as_ref() {
wasm_metadata::Metadata::Module { 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 {
// Otherwise, we used `Linker`, which synthesizes the
// "main" module and thus won't have `my-fake-bindgen`
}
}
_ => 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))
}
/// Parses the core wasm module at `path`, expected as a `*.wat` file.
///
/// The `resolve` and `pkg` are the parsed WIT package from this test's
/// directory and the `path`'s filename is used to find a WIT document of the
/// corresponding name which should have a world that `path` ascribes to.
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")?;
// Add this producer data to the wit-component metadata so we can make sure it gets through the
// translation:
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(())
}
|