wasmtime_test_macros/
lib.rs

1//! Wasmtime test macro.
2//!
3//! This macro is a helper to define tests that exercise multiple configuration
4//! combinations for Wasmtime. Currently compiler strategies and wasm features
5//! are supported.
6//!
7//! Usage
8//!
9//! To exclude a compiler strategy:
10//!
11//! ```rust
12//! #[wasmtime_test(strategies(not(Winch)))]
13//! fn my_test(config: &mut Config) -> Result<()> {
14//!    Ok(())
15//! }
16//! ```
17//!
18//! To use just one specific compiler strategy:
19//!
20//! ```rust
21//! #[wasmtime_test(strategies(only(Winch)))]
22//! fn my_test(config: &mut Config) -> Result<()> {
23//!     Ok(())
24//! }
25//! ```
26//!
27//! To explicitly indicate that a wasm features is needed
28//! ```
29//! #[wasmtime_test(wasm_features(gc))]
30//! fn my_wasm_gc_test(config: &mut Config) -> Result<()> {
31//!   Ok(())
32//! }
33//! ```
34//!
35//! If the specified wasm feature is disabled by default, the macro will enable
36//! the feature in the configuration passed to the test.
37//!
38//! If the wasm feature is not supported by any of the compiler strategies, no
39//! tests will be generated for such strategy.
40use proc_macro::TokenStream;
41use quote::{quote, ToTokens, TokenStreamExt};
42use syn::{
43    braced,
44    meta::ParseNestedMeta,
45    parse::{Parse, ParseStream},
46    parse_macro_input, token, Attribute, Ident, Result, ReturnType, Signature, Visibility,
47};
48use wasmtime_wast_util::Compiler;
49
50/// Test configuration.
51struct TestConfig {
52    strategies: Vec<Compiler>,
53    flags: wasmtime_wast_util::TestConfig,
54    /// The test attribute to use. Defaults to `#[test]`.
55    test_attribute: Option<proc_macro2::TokenStream>,
56}
57
58impl TestConfig {
59    fn strategies_from(&mut self, meta: &ParseNestedMeta) -> Result<()> {
60        meta.parse_nested_meta(|meta| {
61            if meta.path.is_ident("not") {
62                meta.parse_nested_meta(|meta| {
63                    if meta.path.is_ident("Winch") {
64                        self.strategies.retain(|s| *s != Compiler::Winch);
65                        Ok(())
66                    } else if meta.path.is_ident("CraneliftNative") {
67                        self.strategies.retain(|s| *s != Compiler::CraneliftNative);
68                        Ok(())
69                    } else if meta.path.is_ident("CraneliftPulley") {
70                        self.strategies.retain(|s| *s != Compiler::CraneliftPulley);
71                        Ok(())
72                    } else {
73                        Err(meta.error("Unknown strategy"))
74                    }
75                })
76            } else if meta.path.is_ident("only") {
77                meta.parse_nested_meta(|meta| {
78                    if meta.path.is_ident("Winch") {
79                        self.strategies.retain(|s| *s == Compiler::Winch);
80                        Ok(())
81                    } else if meta.path.is_ident("CraneliftNative") {
82                        self.strategies.retain(|s| *s == Compiler::CraneliftNative);
83                        Ok(())
84                    } else if meta.path.is_ident("CraneliftPulley") {
85                        self.strategies.retain(|s| *s == Compiler::CraneliftPulley);
86                        Ok(())
87                    } else {
88                        Err(meta.error("Unknown strategy"))
89                    }
90                })
91            } else {
92                Err(meta.error("Unknown identifier"))
93            }
94        })?;
95
96        if self.strategies.len() == 0 {
97            Err(meta.error("Expected at least one strategy"))
98        } else {
99            Ok(())
100        }
101    }
102
103    fn wasm_features_from(&mut self, meta: &ParseNestedMeta) -> Result<()> {
104        meta.parse_nested_meta(|meta| {
105            for (feature, enabled) in self.flags.options_mut() {
106                if meta.path.is_ident(feature) {
107                    *enabled = Some(true);
108                    return Ok(());
109                }
110            }
111            Err(meta.error("Unsupported test feature"))
112        })?;
113
114        Ok(())
115    }
116
117    fn test_attribute_from(&mut self, meta: &ParseNestedMeta) -> Result<()> {
118        let v: syn::LitStr = meta.value()?.parse()?;
119        self.test_attribute = Some(v.value().parse()?);
120        Ok(())
121    }
122}
123
124impl Default for TestConfig {
125    fn default() -> Self {
126        Self {
127            strategies: vec![
128                Compiler::CraneliftNative,
129                Compiler::Winch,
130                Compiler::CraneliftPulley,
131            ],
132            flags: Default::default(),
133            test_attribute: None,
134        }
135    }
136}
137
138/// A generic function body represented as a braced [`TokenStream`].
139struct Block {
140    brace: token::Brace,
141    rest: proc_macro2::TokenStream,
142}
143
144impl Parse for Block {
145    fn parse(input: ParseStream) -> Result<Self> {
146        let content;
147        Ok(Self {
148            brace: braced!(content in input),
149            rest: content.parse()?,
150        })
151    }
152}
153
154impl ToTokens for Block {
155    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
156        self.brace.surround(tokens, |tokens| {
157            tokens.append_all(self.rest.clone());
158        });
159    }
160}
161
162/// Custom function parser.
163/// Parses the function's attributes, visibility and signature, leaving the
164/// block as an opaque [`TokenStream`].
165struct Fn {
166    attrs: Vec<Attribute>,
167    visibility: Visibility,
168    sig: Signature,
169    body: Block,
170}
171
172impl Parse for Fn {
173    fn parse(input: ParseStream) -> Result<Self> {
174        let attrs = input.call(Attribute::parse_outer)?;
175        let visibility: Visibility = input.parse()?;
176        let sig: Signature = input.parse()?;
177        let body: Block = input.parse()?;
178
179        Ok(Self {
180            attrs,
181            visibility,
182            sig,
183            body,
184        })
185    }
186}
187
188impl ToTokens for Fn {
189    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
190        for attr in &self.attrs {
191            attr.to_tokens(tokens);
192        }
193        self.visibility.to_tokens(tokens);
194        self.sig.to_tokens(tokens);
195        self.body.to_tokens(tokens);
196    }
197}
198
199#[proc_macro_attribute]
200pub fn wasmtime_test(attrs: TokenStream, item: TokenStream) -> TokenStream {
201    let mut test_config = TestConfig::default();
202
203    let config_parser = syn::meta::parser(|meta| {
204        if meta.path.is_ident("strategies") {
205            test_config.strategies_from(&meta)
206        } else if meta.path.is_ident("wasm_features") {
207            test_config.wasm_features_from(&meta)
208        } else if meta.path.is_ident("with") {
209            test_config.test_attribute_from(&meta)
210        } else {
211            Err(meta.error("Unsupported attributes"))
212        }
213    });
214
215    parse_macro_input!(attrs with config_parser);
216
217    match expand(&test_config, parse_macro_input!(item as Fn)) {
218        Ok(tok) => tok,
219        Err(e) => e.into_compile_error().into(),
220    }
221}
222
223fn expand(test_config: &TestConfig, func: Fn) -> Result<TokenStream> {
224    let mut tests = if test_config.strategies == [Compiler::Winch] {
225        vec![quote! {
226            // This prevents dead code warning when the macro is invoked as:
227            //     #[wasmtime_test(strategies(only(Winch))]
228            // Given that Winch only fully supports x86_64.
229            #[allow(dead_code)]
230            #func
231        }]
232    } else {
233        vec![quote! { #func }]
234    };
235    let attrs = &func.attrs;
236
237    let test_attr = test_config
238        .test_attribute
239        .clone()
240        .unwrap_or_else(|| quote! { #[test] });
241
242    for strategy in &test_config.strategies {
243        let strategy_name = format!("{strategy:?}");
244        // Winch currently only offers support for x64, and it requires
245        // signals-based-traps which MIRI disables so disable winch tests on MIRI
246        let target = if *strategy == Compiler::Winch {
247            quote! { #[cfg(all(target_arch = "x86_64", not(miri)))] }
248        } else {
249            quote! {}
250        };
251        let (asyncness, await_) = if func.sig.asyncness.is_some() {
252            (quote! { async }, quote! { .await })
253        } else {
254            (quote! {}, quote! {})
255        };
256        let func_name = &func.sig.ident;
257        let expect = match &func.sig.output {
258            ReturnType::Default => quote! {},
259            ReturnType::Type(..) => quote! { .expect("test is expected to pass") },
260        };
261        let test_name = Ident::new(
262            &format!("{}_{}", strategy_name.to_lowercase(), func_name),
263            func_name.span(),
264        );
265
266        let should_panic = if strategy.should_fail(&test_config.flags) {
267            quote!(#[should_panic])
268        } else {
269            quote!()
270        };
271
272        let test_config = format!("wasmtime_wast_util::{:?}", test_config.flags)
273            .parse::<proc_macro2::TokenStream>()
274            .unwrap();
275        let strategy_ident = quote::format_ident!("{strategy_name}");
276
277        let tok = quote! {
278            #test_attr
279            #target
280            #should_panic
281            #(#attrs)*
282            #asyncness fn #test_name() {
283                let mut config = Config::new();
284                component_test_util::apply_test_config(
285                    &mut config,
286                    &#test_config,
287                );
288                component_test_util::apply_wast_config(
289                    &mut config,
290                    &wasmtime_wast_util::WastConfig {
291                        compiler: wasmtime_wast_util::Compiler::#strategy_ident,
292                        pooling: false,
293                        collector: wasmtime_wast_util::Collector::Auto,
294                    },
295                );
296                #func_name(&mut config) #await_ #expect
297            }
298        };
299
300        tests.push(tok);
301    }
302    Ok(quote! {
303        #(#tests)*
304    }
305    .into())
306}