diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aac63b02b0..a8b00c9cdbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Changed - Make `set_code_hash` generic - [#1906](/~https://github.com/paritytech/ink/pull/1906) +- Clean E2E configuration parsing - [#1922](/~https://github.com/paritytech/ink/pull/1922) ## Version 5.0.0-alpha diff --git a/Cargo.toml b/Cargo.toml index 24a7302f4f0..73f5a133e1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ blake2 = { version = "0.10" } cargo_metadata = { version = "0.18.0" } cfg-if = { version = "1.0" } contract-build = { version = "3.2.0" } +darling = { version = "0.20.3" } derive_more = { version = "0.99.17", default-features = false } drink = { version = "=0.2.0" } either = { version = "1.5", default-features = false } diff --git a/crates/e2e/macro/Cargo.toml b/crates/e2e/macro/Cargo.toml index 9d6198b20bf..831ed40f07d 100644 --- a/crates/e2e/macro/Cargo.toml +++ b/crates/e2e/macro/Cargo.toml @@ -19,6 +19,7 @@ name = "ink_e2e_macro" proc-macro = true [dependencies] +darling = { workspace = true } ink_ir = { workspace = true, default-features = true } derive_more = { workspace = true, default-features = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/crates/e2e/macro/src/config.rs b/crates/e2e/macro/src/config.rs index 123cec21deb..0cc5d31e0cb 100644 --- a/crates/e2e/macro/src/config.rs +++ b/crates/e2e/macro/src/config.rs @@ -12,14 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use ink_ir::{ - ast, - format_err_spanned, - utils::duplicate_config_err, -}; - /// The type of the architecture that should be used to run test. -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, darling::FromMeta)] pub enum Backend { /// The standard approach with running dedicated single-node blockchain in a /// background process. @@ -33,166 +27,43 @@ pub enum Backend { RuntimeOnly, } -impl TryFrom for Backend { - type Error = syn::Error; - - fn try_from(value: syn::LitStr) -> Result { - match value.value().as_str() { - "full" => Ok(Self::Full), - #[cfg(any(test, feature = "drink"))] - "runtime_only" | "runtime-only" => Ok(Self::RuntimeOnly), - #[cfg(not(any(test, feature = "drink")))] - "runtime_only" | "runtime-only" => Err( - format_err_spanned!( - value, - "the `runtime-only` backend is not available because the `drink` feature is not enabled", - ) - ), - _ => { - Err(format_err_spanned!( - value, - "unknown backend `{}` for ink! E2E test configuration argument", - value.value() - )) - } - } - } -} - /// The End-to-End test configuration. -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, PartialEq, Eq, darling::FromMeta)] pub struct E2EConfig { /// Additional contracts that have to be built before executing the test. - additional_contracts: Vec, + #[darling(default)] + additional_contracts: String, /// The [`Environment`](https://docs.rs/ink_env/4.1.0/ink_env/trait.Environment.html) to use /// during test execution. /// /// If no `Environment` is specified, the /// [`DefaultEnvironment`](https://docs.rs/ink_env/4.1.0/ink_env/enum.DefaultEnvironment.html) /// will be used. + #[darling(default)] environment: Option, /// The type of the architecture that should be used to run test. + #[darling(default)] backend: Backend, - /// The runtime to use for the runtime-only test. + /// The runtime to use for the runtime only test. #[cfg(any(test, feature = "drink"))] + #[darling(default)] runtime: Option, } -impl TryFrom for E2EConfig { - type Error = syn::Error; - - fn try_from(args: ast::AttributeArgs) -> Result { - let mut additional_contracts: Option<(syn::LitStr, ast::MetaNameValue)> = None; - let mut environment: Option<(syn::Path, ast::MetaNameValue)> = None; - let mut backend: Option<(syn::LitStr, ast::MetaNameValue)> = None; - #[cfg(any(test, feature = "drink"))] - let mut runtime: Option<(syn::Path, ast::MetaNameValue)> = None; - - for arg in args.into_iter() { - if arg.name.is_ident("additional_contracts") { - if let Some((_, ast)) = additional_contracts { - return Err(duplicate_config_err( - ast, - arg, - "additional_contracts", - "E2E test", - )) - } - if let ast::MetaValue::Lit(syn::Lit::Str(lit_str)) = &arg.value { - additional_contracts = Some((lit_str.clone(), arg)) - } else { - return Err(format_err_spanned!( - arg, - "expected a string literal for `additional_contracts` ink! E2E test configuration argument", - )); - } - } else if arg.name.is_ident("environment") { - if let Some((_, ast)) = environment { - return Err(duplicate_config_err(ast, arg, "environment", "E2E test")) - } - if let ast::MetaValue::Path(path) = &arg.value { - environment = Some((path.clone(), arg)) - } else { - return Err(format_err_spanned!( - arg, - "expected a path for `environment` ink! E2E test configuration argument", - )); - } - } else if arg.name.is_ident("backend") { - if let Some((_, ast)) = backend { - return Err(duplicate_config_err(ast, arg, "backend", "E2E test")) - } - if let ast::MetaValue::Lit(syn::Lit::Str(lit_str)) = &arg.value { - backend = Some((lit_str.clone(), arg)) - } else { - return Err(format_err_spanned!( - arg, - "expected a string literal for `backend` ink! E2E test configuration argument", - )); - } - } else if arg.name.is_ident("runtime") { - #[cfg(any(test, feature = "drink"))] - { - if let Some((_, ast)) = runtime { - return Err(duplicate_config_err(ast, arg, "runtime", "E2E test")) - } - if let ast::MetaValue::Path(path) = &arg.value { - runtime = Some((path.clone(), arg)) - } else { - return Err(format_err_spanned!( - arg, - "expected a path for `runtime` ink! E2E test configuration argument", - )); - } - } - #[cfg(not(any(test, feature = "drink")))] - { - return Err(format_err_spanned!( - arg, - "the `runtime` ink! E2E test configuration argument is not available because the `drink` feature is not enabled", - )); - } - } else { - return Err(format_err_spanned!( - arg, - "encountered unknown or unsupported ink! configuration argument", - )) - } - } - let additional_contracts = additional_contracts - .map(|(value, _)| value.value().split(' ').map(String::from).collect()) - .unwrap_or_else(Vec::new); - let environment = environment.map(|(path, _)| path); - let backend = backend - .map(|(b, _)| Backend::try_from(b)) - .transpose()? - .unwrap_or_default(); - - #[cfg(any(test, feature = "drink"))] - { - if backend == Backend::Full && runtime.is_some() { - return Err(format_err_spanned!( - runtime.unwrap().1, - "ink! E2E test `runtime` configuration argument is available for `runtime-only` backend only", - )); - } - } - - Ok(E2EConfig { - additional_contracts, - environment, - backend, - #[cfg(any(test, feature = "drink"))] - runtime: runtime.map(|(path, _)| path), - }) - } -} - impl E2EConfig { /// Returns a vector of additional contracts that have to be built /// and imported before executing the test. pub fn additional_contracts(&self) -> Vec { - self.additional_contracts.clone() + self.additional_contracts + .split(' ') + .filter_map(|s| { + if s.is_empty() { + None + } else { + Some(s.to_owned()) + } + }) + .collect() } /// Custom environment for the contracts, if specified. @@ -205,7 +76,7 @@ impl E2EConfig { self.backend } - /// The runtime to use for the runtime-only test. + /// The runtime to use for the runtime only test. #[cfg(any(test, feature = "drink"))] pub fn runtime(&self) -> Option { self.runtime.clone() @@ -215,175 +86,35 @@ impl E2EConfig { #[cfg(test)] mod tests { use super::*; - - /// Asserts that the given input configuration attribute argument are converted - /// into the expected ink! configuration or yields the expected error message. - fn assert_try_from( - input: ast::AttributeArgs, - expected: Result, - ) { - assert_eq!( - >::try_from(input) - .map_err(|err| err.to_string()), - expected.map_err(ToString::to_string), - ); - } - - #[test] - fn empty_config_works() { - assert_try_from(syn::parse_quote! {}, Ok(E2EConfig::default())) - } - - #[test] - fn unknown_arg_fails() { - assert_try_from( - syn::parse_quote! { unknown = argument }, - Err("encountered unknown or unsupported ink! configuration argument"), - ); - } - - #[test] - fn duplicate_additional_contracts_fails() { - assert_try_from( - syn::parse_quote! { - additional_contracts = "adder/Cargo.toml", - additional_contracts = "adder/Cargo.toml", - }, - Err( - "encountered duplicate ink! E2E test `additional_contracts` configuration argument", - ), - ); - } + use darling::{ + ast::NestedMeta, + FromMeta, + }; + use quote::quote; #[test] - fn duplicate_environment_fails() { - assert_try_from( - syn::parse_quote! { - environment = crate::CustomEnvironment, - environment = crate::CustomEnvironment, - }, - Err( - "encountered duplicate ink! E2E test `environment` configuration argument", - ), - ); - } - - #[test] - fn environment_as_literal_fails() { - assert_try_from( - syn::parse_quote! { - environment = "crate::CustomEnvironment", - }, - Err("expected a path for `environment` ink! E2E test configuration argument"), - ); - } - - #[test] - fn specifying_environment_works() { - assert_try_from( - syn::parse_quote! { - environment = crate::CustomEnvironment, - }, - Ok(E2EConfig { - environment: Some(syn::parse_quote! { crate::CustomEnvironment }), - ..Default::default() - }), - ); - } - - #[test] - fn backend_must_be_literal() { - assert_try_from( - syn::parse_quote! { backend = full }, - Err("expected a string literal for `backend` ink! E2E test configuration argument"), - ); - } - - #[test] - fn duplicate_backend_fails() { - assert_try_from( - syn::parse_quote! { - backend = "full", - backend = "runtime-only", - }, - Err("encountered duplicate ink! E2E test `backend` configuration argument"), - ); - } - - #[test] - fn specifying_backend_works() { - assert_try_from( - syn::parse_quote! { backend = "runtime-only" }, - Ok(E2EConfig { - backend: Backend::RuntimeOnly, - ..Default::default() - }), - ); - } - - #[test] - fn runtime_must_be_path() { - assert_try_from( - syn::parse_quote! { runtime = "MinimalRuntime" }, - Err("expected a path for `runtime` ink! E2E test configuration argument"), - ); - } - - #[test] - fn duplicate_runtime_fails() { - assert_try_from( - syn::parse_quote! { - runtime = ::drink::MinimalRuntime, - runtime = ::drink::MaximalRuntime, - }, - Err("encountered duplicate ink! E2E test `runtime` configuration argument"), - ); - } + fn config_works() { + let input = quote! { + additional_contracts = "adder/Cargo.toml flipper/Cargo.toml", + environment = crate::CustomEnvironment, + backend = "runtime_only", + runtime = ::drink::MinimalRuntime, + }; + let config = + E2EConfig::from_list(&NestedMeta::parse_meta_list(input).unwrap()).unwrap(); - #[test] - fn runtime_is_for_runtime_only_backend_only() { - assert_try_from( - syn::parse_quote! { - backend = "full", - runtime = ::drink::MinimalRuntime - }, - Err("ink! E2E test `runtime` configuration argument is available for `runtime-only` backend only"), + assert_eq!( + config.additional_contracts(), + vec!["adder/Cargo.toml", "flipper/Cargo.toml"] ); - } - - #[test] - fn specifying_runtime_works() { - assert_try_from( - syn::parse_quote! { - backend = "runtime-only", - runtime = ::drink::MinimalRuntime - }, - Ok(E2EConfig { - backend: Backend::RuntimeOnly, - runtime: Some(syn::parse_quote! { ::drink::MinimalRuntime }), - ..Default::default() - }), + assert_eq!( + config.environment(), + Some(syn::parse_quote! { crate::CustomEnvironment }) ); - } - - #[test] - fn full_config_works() { - assert_try_from( - syn::parse_quote! { - additional_contracts = "adder/Cargo.toml flipper/Cargo.toml", - environment = crate::CustomEnvironment, - backend = "runtime-only", - runtime = ::drink::MinimalRuntime, - }, - Ok(E2EConfig { - additional_contracts: vec![ - "adder/Cargo.toml".into(), - "flipper/Cargo.toml".into(), - ], - environment: Some(syn::parse_quote! { crate::CustomEnvironment }), - backend: Backend::RuntimeOnly, - runtime: Some(syn::parse_quote! { ::drink::MinimalRuntime }), - }), + assert_eq!(config.backend(), Backend::RuntimeOnly); + assert_eq!( + config.runtime(), + Some(syn::parse_quote! { ::drink::MinimalRuntime }) ); } } diff --git a/crates/e2e/macro/src/ir.rs b/crates/e2e/macro/src/ir.rs index 8fe65d7850b..1241a0ef0b7 100644 --- a/crates/e2e/macro/src/ir.rs +++ b/crates/e2e/macro/src/ir.rs @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - config::E2EConfig, - ir, +use crate::config::E2EConfig; +use darling::{ + ast::NestedMeta, + FromMeta, }; use proc_macro2::TokenStream as TokenStream2; @@ -37,8 +38,7 @@ impl InkE2ETest { /// Returns `Ok` if the test matches all requirements for an /// ink! E2E test definition. pub fn new(attrs: TokenStream2, input: TokenStream2) -> Result { - let config = syn::parse2::(attrs)?; - let e2e_config = ir::E2EConfig::try_from(config)?; + let e2e_config = E2EConfig::from_list(&NestedMeta::parse_meta_list(attrs)?)?; let item_fn = syn::parse2::(input)?; let e2e_fn = E2EFn::from(item_fn); Ok(Self { diff --git a/integration-tests/e2e-runtime-only-backend/lib.rs b/integration-tests/e2e-runtime-only-backend/lib.rs index b6fce1848e0..f11a62322f3 100644 --- a/integration-tests/e2e-runtime-only-backend/lib.rs +++ b/integration-tests/e2e-runtime-only-backend/lib.rs @@ -84,7 +84,7 @@ pub mod flipper { /// - flip the flipper /// - get the flipper's value /// - assert that the value is `true` - #[ink_e2e::test(backend = "runtime-only")] + #[ink_e2e::test(backend = "runtime_only")] async fn it_works(mut client: Client) -> E2EResult<()> { // given const INITIAL_VALUE: bool = false; @@ -115,7 +115,7 @@ pub mod flipper { /// - transfer some funds to the contract using runtime call /// - get the contract's balance again /// - assert that the contract's balance increased by the transferred amount - #[ink_e2e::test(backend = "runtime-only")] + #[ink_e2e::test(backend = "runtime_only")] async fn runtime_call_works() -> E2EResult<()> { // given let contract = deploy(&mut client, false).await.expect("deploy failed"); @@ -151,7 +151,7 @@ pub mod flipper { } /// Just instantiate a contract using non-default runtime. - #[ink_e2e::test(backend = "runtime-only", runtime = ink_e2e::MinimalRuntime)] + #[ink_e2e::test(backend = "runtime_only", runtime = ink_e2e::MinimalRuntime)] async fn custom_runtime(mut client: Client) -> E2EResult<()> { client .instantiate(