From c0c7a27ae5bc04518a7c411766d00772d9b9479b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Tue, 3 Oct 2023 16:05:05 +0200 Subject: [PATCH 01/26] Base structs --- drink/src/lib.rs | 2 ++ drink/src/mock.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 drink/src/mock.rs diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 90655a0..82220da 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -6,9 +6,11 @@ pub mod chain_api; pub mod contract_api; mod error; +pub mod mock; pub mod runtime; #[cfg(feature = "session")] pub mod session; + use std::marker::PhantomData; pub use error::Error; diff --git a/drink/src/mock.rs b/drink/src/mock.rs new file mode 100644 index 0000000..13627e3 --- /dev/null +++ b/drink/src/mock.rs @@ -0,0 +1,26 @@ +//! Mocking contract feature. + +use std::marker::PhantomData; + +pub struct MockRegistry { + mocked_contracts: Vec>, +} + +pub struct ContractMock { + mocked_addresses: Vec, + mocked_methods: Vec>, +} + +trait MethodMockT {} +impl MethodMockT for MethodMock {} + +pub struct MethodMock { + selector: [u8; 4], + matchers: Vec>, + _phantom: PhantomData<(Args, Ret)>, +} + +pub struct CallMatcher { + arg_matcher: Box bool>, + ret: Ret, +} From 68f3794903f33bb9f3f2a5b8c6d368d90f084afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Tue, 3 Oct 2023 16:43:43 +0200 Subject: [PATCH 02/26] New example contract (delegator) --- Cargo.toml | 1 + drink/src/lib.rs | 8 +++++ drink/src/mock.rs | 12 +++++++ examples/mocking/Cargo.toml | 28 +++++++++++++++ examples/mocking/lib.rs | 71 +++++++++++++++++++++++++++++++++++++ 5 files changed, 120 insertions(+) create mode 100755 examples/mocking/Cargo.toml create mode 100755 examples/mocking/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 17368cf..e67e75c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ exclude = [ "examples/counter", "examples/flipper", "examples/cross-contract-call-tracing", + "examples/mocking", ] [workspace.package] diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 82220da..df5e062 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -23,6 +23,7 @@ use frame_system::{pallet_prelude::BlockNumberFor, EventRecord, GenesisConfig}; use sp_io::TestExternalities; use crate::{ + mock::{ContractMock, MockRegistry}, pallet_contracts_debugging::DebugExt, runtime::{pallet_contracts_debugging::NoopDebugExt, *}, }; @@ -37,6 +38,7 @@ pub type EventRecordOf = /// A sandboxed runtime. pub struct Sandbox { externalities: TestExternalities, + mock_registry: MockRegistry>, _phantom: PhantomData, } @@ -59,6 +61,7 @@ impl Sandbox { let mut sandbox = Self { externalities: TestExternalities::new(storage), + mock_registry: MockRegistry::new(), _phantom: PhantomData, }; @@ -82,4 +85,9 @@ impl Sandbox { pub fn override_debug_handle(&mut self, d: DebugExt) { self.externalities.register_extension(d); } + + /// Register a new contract mock. + pub fn register_mock(&mut self, mock: ContractMock>) { + self.mock_registry.register_mock(mock); + } } diff --git a/drink/src/mock.rs b/drink/src/mock.rs index 13627e3..15dcd60 100644 --- a/drink/src/mock.rs +++ b/drink/src/mock.rs @@ -6,6 +6,18 @@ pub struct MockRegistry { mocked_contracts: Vec>, } +impl MockRegistry { + pub fn new() -> Self { + Self { + mocked_contracts: Vec::new(), + } + } + + pub fn register_mock(&mut self, contract: ContractMock) { + self.mocked_contracts.push(contract); + } +} + pub struct ContractMock { mocked_addresses: Vec, mocked_methods: Vec>, diff --git a/examples/mocking/Cargo.toml b/examples/mocking/Cargo.toml new file mode 100755 index 0000000..63ec680 --- /dev/null +++ b/examples/mocking/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "mocking" +authors = ["Cardinal", "Aleph Zero Foundation"] +edition = "2021" +homepage = "https://alephzero.org" +repository = "https://github.com/Cardinal-Cryptography/drink" +version = "0.1.0" + +[dependencies] +ink = { version = "=4.2.1", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +drink = { path = "../../drink", features = ["session"] } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/examples/mocking/lib.rs b/examples/mocking/lib.rs new file mode 100755 index 0000000..35e8df9 --- /dev/null +++ b/examples/mocking/lib.rs @@ -0,0 +1,71 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use ink::env::call::Selector; + +const CALLEE_SELECTOR: Selector = Selector::new(ink::selector_bytes!("callee")); + +#[ink::contract] +mod contract { + use ink::env::{ + call::{build_call, ExecutionInput}, + DefaultEnvironment, + }; + + use crate::CALLEE_SELECTOR; + + #[ink(storage)] + pub struct Contract {} + + impl Contract { + #[ink(constructor)] + pub fn new() -> Self { + Self {} + } + + #[ink(message)] + pub fn delegate_call(&self, callee: AccountId, arg: u32) -> (u16, u16) { + build_call::() + .call(callee) + .gas_limit(0) + .transferred_value(0) + .exec_input(ExecutionInput::new(CALLEE_SELECTOR).push_arg(arg)) + .returns::<(u16, u16)>() + .invoke() + } + } +} + +#[cfg(test)] +mod tests { + use std::{error::Error, fs, path::PathBuf, rc::Rc}; + + use drink::{ + runtime::MinimalRuntime, + session::{contract_transcode::ContractMessageTranscoder, Session, NO_ARGS}, + }; + + fn transcoder() -> Rc { + Rc::new( + ContractMessageTranscoder::load(PathBuf::from("./target/ink/mocking.json")) + .expect("Failed to create transcoder"), + ) + } + + fn bytes() -> Vec { + fs::read("./target/ink/mocking.wasm").expect("Failed to find or read contract file") + } + + #[test] + fn initialization() -> Result<(), Box> { + Session::::new()?.deploy_and( + bytes(), + "new", + NO_ARGS, + vec![], + None, + &transcoder(), + )?; + + Ok(()) + } +} From 9e9bce1f0d0df31b2ba5aaac1d187d3403900ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 4 Oct 2023 11:52:06 +0200 Subject: [PATCH 03/26] MockingApi --- drink/src/lib.rs | 7 +------ drink/src/mock.rs | 6 ++++++ drink/src/mock/mocking_api.rs | 16 ++++++++++++++++ drink/src/session.rs | 7 ++++++- 4 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 drink/src/mock/mocking_api.rs diff --git a/drink/src/lib.rs b/drink/src/lib.rs index df5e062..795255a 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -23,7 +23,7 @@ use frame_system::{pallet_prelude::BlockNumberFor, EventRecord, GenesisConfig}; use sp_io::TestExternalities; use crate::{ - mock::{ContractMock, MockRegistry}, + mock::MockRegistry, pallet_contracts_debugging::DebugExt, runtime::{pallet_contracts_debugging::NoopDebugExt, *}, }; @@ -85,9 +85,4 @@ impl Sandbox { pub fn override_debug_handle(&mut self, d: DebugExt) { self.externalities.register_extension(d); } - - /// Register a new contract mock. - pub fn register_mock(&mut self, mock: ContractMock>) { - self.mock_registry.register_mock(mock); - } } diff --git a/drink/src/mock.rs b/drink/src/mock.rs index 15dcd60..c046921 100644 --- a/drink/src/mock.rs +++ b/drink/src/mock.rs @@ -1,7 +1,13 @@ //! Mocking contract feature. +mod builder_utils; +mod message; +mod mocking_api; + use std::marker::PhantomData; +pub use mocking_api::MockingApi; + pub struct MockRegistry { mocked_contracts: Vec>, } diff --git a/drink/src/mock/mocking_api.rs b/drink/src/mock/mocking_api.rs new file mode 100644 index 0000000..bd78dfc --- /dev/null +++ b/drink/src/mock/mocking_api.rs @@ -0,0 +1,16 @@ +use crate::{ + mock::ContractMock, + runtime::{AccountIdFor, Runtime}, + Sandbox, +}; + +pub trait MockingApi { + /// Register a new contract mock. + fn register_mock(&mut self, mock: ContractMock>); +} + +impl MockingApi for Sandbox { + fn register_mock(&mut self, mock: ContractMock>) { + self.mock_registry.register_mock(mock); + } +} diff --git a/drink/src/session.rs b/drink/src/session.rs index d8b9896..54f6edf 100644 --- a/drink/src/session.rs +++ b/drink/src/session.rs @@ -21,7 +21,7 @@ mod transcoding; use errors::{MessageResult, SessionError}; -use crate::session::transcoding::TranscoderRegistry; +use crate::{mock::MockingApi, session::transcoding::TranscoderRegistry}; type Balance = u128; @@ -169,6 +169,11 @@ impl Session { &mut self.sandbox } + /// Returns a reference for mocking API. + pub fn mocking_api(&mut self) -> &mut impl MockingApi { + &mut self.sandbox + } + /// Deploys a contract with a given constructor, arguments, salt and endowment. In case of /// success, returns `self`. pub fn deploy_and + Debug>( From 25e02d1e1c4b36a2c9f768e60859a0562236dc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 4 Oct 2023 11:52:22 +0200 Subject: [PATCH 04/26] Building message mock --- drink/src/mock/builder_utils.rs | 27 +++++++++++++ drink/src/mock/message.rs | 71 +++++++++++++++++++++++++++++++++ examples/mocking/lib.rs | 21 ++++++---- 3 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 drink/src/mock/builder_utils.rs create mode 100644 drink/src/mock/message.rs diff --git a/drink/src/mock/builder_utils.rs b/drink/src/mock/builder_utils.rs new file mode 100644 index 0000000..e32480c --- /dev/null +++ b/drink/src/mock/builder_utils.rs @@ -0,0 +1,27 @@ +use std::marker::PhantomData; + +pub trait Setter {} + +pub struct Set(T); + +impl Setter for Set {} + +impl Set { + pub fn new(t: T) -> Self { + Self(t) + } + + pub fn retrieve(self) -> T { + self.0 + } +} + +pub struct UnSet(PhantomData); + +impl Setter for UnSet {} + +impl UnSet { + pub fn new() -> Self { + Self(PhantomData::default()) + } +} diff --git a/drink/src/mock/message.rs b/drink/src/mock/message.rs new file mode 100644 index 0000000..06aec7d --- /dev/null +++ b/drink/src/mock/message.rs @@ -0,0 +1,71 @@ +use std::marker::PhantomData; + +use crate::mock::builder_utils::{Set, Setter, UnSet}; + +type Body = Box Ret>; +type Selector = [u8; 4]; + +pub struct MessageMock { + selector: Selector, + body: Body, +} + +pub struct MessageMockBuilder< + Args, + Ret, + SelectorSetter: Setter, + BodySetter: Setter>, +> { + selector: SelectorSetter, + body: BodySetter, + _phantom: PhantomData<(Args, Ret)>, +} + +impl MessageMockBuilder, UnSet>> { + pub fn new() -> Self { + Self { + selector: UnSet::new(), + body: UnSet::new(), + _phantom: PhantomData::default(), + } + } +} + +impl>> + MessageMockBuilder, BodySetter> +{ + pub fn with_selector( + self, + selector: Selector, + ) -> MessageMockBuilder, BodySetter> { + MessageMockBuilder::, BodySetter> { + selector: Set::new(selector), + body: self.body, + _phantom: self._phantom, + } + } +} + +impl> + MessageMockBuilder>> +{ + pub fn with_body( + self, + body: Body, + ) -> MessageMockBuilder>> { + MessageMockBuilder::>> { + selector: self.selector, + body: Set::new(body), + _phantom: self._phantom, + } + } +} + +impl MessageMockBuilder, Set>> { + pub fn build(self) -> MessageMock { + MessageMock:: { + selector: self.selector.retrieve(), + body: self.body.retrieve(), + } + } +} diff --git a/examples/mocking/lib.rs b/examples/mocking/lib.rs index 35e8df9..1e53876 100755 --- a/examples/mocking/lib.rs +++ b/examples/mocking/lib.rs @@ -40,10 +40,13 @@ mod tests { use std::{error::Error, fs, path::PathBuf, rc::Rc}; use drink::{ + mock::{ContractMock, MockingApi}, runtime::MinimalRuntime, session::{contract_transcode::ContractMessageTranscoder, Session, NO_ARGS}, }; + use crate::CALLEE_SELECTOR; + fn transcoder() -> Rc { Rc::new( ContractMessageTranscoder::load(PathBuf::from("./target/ink/mocking.json")) @@ -57,14 +60,16 @@ mod tests { #[test] fn initialization() -> Result<(), Box> { - Session::::new()?.deploy_and( - bytes(), - "new", - NO_ARGS, - vec![], - None, - &transcoder(), - )?; + let mut session = Session::::new()?; + session.deploy(bytes(), "new", NO_ARGS, vec![], None, &transcoder())?; + + let mock = MockBuilder::new() + .with_selector(CALLEE_SELECTOR) + .with_matcher(|arg: u32| arg == 42) + .with_return((1, 2)) + .build(); + + session.mocking_api().register_mock(todo!()); Ok(()) } From bc8cd47ad9b76b25e3700e14c79dc61246f705e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 4 Oct 2023 12:08:21 +0200 Subject: [PATCH 05/26] Contract mock --- drink/src/lib.rs | 2 +- drink/src/mock.rs | 34 ++++++---------------------------- drink/src/mock/contract.rs | 18 ++++++++++++++++++ drink/src/mock/message.rs | 3 +++ drink/src/mock/mocking_api.rs | 16 ++++++++++++---- examples/mocking/lib.rs | 13 +++++++------ 6 files changed, 47 insertions(+), 39 deletions(-) create mode 100644 drink/src/mock/contract.rs diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 795255a..0e47002 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -38,7 +38,7 @@ pub type EventRecordOf = /// A sandboxed runtime. pub struct Sandbox { externalities: TestExternalities, - mock_registry: MockRegistry>, + mock_registry: MockRegistry, _phantom: PhantomData, } diff --git a/drink/src/mock.rs b/drink/src/mock.rs index c046921..f3d6a5d 100644 --- a/drink/src/mock.rs +++ b/drink/src/mock.rs @@ -1,44 +1,22 @@ //! Mocking contract feature. mod builder_utils; +mod contract; mod message; mod mocking_api; -use std::marker::PhantomData; - +pub use contract::ContractMock; +pub use message::{MessageMock, MessageMockBuilder}; pub use mocking_api::MockingApi; -pub struct MockRegistry { - mocked_contracts: Vec>, +pub struct MockRegistry { + mocked_contracts: Vec, } -impl MockRegistry { +impl MockRegistry { pub fn new() -> Self { Self { mocked_contracts: Vec::new(), } } - - pub fn register_mock(&mut self, contract: ContractMock) { - self.mocked_contracts.push(contract); - } -} - -pub struct ContractMock { - mocked_addresses: Vec, - mocked_methods: Vec>, -} - -trait MethodMockT {} -impl MethodMockT for MethodMock {} - -pub struct MethodMock { - selector: [u8; 4], - matchers: Vec>, - _phantom: PhantomData<(Args, Ret)>, -} - -pub struct CallMatcher { - arg_matcher: Box bool>, - ret: Ret, } diff --git a/drink/src/mock/contract.rs b/drink/src/mock/contract.rs new file mode 100644 index 0000000..05fd64c --- /dev/null +++ b/drink/src/mock/contract.rs @@ -0,0 +1,18 @@ +use crate::mock::message::MessageMockT; + +pub struct ContractMock { + messages: Vec>, +} + +impl ContractMock { + pub fn new() -> Self { + Self { + messages: Vec::new(), + } + } + + pub fn with_message(mut self, message: M) -> Self { + self.messages.push(Box::new(message)); + self + } +} diff --git a/drink/src/mock/message.rs b/drink/src/mock/message.rs index 06aec7d..6450a5b 100644 --- a/drink/src/mock/message.rs +++ b/drink/src/mock/message.rs @@ -5,6 +5,9 @@ use crate::mock::builder_utils::{Set, Setter, UnSet}; type Body = Box Ret>; type Selector = [u8; 4]; +pub trait MessageMockT {} +impl MessageMockT for MessageMock {} + pub struct MessageMock { selector: Selector, body: Body, diff --git a/drink/src/mock/mocking_api.rs b/drink/src/mock/mocking_api.rs index bd78dfc..571e989 100644 --- a/drink/src/mock/mocking_api.rs +++ b/drink/src/mock/mocking_api.rs @@ -5,12 +5,20 @@ use crate::{ }; pub trait MockingApi { - /// Register a new contract mock. - fn register_mock(&mut self, mock: ContractMock>); + /// Deploy `mock` as a standard contract. Returns the address of the deployed contract. + fn deploy_mock(&mut self, mock: ContractMock) -> AccountIdFor; + + /// Mock part of an existing contract. In particular, allows to override real behavior of + /// deployed contract's messages. + fn mock_existing_contract(&mut self, mock: ContractMock, address: AccountIdFor); } impl MockingApi for Sandbox { - fn register_mock(&mut self, mock: ContractMock>) { - self.mock_registry.register_mock(mock); + fn deploy_mock(&mut self, mock: ContractMock) -> AccountIdFor { + todo!() + } + + fn mock_existing_contract(&mut self, mock: ContractMock, address: AccountIdFor) { + todo!("soon") } } diff --git a/examples/mocking/lib.rs b/examples/mocking/lib.rs index 1e53876..5f5fd37 100755 --- a/examples/mocking/lib.rs +++ b/examples/mocking/lib.rs @@ -40,7 +40,7 @@ mod tests { use std::{error::Error, fs, path::PathBuf, rc::Rc}; use drink::{ - mock::{ContractMock, MockingApi}, + mock::{ContractMock, MessageMockBuilder, MockingApi}, runtime::MinimalRuntime, session::{contract_transcode::ContractMessageTranscoder, Session, NO_ARGS}, }; @@ -63,13 +63,14 @@ mod tests { let mut session = Session::::new()?; session.deploy(bytes(), "new", NO_ARGS, vec![], None, &transcoder())?; - let mock = MockBuilder::new() - .with_selector(CALLEE_SELECTOR) - .with_matcher(|arg: u32| arg == 42) - .with_return((1, 2)) + let mocked_message = MessageMockBuilder::new() + .with_selector(CALLEE_SELECTOR.to_bytes()) + .with_body(Box::new(|_: u32| (0, 0))) .build(); - session.mocking_api().register_mock(todo!()); + let mocked_contract = ContractMock::new().with_message(mocked_message); + + session.mocking_api().deploy_mock(mocked_contract); Ok(()) } From ff78776b3e1e04967ce160d1c3ec9ed604fa1c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 4 Oct 2023 12:41:26 +0200 Subject: [PATCH 06/26] Registering dummy contract --- drink/Cargo.toml | 2 -- drink/src/lib.rs | 4 +++- drink/src/mock.rs | 14 ++++++++++---- drink/src/mock/mocking_api.rs | 30 ++++++++++++++++++++++++++++-- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/drink/Cargo.toml b/drink/Cargo.toml index 3cb6fb7..52a9097 100644 --- a/drink/Cargo.toml +++ b/drink/Cargo.toml @@ -26,8 +26,6 @@ sp-runtime-interface = { workspace = true } scale-info = { workspace = true } thiserror = { workspace = true } - -[dev-dependencies] wat = { workspace = true } [features] diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 0e47002..3ca3c1f 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -38,7 +38,8 @@ pub type EventRecordOf = /// A sandboxed runtime. pub struct Sandbox { externalities: TestExternalities, - mock_registry: MockRegistry, + mock_registry: MockRegistry>, + mock_counter: usize, _phantom: PhantomData, } @@ -62,6 +63,7 @@ impl Sandbox { let mut sandbox = Self { externalities: TestExternalities::new(storage), mock_registry: MockRegistry::new(), + mock_counter: 0, _phantom: PhantomData, }; diff --git a/drink/src/mock.rs b/drink/src/mock.rs index f3d6a5d..59cc122 100644 --- a/drink/src/mock.rs +++ b/drink/src/mock.rs @@ -5,18 +5,24 @@ mod contract; mod message; mod mocking_api; +use std::collections::BTreeMap; + pub use contract::ContractMock; pub use message::{MessageMock, MessageMockBuilder}; pub use mocking_api::MockingApi; -pub struct MockRegistry { - mocked_contracts: Vec, +pub struct MockRegistry { + mocked_contracts: BTreeMap, } -impl MockRegistry { +impl MockRegistry { pub fn new() -> Self { Self { - mocked_contracts: Vec::new(), + mocked_contracts: BTreeMap::new(), } } + + pub fn register(&mut self, address: AccountId, mock: ContractMock) { + self.mocked_contracts.insert(address, mock); + } } diff --git a/drink/src/mock/mocking_api.rs b/drink/src/mock/mocking_api.rs index 571e989..b70bdbf 100644 --- a/drink/src/mock/mocking_api.rs +++ b/drink/src/mock/mocking_api.rs @@ -1,7 +1,8 @@ use crate::{ + contract_api::ContractApi, mock::ContractMock, runtime::{AccountIdFor, Runtime}, - Sandbox, + Sandbox, DEFAULT_GAS_LIMIT, }; pub trait MockingApi { @@ -15,10 +16,35 @@ pub trait MockingApi { impl MockingApi for Sandbox { fn deploy_mock(&mut self, mock: ContractMock) -> AccountIdFor { - todo!() + let mock_bytes = wat::parse_str(DUMMY_CONTRACT).expect("Dummy contract should be valid"); + let mock_address = self + .deploy_contract( + mock_bytes, + 0, + vec![], + vec![self.mock_counter as u8], + R::default_actor(), + DEFAULT_GAS_LIMIT, + None, + ) + .result + .expect("Deployment of a dummy contract should succeed") + .account_id; + + self.mock_counter += 1; + self.mock_registry.register(mock_address.clone(), mock); + + mock_address } fn mock_existing_contract(&mut self, mock: ContractMock, address: AccountIdFor) { todo!("soon") } } + +const DUMMY_CONTRACT: &str = r#" +(module + (import "env" "memory" (memory 1 1)) + (func (export "deploy")) + (func (export "call") (unreachable)) +)"#; From de38242477d10b2eebbe0fd8da71e496a6e6681f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 4 Oct 2023 13:01:52 +0200 Subject: [PATCH 07/26] Full test (not working yet) --- examples/mocking/lib.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/mocking/lib.rs b/examples/mocking/lib.rs index 5f5fd37..c1f9507 100755 --- a/examples/mocking/lib.rs +++ b/examples/mocking/lib.rs @@ -23,12 +23,12 @@ mod contract { } #[ink(message)] - pub fn delegate_call(&self, callee: AccountId, arg: u32) -> (u16, u16) { + pub fn delegate_call(&self, callee: AccountId) -> (u16, u16) { build_call::() .call(callee) .gas_limit(0) .transferred_value(0) - .exec_input(ExecutionInput::new(CALLEE_SELECTOR).push_arg(arg)) + .exec_input(ExecutionInput::new(CALLEE_SELECTOR).push_arg(41u8)) .returns::<(u16, u16)>() .invoke() } @@ -59,18 +59,29 @@ mod tests { } #[test] - fn initialization() -> Result<(), Box> { + fn call_mocked_message() -> Result<(), Box> { let mut session = Session::::new()?; - session.deploy(bytes(), "new", NO_ARGS, vec![], None, &transcoder())?; + + // Firstly, we are creating our mocked contract. let mocked_message = MessageMockBuilder::new() .with_selector(CALLEE_SELECTOR.to_bytes()) - .with_body(Box::new(|_: u32| (0, 0))) + .with_body(Box::new(|_: u8| (4, 1))) .build(); let mocked_contract = ContractMock::new().with_message(mocked_message); - session.mocking_api().deploy_mock(mocked_contract); + // Secondly, we are deploying it, similarly to a standard deployment action.. + let mock_address = session.mocking_api().deploy_mock(mocked_contract); + + // Now, we can deploy our proper contract and invoke it. + let result: (u16, u16) = session + .deploy_and(bytes(), "new", NO_ARGS, vec![], None, &transcoder())? + .call_and("delegate_call", &[mock_address.to_string()], None)? + .last_call_return() + .expect("Call was successful, so there should be a return") + .expect("Call was successful"); + assert_eq!(result, (4, 1)); Ok(()) } From 0192bcabf9a22df19559f245570e1da8d4da1566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 4 Oct 2023 13:14:05 +0200 Subject: [PATCH 08/26] Add new example to the CI --- .github/workflows/rust-checks.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/rust-checks.yml b/.github/workflows/rust-checks.yml index d985042..14be5a8 100644 --- a/.github/workflows/rust-checks.yml +++ b/.github/workflows/rust-checks.yml @@ -73,3 +73,8 @@ jobs: cargo contract build --release cargo test --release popd + + pushd examples/mocking + cargo contract build --release + cargo test --release + popd From faf146c02f068a5851c3b8ee3e99dccd5059504b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 09:17:25 +0200 Subject: [PATCH 09/26] Reorg in drink-debug --- .../src/runtime/pallet_contracts_debugging.rs | 102 ++---------------- .../intercepting.rs | 14 +++ .../pallet_contracts_debugging/runtime.rs | 41 +++++++ .../pallet_contracts_debugging/tracing.rs | 47 ++++++++ 4 files changed, 108 insertions(+), 96 deletions(-) create mode 100644 drink/src/runtime/pallet_contracts_debugging/intercepting.rs create mode 100644 drink/src/runtime/pallet_contracts_debugging/runtime.rs create mode 100644 drink/src/runtime/pallet_contracts_debugging/tracing.rs diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index 6fc871e..b3a92f9 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -21,102 +21,12 @@ //! usually, complex objects will be passed in their encoded form (`Vec` obtained with scale //! encoding). -use pallet_contracts::debug::{CallInterceptor, CallSpan, ExecResult, ExportedFunction, Tracing}; -use pallet_contracts_primitives::ExecReturnValue; -use sp_externalities::{decl_extension, ExternalitiesExt}; -use sp_runtime_interface::runtime_interface; +mod intercepting; +mod runtime; +mod tracing; -use crate::runtime::{AccountIdFor, Runtime}; +pub use runtime::{DebugExt, DebugExtT, NoopDebugExt}; -/// The trait that allows injecting custom logic to handle contract debugging directly in the -/// contracts pallet. -pub trait DebugExtT { - /// Called after a contract call is made. - fn after_call( - &self, - _contract_address: Vec, - _is_call: bool, - _input_data: Vec, - _result: Vec, - ) { - } -} - -decl_extension! { - /// A wrapper type for the `DebugExtT` debug extension. - pub struct DebugExt(Box); -} - -/// The simplest debug extension - does nothing. -pub struct NoopDebugExt {} -impl DebugExtT for NoopDebugExt {} - -#[runtime_interface] -trait ContractCallDebugger { - fn after_call( - &mut self, - contract_address: Vec, - is_call: bool, - input_data: Vec, - result: Vec, - ) { - self.extension::() - .expect("Failed to find `DebugExt` extension") - .after_call(contract_address, is_call, input_data, result); - } -} - -/// Configuration parameter for the contracts pallet. Provides all the necessary trait -/// implementations. +/// Main configuration parameter for the contracts pallet debugging. Provides all the necessary +/// trait implementations. pub enum DrinkDebug {} - -impl CallInterceptor for DrinkDebug { - fn intercept_call( - _contract_address: &AccountIdFor, - _entry_point: &ExportedFunction, - _input_data: &[u8], - ) -> Option { - // We don't want to intercept any calls. At least for now. - None - } -} - -impl Tracing for DrinkDebug { - type CallSpan = DrinkCallSpan>; - - fn new_call_span( - contract_address: &AccountIdFor, - entry_point: ExportedFunction, - input_data: &[u8], - ) -> Self::CallSpan { - DrinkCallSpan { - contract_address: contract_address.clone(), - entry_point, - input_data: input_data.to_vec(), - } - } -} - -/// A contract's call span. -/// -/// It is created just before the call is made and `Self::after_call` is called after the call is -/// done. -pub struct DrinkCallSpan { - /// The address of the contract that has been called. - pub contract_address: AccountId, - /// The entry point that has been called (either constructor or call). - pub entry_point: ExportedFunction, - /// The input data of the call. - pub input_data: Vec, -} - -impl CallSpan for DrinkCallSpan { - fn after_call(self, output: &ExecReturnValue) { - contract_call_debugger::after_call( - self.contract_address.encode(), - matches!(self.entry_point, ExportedFunction::Call), - self.input_data.to_vec(), - output.data.clone(), - ); - } -} diff --git a/drink/src/runtime/pallet_contracts_debugging/intercepting.rs b/drink/src/runtime/pallet_contracts_debugging/intercepting.rs new file mode 100644 index 0000000..2bd281f --- /dev/null +++ b/drink/src/runtime/pallet_contracts_debugging/intercepting.rs @@ -0,0 +1,14 @@ +use pallet_contracts::debug::{CallInterceptor, ExecResult, ExportedFunction}; + +use crate::runtime::{pallet_contracts_debugging::DrinkDebug, AccountIdFor, Runtime}; + +impl CallInterceptor for DrinkDebug { + fn intercept_call( + _contract_address: &AccountIdFor, + _entry_point: &ExportedFunction, + _input_data: &[u8], + ) -> Option { + // We don't want to intercept any calls. At least for now. + None + } +} diff --git a/drink/src/runtime/pallet_contracts_debugging/runtime.rs b/drink/src/runtime/pallet_contracts_debugging/runtime.rs new file mode 100644 index 0000000..1189a57 --- /dev/null +++ b/drink/src/runtime/pallet_contracts_debugging/runtime.rs @@ -0,0 +1,41 @@ +use sp_externalities::{decl_extension, ExternalitiesExt}; +use sp_runtime_interface::runtime_interface; + +/// Contracts pallet outsources debug callbacks through this runtime interface. +#[runtime_interface] +pub trait ContractCallDebugger { + fn after_call( + &mut self, + contract_address: Vec, + is_call: bool, + input_data: Vec, + result: Vec, + ) { + self.extension::() + .expect("Failed to find `DebugExt` extension") + .after_call(contract_address, is_call, input_data, result); + } +} + +/// This trait describes the interface of a runtime extension that can be used to debug contract +/// calls. +pub trait DebugExtT { + /// Called after a contract call is made. + fn after_call( + &self, + _contract_address: Vec, + _is_call: bool, + _input_data: Vec, + _result: Vec, + ) { + } +} + +decl_extension! { + /// A wrapper type for the `DebugExtT` debug extension. + pub struct DebugExt(Box); +} + +/// The simplest debug extension - does nothing. +pub struct NoopDebugExt {} +impl DebugExtT for NoopDebugExt {} diff --git a/drink/src/runtime/pallet_contracts_debugging/tracing.rs b/drink/src/runtime/pallet_contracts_debugging/tracing.rs new file mode 100644 index 0000000..3acd7bb --- /dev/null +++ b/drink/src/runtime/pallet_contracts_debugging/tracing.rs @@ -0,0 +1,47 @@ +use pallet_contracts::{ + debug::{CallSpan, ExportedFunction}, + Tracing, +}; +use pallet_contracts_primitives::ExecReturnValue; + +use crate::runtime::{pallet_contracts_debugging::DrinkDebug, AccountIdFor, Runtime}; + +impl Tracing for DrinkDebug { + type CallSpan = DrinkCallSpan>; + + fn new_call_span( + contract_address: &AccountIdFor, + entry_point: ExportedFunction, + input_data: &[u8], + ) -> Self::CallSpan { + DrinkCallSpan { + contract_address: contract_address.clone(), + entry_point, + input_data: input_data.to_vec(), + } + } +} + +/// A contract's call span. +/// +/// It is created just before the call is made and `Self::after_call` is called after the call is +/// done. +pub struct DrinkCallSpan { + /// The address of the contract that has been called. + pub contract_address: AccountId, + /// The entry point that has been called (either constructor or call). + pub entry_point: ExportedFunction, + /// The input data of the call. + pub input_data: Vec, +} + +impl CallSpan for DrinkCallSpan { + fn after_call(self, output: &ExecReturnValue) { + crate::runtime::pallet_contracts_debugging::runtime::contract_call_debugger::after_call( + self.contract_address.encode(), + matches!(self.entry_point, ExportedFunction::Call), + self.input_data.to_vec(), + output.data.clone(), + ); + } +} From 535afe4550050a40c88335435b99883e389ec5fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 09:19:37 +0200 Subject: [PATCH 10/26] Rename extension --- drink/src/lib.rs | 8 ++++---- drink/src/runtime/pallet_contracts_debugging.rs | 2 +- .../runtime/pallet_contracts_debugging/runtime.rs | 12 ++++++------ drink/src/session.rs | 4 ++-- examples/cross-contract-call-tracing/lib.rs | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 3ca3c1f..ad555bc 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -24,8 +24,8 @@ use sp_io::TestExternalities; use crate::{ mock::MockRegistry, - pallet_contracts_debugging::DebugExt, - runtime::{pallet_contracts_debugging::NoopDebugExt, *}, + pallet_contracts_debugging::TracingExt, + runtime::{pallet_contracts_debugging::NoopTracingExt, *}, }; /// Main result type for the drink crate. @@ -75,7 +75,7 @@ impl Sandbox { .map_err(Error::BlockInitialize)?; // We register a noop debug extension by default. - sandbox.override_debug_handle(DebugExt(Box::new(NoopDebugExt {}))); + sandbox.override_debug_handle(TracingExt(Box::new(NoopTracingExt {}))); Ok(sandbox) } @@ -84,7 +84,7 @@ impl Sandbox { /// /// By default, a new `Sandbox` instance is created with a noop debug extension. This method /// allows to override it with a custom debug extension. - pub fn override_debug_handle(&mut self, d: DebugExt) { + pub fn override_debug_handle(&mut self, d: TracingExt) { self.externalities.register_extension(d); } } diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index b3a92f9..808601b 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -25,7 +25,7 @@ mod intercepting; mod runtime; mod tracing; -pub use runtime::{DebugExt, DebugExtT, NoopDebugExt}; +pub use runtime::{NoopTracingExt, TracingExt, TracingExtT}; /// Main configuration parameter for the contracts pallet debugging. Provides all the necessary /// trait implementations. diff --git a/drink/src/runtime/pallet_contracts_debugging/runtime.rs b/drink/src/runtime/pallet_contracts_debugging/runtime.rs index 1189a57..6815c46 100644 --- a/drink/src/runtime/pallet_contracts_debugging/runtime.rs +++ b/drink/src/runtime/pallet_contracts_debugging/runtime.rs @@ -11,7 +11,7 @@ pub trait ContractCallDebugger { input_data: Vec, result: Vec, ) { - self.extension::() + self.extension::() .expect("Failed to find `DebugExt` extension") .after_call(contract_address, is_call, input_data, result); } @@ -19,7 +19,7 @@ pub trait ContractCallDebugger { /// This trait describes the interface of a runtime extension that can be used to debug contract /// calls. -pub trait DebugExtT { +pub trait TracingExtT { /// Called after a contract call is made. fn after_call( &self, @@ -32,10 +32,10 @@ pub trait DebugExtT { } decl_extension! { - /// A wrapper type for the `DebugExtT` debug extension. - pub struct DebugExt(Box); + /// A wrapper type for the `TracingExtT` debug extension. + pub struct TracingExt(Box); } /// The simplest debug extension - does nothing. -pub struct NoopDebugExt {} -impl DebugExtT for NoopDebugExt {} +pub struct NoopTracingExt {} +impl TracingExtT for NoopTracingExt {} diff --git a/drink/src/session.rs b/drink/src/session.rs index 54f6edf..ac14e6a 100644 --- a/drink/src/session.rs +++ b/drink/src/session.rs @@ -11,7 +11,7 @@ use parity_scale_codec::Decode; use crate::{ chain_api::ChainApi, contract_api::ContractApi, - pallet_contracts_debugging::DebugExt, + pallet_contracts_debugging::TracingExt, runtime::{AccountIdFor, HashFor, Runtime}, EventRecordOf, Sandbox, DEFAULT_GAS_LIMIT, }; @@ -391,7 +391,7 @@ impl Session { /// /// By default, a new `Session` instance will use a noop debug extension. This method allows to /// override it with a custom debug extension. - pub fn override_debug_handle(&mut self, d: DebugExt) { + pub fn override_debug_handle(&mut self, d: TracingExt) { self.sandbox.override_debug_handle(d); } } diff --git a/examples/cross-contract-call-tracing/lib.rs b/examples/cross-contract-call-tracing/lib.rs index e32413c..bacb6a1 100644 --- a/examples/cross-contract-call-tracing/lib.rs +++ b/examples/cross-contract-call-tracing/lib.rs @@ -66,7 +66,7 @@ mod tests { use drink::{ runtime::{ - pallet_contracts_debugging::{DebugExt, DebugExtT}, + pallet_contracts_debugging::{TracingExt, TracingExtT}, MinimalRuntime, }, session::{ @@ -94,7 +94,7 @@ mod tests { } struct TestDebugger; - impl DebugExtT for TestDebugger { + impl TracingExtT for TestDebugger { fn after_call( &self, contract_address: Vec, @@ -143,7 +143,7 @@ mod tests { #[test] fn test() -> Result<(), Box> { let mut session = Session::::new()?; - session.override_debug_handle(DebugExt(Box::new(TestDebugger {}))); + session.override_debug_handle(TracingExt(Box::new(TestDebugger {}))); let outer_address = session.deploy(bytes(), "new", NO_ARGS, vec![1], None, &transcoder())?; From f734d24d9550fe17b525bb92bdb4e2570e6320ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 10:13:26 +0200 Subject: [PATCH 11/26] Finish runtime side --- Cargo.lock | 4 +- Cargo.toml | 2 +- drink/src/lib.rs | 4 +- .../src/runtime/pallet_contracts_debugging.rs | 2 +- .../intercepting.rs | 23 ++++++++--- .../pallet_contracts_debugging/runtime.rs | 38 +++++++++++++++++-- 6 files changed, 58 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c98ef8e..7c42230 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2478,9 +2478,9 @@ dependencies = [ [[package]] name = "pallet-contracts-for-drink" -version = "22.0.0" +version = "22.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef4598e638732ef5487b972c68b3b521d2953436cb3dd51b575ed6cbd16783f" +checksum = "3f039ee77b4ff6ab8b4e427bab636dd23ba1257ed631616c98c0859a83a5e690" dependencies = [ "bitflags", "environmental", diff --git a/Cargo.toml b/Cargo.toml index e67e75c..5f4f37a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ frame-metadata = { version = "16.0.0" } frame-support = { version = "23.0.0" } frame-system = { version = "23.0.0" } pallet-balances = { version = "23.0.0" } -pallet-contracts = { package = "pallet-contracts-for-drink", version = "22.0.0" } +pallet-contracts = { package = "pallet-contracts-for-drink", version = "22.0.1" } pallet-contracts-primitives = { version = "26.0.0" } pallet-timestamp = { version = "22.0.0" } sp-core = { version = "23.0.0" } diff --git a/drink/src/lib.rs b/drink/src/lib.rs index ad555bc..415bbc6 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -25,7 +25,7 @@ use sp_io::TestExternalities; use crate::{ mock::MockRegistry, pallet_contracts_debugging::TracingExt, - runtime::{pallet_contracts_debugging::NoopTracingExt, *}, + runtime::{pallet_contracts_debugging::NoopExt, *}, }; /// Main result type for the drink crate. @@ -75,7 +75,7 @@ impl Sandbox { .map_err(Error::BlockInitialize)?; // We register a noop debug extension by default. - sandbox.override_debug_handle(TracingExt(Box::new(NoopTracingExt {}))); + sandbox.override_debug_handle(TracingExt(Box::new(NoopExt {}))); Ok(sandbox) } diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index 808601b..1f5af80 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -25,7 +25,7 @@ mod intercepting; mod runtime; mod tracing; -pub use runtime::{NoopTracingExt, TracingExt, TracingExtT}; +pub use runtime::{NoopExt, TracingExt, TracingExtT}; /// Main configuration parameter for the contracts pallet debugging. Provides all the necessary /// trait implementations. diff --git a/drink/src/runtime/pallet_contracts_debugging/intercepting.rs b/drink/src/runtime/pallet_contracts_debugging/intercepting.rs index 2bd281f..57aa90d 100644 --- a/drink/src/runtime/pallet_contracts_debugging/intercepting.rs +++ b/drink/src/runtime/pallet_contracts_debugging/intercepting.rs @@ -1,14 +1,25 @@ use pallet_contracts::debug::{CallInterceptor, ExecResult, ExportedFunction}; +use parity_scale_codec::{Decode, Encode}; -use crate::runtime::{pallet_contracts_debugging::DrinkDebug, AccountIdFor, Runtime}; +use crate::runtime::{ + pallet_contracts_debugging::{runtime::contract_call_debugger, DrinkDebug}, + AccountIdFor, Runtime, +}; impl CallInterceptor for DrinkDebug { fn intercept_call( - _contract_address: &AccountIdFor, - _entry_point: &ExportedFunction, - _input_data: &[u8], + contract_address: &AccountIdFor, + entry_point: &ExportedFunction, + input_data: &[u8], ) -> Option { - // We don't want to intercept any calls. At least for now. - None + // Pass the data to the runtime interface. The data must be encoded (only simple types are + // supported). + let intercepting_result = contract_call_debugger::intercept_call( + contract_address.encode(), + matches!(*entry_point, ExportedFunction::Call), + input_data.to_vec(), + ); + + Decode::decode(&mut intercepting_result.as_slice()).expect("Decoding should succeed") } } diff --git a/drink/src/runtime/pallet_contracts_debugging/runtime.rs b/drink/src/runtime/pallet_contracts_debugging/runtime.rs index 6815c46..d038d5e 100644 --- a/drink/src/runtime/pallet_contracts_debugging/runtime.rs +++ b/drink/src/runtime/pallet_contracts_debugging/runtime.rs @@ -1,3 +1,4 @@ +use parity_scale_codec::Encode; use sp_externalities::{decl_extension, ExternalitiesExt}; use sp_runtime_interface::runtime_interface; @@ -15,6 +16,17 @@ pub trait ContractCallDebugger { .expect("Failed to find `DebugExt` extension") .after_call(contract_address, is_call, input_data, result); } + + fn intercept_call( + &mut self, + contract_address: Vec, + is_call: bool, + input_data: Vec, + ) -> Vec { + self.extension::() + .expect("Failed to find `InterceptingExt` extension") + .intercept_call(contract_address, is_call, input_data) + } } /// This trait describes the interface of a runtime extension that can be used to debug contract @@ -36,6 +48,26 @@ decl_extension! { pub struct TracingExt(Box); } -/// The simplest debug extension - does nothing. -pub struct NoopTracingExt {} -impl TracingExtT for NoopTracingExt {} +/// This trait describes the interface of a runtime extension that can be used to intercept contract +/// calls. +pub trait InterceptingExtT { + /// Called when a contract call is made. + fn intercept_call( + &self, + _contract_address: Vec, + _is_call: bool, + _input_data: Vec, + ) -> Vec { + None::<()>.encode() // do not intercept, continue standard procedure + } +} + +decl_extension! { + /// A wrapper type for the `InterceptingExtT` debug extension. + pub struct InterceptingExt(Box); +} + +/// The simplest extension - uses default implementation. +pub struct NoopExt {} +impl TracingExtT for NoopExt {} +impl InterceptingExtT for NoopExt {} From 9672293253c109bc7021296866e637876d1da7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 10:19:48 +0200 Subject: [PATCH 12/26] Register extension --- drink/src/lib.rs | 27 +++++++++++++++++-- .../src/runtime/pallet_contracts_debugging.rs | 2 +- .../pallet_contracts_debugging/runtime.rs | 2 ++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 415bbc6..c06d97c 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -20,12 +20,16 @@ pub use frame_support::{ weights::Weight, }; use frame_system::{pallet_prelude::BlockNumberFor, EventRecord, GenesisConfig}; +use parity_scale_codec::Encode; use sp_io::TestExternalities; use crate::{ mock::MockRegistry, - pallet_contracts_debugging::TracingExt, - runtime::{pallet_contracts_debugging::NoopExt, *}, + pallet_contracts_debugging::{InterceptingExt, TracingExt}, + runtime::{ + pallet_contracts_debugging::{InterceptingExtT, NoopExt}, + *, + }, }; /// Main result type for the drink crate. @@ -77,6 +81,8 @@ impl Sandbox { // We register a noop debug extension by default. sandbox.override_debug_handle(TracingExt(Box::new(NoopExt {}))); + sandbox.setup_mock_extension(); + Ok(sandbox) } @@ -87,4 +93,21 @@ impl Sandbox { pub fn override_debug_handle(&mut self, d: TracingExt) { self.externalities.register_extension(d); } + + fn setup_mock_extension(&mut self) { + self.externalities + .register_extension(InterceptingExt(Box::new(MockExtension {}))); + } +} + +struct MockExtension; +impl InterceptingExtT for MockExtension { + fn intercept_call( + &self, + contract_address: Vec, + is_call: bool, + input_data: Vec, + ) -> Vec { + None::<()>.encode() + } } diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index 1f5af80..98d0c25 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -25,7 +25,7 @@ mod intercepting; mod runtime; mod tracing; -pub use runtime::{NoopExt, TracingExt, TracingExtT}; +pub use runtime::{InterceptingExt, InterceptingExtT, NoopExt, TracingExt, TracingExtT}; /// Main configuration parameter for the contracts pallet debugging. Provides all the necessary /// trait implementations. diff --git a/drink/src/runtime/pallet_contracts_debugging/runtime.rs b/drink/src/runtime/pallet_contracts_debugging/runtime.rs index d038d5e..a7acfae 100644 --- a/drink/src/runtime/pallet_contracts_debugging/runtime.rs +++ b/drink/src/runtime/pallet_contracts_debugging/runtime.rs @@ -52,6 +52,8 @@ decl_extension! { /// calls. pub trait InterceptingExtT { /// Called when a contract call is made. + /// + /// The returned value must be a valid codec encoding for `Option`. fn intercept_call( &self, _contract_address: Vec, From b6a663bdc7bad3d09bbeb734cfc4ecae348260f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 11:06:56 +0200 Subject: [PATCH 13/26] Share registry --- drink/src/lib.rs | 22 +++++++++++++++------- drink/src/mock/contract.rs | 4 ++-- drink/src/mock/message.rs | 2 +- drink/src/mock/mocking_api.rs | 6 +++++- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/drink/src/lib.rs b/drink/src/lib.rs index c06d97c..4f75c8c 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -11,7 +11,10 @@ pub mod runtime; #[cfg(feature = "session")] pub mod session; -use std::marker::PhantomData; +use std::{ + marker::PhantomData, + sync::{Arc, Mutex}, +}; pub use error::Error; use frame_support::sp_runtime::{traits::One, BuildStorage}; @@ -42,7 +45,7 @@ pub type EventRecordOf = /// A sandboxed runtime. pub struct Sandbox { externalities: TestExternalities, - mock_registry: MockRegistry>, + mock_registry: Arc>>>, mock_counter: usize, _phantom: PhantomData, } @@ -66,7 +69,7 @@ impl Sandbox { let mut sandbox = Self { externalities: TestExternalities::new(storage), - mock_registry: MockRegistry::new(), + mock_registry: Arc::new(Mutex::new(MockRegistry::new())), mock_counter: 0, _phantom: PhantomData, }; @@ -96,16 +99,21 @@ impl Sandbox { fn setup_mock_extension(&mut self) { self.externalities - .register_extension(InterceptingExt(Box::new(MockExtension {}))); + .register_extension(InterceptingExt(Box::new(MockExtension { + mock_registry: Arc::clone(&self.mock_registry), + }))); } } -struct MockExtension; -impl InterceptingExtT for MockExtension { +struct MockExtension { + mock_registry: Arc>>, +} + +impl InterceptingExtT for MockExtension { fn intercept_call( &self, contract_address: Vec, - is_call: bool, + _is_call: bool, input_data: Vec, ) -> Vec { None::<()>.encode() diff --git a/drink/src/mock/contract.rs b/drink/src/mock/contract.rs index 05fd64c..e2c89dd 100644 --- a/drink/src/mock/contract.rs +++ b/drink/src/mock/contract.rs @@ -1,7 +1,7 @@ use crate::mock::message::MessageMockT; pub struct ContractMock { - messages: Vec>, + messages: Vec>, } impl ContractMock { @@ -11,7 +11,7 @@ impl ContractMock { } } - pub fn with_message(mut self, message: M) -> Self { + pub fn with_message(mut self, message: M) -> Self { self.messages.push(Box::new(message)); self } diff --git a/drink/src/mock/message.rs b/drink/src/mock/message.rs index 6450a5b..841c34a 100644 --- a/drink/src/mock/message.rs +++ b/drink/src/mock/message.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use crate::mock::builder_utils::{Set, Setter, UnSet}; -type Body = Box Ret>; +type Body = Box Ret + Send + Sync>; type Selector = [u8; 4]; pub trait MessageMockT {} diff --git a/drink/src/mock/mocking_api.rs b/drink/src/mock/mocking_api.rs index b70bdbf..d91004b 100644 --- a/drink/src/mock/mocking_api.rs +++ b/drink/src/mock/mocking_api.rs @@ -32,7 +32,11 @@ impl MockingApi for Sandbox { .account_id; self.mock_counter += 1; - self.mock_registry.register(mock_address.clone(), mock); + self.mock_registry + .as_ref() + .lock() + .expect("Should be able to acquire lock on registry") + .register(mock_address.clone(), mock); mock_address } From 2f2f25a684dc4d5e831d8946a20a1382dc65a51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 14:28:55 +0200 Subject: [PATCH 14/26] Somehow connected --- drink/src/lib.rs | 38 +++++++++++++++-- drink/src/mock.rs | 13 ++++-- drink/src/mock/builder_utils.rs | 27 ------------ drink/src/mock/contract.rs | 22 +++++++--- drink/src/mock/error.rs | 11 +++++ drink/src/mock/message.rs | 74 --------------------------------- examples/mocking/lib.rs | 25 ++++------- 7 files changed, 80 insertions(+), 130 deletions(-) delete mode 100644 drink/src/mock/builder_utils.rs create mode 100644 drink/src/mock/error.rs delete mode 100644 drink/src/mock/message.rs diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 4f75c8c..3ecb125 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -6,7 +6,7 @@ pub mod chain_api; pub mod contract_api; mod error; -pub mod mock; +mod mock; pub mod runtime; #[cfg(feature = "session")] pub mod session; @@ -23,7 +23,10 @@ pub use frame_support::{ weights::Weight, }; use frame_system::{pallet_prelude::BlockNumberFor, EventRecord, GenesisConfig}; -use parity_scale_codec::Encode; +pub use mock::{ContractMock, MessageMock, MockingApi, Selector}; +use pallet_contracts::debug::ExecResult; +use pallet_contracts_primitives::{ExecReturnValue, ReturnFlags}; +use parity_scale_codec::{Decode, Encode}; use sp_io::TestExternalities; use crate::{ @@ -109,13 +112,40 @@ struct MockExtension { mock_registry: Arc>>, } -impl InterceptingExtT for MockExtension { +impl InterceptingExtT for MockExtension { fn intercept_call( &self, contract_address: Vec, _is_call: bool, input_data: Vec, ) -> Vec { - None::<()>.encode() + let contract_address = Decode::decode(&mut &contract_address[..]) + .expect("Contract address should be decodable"); + + match self + .mock_registry + .lock() + .expect("Should be able to acquire registry") + .get(&contract_address) + { + None => None::<()>.encode(), + Some(mock) => { + let (selector, call_data) = input_data.split_at(4); + let selector: Selector = selector + .try_into() + .expect("Input data should contain at least selector bytes"); + + let result = mock + .call(selector, call_data.to_vec()) + .expect("TODO: let the user define the fallback mechanism"); + + let result: ExecResult = Ok(ExecReturnValue { + flags: ReturnFlags::empty(), + data: result, + }); + + Some(result).encode() + } + } } } diff --git a/drink/src/mock.rs b/drink/src/mock.rs index 59cc122..a455388 100644 --- a/drink/src/mock.rs +++ b/drink/src/mock.rs @@ -1,16 +1,17 @@ //! Mocking contract feature. -mod builder_utils; mod contract; -mod message; +mod error; mod mocking_api; use std::collections::BTreeMap; -pub use contract::ContractMock; -pub use message::{MessageMock, MessageMockBuilder}; +pub use contract::{ContractMock, MessageMock, Selector}; +use error::MockingError; pub use mocking_api::MockingApi; +pub type MockedCallResult = Result, MockingError>; + pub struct MockRegistry { mocked_contracts: BTreeMap, } @@ -25,4 +26,8 @@ impl MockRegistry { pub fn register(&mut self, address: AccountId, mock: ContractMock) { self.mocked_contracts.insert(address, mock); } + + pub fn get(&self, address: &AccountId) -> Option<&ContractMock> { + self.mocked_contracts.get(address) + } } diff --git a/drink/src/mock/builder_utils.rs b/drink/src/mock/builder_utils.rs deleted file mode 100644 index e32480c..0000000 --- a/drink/src/mock/builder_utils.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::marker::PhantomData; - -pub trait Setter {} - -pub struct Set(T); - -impl Setter for Set {} - -impl Set { - pub fn new(t: T) -> Self { - Self(t) - } - - pub fn retrieve(self) -> T { - self.0 - } -} - -pub struct UnSet(PhantomData); - -impl Setter for UnSet {} - -impl UnSet { - pub fn new() -> Self { - Self(PhantomData::default()) - } -} diff --git a/drink/src/mock/contract.rs b/drink/src/mock/contract.rs index e2c89dd..bd2df60 100644 --- a/drink/src/mock/contract.rs +++ b/drink/src/mock/contract.rs @@ -1,18 +1,30 @@ -use crate::mock::message::MessageMockT; +use std::collections::BTreeMap; + +use crate::mock::{error::MockingError, MockedCallResult}; + +pub type Selector = [u8; 4]; +pub type MessageMock = Box) -> Vec + Send + Sync>; pub struct ContractMock { - messages: Vec>, + messages: BTreeMap, } impl ContractMock { pub fn new() -> Self { Self { - messages: Vec::new(), + messages: BTreeMap::new(), } } - pub fn with_message(mut self, message: M) -> Self { - self.messages.push(Box::new(message)); + pub fn with_message(mut self, selector: Selector, message: MessageMock) -> Self { + self.messages.insert(selector, message); self } + + pub fn call(&self, selector: Selector, input: Vec) -> MockedCallResult { + match self.messages.get(&selector) { + None => Err(MockingError::MessageNotFound(selector)), + Some(message) => Ok(message(input)), + } + } } diff --git a/drink/src/mock/error.rs b/drink/src/mock/error.rs new file mode 100644 index 0000000..ce3f216 --- /dev/null +++ b/drink/src/mock/error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +use crate::Selector; + +#[derive(Error, Debug)] +pub enum MockingError { + #[error("Message not found (unknown selector: {0:?})")] + MessageNotFound(Selector), + #[error("Message failed")] + MessageFailed, +} diff --git a/drink/src/mock/message.rs b/drink/src/mock/message.rs deleted file mode 100644 index 841c34a..0000000 --- a/drink/src/mock/message.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::marker::PhantomData; - -use crate::mock::builder_utils::{Set, Setter, UnSet}; - -type Body = Box Ret + Send + Sync>; -type Selector = [u8; 4]; - -pub trait MessageMockT {} -impl MessageMockT for MessageMock {} - -pub struct MessageMock { - selector: Selector, - body: Body, -} - -pub struct MessageMockBuilder< - Args, - Ret, - SelectorSetter: Setter, - BodySetter: Setter>, -> { - selector: SelectorSetter, - body: BodySetter, - _phantom: PhantomData<(Args, Ret)>, -} - -impl MessageMockBuilder, UnSet>> { - pub fn new() -> Self { - Self { - selector: UnSet::new(), - body: UnSet::new(), - _phantom: PhantomData::default(), - } - } -} - -impl>> - MessageMockBuilder, BodySetter> -{ - pub fn with_selector( - self, - selector: Selector, - ) -> MessageMockBuilder, BodySetter> { - MessageMockBuilder::, BodySetter> { - selector: Set::new(selector), - body: self.body, - _phantom: self._phantom, - } - } -} - -impl> - MessageMockBuilder>> -{ - pub fn with_body( - self, - body: Body, - ) -> MessageMockBuilder>> { - MessageMockBuilder::>> { - selector: self.selector, - body: Set::new(body), - _phantom: self._phantom, - } - } -} - -impl MessageMockBuilder, Set>> { - pub fn build(self) -> MessageMock { - MessageMock:: { - selector: self.selector.retrieve(), - body: self.body.retrieve(), - } - } -} diff --git a/examples/mocking/lib.rs b/examples/mocking/lib.rs index c1f9507..008609d 100755 --- a/examples/mocking/lib.rs +++ b/examples/mocking/lib.rs @@ -1,8 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] -use ink::env::call::Selector; - -const CALLEE_SELECTOR: Selector = Selector::new(ink::selector_bytes!("callee")); +const CALLEE_SELECTOR: [u8; 4] = ink::selector_bytes!("callee"); #[ink::contract] mod contract { @@ -23,13 +21,13 @@ mod contract { } #[ink(message)] - pub fn delegate_call(&self, callee: AccountId) -> (u16, u16) { + pub fn delegate_call(&self, callee: AccountId) -> () { build_call::() .call(callee) .gas_limit(0) .transferred_value(0) - .exec_input(ExecutionInput::new(CALLEE_SELECTOR).push_arg(41u8)) - .returns::<(u16, u16)>() + .exec_input(ExecutionInput::new(CALLEE_SELECTOR.into()).push_arg(41u8)) + .returns::<()>() .invoke() } } @@ -40,9 +38,9 @@ mod tests { use std::{error::Error, fs, path::PathBuf, rc::Rc}; use drink::{ - mock::{ContractMock, MessageMockBuilder, MockingApi}, runtime::MinimalRuntime, session::{contract_transcode::ContractMessageTranscoder, Session, NO_ARGS}, + ContractMock, MockingApi, }; use crate::CALLEE_SELECTOR; @@ -63,25 +61,20 @@ mod tests { let mut session = Session::::new()?; // Firstly, we are creating our mocked contract. - - let mocked_message = MessageMockBuilder::new() - .with_selector(CALLEE_SELECTOR.to_bytes()) - .with_body(Box::new(|_: u8| (4, 1))) - .build(); - - let mocked_contract = ContractMock::new().with_message(mocked_message); + let mocked_contract = + ContractMock::new().with_message(CALLEE_SELECTOR, Box::new(|_| vec![0])); // Secondly, we are deploying it, similarly to a standard deployment action.. let mock_address = session.mocking_api().deploy_mock(mocked_contract); // Now, we can deploy our proper contract and invoke it. - let result: (u16, u16) = session + let result: () = session .deploy_and(bytes(), "new", NO_ARGS, vec![], None, &transcoder())? .call_and("delegate_call", &[mock_address.to_string()], None)? .last_call_return() .expect("Call was successful, so there should be a return") .expect("Call was successful"); - assert_eq!(result, (4, 1)); + // assert_eq!(result, (4, 1)); Ok(()) } From cbbfabf711794f1d88f779659a8c102c71f12007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 17:39:45 +0200 Subject: [PATCH 15/26] Woohoo --- drink/src/lib.rs | 9 ++++----- drink/src/mock.rs | 2 +- drink/src/mock/contract.rs | 20 ++++++++++++++++--- drink/src/mock/error.rs | 4 ++-- drink/src/mock/mocking_api.rs | 3 +-- .../pallet_contracts_debugging/runtime.rs | 8 +++++++- examples/mocking/lib.rs | 16 +++++++++------ 7 files changed, 42 insertions(+), 20 deletions(-) diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 3ecb125..4207b26 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -23,7 +23,7 @@ pub use frame_support::{ weights::Weight, }; use frame_system::{pallet_prelude::BlockNumberFor, EventRecord, GenesisConfig}; -pub use mock::{ContractMock, MessageMock, MockingApi, Selector}; +pub use mock::{mock_message, ContractMock, MessageMock, MockingApi, Selector}; use pallet_contracts::debug::ExecResult; use pallet_contracts_primitives::{ExecReturnValue, ReturnFlags}; use parity_scale_codec::{Decode, Encode}; @@ -102,17 +102,16 @@ impl Sandbox { fn setup_mock_extension(&mut self) { self.externalities - .register_extension(InterceptingExt(Box::new(MockExtension { + .register_extension(InterceptingExt(Box::new(MockingExtension { mock_registry: Arc::clone(&self.mock_registry), }))); } } - -struct MockExtension { +struct MockingExtension { mock_registry: Arc>>, } -impl InterceptingExtT for MockExtension { +impl InterceptingExtT for MockingExtension { fn intercept_call( &self, contract_address: Vec, diff --git a/drink/src/mock.rs b/drink/src/mock.rs index a455388..7a9443a 100644 --- a/drink/src/mock.rs +++ b/drink/src/mock.rs @@ -6,7 +6,7 @@ mod mocking_api; use std::collections::BTreeMap; -pub use contract::{ContractMock, MessageMock, Selector}; +pub use contract::{mock_message, ContractMock, MessageMock, Selector}; use error::MockingError; pub use mocking_api::MockingApi; diff --git a/drink/src/mock/contract.rs b/drink/src/mock/contract.rs index bd2df60..9087006 100644 --- a/drink/src/mock/contract.rs +++ b/drink/src/mock/contract.rs @@ -1,9 +1,14 @@ use std::collections::BTreeMap; -use crate::mock::{error::MockingError, MockedCallResult}; +use parity_scale_codec::{Decode, Encode}; + +use crate::{ + mock::{error::MockingError, MockedCallResult}, + session::errors::LangError, +}; pub type Selector = [u8; 4]; -pub type MessageMock = Box) -> Vec + Send + Sync>; +pub type MessageMock = Box) -> MockedCallResult + Send + Sync>; pub struct ContractMock { messages: BTreeMap, @@ -24,7 +29,16 @@ impl ContractMock { pub fn call(&self, selector: Selector, input: Vec) -> MockedCallResult { match self.messages.get(&selector) { None => Err(MockingError::MessageNotFound(selector)), - Some(message) => Ok(message(input)), + Some(message) => message(input), } } } + +pub fn mock_message Ret + Send + Sync + 'static>( + body: Body, +) -> MessageMock { + Box::new(move |encoded_input| { + let input = Decode::decode(&mut &*encoded_input).map_err(MockingError::ArgumentDecoding)?; + Ok(Ok::(body(input)).encode()) + }) +} diff --git a/drink/src/mock/error.rs b/drink/src/mock/error.rs index ce3f216..41c35a7 100644 --- a/drink/src/mock/error.rs +++ b/drink/src/mock/error.rs @@ -6,6 +6,6 @@ use crate::Selector; pub enum MockingError { #[error("Message not found (unknown selector: {0:?})")] MessageNotFound(Selector), - #[error("Message failed")] - MessageFailed, + #[error("Decoding message arguments failed: {0:?}")] + ArgumentDecoding(parity_scale_codec::Error), } diff --git a/drink/src/mock/mocking_api.rs b/drink/src/mock/mocking_api.rs index d91004b..76e94bd 100644 --- a/drink/src/mock/mocking_api.rs +++ b/drink/src/mock/mocking_api.rs @@ -33,7 +33,6 @@ impl MockingApi for Sandbox { self.mock_counter += 1; self.mock_registry - .as_ref() .lock() .expect("Should be able to acquire lock on registry") .register(mock_address.clone(), mock); @@ -41,7 +40,7 @@ impl MockingApi for Sandbox { mock_address } - fn mock_existing_contract(&mut self, mock: ContractMock, address: AccountIdFor) { + fn mock_existing_contract(&mut self, _mock: ContractMock, _address: AccountIdFor) { todo!("soon") } } diff --git a/drink/src/runtime/pallet_contracts_debugging/runtime.rs b/drink/src/runtime/pallet_contracts_debugging/runtime.rs index a7acfae..640a92c 100644 --- a/drink/src/runtime/pallet_contracts_debugging/runtime.rs +++ b/drink/src/runtime/pallet_contracts_debugging/runtime.rs @@ -3,6 +3,11 @@ use sp_externalities::{decl_extension, ExternalitiesExt}; use sp_runtime_interface::runtime_interface; /// Contracts pallet outsources debug callbacks through this runtime interface. +/// +/// Essentially, in our case, it just exposes extensions to the runtime. +/// +/// At this level, data passed back/forth must be either primitive or implement some specific +/// traits. For simplicity, we just go with primitives and codec encoded data. #[runtime_interface] pub trait ContractCallDebugger { fn after_call( @@ -60,7 +65,8 @@ pub trait InterceptingExtT { _is_call: bool, _input_data: Vec, ) -> Vec { - None::<()>.encode() // do not intercept, continue standard procedure + // By default, do not intercept, continue with the standard procedure. + None::<()>.encode() } } diff --git a/examples/mocking/lib.rs b/examples/mocking/lib.rs index 008609d..876df99 100755 --- a/examples/mocking/lib.rs +++ b/examples/mocking/lib.rs @@ -21,13 +21,13 @@ mod contract { } #[ink(message)] - pub fn delegate_call(&self, callee: AccountId) -> () { + pub fn delegate_call(&self, callee: AccountId) -> (u8, u8) { build_call::() .call(callee) .gas_limit(0) .transferred_value(0) .exec_input(ExecutionInput::new(CALLEE_SELECTOR.into()).push_arg(41u8)) - .returns::<()>() + .returns::<(u8, u8)>() .invoke() } } @@ -38,6 +38,7 @@ mod tests { use std::{error::Error, fs, path::PathBuf, rc::Rc}; use drink::{ + mock_message, runtime::MinimalRuntime, session::{contract_transcode::ContractMessageTranscoder, Session, NO_ARGS}, ContractMock, MockingApi, @@ -61,20 +62,23 @@ mod tests { let mut session = Session::::new()?; // Firstly, we are creating our mocked contract. + const RETURN_VALUE: (u8, u8) = (1, 4); let mocked_contract = - ContractMock::new().with_message(CALLEE_SELECTOR, Box::new(|_| vec![0])); + ContractMock::new().with_message(CALLEE_SELECTOR, mock_message(|_: u8| RETURN_VALUE)); - // Secondly, we are deploying it, similarly to a standard deployment action.. + // Secondly, we are deploying it, similarly to a standard deployment action. let mock_address = session.mocking_api().deploy_mock(mocked_contract); // Now, we can deploy our proper contract and invoke it. - let result: () = session + let result: (u8, u8) = session .deploy_and(bytes(), "new", NO_ARGS, vec![], None, &transcoder())? .call_and("delegate_call", &[mock_address.to_string()], None)? .last_call_return() .expect("Call was successful, so there should be a return") .expect("Call was successful"); - // assert_eq!(result, (4, 1)); + + // And verify whether the value returned from the mock has been successfully passed. + assert_eq!(result, RETURN_VALUE); Ok(()) } From e7725491a6cc46affa2c668f0389413db7abe8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 18:09:22 +0200 Subject: [PATCH 16/26] Describe and document the example --- examples/mocking/README.md | 19 +++++++++++++++++++ examples/mocking/lib.rs | 22 +++++++++++----------- 2 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 examples/mocking/README.md diff --git a/examples/mocking/README.md b/examples/mocking/README.md new file mode 100644 index 0000000..b64a6d1 --- /dev/null +++ b/examples/mocking/README.md @@ -0,0 +1,19 @@ +# Mocking contracts + +This example shows how we can easily mock contracts with the `drink!` library. + +## Scenario + +Say we want to test a contract that simply delegates call to another contract (i.e. a _proxy_ pattern). +Our contract has a single message `delegate_call(AccountId) -> (u8, u8)`. +We want to test that this proxy correctly calls the callee (with some fixed selector) and returns the unchanged result (a pair of two `u8`). + +Normally, we would have to implement and build a mock contract that would be deployed alongside the tested contract. +With drink, we can simply mock the logic with some closures and test our contract in isolation. + +## Running + +```bash +cargo contract build --release +cargo test --release +``` diff --git a/examples/mocking/lib.rs b/examples/mocking/lib.rs index 876df99..81dde62 100755 --- a/examples/mocking/lib.rs +++ b/examples/mocking/lib.rs @@ -1,9 +1,10 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] +/// This is a fixed selector of the `callee` message. const CALLEE_SELECTOR: [u8; 4] = ink::selector_bytes!("callee"); #[ink::contract] -mod contract { +mod proxy { use ink::env::{ call::{build_call, ExecutionInput}, DefaultEnvironment, @@ -12,21 +13,22 @@ mod contract { use crate::CALLEE_SELECTOR; #[ink(storage)] - pub struct Contract {} + pub struct Proxy {} - impl Contract { + impl Proxy { #[ink(constructor)] pub fn new() -> Self { Self {} } + /// Calls `callee` with the selector `CALLEE_SELECTOR` and forwards the result. #[ink(message)] pub fn delegate_call(&self, callee: AccountId) -> (u8, u8) { build_call::() .call(callee) .gas_limit(0) .transferred_value(0) - .exec_input(ExecutionInput::new(CALLEE_SELECTOR.into()).push_arg(41u8)) + .exec_input(ExecutionInput::new(CALLEE_SELECTOR.into())) .returns::<(u8, u8)>() .invoke() } @@ -61,23 +63,21 @@ mod tests { fn call_mocked_message() -> Result<(), Box> { let mut session = Session::::new()?; - // Firstly, we are creating our mocked contract. - const RETURN_VALUE: (u8, u8) = (1, 4); + // Firstly, we create the mocked contract. + const RETURN_VALUE: (u8, u8) = (4, 1); let mocked_contract = - ContractMock::new().with_message(CALLEE_SELECTOR, mock_message(|_: u8| RETURN_VALUE)); + ContractMock::new().with_message(CALLEE_SELECTOR, mock_message(|()| RETURN_VALUE)); - // Secondly, we are deploying it, similarly to a standard deployment action. + // Secondly, we are deploy it, similarly to a standard deployment action. let mock_address = session.mocking_api().deploy_mock(mocked_contract); - // Now, we can deploy our proper contract and invoke it. + // Now, we can deploy our proper contract and verify its behavior. let result: (u8, u8) = session .deploy_and(bytes(), "new", NO_ARGS, vec![], None, &transcoder())? .call_and("delegate_call", &[mock_address.to_string()], None)? .last_call_return() .expect("Call was successful, so there should be a return") .expect("Call was successful"); - - // And verify whether the value returned from the mock has been successfully passed. assert_eq!(result, RETURN_VALUE); Ok(()) From 5be192c8e75e5fbc05102dd67d99fe1f94d1dc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 18:09:58 +0200 Subject: [PATCH 17/26] CI todo --- .github/workflows/rust-checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rust-checks.yml b/.github/workflows/rust-checks.yml index 14be5a8..3380162 100644 --- a/.github/workflows/rust-checks.yml +++ b/.github/workflows/rust-checks.yml @@ -59,6 +59,8 @@ jobs: - name: Run tests for examples shell: bash run: | + # todo: use loop xD + pushd examples/flipper cargo contract build --release cargo test --release From 035b25602d0cf6050bfd3530b6416aaa7a9015ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 18:11:03 +0200 Subject: [PATCH 18/26] Bump version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5f4f37a..9e618f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ homepage = "https://github.com/Cardinal-Cryptography/drink" license = "Apache-2.0" readme = "README.md" repository = "https://github.com/Cardinal-Cryptography/drink" -version = "0.4.1" +version = "0.4.2" [workspace.dependencies] anyhow = { version = "1.0.71" } @@ -51,4 +51,4 @@ sp-runtime-interface = { version = "19.0.0" } # Local dependencies -drink = { version = "0.4.1", path = "drink" } +drink = { version = "0.4.2", path = "drink" } From b16f4a9fc16bc575f9c46c3bc6820569cfb4220a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 18:38:06 +0200 Subject: [PATCH 19/26] Docs, errors moved --- Cargo.lock | 4 +- drink/src/error.rs | 15 ------- drink/src/errors.rs | 43 +++++++++++++++++++ drink/src/lib.rs | 16 +++++-- drink/src/mock.rs | 9 ++-- drink/src/mock/contract.rs | 22 +++++++++- drink/src/mock/error.rs | 1 + drink/src/mock/mocking_api.rs | 8 ++++ .../src/runtime/pallet_contracts_debugging.rs | 7 ++- .../pallet_contracts_debugging/runtime.rs | 6 +-- drink/src/session.rs | 8 ++-- drink/src/session/{errors.rs => error.rs} | 26 ----------- 12 files changed, 103 insertions(+), 62 deletions(-) delete mode 100644 drink/src/error.rs create mode 100644 drink/src/errors.rs rename drink/src/session/{errors.rs => error.rs} (69%) diff --git a/Cargo.lock b/Cargo.lock index 7c42230..2fa18b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1015,7 +1015,7 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "drink" -version = "0.4.1" +version = "0.4.2" dependencies = [ "contract-transcode", "frame-metadata", @@ -1037,7 +1037,7 @@ dependencies = [ [[package]] name = "drink-cli" -version = "0.4.1" +version = "0.4.2" dependencies = [ "anyhow", "clap", diff --git a/drink/src/error.rs b/drink/src/error.rs deleted file mode 100644 index f1fdcf2..0000000 --- a/drink/src/error.rs +++ /dev/null @@ -1,15 +0,0 @@ -use thiserror::Error; - -/// Main error type for the drink crate. -#[derive(Error, Debug)] -pub enum Error { - /// Externalities could not be initialized. - #[error("Failed to build storage: {0}")] - StorageBuilding(String), - /// Block couldn't have been initialized. - #[error("Failed to initialize block: {0}")] - BlockInitialize(String), - /// Block couldn't have been finalized. - #[error("Failed to finalize block: {0}")] - BlockFinalize(String), -} diff --git a/drink/src/errors.rs b/drink/src/errors.rs new file mode 100644 index 0000000..915126a --- /dev/null +++ b/drink/src/errors.rs @@ -0,0 +1,43 @@ +//! Module gathering common error and result types. + +use thiserror::Error; + +/// Main error type for the drink crate. +#[derive(Error, Debug)] +pub enum Error { + /// Externalities could not be initialized. + #[error("Failed to build storage: {0}")] + StorageBuilding(String), + /// Block couldn't have been initialized. + #[error("Failed to initialize block: {0}")] + BlockInitialize(String), + /// Block couldn't have been finalized. + #[error("Failed to finalize block: {0}")] + BlockFinalize(String), +} + +/// Every contract message wraps its return value in `Result`. This is the error +/// type. +/// +/// Copied from ink primitives. +#[non_exhaustive] +#[repr(u32)] +#[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + parity_scale_codec::Encode, + parity_scale_codec::Decode, + scale_info::TypeInfo, + Error, +)] +pub enum LangError { + /// Failed to read execution input for the dispatchable. + #[error("Failed to read execution input for the dispatchable.")] + CouldNotReadInput = 1u32, +} + +/// The `Result` type for ink! messages. +pub type MessageResult = Result; diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 4207b26..44d8c46 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -5,7 +5,7 @@ pub mod chain_api; pub mod contract_api; -mod error; +pub mod errors; mod mock; pub mod runtime; #[cfg(feature = "session")] @@ -16,14 +16,14 @@ use std::{ sync::{Arc, Mutex}, }; -pub use error::Error; +pub use errors::Error; use frame_support::sp_runtime::{traits::One, BuildStorage}; pub use frame_support::{ sp_runtime::{AccountId32, DispatchError}, weights::Weight, }; use frame_system::{pallet_prelude::BlockNumberFor, EventRecord, GenesisConfig}; -pub use mock::{mock_message, ContractMock, MessageMock, MockingApi, Selector}; +pub use mock::{mock_message, ContractMock, MessageMock, MockedCallResult, MockingApi, Selector}; use pallet_contracts::debug::ExecResult; use pallet_contracts_primitives::{ExecReturnValue, ReturnFlags}; use parity_scale_codec::{Decode, Encode}; @@ -100,6 +100,7 @@ impl Sandbox { self.externalities.register_extension(d); } + /// Registers the extension for intercepting calls to contracts. fn setup_mock_extension(&mut self) { self.externalities .register_extension(InterceptingExt(Box::new(MockingExtension { @@ -107,7 +108,13 @@ impl Sandbox { }))); } } + +/// Runtime extension enabling contract call interception. struct MockingExtension { + /// Mock registry, shared with the sandbox. + /// + /// Potentially the runtime is executed in parallel and thus we need to wrap the registry in + /// `Arc` instead of `Rc`. mock_registry: Arc>>, } @@ -127,7 +134,10 @@ impl InterceptingExtT for MockingExtension { .expect("Should be able to acquire registry") .get(&contract_address) { + // There is no mock registered for this address, so we return `None` to indicate that + // the call should be executed normally. None => None::<()>.encode(), + // We intercept the call and return the result of the mock. Some(mock) => { let (selector, call_data) = input_data.split_at(4); let selector: Selector = selector diff --git a/drink/src/mock.rs b/drink/src/mock.rs index 7a9443a..da235c5 100644 --- a/drink/src/mock.rs +++ b/drink/src/mock.rs @@ -1,5 +1,3 @@ -//! Mocking contract feature. - mod contract; mod error; mod mocking_api; @@ -10,23 +8,28 @@ pub use contract::{mock_message, ContractMock, MessageMock, Selector}; use error::MockingError; pub use mocking_api::MockingApi; +/// Untyped result of a mocked call. pub type MockedCallResult = Result, MockingError>; -pub struct MockRegistry { +/// A registry of mocked contracts. +pub(crate) struct MockRegistry { mocked_contracts: BTreeMap, } impl MockRegistry { + /// Creates a new registry. pub fn new() -> Self { Self { mocked_contracts: BTreeMap::new(), } } + /// Registers `mock` for `address`. pub fn register(&mut self, address: AccountId, mock: ContractMock) { self.mocked_contracts.insert(address, mock); } + /// Returns the mock for `address`, if any. pub fn get(&self, address: &AccountId) -> Option<&ContractMock> { self.mocked_contracts.get(address) } diff --git a/drink/src/mock/contract.rs b/drink/src/mock/contract.rs index 9087006..525585a 100644 --- a/drink/src/mock/contract.rs +++ b/drink/src/mock/contract.rs @@ -3,29 +3,39 @@ use std::collections::BTreeMap; use parity_scale_codec::{Decode, Encode}; use crate::{ + errors::LangError, mock::{error::MockingError, MockedCallResult}, - session::errors::LangError, }; +/// Alias for a 4-byte selector. pub type Selector = [u8; 4]; +/// An untyped message mock. +/// +/// Notice that in the end, we cannot operate on specific argument/return types. Rust won't let us +/// have a collection of differently typed closures. Fortunately, we can assume that all types are +/// en/decodable, so we can use `Vec` as a common denominator. pub type MessageMock = Box) -> MockedCallResult + Send + Sync>; +/// A contract mock. pub struct ContractMock { messages: BTreeMap, } impl ContractMock { + /// Creates a new mock without any message. pub fn new() -> Self { Self { messages: BTreeMap::new(), } } + /// Adds a message mock. pub fn with_message(mut self, selector: Selector, message: MessageMock) -> Self { self.messages.insert(selector, message); self } + /// Try to call a message mock. Returns an error if there is no message mock for `selector`. pub fn call(&self, selector: Selector, input: Vec) -> MockedCallResult { match self.messages.get(&selector) { None => Err(MockingError::MessageNotFound(selector)), @@ -34,6 +44,16 @@ impl ContractMock { } } +impl Default for ContractMock { + fn default() -> Self { + Self::new() + } +} + +/// A helper function to create a message mock out of a typed closure. +/// +/// In particular, it takes care of decoding the input and encoding the output. Also, wraps the +/// return value in a `Result`, which is normally done implicitly by ink!. pub fn mock_message Ret + Send + Sync + 'static>( body: Body, ) -> MessageMock { diff --git a/drink/src/mock/error.rs b/drink/src/mock/error.rs index 41c35a7..d526af1 100644 --- a/drink/src/mock/error.rs +++ b/drink/src/mock/error.rs @@ -2,6 +2,7 @@ use thiserror::Error; use crate::Selector; +/// Error type for mocking operations. #[derive(Error, Debug)] pub enum MockingError { #[error("Message not found (unknown selector: {0:?})")] diff --git a/drink/src/mock/mocking_api.rs b/drink/src/mock/mocking_api.rs index 76e94bd..039643e 100644 --- a/drink/src/mock/mocking_api.rs +++ b/drink/src/mock/mocking_api.rs @@ -5,6 +5,7 @@ use crate::{ Sandbox, DEFAULT_GAS_LIMIT, }; +/// Interface for basic mocking operations. pub trait MockingApi { /// Deploy `mock` as a standard contract. Returns the address of the deployed contract. fn deploy_mock(&mut self, mock: ContractMock) -> AccountIdFor; @@ -16,12 +17,16 @@ pub trait MockingApi { impl MockingApi for Sandbox { fn deploy_mock(&mut self, mock: ContractMock) -> AccountIdFor { + // We have to deploy some contract. We use a dummy contract for that. Thanks to that, we + // ensure that the pallet will treat our mock just as a regular contract, until we actually + // call it. let mock_bytes = wat::parse_str(DUMMY_CONTRACT).expect("Dummy contract should be valid"); let mock_address = self .deploy_contract( mock_bytes, 0, vec![], + // We have to use a different account ID for each contract. vec![self.mock_counter as u8], R::default_actor(), DEFAULT_GAS_LIMIT, @@ -45,6 +50,9 @@ impl MockingApi for Sandbox { } } +/// A dummy contract that is used to deploy a mock. +/// +/// Has a single noop constructor and a single panicking message. const DUMMY_CONTRACT: &str = r#" (module (import "env" "memory" (memory 1 1)) diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index 98d0c25..d3027e6 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -16,10 +16,9 @@ //! //! # Passing objects between runtime and runtime extension //! -//! Unfortunately, runtime interface that lies between runtime and the end-user accepts only -//! very simple argument types and those that implement some specific traits. This means that -//! usually, complex objects will be passed in their encoded form (`Vec` obtained with scale -//! encoding). +//! Unfortunately, runtime interface that lies between runtime, and the end-user accepts only +//! simple argument types, and those that implement some specific traits. This means that usually, +//! complex objects will be passed in their encoded form (`Vec` obtained with scale encoding). mod intercepting; mod runtime; diff --git a/drink/src/runtime/pallet_contracts_debugging/runtime.rs b/drink/src/runtime/pallet_contracts_debugging/runtime.rs index 640a92c..9b31160 100644 --- a/drink/src/runtime/pallet_contracts_debugging/runtime.rs +++ b/drink/src/runtime/pallet_contracts_debugging/runtime.rs @@ -34,8 +34,7 @@ pub trait ContractCallDebugger { } } -/// This trait describes the interface of a runtime extension that can be used to debug contract -/// calls. +/// This trait describes a runtime extension that can be used to debug contract calls. pub trait TracingExtT { /// Called after a contract call is made. fn after_call( @@ -53,8 +52,7 @@ decl_extension! { pub struct TracingExt(Box); } -/// This trait describes the interface of a runtime extension that can be used to intercept contract -/// calls. +/// This trait describes a runtime extension that can be used to intercept contract calls. pub trait InterceptingExtT { /// Called when a contract call is made. /// diff --git a/drink/src/session.rs b/drink/src/session.rs index ac14e6a..1a87ca0 100644 --- a/drink/src/session.rs +++ b/drink/src/session.rs @@ -16,10 +16,10 @@ use crate::{ EventRecordOf, Sandbox, DEFAULT_GAS_LIMIT, }; -pub mod errors; +pub mod error; mod transcoding; -use errors::{MessageResult, SessionError}; +use error::{MessageResult, SessionError}; use crate::{mock::MockingApi, session::transcoding::TranscoderRegistry}; @@ -56,7 +56,7 @@ pub const NO_ARGS: &[String] = &[]; /// # fn contract_bytes() -> Vec { vec![] } /// # fn bob() -> AccountId32 { AccountId32::new([0; 32]) } /// -/// # fn main() -> Result<(), drink::session::errors::SessionError> { +/// # fn main() -> Result<(), drink::session::error::SessionError> { /// /// Session::::new()? /// .deploy_and(contract_bytes(), "new", NO_ARGS, vec![], None, &get_transcoder())? @@ -82,7 +82,7 @@ pub const NO_ARGS: &[String] = &[]; /// # fn contract_bytes() -> Vec { vec![] } /// # fn bob() -> AccountId32 { AccountId32::new([0; 32]) } /// -/// # fn main() -> Result<(), drink::session::errors::SessionError> { +/// # fn main() -> Result<(), drink::session::error::SessionError> { /// /// let mut session = Session::::new()?; /// let _address = session.deploy(contract_bytes(), "new", NO_ARGS, vec![], None, &get_transcoder())?; diff --git a/drink/src/session/errors.rs b/drink/src/session/error.rs similarity index 69% rename from drink/src/session/errors.rs rename to drink/src/session/error.rs index 346c0d2..5b94280 100644 --- a/drink/src/session/errors.rs +++ b/drink/src/session/error.rs @@ -37,29 +37,3 @@ pub enum SessionError { #[error("Missing transcoder")] NoTranscoder, } - -/// Every contract message wraps its return value in `Result`. This is the error -/// type. -/// -/// Copied from ink primitives. -#[non_exhaustive] -#[repr(u32)] -#[derive( - Debug, - Copy, - Clone, - PartialEq, - Eq, - parity_scale_codec::Encode, - parity_scale_codec::Decode, - scale_info::TypeInfo, - Error, -)] -pub enum LangError { - /// Failed to read execution input for the dispatchable. - #[error("Failed to read execution input for the dispatchable.")] - CouldNotReadInput = 1u32, -} - -/// The `Result` type for ink! messages. -pub type MessageResult = Result; From ebebecc41f028afc48463604c4006da6657acbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 18:38:51 +0200 Subject: [PATCH 20/26] Fix imports --- drink/src/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drink/src/session.rs b/drink/src/session.rs index 1a87ca0..403d4f8 100644 --- a/drink/src/session.rs +++ b/drink/src/session.rs @@ -19,9 +19,9 @@ use crate::{ pub mod error; mod transcoding; -use error::{MessageResult, SessionError}; +use error::SessionError; -use crate::{mock::MockingApi, session::transcoding::TranscoderRegistry}; +use crate::{errors::MessageResult, mock::MockingApi, session::transcoding::TranscoderRegistry}; type Balance = u128; From 1e090ce6b253cbe17ee77db88208371e6faa92ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Thu, 5 Oct 2023 18:40:56 +0200 Subject: [PATCH 21/26] Fatter bump --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9e618f0..df9e241 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ homepage = "https://github.com/Cardinal-Cryptography/drink" license = "Apache-2.0" readme = "README.md" repository = "https://github.com/Cardinal-Cryptography/drink" -version = "0.4.2" +version = "0.5.0" [workspace.dependencies] anyhow = { version = "1.0.71" } @@ -51,4 +51,4 @@ sp-runtime-interface = { version = "19.0.0" } # Local dependencies -drink = { version = "0.4.2", path = "drink" } +drink = { version = "0.5.0", path = "drink" } From 8f4f08ab749afc5f31a1f91bd7d71295768570f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Tue, 17 Oct 2023 18:00:41 +0200 Subject: [PATCH 22/26] comment fix --- Cargo.lock | 4 ++-- examples/mocking/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2fa18b8..66f3106 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1015,7 +1015,7 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "drink" -version = "0.4.2" +version = "0.5.0" dependencies = [ "contract-transcode", "frame-metadata", @@ -1037,7 +1037,7 @@ dependencies = [ [[package]] name = "drink-cli" -version = "0.4.2" +version = "0.5.0" dependencies = [ "anyhow", "clap", diff --git a/examples/mocking/lib.rs b/examples/mocking/lib.rs index 81dde62..ef26171 100755 --- a/examples/mocking/lib.rs +++ b/examples/mocking/lib.rs @@ -68,7 +68,7 @@ mod tests { let mocked_contract = ContractMock::new().with_message(CALLEE_SELECTOR, mock_message(|()| RETURN_VALUE)); - // Secondly, we are deploy it, similarly to a standard deployment action. + // Secondly, we deploy it, similarly to a standard deployment action. let mock_address = session.mocking_api().deploy_mock(mocked_contract); // Now, we can deploy our proper contract and verify its behavior. From acfb5caaacb2cf71112a5d8cc1d4f903fdba3200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Tue, 17 Oct 2023 18:02:10 +0200 Subject: [PATCH 23/26] deploy_mock -> deploy --- drink/src/mock/mocking_api.rs | 4 ++-- examples/mocking/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/drink/src/mock/mocking_api.rs b/drink/src/mock/mocking_api.rs index 039643e..92dbf04 100644 --- a/drink/src/mock/mocking_api.rs +++ b/drink/src/mock/mocking_api.rs @@ -8,7 +8,7 @@ use crate::{ /// Interface for basic mocking operations. pub trait MockingApi { /// Deploy `mock` as a standard contract. Returns the address of the deployed contract. - fn deploy_mock(&mut self, mock: ContractMock) -> AccountIdFor; + fn deploy(&mut self, mock: ContractMock) -> AccountIdFor; /// Mock part of an existing contract. In particular, allows to override real behavior of /// deployed contract's messages. @@ -16,7 +16,7 @@ pub trait MockingApi { } impl MockingApi for Sandbox { - fn deploy_mock(&mut self, mock: ContractMock) -> AccountIdFor { + fn deploy(&mut self, mock: ContractMock) -> AccountIdFor { // We have to deploy some contract. We use a dummy contract for that. Thanks to that, we // ensure that the pallet will treat our mock just as a regular contract, until we actually // call it. diff --git a/examples/mocking/lib.rs b/examples/mocking/lib.rs index ef26171..4af36af 100755 --- a/examples/mocking/lib.rs +++ b/examples/mocking/lib.rs @@ -69,7 +69,7 @@ mod tests { ContractMock::new().with_message(CALLEE_SELECTOR, mock_message(|()| RETURN_VALUE)); // Secondly, we deploy it, similarly to a standard deployment action. - let mock_address = session.mocking_api().deploy_mock(mocked_contract); + let mock_address = session.mocking_api().deploy(mocked_contract); // Now, we can deploy our proper contract and verify its behavior. let result: (u8, u8) = session From 49f2acbbbe09b6dc13646459841302fe061447fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Tue, 17 Oct 2023 18:07:04 +0200 Subject: [PATCH 24/26] Return old mock --- drink/src/mock.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drink/src/mock.rs b/drink/src/mock.rs index da235c5..06bebea 100644 --- a/drink/src/mock.rs +++ b/drink/src/mock.rs @@ -24,9 +24,9 @@ impl MockRegistry { } } - /// Registers `mock` for `address`. - pub fn register(&mut self, address: AccountId, mock: ContractMock) { - self.mocked_contracts.insert(address, mock); + /// Registers `mock` for `address`. Returns the previous mock, if any. + pub fn register(&mut self, address: AccountId, mock: ContractMock) -> Option { + self.mocked_contracts.insert(address, mock) } /// Returns the mock for `address`, if any. From 024b20818c1d89bec5adcdd4659cae7dfdb37bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 18 Oct 2023 17:32:44 +0200 Subject: [PATCH 25/26] hacky way to check if we should set flag to revert --- drink/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 44d8c46..be1a5d4 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -30,6 +30,7 @@ use parity_scale_codec::{Decode, Encode}; use sp_io::TestExternalities; use crate::{ + errors::MessageResult, mock::MockRegistry, pallet_contracts_debugging::{InterceptingExt, TracingExt}, runtime::{ @@ -148,8 +149,16 @@ impl InterceptingExtT for MockingExtension { .call(selector, call_data.to_vec()) .expect("TODO: let the user define the fallback mechanism"); + let decoded_result: MessageResult<()> = + Decode::decode(&mut &result[..]).expect("Mock result should be decodable"); + + let flags = match decoded_result { + Ok(_) => ReturnFlags::empty(), + Err(_) => ReturnFlags::REVERT, + }; + let result: ExecResult = Ok(ExecReturnValue { - flags: ReturnFlags::empty(), + flags, data: result, }); From 5a15ee8353991944076ef1e07b1fccb335009100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 18 Oct 2023 17:33:53 +0200 Subject: [PATCH 26/26] explanation --- drink/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drink/src/lib.rs b/drink/src/lib.rs index be1a5d4..a7904ac 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -149,6 +149,8 @@ impl InterceptingExtT for MockingExtension { .call(selector, call_data.to_vec()) .expect("TODO: let the user define the fallback mechanism"); + // Although we don't know the exact type, thanks to the SCALE encoding we know + // that `()` will always succeed (we only care about the `Ok`/`Err` distinction). let decoded_result: MessageResult<()> = Decode::decode(&mut &result[..]).expect("Mock result should be decodable");