Skip to content

Commit

Permalink
Basic contract mocking (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmikolajczyk41 authored Oct 18, 2023
1 parent ae2e356 commit ec376f5
Show file tree
Hide file tree
Showing 21 changed files with 633 additions and 167 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/rust-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -73,3 +75,8 @@ jobs:
cargo contract build --release
cargo test --release
popd
pushd examples/mocking
cargo contract build --release
cargo test --release
popd
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ exclude = [
"examples/counter",
"examples/flipper",
"examples/cross-contract-call-tracing",
"examples/mocking",
]

[workspace.package]
Expand All @@ -19,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.5.0"

[workspace.dependencies]
anyhow = { version = "1.0.71" }
Expand All @@ -40,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" }
Expand All @@ -50,4 +51,4 @@ sp-runtime-interface = { version = "19.0.0" }

# Local dependencies

drink = { version = "0.4.1", path = "drink" }
drink = { version = "0.5.0", path = "drink" }
2 changes: 0 additions & 2 deletions drink/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ sp-runtime-interface = { workspace = true }

scale-info = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
wat = { workspace = true }

[features]
Expand Down
15 changes: 0 additions & 15 deletions drink/src/error.rs

This file was deleted.

43 changes: 43 additions & 0 deletions drink/src/errors.rs
Original file line number Diff line number Diff line change
@@ -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<T, LangResult>`. 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<T> = Result<T, LangError>;
102 changes: 95 additions & 7 deletions drink/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,38 @@

pub mod chain_api;
pub mod contract_api;
mod error;
pub mod errors;
mod mock;
pub mod runtime;
#[cfg(feature = "session")]
pub mod session;
use std::marker::PhantomData;

pub use error::Error;
use std::{
marker::PhantomData,
sync::{Arc, Mutex},
};

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, MockedCallResult, 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::{
pallet_contracts_debugging::DebugExt,
runtime::{pallet_contracts_debugging::NoopDebugExt, *},
errors::MessageResult,
mock::MockRegistry,
pallet_contracts_debugging::{InterceptingExt, TracingExt},
runtime::{
pallet_contracts_debugging::{InterceptingExtT, NoopExt},
*,
},
};

/// Main result type for the drink crate.
Expand All @@ -35,6 +49,8 @@ pub type EventRecordOf<T> =
/// A sandboxed runtime.
pub struct Sandbox<R: Runtime> {
externalities: TestExternalities,
mock_registry: Arc<Mutex<MockRegistry<AccountIdFor<R>>>>,
mock_counter: usize,
_phantom: PhantomData<R>,
}

Expand All @@ -57,6 +73,8 @@ impl<R: Runtime> Sandbox<R> {

let mut sandbox = Self {
externalities: TestExternalities::new(storage),
mock_registry: Arc::new(Mutex::new(MockRegistry::new())),
mock_counter: 0,
_phantom: PhantomData,
};

Expand All @@ -68,7 +86,9 @@ impl<R: Runtime> Sandbox<R> {
.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(NoopExt {})));

sandbox.setup_mock_extension();

Ok(sandbox)
}
Expand All @@ -77,7 +97,75 @@ impl<R: Runtime> Sandbox<R> {
///
/// 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);
}

/// Registers the extension for intercepting calls to contracts.
fn setup_mock_extension(&mut self) {
self.externalities
.register_extension(InterceptingExt(Box::new(MockingExtension {
mock_registry: Arc::clone(&self.mock_registry),
})));
}
}

/// Runtime extension enabling contract call interception.
struct MockingExtension<AccountId: Ord> {
/// Mock registry, shared with the sandbox.
///
/// Potentially the runtime is executed in parallel and thus we need to wrap the registry in
/// `Arc<Mutex>` instead of `Rc<RefCell>`.
mock_registry: Arc<Mutex<MockRegistry<AccountId>>>,
}

impl<AccountId: Ord + Decode> InterceptingExtT for MockingExtension<AccountId> {
fn intercept_call(
&self,
contract_address: Vec<u8>,
_is_call: bool,
input_data: Vec<u8>,
) -> Vec<u8> {
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)
{
// 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
.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");

// 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");

let flags = match decoded_result {
Ok(_) => ReturnFlags::empty(),
Err(_) => ReturnFlags::REVERT,
};

let result: ExecResult = Ok(ExecReturnValue {
flags,
data: result,
});

Some(result).encode()
}
}
}
}
36 changes: 36 additions & 0 deletions drink/src/mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
mod contract;
mod error;
mod mocking_api;

use std::collections::BTreeMap;

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<Vec<u8>, MockingError>;

/// A registry of mocked contracts.
pub(crate) struct MockRegistry<AccountId: Ord> {
mocked_contracts: BTreeMap<AccountId, ContractMock>,
}

impl<AccountId: Ord> MockRegistry<AccountId> {
/// Creates a new registry.
pub fn new() -> Self {
Self {
mocked_contracts: BTreeMap::new(),
}
}

/// Registers `mock` for `address`. Returns the previous mock, if any.
pub fn register(&mut self, address: AccountId, mock: ContractMock) -> Option<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)
}
}
Loading

0 comments on commit ec376f5

Please sign in to comment.