//! 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))) } }