1 use libtest_mimic::{Arguments, FormatSetting, Trial}; 2 use std::sync::{Condvar, LazyLock, Mutex}; 3 use wasmtime::{ 4 Config, Enabled, Engine, InstanceAllocationStrategy, PoolingAllocationConfig, bail, 5 error::Context as _, 6 }; 7 use wasmtime_test_util::wast::{Collector, Compiler, WastConfig, WastTest, limits}; 8 use wasmtime_wast::{Async, SpectestConfig, WastContext}; 9 10 fn main() { 11 env_logger::init(); 12 13 let tests = if cfg!(miri) { 14 Vec::new() 15 } else { 16 wasmtime_test_util::wast::find_tests(env!("CARGO_MANIFEST_DIR").as_ref()).unwrap() 17 }; 18 19 let mut trials = Vec::new(); 20 21 let mut add_trial = |test: &WastTest, config: WastConfig| { 22 let trial = Trial::test( 23 format!( 24 "{:?}/{}{}{}", 25 config.compiler, 26 if config.pooling { "pooling/" } else { "" }, 27 if config.collector != Collector::Auto { 28 format!("{:?}/", config.collector) 29 } else { 30 String::new() 31 }, 32 test.path.to_str().unwrap() 33 ), 34 { 35 let test = test.clone(); 36 move || run_wast(&test, config).map_err(|e| format!("{e:?}").into()) 37 }, 38 ); 39 40 trials.push(trial); 41 }; 42 43 // List of supported compilers, filtered by what our current host supports. 44 let mut compilers = vec![ 45 Compiler::CraneliftNative, 46 Compiler::Winch, 47 Compiler::CraneliftPulley, 48 ]; 49 compilers.retain(|c| c.supports_host()); 50 51 // Only test one compiler in ASAN since we're mostly interested in testing 52 // runtime code, not compiler-generated code. 53 if cfg!(asan) { 54 compilers.truncate(1); 55 } 56 57 // Run each wast test in a few interesting configuration combinations, but 58 // leave the full combinatorial matrix and such to fuzz testing which 59 // configures many more settings than those configured here. 60 for test in tests { 61 let collector = if test.test_uses_gc_types() { 62 Collector::DeferredReferenceCounting 63 } else { 64 Collector::Auto 65 }; 66 67 // Run this test in all supported compilers. 68 for compiler in compilers.iter().copied() { 69 add_trial( 70 &test, 71 WastConfig { 72 compiler, 73 pooling: false, 74 collector, 75 }, 76 ); 77 } 78 79 // Don't do extra tests in ASAN as it takes awhile and is unlikely to 80 // reap much benefit. 81 if cfg!(asan) { 82 continue; 83 } 84 85 let compiler = compilers[0]; 86 87 // Run this test with the pooling allocator under the default compiler. 88 add_trial( 89 &test, 90 WastConfig { 91 compiler, 92 pooling: true, 93 collector, 94 }, 95 ); 96 97 // If applicable, also run with the null collector in addition to the 98 // default collector. 99 if test.test_uses_gc_types() { 100 add_trial( 101 &test, 102 WastConfig { 103 compiler, 104 pooling: false, 105 collector: Collector::Null, 106 }, 107 ); 108 } 109 } 110 111 // There's a lot of tests so print only a `.` to keep the output a 112 // bit more terse by default. 113 let mut args = Arguments::from_args(); 114 if args.format.is_none() { 115 args.format = Some(FormatSetting::Terse); 116 } 117 libtest_mimic::run(&args, trials).exit() 118 } 119 120 // Each of the tests included from `wast_testsuite_tests` will call this 121 // function which actually executes the `wast` test suite given the `strategy` 122 // to compile it. 123 fn run_wast(test: &WastTest, config: WastConfig) -> wasmtime::Result<()> { 124 let test_config = test.config.clone(); 125 126 // Determine whether this test is expected to fail or pass. Regardless the 127 // test is executed and the result of the execution is asserted to match 128 // this expectation. Note that this means that the test can't, for example, 129 // panic or segfault as a result. 130 // 131 // Updates to whether a test should pass or fail should be done in the 132 // `crates/wast-util/src/lib.rs` file. 133 let should_fail = test.should_fail(&config); 134 135 let multi_memory = test_config.multi_memory(); 136 let test_hogs_memory = test_config.hogs_memory(); 137 let relaxed_simd = test_config.relaxed_simd(); 138 139 let is_cranelift = match config.compiler { 140 Compiler::CraneliftNative | Compiler::CraneliftPulley => true, 141 _ => false, 142 }; 143 144 let mut cfg = Config::new(); 145 cfg.shared_memory(true); 146 wasmtime_test_util::wasmtime_wast::apply_test_config(&mut cfg, &test_config); 147 wasmtime_test_util::wasmtime_wast::apply_wast_config(&mut cfg, &config); 148 149 if is_cranelift { 150 cfg.cranelift_debug_verifier(true); 151 cfg.cranelift_wasmtime_debug_checks(true); 152 } 153 154 // By default we'll allocate huge chunks (6gb) of the address space for each 155 // linear memory. This is typically fine but when we emulate tests with QEMU 156 // it turns out that it causes memory usage to balloon massively. Leave a 157 // knob here so on CI we can cut down the memory usage of QEMU and avoid the 158 // OOM killer. 159 // 160 // Locally testing this out this drops QEMU's memory usage running this 161 // tests suite from 10GiB to 600MiB. Previously we saw that crossing the 162 // 10GiB threshold caused our processes to get OOM killed on CI. 163 // 164 // Note that this branch is also taken for 32-bit platforms which generally 165 // can't test much of the pooling allocator as the virtual address space is 166 // so limited. 167 if cfg!(target_pointer_width = "32") || std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() { 168 // The pooling allocator hogs ~6TB of virtual address space for each 169 // store, so if we don't to hog memory then ignore pooling tests. 170 if config.pooling { 171 return Ok(()); 172 } 173 174 // If the test allocates a lot of memory, that's considered "hogging" 175 // memory, so skip it. 176 if test_hogs_memory { 177 return Ok(()); 178 } 179 180 // Don't use 4gb address space reservations when not hogging memory, and 181 // also don't reserve lots of memory after dynamic memories for growth 182 // (makes growth slower). 183 cfg.memory_reservation(2 * u64::from(wasmtime_environ::Memory::DEFAULT_PAGE_SIZE)); 184 cfg.memory_reservation_for_growth(0); 185 186 let small_guard = 64 * 1024; 187 cfg.memory_guard_size(small_guard); 188 } 189 190 let _pooling_lock = if config.pooling { 191 // Some memory64 tests take more than 4gb of resident memory to test, 192 // but we don't want to configure the pooling allocator to allow that 193 // (that's a ton of memory to reserve), so we skip those tests. 194 if test_hogs_memory { 195 return Ok(()); 196 } 197 198 // Reduce the virtual memory required to run multi-memory-based tests. 199 // 200 // The configuration parameters below require that a bare minimum 201 // virtual address space reservation of 450*9*805*65536 == 200G be made 202 // to support each test. If 6G reservations are made for each linear 203 // memory then not that many tests can run concurrently with much else. 204 // 205 // When multiple memories are used and are configured in the pool then 206 // force the usage of static memories without guards to reduce the VM 207 // impact. 208 let max_memory_size = limits::MEMORY_SIZE; 209 if multi_memory { 210 cfg.memory_reservation(max_memory_size as u64); 211 cfg.memory_reservation_for_growth(0); 212 cfg.memory_guard_size(0); 213 } 214 215 let mut pool = PoolingAllocationConfig::default(); 216 pool.total_memories(limits::MEMORIES * 2) 217 .max_memory_protection_keys(2) 218 .max_memory_size(max_memory_size) 219 .max_memories_per_module(if multi_memory { 220 limits::MEMORIES_PER_MODULE 221 } else { 222 1 223 }) 224 .max_tables_per_module(limits::TABLES_PER_MODULE); 225 226 // When testing, we may choose to start with MPK force-enabled to ensure 227 // we use that functionality. 228 if std::env::var("WASMTIME_TEST_FORCE_MPK").is_ok() { 229 pool.memory_protection_keys(Enabled::Yes); 230 } 231 232 cfg.allocation_strategy(InstanceAllocationStrategy::Pooling(pool)); 233 Some(lock_pooling()) 234 } else { 235 None 236 }; 237 238 let mut engines = vec![(Engine::new(&cfg), "default")]; 239 240 // For tests that use relaxed-simd test both the default engine and the 241 // guaranteed-deterministic engine to ensure that both the 'native' 242 // semantics of the instructions plus the canonical semantics work. 243 if relaxed_simd { 244 engines.push(( 245 Engine::new(cfg.relaxed_simd_deterministic(true)), 246 "deterministic", 247 )); 248 } 249 250 for (engine, desc) in engines { 251 let result = engine.and_then(|engine| { 252 let mut wast_context = WastContext::new(&engine, Async::Yes, |_store| {}); 253 wast_context.generate_dwarf(true); 254 wast_context.register_spectest(&SpectestConfig { 255 use_shared_memory: true, 256 suppress_prints: true, 257 })?; 258 wast_context 259 .run_wast(test.path.to_str().unwrap(), test.contents.as_bytes()) 260 .with_context(|| format!("failed to run spec test with {desc} engine")) 261 }); 262 263 if should_fail { 264 if result.is_ok() { 265 bail!("this test is flagged as should-fail but it succeeded") 266 } 267 } else { 268 result?; 269 } 270 } 271 272 Ok(()) 273 } 274 275 // The pooling tests make about 6TB of address space reservation which means 276 // that we shouldn't let too many of them run concurrently at once. On 277 // high-cpu-count systems (e.g. 80 threads) this leads to mmap failures because 278 // presumably too much of the address space has been reserved with our limits 279 // specified above. By keeping the number of active pooling-related tests to a 280 // specified maximum we can put a cap on the virtual address space reservations 281 // made. 282 fn lock_pooling() -> impl Drop { 283 const MAX_CONCURRENT_POOLING: u32 = 4; 284 285 static ACTIVE: LazyLock<MyState> = LazyLock::new(MyState::default); 286 287 #[derive(Default)] 288 struct MyState { 289 lock: Mutex<u32>, 290 waiters: Condvar, 291 } 292 293 impl MyState { 294 fn lock(&self) -> impl Drop + '_ { 295 let state = self.lock.lock().unwrap(); 296 let mut state = self 297 .waiters 298 .wait_while(state, |cnt| *cnt >= MAX_CONCURRENT_POOLING) 299 .unwrap(); 300 *state += 1; 301 LockGuard { state: self } 302 } 303 } 304 305 struct LockGuard<'a> { 306 state: &'a MyState, 307 } 308 309 impl Drop for LockGuard<'_> { 310 fn drop(&mut self) { 311 *self.state.lock.lock().unwrap() -= 1; 312 self.state.waiters.notify_one(); 313 } 314 } 315 316 ACTIVE.lock() 317 } 318