use anyhow::{bail, Context, Result};
use pretty_assertions::assert_eq;
use semver::Version;
use serde::Deserialize;
use std::{
collections::HashMap,
ffi::OsStr,
fs::{self, File},
path::{Path, PathBuf},
};
use wac_graph::{types::Package, CompositionGraph, EncodeOptions, NodeId, PackageId};
use wit_component::{ComponentEncoder, StringEncoding};
use wit_parser::Resolve;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
enum Node {
Import {
name: String,
package: usize,
export: String,
},
Instantiation {
package: usize,
},
Alias {
source: usize,
export: String,
},
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Argument {
source: usize,
target: usize,
name: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct WatPackage {
name: String,
version: Option<Version>,
path: PathBuf,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Name {
node: usize,
name: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Export {
node: usize,
name: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct GraphFile {
#[serde(default)]
packages: Vec<WatPackage>,
#[serde(default)]
nodes: Vec<Node>,
#[serde(default)]
arguments: Vec<Argument>,
#[serde(default)]
names: Vec<Name>,
#[serde(default)]
exports: Vec<Export>,
}
impl GraphFile {
pub fn into_composition_graph(self, root: &Path, test_case: &str) -> Result<CompositionGraph> {
let mut graph = CompositionGraph::new();
let packages = self.register_packages(root, test_case, &mut graph)?;
let nodes = self.add_nodes(test_case, &packages, &mut graph)?;
self.add_arguments(test_case, &nodes, &mut graph)?;
for (index, export) in self.exports.iter().enumerate() {
let id = nodes
.get(&export.node)
.with_context(|| format!("invalid node index {export} referenced in export {index} for test case `{test_case}`", export = export.node))
.copied()?;
graph.export(id, &export.name).with_context(|| {
format!(
"failed to export node {node} in export {index} for test case `{test_case}`",
node = export.node
)
})?;
}
for (index, name) in self.names.iter().enumerate() {
let id = nodes
.get(&name.node)
.with_context(|| format!("invalid node index {node} referenced in name {index} for test case `{test_case}`", node = name.node))
.copied()?;
graph.set_node_name(id, &name.name);
}
Ok(graph)
}
fn load_wit_package(test_case: &str, path: &Path) -> Result<Vec<u8>> {
let mut resolve = Resolve::default();
let (id, _) = resolve.push_path(path).with_context(|| {
format!(
"failed to parse package file `{path}` for test case `{test_case}`",
path = path.display()
)
})?;
let world = resolve.select_world(&[id], None).with_context(|| {
format!(
"failed to select world from `{path}` for test case `{test_case}`",
path = path.display()
)
})?;
let mut module = wit_component::dummy_module(
&resolve,
world,
wit_parser::ManglingAndAbi::Legacy(wit_parser::LiftLowerAbi::Sync),
);
wit_component::embed_component_metadata(
&mut module,
&resolve,
world,
StringEncoding::default(),
)
.with_context(|| {
format!(
"failed to embed component metadata from package `{path}` for test case `{test_case}`",
path = path.display()
)
})?;
let mut encoder = ComponentEncoder::default().validate(true).module(&module)?;
encoder
.encode()
.with_context(|| format!("failed to encode a component from module derived from package `{path}` for test case `{test_case}`", path = path.display()))
}
fn register_packages(
&self,
root: &Path,
test_case: &str,
graph: &mut CompositionGraph,
) -> Result<HashMap<usize, PackageId>> {
let mut packages = HashMap::new();
for (index, package) in self.packages.iter().enumerate() {
let path = root.join(&package.path);
let bytes = match path.extension().and_then(OsStr::to_str) {
Some("wit") => Self::load_wit_package(test_case, &path)?,
Some("wat") => wat::parse_file(&path).with_context(|| {
format!(
"failed to parse package `{path}` for test case `{test_case}`",
path = path.display()
)
})?,
None if path.is_dir() => Self::load_wit_package(test_case, &path)?,
_ => bail!(
"unexpected file extension for package file `{path}`",
path = package.path.display()
),
};
let package = Package::from_bytes(
&package.name,
package.version.as_ref(),
bytes,
graph.types_mut(),
)
.with_context(|| {
format!(
"failed to decode package `{path}` for test case `{test_case}`",
path = path.display()
)
})?;
let id = graph.register_package(package).with_context(|| {
format!(
"failed to register package `{path}` for test case `{test_case}`",
path = path.display()
)
})?;
packages.insert(index, id);
}
Ok(packages)
}
fn add_nodes(
&self,
test_case: &str,
packages: &HashMap<usize, PackageId>,
graph: &mut CompositionGraph,
) -> Result<HashMap<usize, NodeId>> {
let mut nodes = HashMap::new();
for (index, node) in self.nodes.iter().enumerate() {
let id = match node {
Node::Import {
name,
package,
export,
} => {
let id = packages.get(package).with_context(|| {
format!("invalid package index {package} referenced in node {index} for test case `{test_case}`")
}).copied()?;
let kind = graph.types()[graph[id].ty()].exports.get(export).copied().with_context(|| {
format!("invalid package export `{export}` referenced in node {index} for test case `{test_case}`")
})?.promote();
graph.import(name, kind).with_context(|| {
format!("failed to add import node {index} for test case `{test_case}`")
})?
}
Node::Instantiation { package } => {
let package = packages
.get(package)
.with_context(|| {
format!("invalid package index {package} referenced in node {index} for test case `{test_case}`")
})
.copied()?;
graph.instantiate(package)
}
Node::Alias { source, export } => {
let source = nodes.get(source).with_context(|| {
format!("invalid source node index {source} referenced in node {index} for test case `{test_case}`")
})
.copied()?;
graph
.alias_instance_export(source, export)
.with_context(|| {
format!("failed to add alias node {index} for test case `{test_case}`")
})?
}
};
nodes.insert(index, id);
}
Ok(nodes)
}
fn add_arguments(
&self,
test_case: &str,
nodes: &HashMap<usize, NodeId>,
graph: &mut CompositionGraph,
) -> Result<()> {
for (index, argument) in self.arguments.iter().enumerate() {
let source = nodes.get(&argument.source).with_context(|| {
format!("invalid source node index {source} referenced in argument {index} for test case `{test_case}`", source = argument.source)
}).copied()?;
let target = nodes.get(&argument.target).with_context(|| {
format!("invalid target node index {target} referenced in argument {index} for test case `{test_case}`", target = argument.target)
}).copied()?;
graph.set_instantiation_argument(target, &argument.name, source).with_context(|| {
format!("failed to add argument edge from source node {source} to target node {target} referenced in argument {index} for test case `{test_case}`", source = argument.source, target = argument.target)
})?;
}
Ok(())
}
}
#[test]
fn encoding() -> Result<()> {
for entry in fs::read_dir("tests/graphs")? {
let path = entry?.path();
if !path.is_dir() {
continue;
}
let test_case = path.file_stem().unwrap().to_str().unwrap();
println!("================ test: {test_case} ================");
let graph = path.join("graph.json");
let output_path = path.join("encoded.wat");
let error_path = path.join("error.txt");
let file = File::open(&graph).with_context(|| {
format!("failed to read graph file `{path}`", path = graph.display())
})?;
let r = serde_json::from_reader::<_, GraphFile>(file)
.with_context(|| {
format!(
"failed to deserialize graph file `{path}` for test case `{test_case}`",
path = graph.display()
)
})?
.into_composition_graph(&path, test_case)
.and_then(|graph| {
println!("{:?}", graph);
graph
.encode(EncodeOptions {
define_components: false,
validate: true,
..Default::default()
})
.context("failed to encode the graph")
});
let (output, baseline_path) = if error_path.is_file() {
match r {
Ok(_) => bail!("expected a test failure for test case `{test_case}`"),
Err(e) => (format!("{e:?}\n").replace('\\', "/"), &error_path),
}
} else {
let bytes =
r.with_context(|| format!("failed to encode for test case `{test_case}`"))?;
(
wasmprinter::print_bytes(&bytes).with_context(|| {
format!("failed to print encoded bytes for test case `{test_case}`")
})?,
&output_path,
)
};
if std::env::var_os("BLESS").is_some() {
fs::write(baseline_path, output)?;
} else {
assert_eq!(
fs::read_to_string(baseline_path)
.with_context(|| format!(
"failed to read test baseline `{path}`",
path = baseline_path.display()
))?
.replace("\r\n", "\n"),
output,
"failed baseline comparison for test case `{test_case}` ({path})",
path = baseline_path.display(),
);
}
}
Ok(())
}