1 //! Build program to generate a program which runs all the testsuites. 2 //! 3 //! By generating a separate `#[test]` test for each file, we allow cargo test 4 //! to automatically run the files in parallel. 5 6 fn main() { 7 #[cfg(feature = "test_programs")] 8 wasi_tests::build_and_generate_tests() 9 } 10 11 #[cfg(feature = "test_programs")] 12 mod wasi_tests { 13 use std::env; 14 use std::fs::{read_dir, File}; 15 use std::io::{self, Write}; 16 use std::path::{Path, PathBuf}; 17 use std::process::{Command, Stdio}; 18 19 pub(super) fn build_and_generate_tests() { 20 // Validate if any of test sources are present and if they changed 21 // This should always work since there is no submodule to init anymore 22 let bin_tests = std::fs::read_dir("wasi-tests/src/bin").unwrap(); 23 for test in bin_tests { 24 if let Ok(test_file) = test { 25 let test_file_path = test_file 26 .path() 27 .into_os_string() 28 .into_string() 29 .expect("test file path"); 30 println!("cargo:rerun-if-changed={}", test_file_path); 31 } 32 } 33 println!("cargo:rerun-if-changed=wasi-tests/Cargo.toml"); 34 println!("cargo:rerun-if-changed=wasi-tests/src/lib.rs"); 35 // Build tests to OUT_DIR (target/*/build/wasi-common-*/out/wasm32-wasi/release/*.wasm) 36 let out_dir = PathBuf::from( 37 env::var("OUT_DIR").expect("The OUT_DIR environment variable must be set"), 38 ); 39 let mut out = 40 File::create(out_dir.join("wasi_tests.rs")).expect("error generating test source file"); 41 build_tests("wasi-tests", &out_dir).expect("building tests"); 42 test_directory(&mut out, "wasi-cap-std-sync", "cap_std_sync", &out_dir) 43 .expect("generating wasi-cap-std-sync tests"); 44 test_directory(&mut out, "wasi-tokio", "tokio", &out_dir) 45 .expect("generating wasi-tokio tests"); 46 } 47 48 fn build_tests(testsuite: &str, out_dir: &Path) -> io::Result<()> { 49 let mut cmd = Command::new("cargo"); 50 cmd.env("CARGO_PROFILE_RELEASE_DEBUG", "1"); 51 cmd.env_remove("CARGO_ENCODED_RUSTFLAGS"); 52 cmd.args(&[ 53 "build", 54 "--release", 55 "--target=wasm32-wasi", 56 "--target-dir", 57 out_dir.to_str().unwrap(), 58 ]) 59 .stdout(Stdio::inherit()) 60 .stderr(Stdio::inherit()) 61 .current_dir(testsuite); 62 let output = cmd.output()?; 63 64 let status = output.status; 65 if !status.success() { 66 panic!( 67 "Building tests failed: exit code: {}", 68 status.code().unwrap() 69 ); 70 } 71 72 Ok(()) 73 } 74 75 fn test_directory( 76 out: &mut File, 77 testsuite: &str, 78 runtime: &str, 79 out_dir: &Path, 80 ) -> io::Result<()> { 81 let mut dir_entries: Vec<_> = read_dir(out_dir.join("wasm32-wasi/release")) 82 .expect("reading testsuite directory") 83 .map(|r| r.expect("reading testsuite directory entry")) 84 .filter(|dir_entry| { 85 let p = dir_entry.path(); 86 if let Some(ext) = p.extension() { 87 // Only look at wast files. 88 if ext == "wasm" { 89 // Ignore files starting with `.`, which could be editor temporary files 90 if let Some(stem) = p.file_stem() { 91 if let Some(stemstr) = stem.to_str() { 92 if !stemstr.starts_with('.') { 93 return true; 94 } 95 } 96 } 97 } 98 } 99 false 100 }) 101 .collect(); 102 103 dir_entries.sort_by_key(|dir| dir.path()); 104 105 writeln!( 106 out, 107 "mod {} {{", 108 Path::new(testsuite) 109 .file_stem() 110 .expect("testsuite filename should have a stem") 111 .to_str() 112 .expect("testsuite filename should be representable as a string") 113 .replace("-", "_") 114 )?; 115 writeln!( 116 out, 117 " use super::{{runtime::{} as runtime, utils, setup_log}};", 118 runtime 119 )?; 120 for dir_entry in dir_entries { 121 write_testsuite_tests(out, &dir_entry.path(), testsuite)?; 122 } 123 writeln!(out, "}}")?; 124 Ok(()) 125 } 126 127 fn write_testsuite_tests(out: &mut File, path: &Path, testsuite: &str) -> io::Result<()> { 128 let stemstr = path 129 .file_stem() 130 .expect("file_stem") 131 .to_str() 132 .expect("to_str"); 133 134 writeln!(out, " #[test]")?; 135 let test_fn_name = stemstr.replace("-", "_"); 136 if ignore(testsuite, &test_fn_name) { 137 writeln!(out, " #[ignore]")?; 138 } 139 writeln!(out, " fn r#{}() -> anyhow::Result<()> {{", test_fn_name,)?; 140 writeln!(out, " setup_log();")?; 141 writeln!( 142 out, 143 " let path = std::path::Path::new(r#\"{}\"#);", 144 path.display() 145 )?; 146 writeln!(out, " let data = wat::parse_file(path)?;")?; 147 writeln!( 148 out, 149 " let bin_name = utils::extract_exec_name_from_path(path)?;" 150 )?; 151 let workspace = if no_preopens(testsuite, stemstr) { 152 "None" 153 } else { 154 writeln!( 155 out, 156 " let workspace = utils::prepare_workspace(&bin_name)?;" 157 )?; 158 "Some(workspace.path())" 159 }; 160 writeln!( 161 out, 162 " runtime::{}(&data, &bin_name, {})", 163 if inherit_stdio(testsuite, stemstr) { 164 "instantiate_inherit_stdio" 165 } else { 166 "instantiate" 167 }, 168 workspace, 169 )?; 170 writeln!(out, " }}")?; 171 writeln!(out)?; 172 Ok(()) 173 } 174 175 fn ignore(testsuite: &str, name: &str) -> bool { 176 match testsuite { 177 "wasi-cap-std-sync" => cap_std_sync_ignore(name), 178 "wasi-virtfs" => virtfs_ignore(name), 179 "wasi-tokio" => tokio_ignore(name), 180 _ => panic!("unknown test suite: {}", testsuite), 181 } 182 } 183 184 #[cfg(not(windows))] 185 /// Ignore tests that aren't supported yet. 186 fn cap_std_sync_ignore(name: &str) -> bool { 187 [ 188 // Trailing slash related bugs: 189 "path_rename_file_trailing_slashes", 190 "remove_directory_trailing_slashes", 191 ] 192 .contains(&name) 193 } 194 195 #[cfg(windows)] 196 /// Ignore tests that aren't supported yet. 197 fn cap_std_sync_ignore(name: &str) -> bool { 198 [ 199 // Trailing slash related bugs 200 "interesting_paths", 201 "path_rename_file_trailing_slashes", 202 "remove_directory_trailing_slashes", 203 ] 204 .contains(&name) 205 } 206 207 /// Tokio should support the same things as cap_std_sync 208 fn tokio_ignore(name: &str) -> bool { 209 cap_std_sync_ignore(name) 210 } 211 /// Virtfs barely works at all and is not suitable for any purpose 212 fn virtfs_ignore(name: &str) -> bool { 213 [ 214 "dangling_fd", 215 "dangling_symlink", 216 "directory_seek", 217 "fd_advise", 218 "fd_filestat_set", 219 "fd_flags_set", 220 "fd_readdir", 221 "file_allocate", 222 "file_pread_pwrite", 223 "file_seek_tell", 224 "file_truncation", 225 "file_unbuffered_write", 226 "interesting_paths", 227 "isatty", 228 "nofollow_errors", 229 "path_filestat", 230 "path_link", 231 "path_open_create_existing", 232 "path_open_dirfd_not_dir", 233 "path_open_read_without_rights", 234 "path_rename", 235 "path_rename_dir_trailing_slashes", 236 "path_rename_file_trailing_slashes", 237 "path_symlink_trailing_slashes", 238 "poll_oneoff", 239 "poll_oneoff_stdio", 240 "readlink", 241 "remove_directory_trailing_slashes", 242 "remove_nonempty_directory", 243 "renumber", 244 "symlink_create", 245 "symlink_filestat", 246 "symlink_loop", 247 "truncation_rights", 248 "unlink_file_trailing_slashes", 249 ] 250 .contains(&name) 251 } 252 253 /// Mark tests which do not require preopens 254 fn no_preopens(testsuite: &str, name: &str) -> bool { 255 if testsuite.starts_with("wasi-") { 256 match name { 257 "big_random_buf" => true, 258 "clock_time_get" => true, 259 "sched_yield" => true, 260 "poll_oneoff_stdio" => true, 261 _ => false, 262 } 263 } else { 264 panic!("unknown test suite {}", testsuite) 265 } 266 } 267 268 /// Mark tests which require inheriting parent process stdio 269 fn inherit_stdio(testsuite: &str, name: &str) -> bool { 270 match testsuite { 271 "wasi-cap-std-sync" | "wasi-tokio" => match name { 272 "poll_oneoff_stdio" => true, 273 _ => false, 274 }, 275 "wasi-virtfs" => false, 276 _ => panic!("unknown test suite {}", testsuite), 277 } 278 } 279 } 280