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    #[cfg(any(test, feature = "sandbox"))]
28    RuntimeOnly(RuntimeOnly),
29}
30
31impl Default for Backend {
32    fn default() -> Self {
33        Backend::Node(Node::Auto)
34    }
35}
36
37/// Configure whether to automatically spawn a node instance for the test or to use
38/// an already running node at the supplied URL.
39#[derive(Clone, Eq, PartialEq, Debug, darling::FromMeta)]
40pub enum Node {
41    /// A fresh node instance will be spawned for the lifetime of the test.
42    #[darling(word)]
43    #[darling(skip)]
44    Auto,
45    /// The test will run against an already running node at the supplied URL.
46    Url(String),
47}
48
49impl Node {
50    /// The URL to the running node, default value can be overridden with
51    /// `CONTRACTS_NODE_URL`.
52    ///
53    /// Returns `None` if [`Self::Auto`] and `CONTRACTS_NODE_URL` not specified.
54    pub fn url(&self) -> Option<String> {
55        let url = std::env::var("CONTRACTS_NODE_URL").ok().or_else(|| {
56            match self {
57                Node::Auto => None,
58                Node::Url(url) => Some(url.clone()),
59            }
60        });
61        tracing::debug!("[E2E] Using node url {:?}", url);
62        url
63    }
64}
65
66/// The runtime emulator that should be used within `TestExternalities`
67#[cfg(any(test, feature = "sandbox"))]
68#[derive(Clone, Eq, PartialEq, Debug, darling::FromMeta)]
69pub enum RuntimeOnly {
70    #[darling(word)]
71    #[darling(skip)]
72    Default,
73    Sandbox(syn::Path),
74}
75
76#[cfg(any(test, feature = "sandbox"))]
77impl From<RuntimeOnly> for syn::Path {
78    fn from(value: RuntimeOnly) -> Self {
79        match value {
80            RuntimeOnly::Default => syn::parse_quote! { ::ink_e2e::DefaultSandbox },
81            RuntimeOnly::Sandbox(path) => path,
82        }
83    }
84}
85
86/// The End-to-End test configuration.
87#[derive(Debug, Default, PartialEq, Eq, darling::FromMeta)]
88pub struct E2EConfig {
89    /// The [`Environment`](https://docs.rs/ink_env/4.1.0/ink_env/trait.Environment.html) to use
90    /// during test execution.
91    ///
92    /// If no `Environment` is specified, the
93    /// [`DefaultEnvironment`](https://docs.rs/ink_env/4.1.0/ink_env/enum.DefaultEnvironment.html)
94    /// will be used.
95    #[darling(default)]
96    environment: Option<syn::Path>,
97    /// The type of the architecture that should be used to run test.
98    #[darling(default)]
99    backend: Backend,
100    /// Features that are enabled in the contract during the build process.
101    /// todo add tests below in this file
102    #[darling(default)]
103    features: Vec<syn::LitStr>,
104    /// A replacement attribute for `#[test]`. Instead of `#[test]` the E2E code
105    /// generation will output this attribute.
106    ///
107    /// This can be used to supply e.g. `#[quicktest]`, thus transforming the
108    /// test into a fuzzing E2E test.
109    #[darling(default)]
110    replace_test_attr: Option<String>,
111}
112
113impl E2EConfig {
114    /// Custom environment for the contracts, if specified.
115    pub fn environment(&self) -> Option<syn::Path> {
116        self.environment.clone()
117    }
118
119    /// Features for the contract build.
120    pub fn features(&self) -> Vec<String> {
121        self.features.iter().map(|ls| ls.value()).collect()
122    }
123
124    /// The type of the architecture that should be used to run test.
125    pub fn backend(&self) -> Backend {
126        self.backend.clone()
127    }
128
129    /// A custom attribute which the code generation will output instead
130    /// of `#[test]`.
131    pub fn replace_test_attr(&self) -> Option<String> {
132        self.replace_test_attr.clone()
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use darling::{
140        FromMeta,
141        ast::NestedMeta,
142    };
143    use quote::quote;
144
145    #[test]
146    fn config_works_backend_runtime_only() {
147        let input = quote! {
148            environment = crate::CustomEnvironment,
149            backend(runtime_only),
150        };
151        let config =
152            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
153
154        assert_eq!(
155            config.environment(),
156            Some(syn::parse_quote! { crate::CustomEnvironment })
157        );
158
159        assert_eq!(config.backend(), Backend::RuntimeOnly(RuntimeOnly::Default));
160    }
161
162    #[test]
163    #[should_panic(expected = "ErrorUnknownField")]
164    fn config_backend_runtime_only_default_not_allowed() {
165        let input = quote! {
166            backend(runtime_only(default)),
167        };
168        let config =
169            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
170
171        assert_eq!(config.backend(), Backend::RuntimeOnly(RuntimeOnly::Default));
172    }
173
174    #[test]
175    fn config_works_runtime_only_with_custom_backend() {
176        let input = quote! {
177            backend(runtime_only(sandbox = ::ink_e2e::DefaultSandbox)),
178        };
179        let config =
180            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
181
182        assert_eq!(
183            config.backend(),
184            Backend::RuntimeOnly(RuntimeOnly::Sandbox(
185                syn::parse_quote! { ::ink_e2e::DefaultSandbox }
186            ))
187        );
188    }
189
190    #[test]
191    fn config_works_backend_node() {
192        let input = quote! {
193            backend(node),
194        };
195        let config =
196            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
197
198        assert_eq!(config.backend(), Backend::Node(Node::Auto));
199
200        match config.backend() {
201            Backend::Node(node_config) => {
202                assert_eq!(node_config, Node::Auto);
203
204                temp_env::with_vars([("CONTRACTS_NODE_URL", None::<&str>)], || {
205                    assert_eq!(node_config.url(), None);
206                });
207
208                temp_env::with_vars(
209                    [("CONTRACTS_NODE_URL", Some("ws://127.0.0.1:9000"))],
210                    || {
211                        assert_eq!(
212                            node_config.url(),
213                            Some(String::from("ws://127.0.0.1:9000"))
214                        );
215                    },
216                );
217            }
218            _ => panic!("Expected Backend::Node"),
219        }
220    }
221
222    #[test]
223    #[should_panic(expected = "ErrorUnknownField")]
224    fn config_backend_node_auto_not_allowed() {
225        let input = quote! {
226            backend(node(auto)),
227        };
228        let config =
229            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
230
231        assert_eq!(config.backend(), Backend::Node(Node::Auto));
232    }
233
234    #[test]
235    fn config_works_backend_node_url() {
236        let input = quote! {
237            backend(node(url = "ws://0.0.0.0:9999")),
238        };
239        let config =
240            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
241
242        match config.backend() {
243            Backend::Node(node_config) => {
244                assert_eq!(node_config, Node::Url("ws://0.0.0.0:9999".to_owned()));
245
246                temp_env::with_vars([("CONTRACTS_NODE_URL", None::<&str>)], || {
247                    assert_eq!(node_config.url(), Some("ws://0.0.0.0:9999".to_owned()));
248                });
249
250                temp_env::with_vars(
251                    [("CONTRACTS_NODE_URL", Some("ws://127.0.0.1:9000"))],
252                    || {
253                        assert_eq!(
254                            node_config.url(),
255                            Some(String::from("ws://127.0.0.1:9000"))
256                        );
257                    },
258                );
259            }
260            _ => panic!("Expected Backend::Node"),
261        }
262    }
263
264    #[test]
265    fn config_works_test_attr_replacement() {
266        let input = quote! {
267            replace_test_attr = "#[quickcheck]"
268        };
269        let config =
270            E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap();
271
272        assert_eq!(config.replace_test_attr(), Some("#[quickcheck]".to_owned()));
273    }
274}