xref: /wasmtime-44.0.1/tests/wast.rs (revision 763622c3)
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