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.
40 use proc_macro::TokenStream;
41 use quote::{ToTokens, TokenStreamExt, quote};
42 use syn::{
43     Attribute, Ident, Result, ReturnType, Signature, Visibility, braced,
44     meta::ParseNestedMeta,
45     parse::{Parse, ParseStream},
46     parse_macro_input, token,
47 };
48 use wasmtime_test_util::wast::Compiler;
49 
50 /// Test configuration.
51 struct TestConfig {
52     strategies: Vec<Compiler>,
53     flags: wasmtime_test_util::wast::TestConfig,
54 }
55 
56 impl TestConfig {
strategies_from(&mut self, meta: &ParseNestedMeta) -> Result<()>57     fn strategies_from(&mut self, meta: &ParseNestedMeta) -> Result<()> {
58         meta.parse_nested_meta(|meta| {
59             if meta.path.is_ident("not") {
60                 meta.parse_nested_meta(|meta| {
61                     if meta.path.is_ident("Winch") {
62                         self.strategies.retain(|s| *s != Compiler::Winch);
63                         Ok(())
64                     } else if meta.path.is_ident("CraneliftNative") {
65                         self.strategies.retain(|s| *s != Compiler::CraneliftNative);
66                         Ok(())
67                     } else if meta.path.is_ident("CraneliftPulley") {
68                         self.strategies.retain(|s| *s != Compiler::CraneliftPulley);
69                         Ok(())
70                     } else {
71                         Err(meta.error("Unknown strategy"))
72                     }
73                 })
74             } else if meta.path.is_ident("only") {
75                 meta.parse_nested_meta(|meta| {
76                     if meta.path.is_ident("Winch") {
77                         self.strategies.retain(|s| *s == Compiler::Winch);
78                         Ok(())
79                     } else if meta.path.is_ident("CraneliftNative") {
80                         self.strategies.retain(|s| *s == Compiler::CraneliftNative);
81                         Ok(())
82                     } else if meta.path.is_ident("CraneliftPulley") {
83                         self.strategies.retain(|s| *s == Compiler::CraneliftPulley);
84                         Ok(())
85                     } else {
86                         Err(meta.error("Unknown strategy"))
87                     }
88                 })
89             } else {
90                 Err(meta.error("Unknown identifier"))
91             }
92         })?;
93 
94         if self.strategies.len() == 0 {
95             Err(meta.error("Expected at least one strategy"))
96         } else {
97             Ok(())
98         }
99     }
100 
wasm_features_from(&mut self, meta: &ParseNestedMeta) -> Result<()>101     fn wasm_features_from(&mut self, meta: &ParseNestedMeta) -> Result<()> {
102         meta.parse_nested_meta(|meta| {
103             for (feature, enabled) in self.flags.options_mut() {
104                 if meta.path.is_ident(feature) {
105                     *enabled = Some(true);
106                     return Ok(());
107                 }
108             }
109             Err(meta.error("Unsupported test feature"))
110         })?;
111 
112         Ok(())
113     }
114 }
115 
116 impl Default for TestConfig {
default() -> Self117     fn default() -> Self {
118         Self {
119             strategies: vec![
120                 Compiler::CraneliftNative,
121                 Compiler::Winch,
122                 Compiler::CraneliftPulley,
123             ],
124             flags: Default::default(),
125         }
126     }
127 }
128 
129 /// A generic function body represented as a braced [`TokenStream`].
130 struct Block {
131     brace: token::Brace,
132     rest: proc_macro2::TokenStream,
133 }
134 
135 impl Parse for Block {
parse(input: ParseStream) -> Result<Self>136     fn parse(input: ParseStream) -> Result<Self> {
137         let content;
138         Ok(Self {
139             brace: braced!(content in input),
140             rest: content.parse()?,
141         })
142     }
143 }
144 
145 impl ToTokens for Block {
to_tokens(&self, tokens: &mut proc_macro2::TokenStream)146     fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
147         self.brace.surround(tokens, |tokens| {
148             tokens.append_all(self.rest.clone());
149         });
150     }
151 }
152 
153 /// Custom function parser.
154 /// Parses the function's attributes, visibility and signature, leaving the
155 /// block as an opaque [`TokenStream`].
156 struct Fn {
157     attrs: Vec<Attribute>,
158     visibility: Visibility,
159     sig: Signature,
160     body: Block,
161 }
162 
163 impl Parse for Fn {
parse(input: ParseStream) -> Result<Self>164     fn parse(input: ParseStream) -> Result<Self> {
165         let attrs = input.call(Attribute::parse_outer)?;
166         let visibility: Visibility = input.parse()?;
167         let sig: Signature = input.parse()?;
168         let body: Block = input.parse()?;
169 
170         Ok(Self {
171             attrs,
172             visibility,
173             sig,
174             body,
175         })
176     }
177 }
178 
179 impl ToTokens for Fn {
to_tokens(&self, tokens: &mut proc_macro2::TokenStream)180     fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
181         for attr in &self.attrs {
182             attr.to_tokens(tokens);
183         }
184         self.visibility.to_tokens(tokens);
185         self.sig.to_tokens(tokens);
186         self.body.to_tokens(tokens);
187     }
188 }
189 
run(attrs: TokenStream, item: TokenStream) -> TokenStream190 pub fn run(attrs: TokenStream, item: TokenStream) -> TokenStream {
191     let mut test_config = TestConfig::default();
192 
193     let config_parser = syn::meta::parser(|meta| {
194         if meta.path.is_ident("strategies") {
195             test_config.strategies_from(&meta)
196         } else if meta.path.is_ident("wasm_features") {
197             test_config.wasm_features_from(&meta)
198         } else {
199             Err(meta.error("Unsupported attributes"))
200         }
201     });
202 
203     parse_macro_input!(attrs with config_parser);
204 
205     match expand(&test_config, parse_macro_input!(item as Fn)) {
206         Ok(tok) => tok,
207         Err(e) => e.into_compile_error().into(),
208     }
209 }
210 
expand(test_config: &TestConfig, func: Fn) -> Result<TokenStream>211 fn expand(test_config: &TestConfig, func: Fn) -> Result<TokenStream> {
212     let mut tests = vec![quote! { #func }];
213     let attrs = &func.attrs;
214 
215     let test_attr = if func.sig.asyncness.is_some() {
216         quote! { #[tokio::test] }
217     } else {
218         quote! { #[test] }
219     };
220 
221     for strategy in &test_config.strategies {
222         let strategy_name = format!("{strategy:?}");
223         let (asyncness, await_) = if func.sig.asyncness.is_some() {
224             (quote! { async }, quote! { .await })
225         } else {
226             (quote! {}, quote! {})
227         };
228         let func_name = &func.sig.ident;
229         match &func.sig.output {
230             ReturnType::Default => {
231                 return Err(syn::Error::new(func_name.span(), "Expected `Result<()>`"));
232             }
233             ReturnType::Type(..) => {}
234         };
235         let test_name = Ident::new(
236             &format!("{}_{}", strategy_name.to_lowercase(), func_name),
237             func_name.span(),
238         );
239 
240         // Ignore non-pulley tests in Miri as that's the only compiler which
241         // works in Miri.
242         let ignore_miri = match strategy {
243             Compiler::CraneliftPulley => quote!(),
244             _ => quote!(#[cfg_attr(miri, ignore)]),
245         };
246 
247         let test_config = format!("wasmtime_test_util::wast::{:?}", test_config.flags)
248             .parse::<proc_macro2::TokenStream>()
249             .unwrap();
250         let strategy_ident = quote::format_ident!("{strategy_name}");
251 
252         let tok = quote! {
253             #test_attr
254             #(#attrs)*
255             #ignore_miri
256             #asyncness fn #test_name() {
257                 // Skip this test completely if the compiler doesn't support
258                 // this host.
259                 let compiler = wasmtime_test_util::wast::Compiler::#strategy_ident;
260                 if !compiler.supports_host() {
261                     return;
262                 }
263                 let _ = env_logger::try_init();
264                 let mut config = Config::new();
265                 wasmtime_test_util::wasmtime_wast::apply_test_config(
266                     &mut config,
267                     &#test_config,
268                 );
269                 wasmtime_test_util::wasmtime_wast::apply_wast_config(
270                     &mut config,
271                     &wasmtime_test_util::wast::WastConfig {
272                         compiler,
273                         pooling: false,
274                         collector: wasmtime_test_util::wast::Collector::Auto,
275                     },
276                 );
277                 let result = #func_name(&mut config) #await_;
278                 if compiler.should_fail(&#test_config) {
279                     assert!(result.is_err());
280                 } else {
281                     result.unwrap();
282                 }
283             }
284         };
285 
286         tests.push(tok);
287     }
288     Ok(quote! {
289         #(#tests)*
290     }
291     .into())
292 }
293