use anyhow::{Context, bail};
use libtest_mimic::{Arguments, FormatSetting, Trial};
use std::sync::{Condvar, LazyLock, Mutex};
use wasmtime::{
Config, Engine, InstanceAllocationStrategy, MpkEnabled, PoolingAllocationConfig, Store,
};
use wasmtime_test_util::wast::{Collector, Compiler, WastConfig, WastTest, limits};
use wasmtime_wast::{Async, SpectestConfig, WastContext};
fn main() {
env_logger::init();
let tests = if cfg!(miri) {
Vec::new()
} else {
wasmtime_test_util::wast::find_tests(".".as_ref()).unwrap()
};
let mut trials = Vec::new();
let mut add_trial = |test: &WastTest, config: WastConfig| {
let trial = Trial::test(
format!(
"{:?}/{}{}{}",
config.compiler,
if config.pooling { "pooling/" } else { "" },
if config.collector != Collector::Auto {
format!("{:?}/", config.collector)
} else {
String::new()
},
test.path.to_str().unwrap()
),
{
let test = test.clone();
move || run_wast(&test, config).map_err(|e| format!("{e:?}").into())
},
);
trials.push(trial);
};
let mut compilers = vec![
Compiler::CraneliftNative,
Compiler::Winch,
Compiler::CraneliftPulley,
];
compilers.retain(|c| c.supports_host());
for test in tests {
let collector = if test.test_uses_gc_types() {
Collector::DeferredReferenceCounting
} else {
Collector::Auto
};
for compiler in compilers.iter().copied() {
add_trial(
&test,
WastConfig {
compiler,
pooling: false,
collector,
},
);
}
let compiler = compilers[0];
add_trial(
&test,
WastConfig {
compiler,
pooling: true,
collector,
},
);
if test.test_uses_gc_types() {
add_trial(
&test,
WastConfig {
compiler,
pooling: false,
collector: Collector::Null,
},
);
}
}
let mut args = Arguments::from_args();
if args.format.is_none() {
args.format = Some(FormatSetting::Terse);
}
libtest_mimic::run(&args, trials).exit()
}
fn run_wast(test: &WastTest, config: WastConfig) -> anyhow::Result<()> {
let test_config = test.config.clone();
let should_fail = test.should_fail(&config);
let multi_memory = test_config.multi_memory();
let test_hogs_memory = test_config.hogs_memory();
let relaxed_simd = test_config.relaxed_simd();
let is_cranelift = match config.compiler {
Compiler::CraneliftNative | Compiler::CraneliftPulley => true,
_ => false,
};
let mut cfg = Config::new();
cfg.async_support(true);
wasmtime_test_util::wasmtime_wast::apply_test_config(&mut cfg, &test_config);
wasmtime_test_util::wasmtime_wast::apply_wast_config(&mut cfg, &config);
if is_cranelift {
cfg.cranelift_debug_verifier(true);
}
if cfg!(target_pointer_width = "32") || std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() {
if config.pooling {
return Ok(());
}
if test_hogs_memory {
return Ok(());
}
cfg.memory_reservation(2 * u64::from(wasmtime_environ::Memory::DEFAULT_PAGE_SIZE));
cfg.memory_reservation_for_growth(0);
let small_guard = 64 * 1024;
cfg.memory_guard_size(small_guard);
}
let _pooling_lock = if config.pooling {
if test_hogs_memory {
return Ok(());
}
let max_memory_size = limits::MEMORY_SIZE;
if multi_memory {
cfg.memory_reservation(max_memory_size as u64);
cfg.memory_reservation_for_growth(0);
cfg.memory_guard_size(0);
}
let mut pool = PoolingAllocationConfig::default();
pool.total_memories(limits::MEMORIES * 2)
.max_memory_protection_keys(2)
.max_memory_size(max_memory_size)
.max_memories_per_module(if multi_memory {
limits::MEMORIES_PER_MODULE
} else {
1
})
.max_tables_per_module(limits::TABLES_PER_MODULE);
if std::env::var("WASMTIME_TEST_FORCE_MPK").is_ok() {
pool.memory_protection_keys(MpkEnabled::Enable);
}
cfg.allocation_strategy(InstanceAllocationStrategy::Pooling(pool));
Some(lock_pooling())
} else {
None
};
let mut engines = vec![(Engine::new(&cfg), "default")];
if relaxed_simd {
engines.push((
Engine::new(cfg.relaxed_simd_deterministic(true)),
"deterministic",
));
}
for (engine, desc) in engines {
let result = engine.and_then(|engine| {
let store = Store::new(&engine, ());
let mut wast_context = WastContext::new(store, Async::Yes);
wast_context.generate_dwarf(true);
wast_context.register_spectest(&SpectestConfig {
use_shared_memory: true,
suppress_prints: true,
})?;
wast_context
.run_wast(test.path.to_str().unwrap(), test.contents.as_bytes())
.with_context(|| format!("failed to run spec test with {desc} engine"))
});
if should_fail {
if result.is_ok() {
bail!("this test is flagged as should-fail but it succeeded")
}
} else {
result?;
}
}
Ok(())
}
fn lock_pooling() -> impl Drop {
const MAX_CONCURRENT_POOLING: u32 = 4;
static ACTIVE: LazyLock<MyState> = LazyLock::new(MyState::default);
#[derive(Default)]
struct MyState {
lock: Mutex<u32>,
waiters: Condvar,
}
impl MyState {
fn lock(&self) -> impl Drop + '_ {
let state = self.lock.lock().unwrap();
let mut state = self
.waiters
.wait_while(state, |cnt| *cnt >= MAX_CONCURRENT_POOLING)
.unwrap();
*state += 1;
LockGuard { state: self }
}
}
struct LockGuard<'a> {
state: &'a MyState,
}
impl Drop for LockGuard<'_> {
fn drop(&mut self) {
*self.state.lock.lock().unwrap() -= 1;
self.state.waiters.notify_one();
}
}
ACTIVE.lock()
}