drink_test_macro_next/
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
36///
37/// Note: Depending on a non-local contract is not tested yet.
38///
39/// # Creating a session object
40///
41/// The macro will also create a new mutable session object and pass it to the decorated function by value. You can
42/// configure which sandbox should be used (by specifying a path to a type implementing
43/// `drink::runtime::SandboxConfig` trait. Thus, your testcase function should accept a single argument:
44/// `mut session: Session<_>`.
45///
46/// By default, the macro will use `drink::runtime::MinimalRuntime`.
47///
48/// # Example
49///
50/// ```rust, ignore
51/// #[drink::test]
52/// fn testcase(mut session: Session<MinimalRuntime>) {
53///     session
54///         .deploy_bundle(&get_bundle(), "new", NO_ARGS, NO_SALT, NO_ENDOWMENT)
55///         .unwrap();
56/// }
57/// ```
58#[proc_macro_attribute]
59pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
60    match test_internal(attr.into(), item.into()) {
61        Ok(ts) => ts.into(),
62        Err(e) => e.to_compile_error().into(),
63    }
64}
65
66#[derive(FromMeta)]
67struct TestAttributes {
68    config: Option<syn::Path>,
69}
70
71/// Auxiliary function to enter ?-based error propagation.
72fn test_internal(attr: TokenStream2, item: TokenStream2) -> SynResult<TokenStream2> {
73    let item_fn = syn::parse2::<ItemFn>(item)?;
74    let macro_args = TestAttributes::from_list(&NestedMeta::parse_meta_list(attr)?)?;
75
76    build_contracts();
77
78    let fn_vis = item_fn.vis;
79    let fn_attrs = item_fn.attrs;
80    let fn_block = item_fn.block;
81    let fn_name = item_fn.sig.ident;
82    let fn_async = item_fn.sig.asyncness;
83    let fn_generics = item_fn.sig.generics;
84    let fn_output = item_fn.sig.output;
85    let fn_const = item_fn.sig.constness;
86    let fn_unsafety = item_fn.sig.unsafety;
87
88    let config = macro_args
89        .config
90        .unwrap_or(syn::parse2(quote! { ::drink::runtime::MinimalRuntime })?);
91
92    Ok(quote! {
93        #[test]
94        #(#fn_attrs)*
95        #fn_vis #fn_async #fn_const #fn_unsafety fn #fn_name #fn_generics () #fn_output {
96            let mut session = Session::<#config>::new().expect("Failed to create a session");
97            #fn_block
98        }
99    })
100}
101
102/// Defines a contract bundle provider.
103///
104/// # Requirements
105///
106/// - Your crate cannot be part of a cargo workspace.
107/// - Your crate must have `drink` in its dependencies (and it shouldn't be renamed).
108/// - The attributed enum must not:
109///     - be generic
110///     - have variants
111///     - have any attributes conflicting with `#[derive(Copy, Clone, PartialEq, Eq, Debug)]`
112///
113/// # Impact
114///
115/// This macro is intended to be used as an attribute of some empty enum. It will build all
116/// contracts crates (with rules identical to those of `#[drink::test]`), and populate the decorated
117/// enum with variants, one per built contract.
118///
119/// If the current crate is a contract crate, the enum will receive a method `local()` that returns
120/// the contract bundle for the current crate.
121///
122/// Besides that, the enum will receive a method `bundle(self)` that returns the contract bundle
123/// for corresponding contract variant.
124///
125/// Both methods return `DrinkResult<ContractBundle>`.
126///
127/// # Example
128///
129/// ```rust, ignore
130/// #[drink::contract_bundle_provider]
131/// enum BundleProvider {}
132///
133/// fn testcase() {
134///     Session::<MinimalRuntime>::new()?
135///         .deploy_bundle_and(BundleProvider::local()?, "new", NO_ARGS, NO_SALT, NO_ENDOWMENT)
136///         .deploy_bundle_and(BundleProvider::AnotherContract.bundle()?, "new", NO_ARGS, NO_SALT, NO_ENDOWMENT)
137///         .unwrap();
138/// }
139/// ```
140#[proc_macro_attribute]
141pub fn contract_bundle_provider(attr: TokenStream, item: TokenStream) -> TokenStream {
142    match contract_bundle_provider_internal(attr.into(), item.into()) {
143        Ok(ts) => ts.into(),
144        Err(e) => e.to_compile_error().into(),
145    }
146}
147
148/// Auxiliary function to enter ?-based error propagation.
149fn contract_bundle_provider_internal(
150    _attr: TokenStream2,
151    item: TokenStream2,
152) -> SynResult<TokenStream2> {
153    let enum_item = parse_bundle_enum(item)?;
154    let bundle_registry = build_contracts();
155    Ok(bundle_registry.generate_bundle_provision(enum_item))
156}
157
158fn parse_bundle_enum(item: TokenStream2) -> SynResult<ItemEnum> {
159    let enum_item = syn::parse2::<ItemEnum>(item)?;
160
161    if !enum_item.generics.params.is_empty() {
162        return Err(syn::Error::new_spanned(
163            enum_item.generics.params,
164            "ContractBundleProvider must not be generic",
165        ));
166    }
167    if !enum_item.variants.is_empty() {
168        return Err(syn::Error::new_spanned(
169            enum_item.variants,
170            "ContractBundleProvider must not have variants",
171        ));
172    }
173
174    Ok(enum_item)
175}