use anyhow::{Context, bail};1use libtest_mimic::{Arguments, FormatSetting, Trial};2use std::sync::{Condvar, LazyLock, Mutex};3use wasmtime::{4Config, Enabled, Engine, InstanceAllocationStrategy, PoolingAllocationConfig, Store,5};6use wasmtime_test_util::wast::{Collector, Compiler, WastConfig, WastTest, limits};7use wasmtime_wast::{Async, SpectestConfig, WastContext};89fn main() {10env_logger::init();1112let tests = if cfg!(miri) {13Vec::new()14} else {15wasmtime_test_util::wast::find_tests(".".as_ref()).unwrap()16};1718let mut trials = Vec::new();1920let mut add_trial = |test: &WastTest, config: WastConfig| {21let trial = Trial::test(22format!(23"{:?}/{}{}{}",24config.compiler,25if config.pooling { "pooling/" } else { "" },26if config.collector != Collector::Auto {27format!("{:?}/", config.collector)28} else {29String::new()30},31test.path.to_str().unwrap()32),33{34let test = test.clone();35move || run_wast(&test, config).map_err(|e| format!("{e:?}").into())36},37);3839trials.push(trial);40};4142// List of supported compilers, filtered by what our current host supports.43let mut compilers = vec![44Compiler::CraneliftNative,45Compiler::Winch,46Compiler::CraneliftPulley,47];48compilers.retain(|c| c.supports_host());4950// Only test one compiler in ASAN since we're mostly interested in testing51// runtime code, not compiler-generated code.52if cfg!(asan) {53compilers.truncate(1);54}5556// Run each wast test in a few interesting configuration combinations, but57// leave the full combinatorial matrix and such to fuzz testing which58// configures many more settings than those configured here.59for test in tests {60let collector = if test.test_uses_gc_types() {61Collector::DeferredReferenceCounting62} else {63Collector::Auto64};6566// Run this test in all supported compilers.67for compiler in compilers.iter().copied() {68add_trial(69&test,70WastConfig {71compiler,72pooling: false,73collector,74},75);76}7778// Don't do extra tests in ASAN as it takes awhile and is unlikely to79// reap much benefit.80if cfg!(asan) {81continue;82}8384let compiler = compilers[0];8586// Run this test with the pooling allocator under the default compiler.87add_trial(88&test,89WastConfig {90compiler,91pooling: true,92collector,93},94);9596// If applicable, also run with the null collector in addition to the97// default collector.98if test.test_uses_gc_types() {99add_trial(100&test,101WastConfig {102compiler,103pooling: false,104collector: Collector::Null,105},106);107}108}109110// There's a lot of tests so print only a `.` to keep the output a111// bit more terse by default.112let mut args = Arguments::from_args();113if args.format.is_none() {114args.format = Some(FormatSetting::Terse);115}116libtest_mimic::run(&args, trials).exit()117}118119// Each of the tests included from `wast_testsuite_tests` will call this120// function which actually executes the `wast` test suite given the `strategy`121// to compile it.122fn run_wast(test: &WastTest, config: WastConfig) -> anyhow::Result<()> {123let test_config = test.config.clone();124125// Determine whether this test is expected to fail or pass. Regardless the126// test is executed and the result of the execution is asserted to match127// this expectation. Note that this means that the test can't, for example,128// panic or segfault as a result.129//130// Updates to whether a test should pass or fail should be done in the131// `crates/wast-util/src/lib.rs` file.132let should_fail = test.should_fail(&config);133134let multi_memory = test_config.multi_memory();135let test_hogs_memory = test_config.hogs_memory();136let relaxed_simd = test_config.relaxed_simd();137138let is_cranelift = match config.compiler {139Compiler::CraneliftNative | Compiler::CraneliftPulley => true,140_ => false,141};142143let mut cfg = Config::new();144cfg.async_support(true);145wasmtime_test_util::wasmtime_wast::apply_test_config(&mut cfg, &test_config);146wasmtime_test_util::wasmtime_wast::apply_wast_config(&mut cfg, &config);147148if is_cranelift {149cfg.cranelift_debug_verifier(true);150}151152// By default we'll allocate huge chunks (6gb) of the address space for each153// linear memory. This is typically fine but when we emulate tests with QEMU154// it turns out that it causes memory usage to balloon massively. Leave a155// knob here so on CI we can cut down the memory usage of QEMU and avoid the156// OOM killer.157//158// Locally testing this out this drops QEMU's memory usage running this159// tests suite from 10GiB to 600MiB. Previously we saw that crossing the160// 10GiB threshold caused our processes to get OOM killed on CI.161//162// Note that this branch is also taken for 32-bit platforms which generally163// can't test much of the pooling allocator as the virtual address space is164// so limited.165if cfg!(target_pointer_width = "32") || std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() {166// The pooling allocator hogs ~6TB of virtual address space for each167// store, so if we don't to hog memory then ignore pooling tests.168if config.pooling {169return Ok(());170}171172// If the test allocates a lot of memory, that's considered "hogging"173// memory, so skip it.174if test_hogs_memory {175return Ok(());176}177178// Don't use 4gb address space reservations when not hogging memory, and179// also don't reserve lots of memory after dynamic memories for growth180// (makes growth slower).181cfg.memory_reservation(2 * u64::from(wasmtime_environ::Memory::DEFAULT_PAGE_SIZE));182cfg.memory_reservation_for_growth(0);183184let small_guard = 64 * 1024;185cfg.memory_guard_size(small_guard);186}187188let _pooling_lock = if config.pooling {189// Some memory64 tests take more than 4gb of resident memory to test,190// but we don't want to configure the pooling allocator to allow that191// (that's a ton of memory to reserve), so we skip those tests.192if test_hogs_memory {193return Ok(());194}195196// Reduce the virtual memory required to run multi-memory-based tests.197//198// The configuration parameters below require that a bare minimum199// virtual address space reservation of 450*9*805*65536 == 200G be made200// to support each test. If 6G reservations are made for each linear201// memory then not that many tests can run concurrently with much else.202//203// When multiple memories are used and are configured in the pool then204// force the usage of static memories without guards to reduce the VM205// impact.206let max_memory_size = limits::MEMORY_SIZE;207if multi_memory {208cfg.memory_reservation(max_memory_size as u64);209cfg.memory_reservation_for_growth(0);210cfg.memory_guard_size(0);211}212213let mut pool = PoolingAllocationConfig::default();214pool.total_memories(limits::MEMORIES * 2)215.max_memory_protection_keys(2)216.max_memory_size(max_memory_size)217.max_memories_per_module(if multi_memory {218limits::MEMORIES_PER_MODULE219} else {2201221})222.max_tables_per_module(limits::TABLES_PER_MODULE);223224// When testing, we may choose to start with MPK force-enabled to ensure225// we use that functionality.226if std::env::var("WASMTIME_TEST_FORCE_MPK").is_ok() {227pool.memory_protection_keys(Enabled::Yes);228}229230cfg.allocation_strategy(InstanceAllocationStrategy::Pooling(pool));231Some(lock_pooling())232} else {233None234};235236let mut engines = vec![(Engine::new(&cfg), "default")];237238// For tests that use relaxed-simd test both the default engine and the239// guaranteed-deterministic engine to ensure that both the 'native'240// semantics of the instructions plus the canonical semantics work.241if relaxed_simd {242engines.push((243Engine::new(cfg.relaxed_simd_deterministic(true)),244"deterministic",245));246}247248for (engine, desc) in engines {249let result = engine.and_then(|engine| {250let store = Store::new(&engine, ());251let mut wast_context = WastContext::new(store, Async::Yes);252wast_context.generate_dwarf(true);253wast_context.register_spectest(&SpectestConfig {254use_shared_memory: true,255suppress_prints: true,256})?;257wast_context258.run_wast(test.path.to_str().unwrap(), test.contents.as_bytes())259.with_context(|| format!("failed to run spec test with {desc} engine"))260});261262if should_fail {263if result.is_ok() {264bail!("this test is flagged as should-fail but it succeeded")265}266} else {267result?;268}269}270271Ok(())272}273274// The pooling tests make about 6TB of address space reservation which means275// that we shouldn't let too many of them run concurrently at once. On276// high-cpu-count systems (e.g. 80 threads) this leads to mmap failures because277// presumably too much of the address space has been reserved with our limits278// specified above. By keeping the number of active pooling-related tests to a279// specified maximum we can put a cap on the virtual address space reservations280// made.281fn lock_pooling() -> impl Drop {282const MAX_CONCURRENT_POOLING: u32 = 4;283284static ACTIVE: LazyLock<MyState> = LazyLock::new(MyState::default);285286#[derive(Default)]287struct MyState {288lock: Mutex<u32>,289waiters: Condvar,290}291292impl MyState {293fn lock(&self) -> impl Drop + '_ {294let state = self.lock.lock().unwrap();295let mut state = self296.waiters297.wait_while(state, |cnt| *cnt >= MAX_CONCURRENT_POOLING)298.unwrap();299*state += 1;300LockGuard { state: self }301}302}303304struct LockGuard<'a> {305state: &'a MyState,306}307308impl Drop for LockGuard<'_> {309fn drop(&mut self) {310*self.state.lock.lock().unwrap() -= 1;311self.state.waiters.notify_one();312}313}314315ACTIVE.lock()316}317318319