drink_test_macro/
lib.rs

1//! Procedural macro providing a `#[drink::test]` attribute for `drink`-based contract testing.
2
3#![warn(missing_docs)]
4
5mod bundle_provision;
6mod contract_building;
7
8use darling::{ast::NestedMeta, FromMeta};
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{ItemEnum, ItemFn};
13
14use crate::contract_building::build_contracts;
15
16type SynResult<T> = Result<T, syn::Error>;
17
18/// Defines a drink!-based test.
19///
20/// # Requirements
21///
22/// - Your crate must have `drink` in its dependencies (and it shouldn't be renamed).
23/// - You mustn't import `drink::test` in the scope, where the macro is used. In other words, you
24///   should always use the macro only with a qualified path `#[drink::test]`.
25/// - Your crate cannot be part of a cargo workspace.
26///
27/// # Impact
28///
29/// This macro will take care of building all needed contracts for the test. The building process
30/// will be executed during compile time.
31///
32/// Contracts to be built:
33///  - current cargo package if contains a `ink-as-dependency` feature
34///  - all dependencies declared in the `Cargo.toml` file with the `ink-as-dependency` feature
35///    enabled (works with non-local packages as well).
36///
37/// ## Compilation features
38///
39/// 1. The root contract package (if any) is assumed to be built without any features.
40///
41/// 2. All contract dependencies will be built with a union of all features enabled on that package (through potentially
42///    different configurations or dependency paths), **excluding** `ink-as-dependency` and `std` features.
43///
44/// # Creating a session object
45///
46/// The macro will also create a new mutable session object and pass it to the decorated function by value. You can
47/// configure which sandbox should be used (by specifying a path to a type implementing
48/// `ink_sandbox::Sandbox` trait. Thus, your testcase function should accept a single argument:
49/// `mut session: Session<_>`.
50///
51/// By default, the macro will use `drink::minimal::MinimalSandbox`.
52///
53/// # Example
54///
55/// ```rust, ignore
56/// #[drink::test]
57/// fn testcase(mut session: Session<MinimalSandbox>) {
58///     session
59///         .deploy_bundle(&get_bundle(), "new", NO_ARGS, NO_SALT, NO_ENDOWMENT)
60///         .unwrap();
61/// }
62/// ```
63#[proc_macro_attribute]
64pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
65    match test_internal(attr.into(), item.into()) {
66        Ok(ts) => ts.into(),
67        Err(e) => e.to_compile_error().into(),
68    }
69}
70
71#[derive(FromMeta)]
72struct TestAttributes {
73    sandbox: Option<syn::Path>,
74}
75
76/// Auxiliary function to enter ?-based error propagation.
77fn test_internal(attr: TokenStream2, item: TokenStream2) -> SynResult<TokenStream2> {
78    let item_fn = syn::parse2::<ItemFn>(item)?;
79    let macro_args = TestAttributes::from_list(&NestedMeta::parse_meta_list(attr)?)?;
80
81    build_contracts();
82
83    let fn_vis = item_fn.vis;
84    let fn_attrs = item_fn.attrs;
85    let fn_block = item_fn.block;
86    let fn_name = item_fn.sig.ident;
87    let fn_async = item_fn.sig.asyncness;
88    let fn_generics = item_fn.sig.generics;
89    let fn_output = item_fn.sig.output;
90    let fn_const = item_fn.sig.constness;
91    let fn_unsafety = item_fn.sig.unsafety;
92
93    let sandbox = macro_args
94        .sandbox
95        .unwrap_or(syn::parse2(quote! { ::drink::minimal::MinimalSandbox })?);
96
97    Ok(quote! {
98        #[test]
99        #(#fn_attrs)*
100        #fn_vis #fn_async #fn_const #fn_unsafety fn #fn_name #fn_generics () #fn_output {
101            let mut session = Session::<#sandbox>::default();
102            #fn_block
103        }
104    })
105}
106
107/// Defines a contract bundle provider.
108///
109/// # Requirements
110///
111/// - Your crate cannot be part of a cargo workspace.
112/// - Your crate must have `drink` in its dependencies (and it shouldn't be renamed).
113/// - The attributed enum must not:
114///     - be generic
115///     - have variants
116///     - have any attributes conflicting with `#[derive(Copy, Clone, PartialEq, Eq, Debug)]`
117///
118/// # Impact
119///
120/// This macro is intended to be used as an attribute of some empty enum. It will build all
121/// contracts crates (with rules identical to those of `#[drink::test]`), and populate the decorated
122/// enum with variants, one per built contract.
123///
124/// If the current crate is a contract crate, the enum will receive a method `local()` that returns
125/// the contract bundle for the current crate.
126///
127/// Besides that, the enum will receive a method `bundle(self)` that returns the contract bundle
128/// for corresponding contract variant.
129///
130/// Both methods return `DrinkResult<ContractBundle>`.
131///
132/// # Example
133///
134/// ```rust, ignore
135/// #[drink::contract_bundle_provider]
136/// enum BundleProvider {}
137///
138/// fn testcase() {
139///     Session::<MinimalSandbox>::default()
140///         .deploy_bundle_and(BundleProvider::local()?, "new", NO_ARGS, NO_SALT, NO_ENDOWMENT)
141///         .deploy_bundle_and(BundleProvider::AnotherContract.bundle()?, "new", NO_ARGS, NO_SALT, NO_ENDOWMENT)
142///         .unwrap();
143/// }
144/// ```
145#[proc_macro_attribute]
146pub fn contract_bundle_provider(attr: TokenStream, item: TokenStream) -> TokenStream {
147    match contract_bundle_provider_internal(attr.into(), item.into()) {
148        Ok(ts) => ts.into(),
149        Err(e) => e.to_compile_error().into(),
150    }
151}
152
153/// Auxiliary function to enter ?-based error propagation.
154fn contract_bundle_provider_internal(
155    _attr: TokenStream2,
156    item: TokenStream2,
157) -> SynResult<TokenStream2> {
158    let enum_item = parse_bundle_enum(item)?;
159    let bundle_registry = build_contracts();
160    Ok(bundle_registry.generate_bundle_provision(enum_item))
161}
162
163fn parse_bundle_enum(item: TokenStream2) -> SynResult<ItemEnum> {
164    let enum_item = syn::parse2::<ItemEnum>(item)?;
165
166    if !enum_item.generics.params.is_empty() {
167        return Err(syn::Error::new_spanned(
168            enum_item.generics.params,
169            "ContractBundleProvider must not be generic",
170        ));
171    }
172    if !enum_item.variants.is_empty() {
173        return Err(syn::Error::new_spanned(
174            enum_item.variants,
175            "ContractBundleProvider must not have variants",
176        ));
177    }
178
179    Ok(enum_item)
180}