//! Load all wat files to structured tests.
use anyhow::Result;
use proc_macro2::Span;
use quote::{quote, ToTokens};
use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
};
use syn::{parse_quote, ExprArray, ExprMatch, Ident, ItemImpl, ItemMod};
fn main() -> Result<()> {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=wat");
let tests = Tests::new().parse()?;
fs::write(
env::var("OUT_DIR")?.parse::<PathBuf>()?.join("tests.rs"),
tests.to_token_stream().to_string(),
)?;
Ok(())
}
/// Read the contents of a directory, returning
/// all wat files.
fn list_wat(dir: impl AsRef<Path>, files: &mut Vec<PathBuf>) -> Result<()> {
let entry = fs::read_dir(dir)?;
for entry in entry {
let entry = entry?;
let path = entry.path();
if path.ends_with("as_if_else.wat") {
continue;
}
if path.is_dir() {
list_wat(path, files)?;
} else if path.extension().unwrap_or_default() == "wat" {
files.push(path);
}
}
Ok(())
}
/// Batch all wat files.
fn wat_files() -> Result<Vec<PathBuf>> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("wat");
let mut files = Vec::new();
list_wat(&path, &mut files)?;
Ok(files)
}
fn examples() -> Result<Vec<PathBuf>> {
let release = cargo_metadata::MetadataCommand::new()
.no_deps()
.exec()?
.target_directory
.join("wasm32-unknown-unknown")
.join("release")
.join("examples");
if !release.exists() {
return Ok(Default::default());
}
let with_commit_hash = |p: &PathBuf| -> bool {
let name = p
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
// example: addition-6313c94b67ad9699.wasm
let len = name.len();
if let Some(index) = name.rfind('-') {
if len > 22 && index == len - 22 {
return true;
}
}
false
};
let files = fs::read_dir(release)?
.filter_map(|e| {
let path = e.ok()?.path();
if path.extension().unwrap_or_default() == "wasm" && !with_commit_hash(&path) {
Some(path)
} else {
None
}
})
.collect::<Vec<_>>();
for wasm in &files {
zinkc::utils::wasm_opt(wasm, wasm)?;
}
Ok(files)
}
struct Tests {
match_expr: ExprMatch,
item_impl: ItemImpl,
modules: BTreeMap<String, ItemMod>,
}
impl Tests {
fn new() -> Self {
Self {
match_expr: parse_quote! {
match (module, name) {}
},
item_impl: parse_quote! {
impl Test {}
},
modules: Default::default(),
}
}
fn file_name(p: impl AsRef<Path>) -> String {
p.as_ref()
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string()
}
fn get_module(&mut self, mut module: &str) -> &mut ItemMod {
module = match module {
"if" => "_if",
"loop" => "_loop",
_ => module,
};
if !self.modules.contains_key(module) {
let ident = Ident::new(module, Span::call_site());
self.modules.insert(
module.to_string(),
parse_quote! {
#[cfg(test)]
mod #ident {
use anyhow::Result;
use crate::Test;
}
},
);
}
self.modules.get_mut(module).expect("module not found")
}
/// Push test to module.
fn push(&mut self, p: &Path, wasm: &[u8]) -> Result<()> {
let (module, name) = (
Self::file_name(p.parent().expect("parent not found")),
Self::file_name(p.with_extension("")),
);
let ident_name = name.replace('-', "_");
let ident = Ident::new(
&(module.clone() + "_" + &ident_name).to_uppercase(),
Span::call_site(),
);
{
let len = wasm.len();
let mut expr: ExprArray = parse_quote!([]);
for byte in wasm {
expr.elems.push(parse_quote!(#byte));
}
self.item_impl.items.push(parse_quote! {
#[doc = concat!(" path: ", #module, "::", #name)]
pub const #ident: [u8; #len] = #expr;
});
}
self.match_expr.arms.push(parse_quote! {
(#module, #name) => Test {
module: module.into(),
name: name.into(),
wasm: Self::#ident.to_vec(),
}
});
let ident_name = Ident::new(&ident_name, Span::call_site());
self.get_module(&module)
.content
.as_mut()
.expect("")
.1
.push(parse_quote! {
#[test]
fn #ident_name() -> Result<()> {
Test::load(#module, #name)?.compile()
}
});
Ok(())
}
fn parse(mut self) -> Result<Self> {
for wat in wat_files()? {
let wat_bytes = fs::read(&wat)?;
let wasm = wat::parse_bytes(&wat_bytes)?;
self.push(&wat, &wasm)?;
}
for example in examples()? {
let wasm = fs::read(&example)?;
self.push(&example, &wasm)?;
}
self.match_expr.arms.push(parse_quote! {
_ => return Err(anyhow::anyhow!("test not found: {{module: {}, name: {}}}", module, name))
});
let match_expr = self.match_expr.clone();
let funcs: ItemImpl = parse_quote! {
impl Test {
/// Load test from module and name.
pub fn load(module: &str, name: &str) -> anyhow::Result<Self> {
Ok(#match_expr)
}
}
};
self.item_impl.items.extend(funcs.items);
Ok(self)
}
}
impl ToTokens for Tests {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Tests {
item_impl,
modules,
match_expr: _,
} = self;
tokens.extend(quote!(#item_impl));
modules.values().for_each(|m| tokens.extend(quote!(#m)))
}
}