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