//! This file will run at build time to autogenerate the WASI regression tests //! It will compile the files indicated in TESTS, to:executable and .wasm //! - Compile with the native rust target to get the expected output //! - Compile with the latest WASI target to get the wasm //! - Generate the test that will compare the output of running the .wasm file //! with wasmer with the expected output use glob::glob; use std::collections::HashSet; use std::fs; use std::path::PathBuf; use std::process::Command; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; static BANNER: &str = "// !!! THIS IS A GENERATED FILE !!! // ANY MANUAL EDITS MAY BE OVERWRITTEN AT ANY TIME // Files autogenerated with cargo build (build/wasitests.rs).\n"; pub fn compile(file: &str, ignores: &HashSet) -> Option { let mut output_path = PathBuf::from(file); output_path.set_extension("out"); assert!(file.ends_with(".rs")); let normalized_name = { let mut nn = file.to_lowercase(); nn.truncate(file.len() - 3); nn }; println!("Compiling program {} to native", file); let native_out = Command::new("rustc") .arg(file) .arg("-o") .arg(&normalized_name) .output() .expect("Failed to compile program to native code"); print_info_on_error(&native_out, "COMPILATION FAILED"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let normal_path = PathBuf::from(&normalized_name); let mut perm = normal_path .metadata() .expect("native executable") .permissions(); perm.set_mode(0o766); fs::set_permissions(normal_path, perm).expect("set permissions"); } let rs_module_name = { let temp = PathBuf::from(&normalized_name); temp.file_name().unwrap().to_string_lossy().to_string() }; let result = Command::new(&normalized_name) .output() .expect("Failed to execute native program"); print_info_on_error(&result, "NATIVE PROGRAM FAILED"); std::fs::remove_file(&normalized_name).expect("could not delete executable"); let wasm_out_name = format!("{}.wasm", &normalized_name); let mut file_contents = String::new(); { let mut file = std::fs::File::open(file).unwrap(); file.read_to_string(&mut file_contents).unwrap(); } { let mut actual_file = std::fs::OpenOptions::new() .write(true) .truncate(true) .open(file) .unwrap(); actual_file .write_all(format!("#![feature(wasi_ext)]\n{}", &file_contents).as_bytes()) .unwrap(); } let wasm_compilation_out = Command::new("rustc") .arg("+nightly") .arg("--target=wasm32-wasi") .arg("-C") .arg("opt-level=s") .arg(file) .arg("-o") .arg(&wasm_out_name) .output() .expect("Failed to compile program to native code"); print_info_on_error(&wasm_compilation_out, "WASM COMPILATION"); { let mut actual_file = std::fs::OpenOptions::new() .write(true) .truncate(true) .open(file) .unwrap(); actual_file.write_all(file_contents.as_bytes()).unwrap(); } // to prevent commiting huge binary blobs forever let wasm_strip_out = Command::new("wasm-strip") .arg(&wasm_out_name) .output() .expect("Failed to strip compiled wasm module"); print_info_on_error(&wasm_strip_out, "STRIPPING WASM"); let ignored = if ignores.contains(&rs_module_name) { "\n#[ignore]" } else { "" }; let src_code = fs::read_to_string(file).expect("read src file"); let args: Args = extract_args_from_source_file(&src_code).unwrap_or_default(); let mapdir_args = { let mut out_str = String::new(); out_str.push_str("vec!["); for (alias, real_dir) in args.mapdir { out_str.push_str(&format!( "(\"{}\".to_string(), ::std::path::PathBuf::from(\"{}\")),", alias, real_dir )); } out_str.push_str("]"); out_str }; let envvar_args = { let mut out_str = String::new(); out_str.push_str("vec!["); for entry in args.envvars { out_str.push_str(&format!("\"{}={}\".to_string(),", entry.0, entry.1)); } out_str.push_str("]"); out_str }; let dir_args = { let mut out_str = String::new(); out_str.push_str("vec!["); for entry in args.po_dirs { out_str.push_str(&format!("std::path::PathBuf::from(\"{}\"),", entry)); } out_str.push_str("]"); out_str }; let contents = format!( "{banner} #[test]{ignore} fn test_{rs_module_name}() {{ assert_wasi_output!( \"../../{module_path}\", \"{rs_module_name}\", {dir_args}, {mapdir_args}, {envvar_args}, \"../../{test_output_path}\" ); }} ", banner = BANNER, ignore = ignored, module_path = wasm_out_name, rs_module_name = rs_module_name, test_output_path = format!("{}.out", normalized_name), dir_args = dir_args, mapdir_args = mapdir_args, envvar_args = envvar_args ); let rust_test_filepath = format!( concat!(env!("CARGO_MANIFEST_DIR"), "/tests/{}.rs"), normalized_name, ); fs::write(&rust_test_filepath, contents.as_bytes()).expect("writing test file"); fs::write(&output_path, result.stdout).expect("writing output to file"); Some(rs_module_name) } pub fn build() { let rust_test_modpath = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/wasitests/mod.rs"); let mut modules: Vec = Vec::new(); let ignores = read_ignore_list(); for entry in glob("wasitests/*.rs").unwrap() { match entry { Ok(path) => { let test = path.to_str().unwrap(); if let Some(module_name) = compile(test, &ignores) { modules.push(module_name); } } Err(e) => println!("{:?}", e), } } modules.sort(); let mut modules: Vec = modules.iter().map(|m| format!("mod {};", m)).collect(); assert!(modules.len() > 0, "Expected > 0 modules found"); modules.insert(0, BANNER.to_string()); modules.insert(1, "// The _common module is not autogenerated. It provides common macros for the wasitests\n#[macro_use]\nmod _common;".to_string()); // We add an empty line modules.push("".to_string()); let modfile: String = modules.join("\n"); let source = fs::read(&rust_test_modpath).unwrap(); // We only modify the mod file if has changed if source != modfile.as_bytes() { fs::write(&rust_test_modpath, modfile.as_bytes()).unwrap(); } } fn read_ignore_list() -> HashSet { let f = File::open("wasitests/ignores.txt").unwrap(); let f = BufReader::new(f); f.lines() .filter_map(Result::ok) .map(|v| v.to_lowercase()) .collect() } #[derive(Debug, Default)] struct Args { pub mapdir: Vec<(String, String)>, pub envvars: Vec<(String, String)>, /// pre-opened directories pub po_dirs: Vec, } /// Pulls args to the program out of a comment at the top of the file starting with "// Args:" fn extract_args_from_source_file(source_code: &str) -> Option { if source_code.starts_with("// Args:") { let mut args = Args::default(); for arg_line in source_code .lines() .skip(1) .take_while(|line| line.starts_with("// ")) { let tokenized = arg_line .split_whitespace() // skip trailing space .skip(1) .map(String::from) .collect::>(); let command_name = { let mut cn = tokenized[0].clone(); assert_eq!( cn.pop(), Some(':'), "Final character of argname must be a colon" ); cn }; match command_name.as_ref() { "mapdir" => { if let [alias, real_dir] = &tokenized[1].split(':').collect::>()[..] { args.mapdir.push((alias.to_string(), real_dir.to_string())); } else { eprintln!( "Parse error in mapdir {} not parsed correctly", &tokenized[1] ); } } "env" => { if let [name, val] = &tokenized[1].split('=').collect::>()[..] { args.envvars.push((name.to_string(), val.to_string())); } else { eprintln!("Parse error in env {} not parsed correctly", &tokenized[1]); } } "dir" => { args.po_dirs.push(tokenized[1].to_string()); } e => { eprintln!("WARN: comment arg: {} is not supported", e); } } } return Some(args); } None } fn print_info_on_error(output: &std::process::Output, context: &str) { if !output.status.success() { println!("{}", context); println!( "stdout:\n{}", std::str::from_utf8(&output.stdout[..]).unwrap() ); eprintln!( "stderr:\n{}", std::str::from_utf8(&output.stderr[..]).unwrap() ); } }