Skip to content

Commit 8357599

Browse files
authored
Enable async support in wast tests (bytecodealliance#10366)
* Enable async support in wast tests This commit updates the `wasmtime-wast` crate to conditionally use the `Config::async_support` mode of Wasmtime, notably executing `instantiate_async` and `call_async`. At this time no actual concurrency is supported and everything is immediately await'd on via `block_on` and a local single-thread tokio runtime. The motivation for this commit is that in the upcoming implementation of WASIp3 async will effectively be the only way to invoke components. Furthermore to test these components we're wanting to use async APIs as the root of invocations for writing `*.wast` tests for testing component-model async features. In enabling this support this commit additionally updates the `wast_tests` fuzzer to execute all tests both with and without async. Effectively the async configuration is now a fuzz option, meaning that all tests are being executed with async now as well to ensure that they work (yay!). The `--test wast` testing mode and `wasmtime wast` CLI command are both updated to unconditionally use async. There should be no loss in test coverage due to the fuzzer update, and right now knobs aren't provided to conditionally use sync in these two modes, again because it's currently expected that this won't be able to run component model async tests, so the defaults are changing. * Fix building wasmtime-wast in isolation
1 parent 5e56600 commit 8357599

File tree

9 files changed

+147
-45
lines changed

9 files changed

+147
-45
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fuzzing/src/generators/config.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ impl Config {
413413
}
414414

415415
if self.wasmtime.async_config != AsyncConfig::Disabled {
416-
log::debug!("async config in used {:?}", self.wasmtime.async_config);
416+
log::debug!("async config in use {:?}", self.wasmtime.async_config);
417417
self.wasmtime.async_config.configure(&mut cfg);
418418
}
419419

@@ -432,20 +432,25 @@ impl Config {
432432
/// Configures a store based on this configuration.
433433
pub fn configure_store(&self, store: &mut Store<StoreLimits>) {
434434
store.limiter(|s| s as &mut dyn wasmtime::ResourceLimiter);
435+
436+
// Configure the store to never abort by default, that is it'll have
437+
// max fuel or otherwise trap on an epoch change but the epoch won't
438+
// ever change.
439+
//
440+
// Afterwards though see what `AsyncConfig` is being used an further
441+
// refine the store's configuration based on that.
442+
if self.wasmtime.consume_fuel {
443+
store.set_fuel(u64::MAX).unwrap();
444+
}
445+
if self.wasmtime.epoch_interruption {
446+
store.epoch_deadline_trap();
447+
store.set_epoch_deadline(1);
448+
}
435449
match self.wasmtime.async_config {
436-
AsyncConfig::Disabled => {
437-
if self.wasmtime.consume_fuel {
438-
store.set_fuel(u64::MAX).unwrap();
439-
}
440-
if self.wasmtime.epoch_interruption {
441-
store.epoch_deadline_trap();
442-
store.set_epoch_deadline(1);
443-
}
444-
}
450+
AsyncConfig::Disabled => {}
445451
AsyncConfig::YieldWithFuel(amt) => {
446452
assert!(self.wasmtime.consume_fuel);
447453
store.fuel_async_yield_interval(Some(amt)).unwrap();
448-
store.set_fuel(amt).unwrap();
449454
}
450455
AsyncConfig::YieldWithEpochs { ticks, .. } => {
451456
assert!(self.wasmtime.epoch_interruption);

crates/fuzzing/src/oracles.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -654,22 +654,29 @@ pub fn make_api_calls(api: generators::api::ApiCalls) {
654654
/// Executes the wast `test` with the `config` specified.
655655
///
656656
/// Ensures that wast tests pass regardless of the `Config`.
657-
pub fn wast_test(mut fuzz_config: generators::Config, test: generators::WastTest) {
657+
pub fn wast_test(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
658658
crate::init_fuzzing();
659+
660+
let mut fuzz_config: generators::Config = u.arbitrary()?;
661+
let test: generators::WastTest = u.arbitrary()?;
662+
if u.arbitrary()? {
663+
fuzz_config.enable_async(u)?;
664+
}
665+
659666
let test = &test.test;
660667

661668
// Discard tests that allocate a lot of memory as we don't want to OOM the
662669
// fuzzer and we also limit memory growth which would cause the test to
663670
// fail.
664671
if test.config.hogs_memory.unwrap_or(false) {
665-
return;
672+
return Err(arbitrary::Error::IncorrectFormat);
666673
}
667674

668675
// Transform `fuzz_config` to be valid for `test` and make sure that this
669676
// test is supposed to pass.
670677
let wast_config = fuzz_config.make_wast_test_compliant(test);
671678
if test.should_fail(&wast_config) {
672-
return;
679+
return Err(arbitrary::Error::IncorrectFormat);
673680
}
674681

675682
// Winch requires AVX and AVX2 for SIMD tests to pass so don't run the test
@@ -688,19 +695,24 @@ pub fn wast_test(mut fuzz_config: generators::Config, test: generators::WastTest
688695
log::warn!(
689696
"Skipping Wast test because Winch doesn't support SIMD tests with AVX or AVX2 disabled"
690697
);
691-
return;
698+
return Err(arbitrary::Error::IncorrectFormat);
692699
}
693700

694701
// Fuel and epochs don't play well with threads right now, so exclude any
695702
// thread-spawning test if it looks like threads are spawned in that case.
696703
if fuzz_config.wasmtime.consume_fuel || fuzz_config.wasmtime.epoch_interruption {
697704
if test.contents.contains("(thread") {
698-
return;
705+
return Err(arbitrary::Error::IncorrectFormat);
699706
}
700707
}
701708

702709
log::debug!("running {:?}", test.path);
703-
let mut wast_context = WastContext::new(fuzz_config.to_store());
710+
let async_ = if fuzz_config.wasmtime.async_config == generators::AsyncConfig::Disabled {
711+
wasmtime_wast::Async::No
712+
} else {
713+
wasmtime_wast::Async::Yes
714+
};
715+
let mut wast_context = WastContext::new(fuzz_config.to_store(), async_);
704716
wast_context
705717
.register_spectest(&wasmtime_wast::SpectestConfig {
706718
use_shared_memory: true,
@@ -710,6 +722,7 @@ pub fn wast_test(mut fuzz_config: generators::Config, test: generators::WastTest
710722
wast_context
711723
.run_buffer(test.path.to_str().unwrap(), test.contents.as_bytes())
712724
.unwrap();
725+
Ok(())
713726
}
714727

715728
/// Execute a series of `table.get` and `table.set` operations.
@@ -1268,6 +1281,22 @@ mod tests {
12681281
false
12691282
}
12701283

1284+
/// Runs `f` with random data until it returns `Ok(())` `iters` times.
1285+
fn test_n_times<T: for<'a> Arbitrary<'a>>(
1286+
iters: u32,
1287+
mut f: impl FnMut(T, &mut Unstructured<'_>) -> arbitrary::Result<()>,
1288+
) {
1289+
let mut to_test = 0..iters;
1290+
let ok = gen_until_pass(|a, b| {
1291+
if f(a, b).is_ok() {
1292+
Ok(to_test.next().is_none())
1293+
} else {
1294+
Ok(false)
1295+
}
1296+
});
1297+
assert!(ok);
1298+
}
1299+
12711300
// Test that the `table_ops` fuzzer eventually runs the gc function in the host.
12721301
// We've historically had issues where this fuzzer accidentally wasn't fuzzing
12731302
// anything for a long time so this is an attempt to prevent that from happening
@@ -1353,4 +1382,9 @@ mod tests {
13531382
panic!("never generated wasm module using {expected:?}");
13541383
}
13551384
}
1385+
1386+
#[test]
1387+
fn wast_smoke_test() {
1388+
test_n_times(50, |(), u| super::wast_test(u));
1389+
}
13561390
}

crates/wast/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ workspace = true
1515

1616
[dependencies]
1717
anyhow = { workspace = true }
18-
wasmtime = { workspace = true, features = ['cranelift', 'wat', 'runtime', 'gc'] }
18+
wasmtime = { workspace = true, features = ['cranelift', 'wat', 'runtime', 'gc', 'async'] }
1919
wast = { workspace = true }
2020
log = { workspace = true }
21+
tokio = { workspace = true, features = ['rt'] }
2122

2223
[features]
2324
component-model = ['wasmtime/component-model', 'wasmtime/component-model-async']

crates/wast/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod spectest;
99
mod wast;
1010

1111
pub use crate::spectest::{link_spectest, SpectestConfig};
12-
pub use crate::wast::WastContext;
12+
pub use crate::wast::{Async, WastContext};
1313

1414
/// Version number of this crate.
1515
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

crates/wast/src/wast.rs

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub struct WastContext<T> {
2323
#[cfg(feature = "component-model")]
2424
component_linker: component::Linker<T>,
2525
store: Store<T>,
26+
async_runtime: Option<tokio::runtime::Runtime>,
2627
}
2728

2829
enum Outcome<T = Results> {
@@ -72,12 +73,26 @@ enum Export {
7273
Component(component::Func),
7374
}
7475

76+
/// Whether or not to use async APIs when calling wasm during wast testing.
77+
///
78+
/// Passed to [`WastContext::new`].
79+
#[derive(Copy, Clone, PartialEq)]
80+
#[expect(missing_docs, reason = "self-describing variants")]
81+
pub enum Async {
82+
Yes,
83+
No,
84+
}
85+
7586
impl<T> WastContext<T>
7687
where
7788
T: Clone + Send + 'static,
7889
{
7990
/// Construct a new instance of `WastContext`.
80-
pub fn new(store: Store<T>) -> Self {
91+
///
92+
/// Note that the provided `Store<T>` must have `Config::async_support`
93+
/// enabled as all functions will be run with `call_async`. This is done to
94+
/// support the component model async features that tests might use.
95+
pub fn new(store: Store<T>, async_: Async) -> Self {
8196
// Spec tests will redefine the same module/name sometimes, so we need
8297
// to allow shadowing in the linker which picks the most recent
8398
// definition as what to link when linking.
@@ -94,6 +109,15 @@ where
94109
},
95110
store,
96111
modules: Default::default(),
112+
async_runtime: if async_ == Async::Yes {
113+
Some(
114+
tokio::runtime::Builder::new_current_thread()
115+
.build()
116+
.unwrap(),
117+
)
118+
} else {
119+
None
120+
},
97121
}
98122
}
99123

@@ -124,28 +148,34 @@ where
124148
}
125149

126150
fn instantiate_module(&mut self, module: &Module) -> Result<Outcome<Instance>> {
127-
Ok(
128-
match self.core_linker.instantiate(&mut self.store, &module) {
129-
Ok(i) => Outcome::Ok(i),
130-
Err(e) => Outcome::Trap(e),
131-
},
132-
)
151+
let instance = match &self.async_runtime {
152+
Some(rt) => rt.block_on(self.core_linker.instantiate_async(&mut self.store, &module)),
153+
None => self.core_linker.instantiate(&mut self.store, &module),
154+
};
155+
Ok(match instance {
156+
Ok(i) => Outcome::Ok(i),
157+
Err(e) => Outcome::Trap(e),
158+
})
133159
}
134160

135161
#[cfg(feature = "component-model")]
136162
fn instantiate_component(
137163
&mut self,
138164
component: &component::Component,
139165
) -> Result<Outcome<(component::Component, component::Instance)>> {
140-
Ok(
141-
match self
166+
let instance = match &self.async_runtime {
167+
Some(rt) => rt.block_on(
168+
self.component_linker
169+
.instantiate_async(&mut self.store, &component),
170+
),
171+
None => self
142172
.component_linker
143-
.instantiate(&mut self.store, &component)
144-
{
145-
Ok(i) => Outcome::Ok((component.clone(), i)),
146-
Err(e) => Outcome::Trap(e),
147-
},
148-
)
173+
.instantiate(&mut self.store, &component),
174+
};
175+
Ok(match instance {
176+
Ok(i) => Outcome::Ok((component.clone(), i)),
177+
Err(e) => Outcome::Trap(e),
178+
})
149179
}
150180

151181
/// Register "spectest" which is used by the spec testsuite.
@@ -191,7 +221,14 @@ where
191221
.collect::<Result<Vec<_>>>()?;
192222

193223
let mut results = vec![Val::null_func_ref(); func.ty(&self.store).results().len()];
194-
Ok(match func.call(&mut self.store, &values, &mut results) {
224+
let result = match &self.async_runtime {
225+
Some(rt) => {
226+
rt.block_on(func.call_async(&mut self.store, &values, &mut results))
227+
}
228+
None => func.call(&mut self.store, &values, &mut results),
229+
};
230+
231+
Ok(match result {
195232
Ok(()) => Outcome::Ok(Results::Core(results.into())),
196233
Err(e) => Outcome::Trap(e),
197234
})
@@ -209,9 +246,19 @@ where
209246

210247
let mut results =
211248
vec![component::Val::Bool(false); func.results(&self.store).len()];
212-
Ok(match func.call(&mut self.store, &values, &mut results) {
249+
let result = match &self.async_runtime {
250+
Some(rt) => {
251+
rt.block_on(func.call_async(&mut self.store, &values, &mut results))
252+
}
253+
None => func.call(&mut self.store, &values, &mut results),
254+
};
255+
Ok(match result {
213256
Ok(()) => {
214-
func.post_return(&mut self.store)?;
257+
match &self.async_runtime {
258+
Some(rt) => rt.block_on(func.post_return_async(&mut self.store))?,
259+
None => func.post_return(&mut self.store)?,
260+
}
261+
215262
Outcome::Ok(Results::Component(results.into()))
216263
}
217264
Err(e) => Outcome::Trap(e),
@@ -580,6 +627,11 @@ where
580627
component_linker: component::Linker::new(self.store.engine()),
581628
store: Store::new(self.store.engine(), self.store.data().clone()),
582629
modules: self.modules.clone(),
630+
async_runtime: self.async_runtime.as_ref().map(|_| {
631+
tokio::runtime::Builder::new_current_thread()
632+
.build()
633+
.unwrap()
634+
}),
583635
};
584636
let name = thread.name.name();
585637
let child =

fuzz/fuzz_targets/wast_tests.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
#![no_main]
22

3+
use libfuzzer_sys::arbitrary::Unstructured;
34
use libfuzzer_sys::fuzz_target;
4-
use wasmtime_fuzzing::generators::{Config, WastTest};
55

6-
fuzz_target!(|pair: (Config, WastTest)| {
7-
let (config, test) = pair;
8-
wasmtime_fuzzing::oracles::wast_test(config, test);
6+
fuzz_target!(|data: &[u8]| {
7+
// Errors in `wast_test` have to do with not enough input in `data` or the
8+
// test case being thrown out, which we ignore here since it doesn't affect
9+
// how we'd like to fuzz.
10+
let mut u = Unstructured::new(data);
11+
let _ = wasmtime_fuzzing::oracles::wast_test(&mut u);
912
});

src/commands/wast.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@ impl WastCommand {
2323
pub fn execute(mut self) -> Result<()> {
2424
self.common.init_logging()?;
2525

26-
let config = self.common.config(None)?;
26+
let mut config = self.common.config(None)?;
27+
config.async_support(true);
2728
let mut store = Store::new(&Engine::new(&config)?, ());
2829
if let Some(fuel) = self.common.wasm.fuel {
2930
store.set_fuel(fuel)?;
3031
}
31-
let mut wast_context = WastContext::new(store);
32+
if let Some(true) = self.common.wasm.epoch_interruption {
33+
store.epoch_deadline_trap();
34+
store.set_epoch_deadline(1);
35+
}
36+
let mut wast_context = WastContext::new(store, wasmtime_wast::Async::Yes);
3237

3338
wast_context
3439
.register_spectest(&SpectestConfig {

tests/wast.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::sync::{Condvar, LazyLock, Mutex};
44
use wasmtime::{
55
Config, Engine, InstanceAllocationStrategy, MpkEnabled, PoolingAllocationConfig, Store,
66
};
7-
use wasmtime_wast::{SpectestConfig, WastContext};
7+
use wasmtime_wast::{Async, SpectestConfig, WastContext};
88
use wasmtime_wast_util::{limits, Collector, Compiler, WastConfig, WastTest};
99

1010
fn main() {
@@ -123,6 +123,7 @@ fn run_wast(test: &WastTest, config: WastConfig) -> anyhow::Result<()> {
123123
};
124124

125125
let mut cfg = Config::new();
126+
cfg.async_support(true);
126127
component_test_util::apply_test_config(&mut cfg, &test_config);
127128
component_test_util::apply_wast_config(&mut cfg, &config);
128129

@@ -229,7 +230,7 @@ fn run_wast(test: &WastTest, config: WastConfig) -> anyhow::Result<()> {
229230
for (engine, desc) in engines {
230231
let result = engine.and_then(|engine| {
231232
let store = Store::new(&engine, ());
232-
let mut wast_context = WastContext::new(store);
233+
let mut wast_context = WastContext::new(store, Async::Yes);
233234
wast_context.register_spectest(&SpectestConfig {
234235
use_shared_memory: true,
235236
suppress_prints: true,

0 commit comments

Comments
 (0)