| 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/[email protected]`. |
| /// * [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(()) |
| } |