Path: blob/main/crates/test-macros/src/wasmtime_test.rs
1692 views
//! Wasmtime test macro.1//!2//! This macro is a helper to define tests that exercise multiple configuration3//! combinations for Wasmtime. Currently compiler strategies and wasm features4//! are supported.5//!6//! Usage7//!8//! To exclude a compiler strategy:9//!10//! ```rust11//! #[wasmtime_test(strategies(not(Winch)))]12//! fn my_test(config: &mut Config) -> Result<()> {13//! Ok(())14//! }15//! ```16//!17//! To use just one specific compiler strategy:18//!19//! ```rust20//! #[wasmtime_test(strategies(only(Winch)))]21//! fn my_test(config: &mut Config) -> Result<()> {22//! Ok(())23//! }24//! ```25//!26//! To explicitly indicate that a wasm features is needed27//! ```28//! #[wasmtime_test(wasm_features(gc))]29//! fn my_wasm_gc_test(config: &mut Config) -> Result<()> {30//! Ok(())31//! }32//! ```33//!34//! If the specified wasm feature is disabled by default, the macro will enable35//! the feature in the configuration passed to the test.36//!37//! If the wasm feature is not supported by any of the compiler strategies, no38//! tests will be generated for such strategy.39use proc_macro::TokenStream;40use quote::{ToTokens, TokenStreamExt, quote};41use syn::{42Attribute, Ident, Result, ReturnType, Signature, Visibility, braced,43meta::ParseNestedMeta,44parse::{Parse, ParseStream},45parse_macro_input, token,46};47use wasmtime_test_util::wast::Compiler;4849/// Test configuration.50struct TestConfig {51strategies: Vec<Compiler>,52flags: wasmtime_test_util::wast::TestConfig,53/// The test attribute to use. Defaults to `#[test]`.54test_attribute: Option<proc_macro2::TokenStream>,55}5657impl TestConfig {58fn strategies_from(&mut self, meta: &ParseNestedMeta) -> Result<()> {59meta.parse_nested_meta(|meta| {60if meta.path.is_ident("not") {61meta.parse_nested_meta(|meta| {62if meta.path.is_ident("Winch") {63self.strategies.retain(|s| *s != Compiler::Winch);64Ok(())65} else if meta.path.is_ident("CraneliftNative") {66self.strategies.retain(|s| *s != Compiler::CraneliftNative);67Ok(())68} else if meta.path.is_ident("CraneliftPulley") {69self.strategies.retain(|s| *s != Compiler::CraneliftPulley);70Ok(())71} else {72Err(meta.error("Unknown strategy"))73}74})75} else if meta.path.is_ident("only") {76meta.parse_nested_meta(|meta| {77if meta.path.is_ident("Winch") {78self.strategies.retain(|s| *s == Compiler::Winch);79Ok(())80} else if meta.path.is_ident("CraneliftNative") {81self.strategies.retain(|s| *s == Compiler::CraneliftNative);82Ok(())83} else if meta.path.is_ident("CraneliftPulley") {84self.strategies.retain(|s| *s == Compiler::CraneliftPulley);85Ok(())86} else {87Err(meta.error("Unknown strategy"))88}89})90} else {91Err(meta.error("Unknown identifier"))92}93})?;9495if self.strategies.len() == 0 {96Err(meta.error("Expected at least one strategy"))97} else {98Ok(())99}100}101102fn wasm_features_from(&mut self, meta: &ParseNestedMeta) -> Result<()> {103meta.parse_nested_meta(|meta| {104for (feature, enabled) in self.flags.options_mut() {105if meta.path.is_ident(feature) {106*enabled = Some(true);107return Ok(());108}109}110Err(meta.error("Unsupported test feature"))111})?;112113Ok(())114}115116fn test_attribute_from(&mut self, meta: &ParseNestedMeta) -> Result<()> {117let v: syn::LitStr = meta.value()?.parse()?;118self.test_attribute = Some(v.value().parse()?);119Ok(())120}121}122123impl Default for TestConfig {124fn default() -> Self {125Self {126strategies: vec![127Compiler::CraneliftNative,128Compiler::Winch,129Compiler::CraneliftPulley,130],131flags: Default::default(),132test_attribute: None,133}134}135}136137/// A generic function body represented as a braced [`TokenStream`].138struct Block {139brace: token::Brace,140rest: proc_macro2::TokenStream,141}142143impl Parse for Block {144fn parse(input: ParseStream) -> Result<Self> {145let content;146Ok(Self {147brace: braced!(content in input),148rest: content.parse()?,149})150}151}152153impl ToTokens for Block {154fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {155self.brace.surround(tokens, |tokens| {156tokens.append_all(self.rest.clone());157});158}159}160161/// Custom function parser.162/// Parses the function's attributes, visibility and signature, leaving the163/// block as an opaque [`TokenStream`].164struct Fn {165attrs: Vec<Attribute>,166visibility: Visibility,167sig: Signature,168body: Block,169}170171impl Parse for Fn {172fn parse(input: ParseStream) -> Result<Self> {173let attrs = input.call(Attribute::parse_outer)?;174let visibility: Visibility = input.parse()?;175let sig: Signature = input.parse()?;176let body: Block = input.parse()?;177178Ok(Self {179attrs,180visibility,181sig,182body,183})184}185}186187impl ToTokens for Fn {188fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {189for attr in &self.attrs {190attr.to_tokens(tokens);191}192self.visibility.to_tokens(tokens);193self.sig.to_tokens(tokens);194self.body.to_tokens(tokens);195}196}197198pub fn run(attrs: TokenStream, item: TokenStream) -> TokenStream {199let mut test_config = TestConfig::default();200201let config_parser = syn::meta::parser(|meta| {202if meta.path.is_ident("strategies") {203test_config.strategies_from(&meta)204} else if meta.path.is_ident("wasm_features") {205test_config.wasm_features_from(&meta)206} else if meta.path.is_ident("with") {207test_config.test_attribute_from(&meta)208} else {209Err(meta.error("Unsupported attributes"))210}211});212213parse_macro_input!(attrs with config_parser);214215match expand(&test_config, parse_macro_input!(item as Fn)) {216Ok(tok) => tok,217Err(e) => e.into_compile_error().into(),218}219}220221fn expand(test_config: &TestConfig, func: Fn) -> Result<TokenStream> {222let mut tests = vec![quote! { #func }];223let attrs = &func.attrs;224225let test_attr = test_config226.test_attribute227.clone()228.unwrap_or_else(|| quote! { #[test] });229230for strategy in &test_config.strategies {231let strategy_name = format!("{strategy:?}");232let (asyncness, await_) = if func.sig.asyncness.is_some() {233(quote! { async }, quote! { .await })234} else {235(quote! {}, quote! {})236};237let func_name = &func.sig.ident;238match &func.sig.output {239ReturnType::Default => {240return Err(syn::Error::new(func_name.span(), "Expected `Restult<()>`"));241}242ReturnType::Type(..) => {}243};244let test_name = Ident::new(245&format!("{}_{}", strategy_name.to_lowercase(), func_name),246func_name.span(),247);248249// Ignore non-pulley tests in Miri as that's the only compiler which250// works in Miri.251let ignore_miri = match strategy {252Compiler::CraneliftPulley => quote!(),253_ => quote!(#[cfg_attr(miri, ignore)]),254};255256let test_config = format!("wasmtime_test_util::wast::{:?}", test_config.flags)257.parse::<proc_macro2::TokenStream>()258.unwrap();259let strategy_ident = quote::format_ident!("{strategy_name}");260261let tok = quote! {262#test_attr263#(#attrs)*264#ignore_miri265#asyncness fn #test_name() {266// Skip this test completely if the compiler doesn't support267// this host.268let compiler = wasmtime_test_util::wast::Compiler::#strategy_ident;269if !compiler.supports_host() {270return;271}272let _ = env_logger::try_init();273let mut config = Config::new();274wasmtime_test_util::wasmtime_wast::apply_test_config(275&mut config,276&#test_config,277);278wasmtime_test_util::wasmtime_wast::apply_wast_config(279&mut config,280&wasmtime_test_util::wast::WastConfig {281compiler,282pooling: false,283collector: wasmtime_test_util::wast::Collector::Auto,284},285);286let result = #func_name(&mut config) #await_;287if compiler.should_fail(&#test_config) {288assert!(result.is_err());289} else {290result.unwrap();291}292}293};294295tests.push(tok);296}297Ok(quote! {298#(#tests)*299}300.into())301}302303304