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}