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