Skip to content

Commit f0845e5

Browse files
authored
wasmtime: Introduce the test-macros crate (#8622)
* wasmtime: Introduce the `test-macros` crate This commit introduces the `test-macros` crate. The whole idea behind this crate is to export a single or multiple macros to make it easier to configure Wasmtime for integration tests. The main use-case at this time, is running a subset of the integration tests with Cranelift and Winch. This crate could be extended to serve other use-cases, like testing pooling allocator and/or GC configurations. This commit introduces a single example of how this macro could be used. If there's agreement on merging this change in some shape or form, I'll follow up with migrating the current tests to use `#[wasmtime_test]` where applicable. Part of what's implemented in this PR was discussed in Cranelift's meeting on [April 24th, 2024](https://github.com/bytecodealliance/meetings/blob/main/cranelift/2024/cranelift-04-24.md), however there are several discussion points that are still "in the air", like for example, what's the best way to avoid the combinatorial explosion problem for the potential test matrix. * Add crate license * Clippy fixes * Remove test-macros from members * Clean up test-macros Cargo.toml - Fix version - Add `[lints]` - Add publish key * Add `TestConfig` and simpify parsing This commit adds a `TestConfig` struct that holds the supported Wasmtime test configuration. Additonally this commit introduces a partial function parser, in which only the attributes, visibility, and signature are fully parsed, leaving the function body as an opaque `TokenStream`
1 parent 9a443bc commit f0845e5

File tree

5 files changed

+218
-3
lines changed

5 files changed

+218
-3
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ similar = { workspace = true }
115115
libtest-mimic = "0.7.0"
116116
capstone = { workspace = true }
117117
object = { workspace = true, features = ['std'] }
118+
wasmtime-test-macros = { path = "crates/test-macros" }
118119

119120
[target.'cfg(windows)'.dev-dependencies]
120121
windows-sys = { workspace = true, features = ["Win32_System_Memory"] }

crates/test-macros/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
description = "Macros for testing Wasmtime"
3+
name = "wasmtime-test-macros"
4+
license = "Apache-2.0 WITH LLVM-exception"
5+
version = "0.0.0"
6+
authors.workspace = true
7+
edition.workspace = true
8+
rust-version.workspace = true
9+
publish = false
10+
11+
[lib]
12+
proc-macro = true
13+
test = false
14+
doctest = false
15+
16+
[lints]
17+
workspace = true
18+
19+
[dependencies]
20+
quote = "1.0"
21+
proc-macro2 = "1.0"
22+
syn = { workspace = true, features = ["full"] }
23+
anyhow = { workspace = true }

crates/test-macros/src/lib.rs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
//! Wasmtime test macro.
2+
//!
3+
//! This macro is a helper to define tests that exercise multiple configuration
4+
//! combinations for Wasmtime. Currently, only compiler strategies are
5+
//! supported.
6+
//!
7+
//! Usage
8+
//!
9+
//! #[wasmtime_test(strategies(Cranelift, Winch))]
10+
//! fn my_test(config: &mut Config) -> Result<()> {
11+
//! Ok(())
12+
//! }
13+
use proc_macro::TokenStream;
14+
use quote::{quote, ToTokens, TokenStreamExt};
15+
use syn::{
16+
braced,
17+
parse::{Parse, ParseStream},
18+
parse_macro_input, token, Attribute, Ident, Result, ReturnType, Signature, Visibility,
19+
};
20+
21+
/// Test configuration.
22+
struct TestConfig {
23+
/// Supported compiler strategies.
24+
strategies: Vec<(String, Ident)>,
25+
}
26+
27+
impl TestConfig {
28+
/// Validate the test configuration.
29+
/// Only the number of strategies is validated, as this avoid expansions of
30+
/// empty strategies or more strategies than supported.
31+
///
32+
/// The supported strategies are validated inline when parsing.
33+
fn validate(&self) -> anyhow::Result<()> {
34+
if self.strategies.len() > 2 {
35+
Err(anyhow::anyhow!("Expected at most 2 strategies"))
36+
} else if self.strategies.len() == 0 {
37+
Err(anyhow::anyhow!("Expected at least 1 strategy"))
38+
} else {
39+
Ok(())
40+
}
41+
}
42+
}
43+
44+
impl Default for TestConfig {
45+
fn default() -> Self {
46+
Self { strategies: vec![] }
47+
}
48+
}
49+
50+
/// A generic function body represented as a braced [`TokenStream`].
51+
struct Block {
52+
brace: token::Brace,
53+
rest: proc_macro2::TokenStream,
54+
}
55+
56+
impl Parse for Block {
57+
fn parse(input: ParseStream) -> Result<Self> {
58+
let content;
59+
Ok(Self {
60+
brace: braced!(content in input),
61+
rest: content.parse()?,
62+
})
63+
}
64+
}
65+
66+
impl ToTokens for Block {
67+
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
68+
self.brace.surround(tokens, |tokens| {
69+
tokens.append_all(self.rest.clone());
70+
});
71+
}
72+
}
73+
74+
/// Custom function parser.
75+
/// Parses the function's attributes, visibility and signature, leaving the
76+
/// block as an opaque [`TokenStream`].
77+
struct Fn {
78+
attrs: Vec<Attribute>,
79+
visibility: Visibility,
80+
sig: Signature,
81+
body: Block,
82+
}
83+
84+
impl Parse for Fn {
85+
fn parse(input: ParseStream) -> Result<Self> {
86+
let attrs = input.call(Attribute::parse_outer)?;
87+
let visibility: Visibility = input.parse()?;
88+
let sig: Signature = input.parse()?;
89+
let body: Block = input.parse()?;
90+
91+
Ok(Self {
92+
attrs,
93+
visibility,
94+
sig,
95+
body,
96+
})
97+
}
98+
}
99+
100+
impl ToTokens for Fn {
101+
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
102+
for attr in &self.attrs {
103+
attr.to_tokens(tokens);
104+
}
105+
self.visibility.to_tokens(tokens);
106+
self.sig.to_tokens(tokens);
107+
self.body.to_tokens(tokens);
108+
}
109+
}
110+
111+
#[proc_macro_attribute]
112+
pub fn wasmtime_test(attrs: TokenStream, item: TokenStream) -> TokenStream {
113+
let mut test_config = TestConfig::default();
114+
115+
let config_parser = syn::meta::parser(|meta| {
116+
if meta.path.is_ident("strategies") {
117+
meta.parse_nested_meta(|meta| {
118+
if meta.path.is_ident("Winch") || meta.path.is_ident("Cranelift") {
119+
let id = meta.path.require_ident()?.clone();
120+
test_config.strategies.push((id.to_string(), id));
121+
Ok(())
122+
} else {
123+
Err(meta.error("Unknown strategy"))
124+
}
125+
})?;
126+
127+
test_config.validate().map_err(|e| meta.error(e))
128+
} else {
129+
Err(meta.error("Unsupported attributes"))
130+
}
131+
});
132+
133+
parse_macro_input!(attrs with config_parser);
134+
135+
match expand(&test_config, parse_macro_input!(item as Fn)) {
136+
Ok(tok) => tok,
137+
Err(e) => e.into_compile_error().into(),
138+
}
139+
}
140+
141+
fn expand(test_config: &TestConfig, func: Fn) -> Result<TokenStream> {
142+
let mut tests = vec![quote! { #func }];
143+
let attrs = &func.attrs;
144+
145+
for (strategy_name, ident) in &test_config.strategies {
146+
// Winch currently only offers support for x64.
147+
let target = if strategy_name == "Winch" {
148+
quote! { #[cfg(target_arch = "x86_64")] }
149+
} else {
150+
quote! {}
151+
};
152+
let func_name = &func.sig.ident;
153+
let ret = match &func.sig.output {
154+
ReturnType::Default => quote! { () },
155+
ReturnType::Type(_, ty) => quote! { -> #ty },
156+
};
157+
let test_name = Ident::new(
158+
&format!("{}_{}", strategy_name.to_lowercase(), func_name),
159+
func_name.span(),
160+
);
161+
let tok = quote! {
162+
#[test]
163+
#target
164+
#(#attrs)*
165+
fn #test_name() #ret {
166+
let mut config = Config::new();
167+
config.strategy(Strategy::#ident);
168+
#func_name(&mut config)
169+
}
170+
};
171+
172+
tests.push(tok);
173+
}
174+
Ok(quote! {
175+
#(#tests)*
176+
}
177+
.into())
178+
}

tests/all/func.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ use std::sync::atomic::Ordering;
33
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst};
44
use std::sync::Arc;
55
use wasmtime::*;
6+
use wasmtime_test_macros::wasmtime_test;
67

7-
#[test]
8+
#[wasmtime_test(strategies(Cranelift, Winch))]
89
#[cfg_attr(miri, ignore)]
9-
fn call_wasm_to_wasm() -> Result<()> {
10+
fn call_wasm_to_wasm(config: &mut Config) -> Result<()> {
1011
let wasm = wat::parse_str(
1112
r#"
1213
(module
@@ -21,7 +22,8 @@ fn call_wasm_to_wasm() -> Result<()> {
2122
)
2223
"#,
2324
)?;
24-
let mut store = Store::<()>::default();
25+
let engine = Engine::new(&config)?;
26+
let mut store = Store::<()>::new(&engine, ());
2527
let module = Module::new(store.engine(), &wasm)?;
2628
let instance = Instance::new(&mut store, &module, &[])?;
2729
let func = instance

0 commit comments

Comments
 (0)