ink_e2e_macro/
config.rs

1// Copyright (C) Use Ink (UK) Ltd.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/// The type of the architecture that should be used to run test.
16#[derive(Clone, Eq, PartialEq, Debug, darling::FromMeta)]
17#[darling(rename_all = "snake_case")]
18pub enum Backend {
19    /// The standard approach with running dedicated single-node blockchain in a
20    /// background process.
21    Node(Node),
22
23    /// The lightweight approach skipping node layer.
24    ///
25    /// This runs a runtime emulator within `TestExternalities`
26    /// the same process as the test.
27    RuntimeOnly(RuntimeOnly),
28}
29
30impl Default for Backend {
31    fn default() -> Self {
32        Backend::Node(Node::Auto)
33    }
34}
35
36/// Configure whether to automatically spawn a node instance for the test or to use
37/// an already running node at the supplied URL.
38#[derive(Clone, Eq, PartialEq, Debug, darling::FromMeta)]
39pub enum Node {
40    /// A fresh node instance will be spawned for the lifetime of the test.
41    #[darling(word)]
42    #[darling(skip)]
43    Auto,
44    /// The test will run against an already running node at the supplied URL.
45    Url(String),
46}
47
48impl Node {
49    /// The URL to the running node, default value can be overridden with
50    /// `CONTRACTS_NODE_URL`.
51    ///
52    /// Returns `None` if [`Self::Auto`] and `CONTRACTS_NODE_URL` not specified.
53    pub fn url(&self) -> Option<String> {
54        let url = std::env::var("CONTRACTS_NODE_URL").ok().or_else(|| {
55            match self {
56                Node::Auto => None,
57                Node::Url(url) => Some(url.clone()),
58            }
59        });
60        tracing::debug!("[E2E] Using node url {:?}", url);
61        url
62    }
63}
64
65/// The runtime emulator that should be used within `TestExternalities`
66#[derive(Clone, Eq, PartialEq, Debug, darling::FromMeta)]
67pub struct RuntimeOnly {
68    /// The sandbox runtime type (e.g., `ink_sandbox::DefaultSandbox`)
69    pub sandbox: syn::Path,
70    /// The client type implementing the backend traits (e.g.,
71    /// `ink_sandbox::SandboxClient`)
72    pub client: syn::Path,
73}
74
75impl RuntimeOnly {
76    pub fn runtime_path(&self) -> syn::Path {
77        self.sandbox.clone()
78    }
79    pub fn client_path(&self) -> syn::Path {
80        self.client.clone()
81    }
82}
83
84/// The End-to-End test configuration.
85#[derive(Debug, Default, PartialEq, Eq, darling::FromMeta)]
86pub struct E2EConfig {
87    /// The [`Environment`](https://docs.rs/ink_env/4.1.0/ink_env/trait.Environment.html) to use
88    /// during test execution.
89    ///
90    /// If no `Environment` is specified, the
91    /// [`DefaultEnvironment`](https://docs.rs/ink_env/4.1.0/ink_env/enum.DefaultEnvironment.html)
92    /// will be used.
93    #[darling(default)]
94    environment: Option<syn::Path>,
95    /// The type of the architecture that should be used to run test.
96    #[darling(default)]
97    backend: Backend,
98    /// Features that are enabled in the contract during the build process.
99    /// todo add tests below in this file
100    #[darling(default)]
101    features: Vec<syn::LitStr>,
102    /// A replacement attribute for `#[test]`. Instead of `#[test]` the E2E code
103    /// generation will output this attribute.
104    ///
105    /// This can be used to supply e.g. `#[quicktest]`, thus transforming the
106    /// test into a fuzzing E2E test.
107    #[darling(default)]
108    replace_test_attr: Option<String>,
109}
110
111impl E2EConfig {
112    /// Custom environment for the contracts, if specified.
113    pub fn environment(&self) -> Option<syn::Path> {
114        self.environment.clone()
115    }
116
117    /// Features for the contract build.
118    pub fn features(&self) -> Vec<String> {
119        self.features.iter().map(|ls| ls.value()).collect()
120    }
121
122    /// The type of the architecture that should be used to run test.
123    pub fn backend(&self) -> Backend {
124        self.backend.clone()
125    }
126
127    /// A custom attribute which the code generation will output instead
128    /// of `#[test]`.
129    pub fn replace_test_attr(&self) -> Option<String> {
130        self.replace_test_attr.clone()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use darling::{
138        FromMeta,
139        ast::NestedMeta,
140    };
141    use quote::quote;
142
143    #[test]
144    fn config_works_backend_runtime_only() {
145        let input = quote! {
146            environment = crate::CustomEnvironment,
147            backend(runtime_only(sandbox = ::ink_sandbox::DefaultSandbox, client = ::ink_sandbox::SandboxClient)),
148        };
149        let config =
150            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
151
152        assert_eq!(
153            config.environment(),
154            Some(syn::parse_quote! { crate::CustomEnvironment })
155        );
156
157        assert_eq!(
158            config.backend(),
159            Backend::RuntimeOnly(RuntimeOnly {
160                sandbox: syn::parse_quote! { ::ink_sandbox::DefaultSandbox },
161                client: syn::parse_quote! { ::ink_sandbox::SandboxClient },
162            })
163        );
164    }
165
166    #[test]
167    #[should_panic(expected = "ErrorUnknownField")]
168    fn config_backend_runtime_only_default_not_allowed() {
169        let input = quote! {
170            backend(runtime_only(default)),
171        };
172        let config =
173            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
174
175        assert_eq!(
176            config.backend(),
177            Backend::RuntimeOnly(RuntimeOnly {
178                sandbox: syn::parse_quote! { ::ink_sandbox::DefaultSandbox },
179                client: syn::parse_quote! { ::ink_sandbox::SandboxClient },
180            })
181        );
182    }
183
184    #[test]
185    fn config_works_runtime_only_with_custom_backend() {
186        let input = quote! {
187            backend(runtime_only(sandbox = ::ink_sandbox::DefaultSandbox, client = ::ink_sandbox::SandboxClient)),
188        };
189        let config =
190            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
191
192        assert_eq!(
193            config.backend(),
194            Backend::RuntimeOnly(RuntimeOnly {
195                sandbox: syn::parse_quote! { ::ink_sandbox::DefaultSandbox },
196                client: syn::parse_quote! { ::ink_sandbox::SandboxClient },
197            })
198        );
199    }
200
201    #[test]
202    fn config_works_backend_node() {
203        let input = quote! {
204            backend(node),
205        };
206        let config =
207            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
208
209        assert_eq!(config.backend(), Backend::Node(Node::Auto));
210
211        match config.backend() {
212            Backend::Node(node_config) => {
213                assert_eq!(node_config, Node::Auto);
214
215                temp_env::with_vars([("CONTRACTS_NODE_URL", None::<&str>)], || {
216                    assert_eq!(node_config.url(), None);
217                });
218
219                temp_env::with_vars(
220                    [("CONTRACTS_NODE_URL", Some("ws://127.0.0.1:9000"))],
221                    || {
222                        assert_eq!(
223                            node_config.url(),
224                            Some(String::from("ws://127.0.0.1:9000"))
225                        );
226                    },
227                );
228            }
229            _ => panic!("Expected Backend::Node"),
230        }
231    }
232
233    #[test]
234    #[should_panic(expected = "ErrorUnknownField")]
235    fn config_backend_node_auto_not_allowed() {
236        let input = quote! {
237            backend(node(auto)),
238        };
239        let config =
240            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
241
242        assert_eq!(config.backend(), Backend::Node(Node::Auto));
243    }
244
245    #[test]
246    fn config_works_backend_node_url() {
247        let input = quote! {
248            backend(node(url = "ws://0.0.0.0:9999")),
249        };
250        let config =
251            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
252
253        match config.backend() {
254            Backend::Node(node_config) => {
255                assert_eq!(node_config, Node::Url("ws://0.0.0.0:9999".to_owned()));
256
257                temp_env::with_vars([("CONTRACTS_NODE_URL", None::<&str>)], || {
258                    assert_eq!(node_config.url(), Some("ws://0.0.0.0:9999".to_owned()));
259                });
260
261                temp_env::with_vars(
262                    [("CONTRACTS_NODE_URL", Some("ws://127.0.0.1:9000"))],
263                    || {
264                        assert_eq!(
265                            node_config.url(),
266                            Some(String::from("ws://127.0.0.1:9000"))
267                        );
268                    },
269                );
270            }
271            _ => panic!("Expected Backend::Node"),
272        }
273    }
274
275    #[test]
276    fn config_works_test_attr_replacement() {
277        let input = quote! {
278            replace_test_attr = "#[quickcheck]"
279        };
280        let config =
281            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
282
283        assert_eq!(config.replace_test_attr(), Some("#[quickcheck]".to_owned()));
284    }
285}