Skip to content

Commit

Permalink
support mocks
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronc committed Oct 10, 2024
1 parent f761c54 commit 0722800
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 43 deletions.
1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ array-concat = "0.5.3"
ixc_testing = { path = "testing", version = "0.0.2" }
arrayvec = "0.7.6"
thiserror = "1.0.63"
mockall = "0.13.0"

[lints]
workspace = true
Expand Down
21 changes: 16 additions & 5 deletions rust/core/src/account_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,31 @@ use crate::result::ClientResult;

/// Creates a new account for the specified handler.
pub fn create_account<'a, I: InitMessage<'a>>(ctx: &mut Context, init: I) -> ClientResult<<<I as InitMessage<'a>>::Handler as ClientFactory>::Client> {
let mut packet = create_packet(ctx, ROOT_ACCOUNT, CREATE_SELECTOR)?;

let cdc = I::Codec::default();
let init_bz = cdc.encode_value(&init, ctx.memory_manager())?;

let account_id = do_create_account(ctx, I::Handler::NAME, &init_bz)?;
Ok(I::Handler::new_client(account_id))
}

/// Creates a new account for the named handler with opaque initialization data.
pub fn create_account_raw<'a>(ctx: &mut Context, name: &str, init: &[u8]) -> ClientResult<AccountID> {
do_create_account(ctx, name, init)
}

/// Creates a new account for the named handler with opaque initialization data.
fn do_create_account<'a>(ctx: &Context, name: &str, init: &[u8]) -> ClientResult<AccountID> {
let mut packet = create_packet(ctx, ROOT_ACCOUNT, CREATE_SELECTOR)?;

unsafe {
packet.header_mut().in_pointer1.set_slice(I::Handler::NAME.as_bytes());
packet.header_mut().in_pointer2.set_slice(init_bz);
packet.header_mut().in_pointer1.set_slice(name.as_bytes());
packet.header_mut().in_pointer2.set_slice(init);

ctx.host_backend().invoke(&mut packet, ctx.memory_manager())?;

let new_account_id = packet.header().in_pointer1.get_u64();

Ok(I::Handler::new_client(AccountID::new(new_account_id)))
Ok(AccountID::new(new_account_id))
}
}

Expand Down
14 changes: 14 additions & 0 deletions rust/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use core::fmt::{Debug, Display, Formatter};
use ixc_message_api::code::{ErrorCode, SystemCode};
use ixc_schema::decoder::DecodeError;
use ixc_schema::encoder::EncodeError;
use crate::result::ClientResult;

/// The standard error type returned by handlers.
#[derive(Clone)]
Expand Down Expand Up @@ -208,3 +209,16 @@ pub fn convert_client_error<E: Into<u8> + TryFrom<u8> + Debug, F: Into<u8> + Try
message: err.message,
}
}

/// Returns a default result if the error is `MessageNotHandled`.
pub fn unimplemented_ok<R: Default, E: Into<u8> + TryFrom<u8> + Debug>(res: ClientResult<R, E>) -> ClientResult<R, E> {
match res {
Ok(r) => { Ok(r) }
Err(e) => {
match e.code {
ErrorCode::SystemCode(SystemCode::MessageNotHandled) => { Ok(Default::default()) }
_ => Err(e)
}
}
}
}
13 changes: 9 additions & 4 deletions rust/core_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,10 +341,15 @@ fn derive_api_method(handler_ident: &Ident, handler_ty: &TokenStream2, publish_t
let mut ty = pat_type.ty.clone();
match ty.as_mut() {
Type::Reference(tyref) => {
if tyref.elem == parse_quote!(Context) {
context_name = Some(ident.ident.clone());
new_inputs.push(field.clone());
continue;
match tyref.elem.borrow() {
Type::Path(path) => {
if path.path.segments.first().unwrap().ident == "Context" {
context_name = Some(ident.ident.clone());
new_inputs.push(field.clone());
continue;
}
}
_ => {}
}

have_lifetimes = true;
Expand Down
85 changes: 67 additions & 18 deletions rust/examples/bank.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#![allow(missing_docs)]
#[ixc::handler(Bank)]
pub mod bank {
use mockall::automock;
use ixc::*;
use ixc_core::error::unimplemented_ok;
use ixc_core::handler::ClientFactory;
use ixc_message_api::code::{ErrorCode};

Expand All @@ -14,11 +16,13 @@ pub mod bank {
#[state(prefix = 3)]
super_admin: Item<AccountID>,
#[state(prefix = 4)]
denom_admins: Map<Str, AccountID>,
global_send_hook: Item<AccountID>,
#[state(prefix = 5)]
denom_admins: Map<Str, AccountID>,
#[state(prefix = 6)]
denom_send_hooks: Map<Str, AccountID>,
#[state(prefix = 6)]
global_send_hook: Item<AccountID>
denom_burn_hooks: Map<Str, AccountID>,
}

#[derive(SchemaValue, Clone)]
Expand All @@ -37,13 +41,21 @@ pub mod bank {
}

#[handler_api]
#[automock]
pub trait SendHook {
fn on_send(&self, ctx: &mut Context, from: AccountID, to: AccountID, denom: &str, amount: u128) -> Result<()>;
fn on_send<'a>(&self, ctx: &mut Context<'a>, from: AccountID, to: AccountID, denom: &str, amount: u128) -> Result<()>;
}

#[handler_api]
#[automock]
pub trait BurnHook {
fn on_burn<'a>(&self, ctx: &mut Context<'a>, from: AccountID, denom: &str, amount: u128) -> Result<()>;
}

#[handler_api]
#[automock]
pub trait ReceiveHook {
fn on_receive(&self, ctx: &mut Context, from: AccountID, denom: &str, amount: u128) -> Result<()>;
fn on_receive<'a>(&self, ctx: &mut Context<'a>, from: AccountID, denom: &str, amount: u128) -> Result<()>;
}

#[derive(SchemaValue)]
Expand All @@ -70,8 +82,22 @@ pub mod bank {

impl Bank {
#[on_create]
fn create(&self, ctx: &mut Context, init_denom: &str, init_balance: u128) -> Result<()> {
self.balances.add(ctx, (ctx.caller(), init_denom), init_balance)?;
pub fn create(&self, ctx: &mut Context) -> Result<()> {
self.super_admin.set(ctx, ctx.caller())?;
Ok(())
}

#[publish]
pub fn create_denom(&self, ctx: &mut Context, denom: &str, admin: AccountID) -> Result<()> {
ensure!(self.super_admin.get(ctx)? == ctx.caller(), "not authorized");
self.denom_admins.set(ctx, denom, admin)?;
Ok(())
}

#[publish]
pub fn set_global_send_hook(&self, ctx: &mut Context, hook: AccountID) -> Result<()> {
ensure!(self.super_admin.get(ctx)? == ctx.caller(), "not authorized");
self.global_send_hook.set(ctx, hook)?;
Ok(())
}
}
Expand All @@ -96,15 +122,7 @@ pub mod bank {
}
let from = ctx.caller();
let receive_hook = <dyn ReceiveHook>::new_client(to);
match receive_hook.on_receive(ctx, from, coin.denom, coin.amount) {
Ok(_) => {}
Err(e) => {
match e.code {
ErrorCode::SystemCode(ixc_message_api::code::SystemCode::MessageNotHandled) => {}
_ => bail!("receive blocked: {:?}", e),
}
}
}
unimplemented_ok(receive_hook.on_receive(ctx, from, coin.denom, coin.amount))?;
self.balances.safe_sub(ctx, (from, coin.denom), coin.amount)?;
self.balances.add(ctx, (to, coin.denom), coin.amount)?;
evt.emit(ctx, &EventSend {
Expand Down Expand Up @@ -138,20 +156,51 @@ pub mod bank {

#[cfg(test)]
mod tests {
use ixc_core::account_api::ROOT_ACCOUNT;
use ixc_core::handler::{Client, ClientFactory};
use ixc_core::routes::{find_route, Router};
use ixc_message_api::code::ErrorCode;
use ixc_message_api::handler::{Allocator, HostBackend, RawHandler};
use ixc_message_api::packet::MessagePacket;
use super::bank::*;
use ixc_testing::*;

#[test]
fn test() {
// initialize the app
let mut app = TestApp::default();
// register the Bank handler
app.register_handler::<Bank>().unwrap();

// create a new client context for the root account and initialize bank
let mut root = app.client_context_for(ROOT_ACCOUNT);
let bank_client = create_account(&mut root, BankCreate {}).unwrap();

// register a mock global send hook to test that it is called
let mut mock_global_send_hook = MockSendHook::new();
// expect that the send hook is only called 1x in this test
mock_global_send_hook.expect_on_send().times(1).returning(|_, _, _, _, _| Ok(()));
let mut mock = MockHandler::new();
mock.add_handler::<dyn SendHook>(Box::new(mock_global_send_hook));
let mock_id = app.add_mock(&mut root, mock).unwrap();
bank_client.set_global_send_hook(&mut root, mock_id).unwrap();

// alice gets to manage the "foo" denom and mints herself 1000 foo coins
let mut alice = app.new_client_context().unwrap();
let mut bob = app.new_client_context().unwrap();
let bank_client = create_account(&mut alice, BankCreate { init_denom: "foo", init_balance: 1000 }).unwrap();
let alice_balance = bank_client.get_balance(&alice, alice.account_id(), "foo").unwrap();
let alice_id = alice.account_id();
bank_client.create_denom(&mut root, "foo", alice_id).unwrap();
bank_client.mint(&mut alice, alice_id, "foo", 1000).unwrap();

// ensure alice has 1000 foo coins
let alice_balance = bank_client.get_balance(&alice, alice_id, "foo").unwrap();
assert_eq!(alice_balance, 1000);


// alice sends 100 foo coins to bob
let mut bob = app.new_client_context().unwrap();
bank_client.send(&mut alice, bob.account_id(), &[Coin { denom: "foo", amount: 100 }]).unwrap();

// ensure alice has 900 foo coins and bob has 100 foo coins
let alice_balance = bank_client.get_balance(&alice, alice.account_id(), "foo").unwrap();
assert_eq!(alice_balance, 900);
let bob_balance = bank_client.get_balance(&bob, bob.account_id(), "foo").unwrap();
Expand Down
68 changes: 54 additions & 14 deletions rust/testing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
mod store;
mod vm;

use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use allocator_api2::alloc::Allocator;
use allocator_api2::boxed::Box;
use ixc::SchemaValue;
use ixc_message_api::{AccountID};
use ixc_core::{Context};
use ixc_core::account_api::{ROOT_ACCOUNT};
use ixc_core::account_api::{create_account_raw, ROOT_ACCOUNT};
use ixc_core::handler::{HandlerAPI, Handler, ClientFactory, Client, InitMessage};
use ixc_core::resource::{InitializationError, ResourceScope, Resources};
use ixc_core::routes::{Route, Router};
use ixc_hypervisor::Hypervisor;
use ixc_message_api::code::ErrorCode;
use ixc_message_api::code::{ErrorCode, SystemCode};
use ixc_message_api::handler::{HostBackend, RawHandler};
use ixc_message_api::header::{ContextInfo, MessageHeader};
use ixc_message_api::packet::MessagePacket;
Expand All @@ -24,12 +24,14 @@ use crate::store::{Store, VersionedMultiStore};
use crate::vm::{NativeVM, NativeVMImpl};

pub use ixc_core::account_api::create_account;
use ixc_core::result::ClientResult;

/// Defines a test harness for running tests against account and module implementations.
pub struct TestApp {
hypervisor: RefCell<Hypervisor<VersionedMultiStore>>,
native_vm: NativeVM,
mem: MemoryManager,
mock_id: Cell<u64>,
}

impl Default for TestApp {
Expand All @@ -43,6 +45,7 @@ impl Default for TestApp {
hypervisor: RefCell::new(hypervisor),
native_vm,
mem,
mock_id: Cell::new(0),
};
test_app.register_handler::<DefaultAccount>().unwrap();
test_app
Expand Down Expand Up @@ -98,7 +101,7 @@ impl TestApp {
/// Registers a handler with the test harness so that accounts backed by this handler can be created.
pub fn register_handler<H: Handler>(&mut self) -> core::result::Result<(), InitializationError> {
let scope = ResourceScope::default();
unsafe { self.native_vm.register_handler::<H>(H::NAME, H::new(&scope)?); }
unsafe { self.native_vm.register_handler(H::NAME, std::boxed::Box::new(H::new(&scope)?)); }
Ok(())
}
// /// Adds a module to the test harness.
Expand Down Expand Up @@ -134,15 +137,14 @@ impl TestApp {
//

/// Creates a new random client account that can be used in calls.
pub fn new_client_account(&self) -> core::result::Result<AccountID, ()> {
pub fn new_client_account(&self) -> ClientResult<AccountID> {
let mut ctx = self.client_context_for(ROOT_ACCOUNT);
let client = create_account(&mut ctx, CreateDefaultAccount)
.map_err(|_| ())?;
let client = create_account(&mut ctx, CreateDefaultAccount)?;
Ok(client.0)
}

/// Creates a new random client account that can be used in calls and wraps it in a context.
pub fn new_client_context(&self) -> core::result::Result<Context, ()> {
pub fn new_client_context(&self) -> ClientResult<Context> {
let account_id = self.new_client_account()?;
Ok(self.client_context_for(account_id))
}
Expand All @@ -160,6 +162,15 @@ impl TestApp {
}
}

/// Adds a mock account handler to the test harness, instantiates it as an account and returns the account ID.
pub fn add_mock(&self, ctx: &mut Context, mock: MockHandler) -> ClientResult<AccountID> {
let mock_id = self.mock_id.get();
self.mock_id.set(mock_id + 1);
let handler_id = format!("mock{}", mock_id);
self.native_vm.register_handler(&handler_id, std::boxed::Box::new(mock));
create_account_raw(ctx, &handler_id, &[])
}

//
// /// Creates a new client context with a random address.
// pub fn client_context(&mut self, address: &Address) -> &mut Context {
Expand Down Expand Up @@ -255,12 +266,41 @@ impl<'a, H: Handler> AccountInstance<'a, H> {
// }
//

/// Defines a mock account handler composed of mock account API trait implementations.
pub struct MockAccount {}
/// Defines a mock account handler composed of mock handler API trait implementations.
pub struct MockHandler {
mocks: Vec<std::boxed::Box<dyn RawHandler>>,
}

impl MockHandler {
/// Creates a new mock handler.
pub fn new() -> Self {
MockHandler {
mocks: Vec::new(),
}
}

impl MockAccount {
/// Adds a mock account API implementation to the mock account handler.
fn add_mock_account_api<A: HandlerAPI>(&mut self, mock: A) {
todo!()
pub fn add_handler<T: RawHandler + ?Sized + 'static>(&mut self, mock: std::boxed::Box<T>) {
self.mocks.push(std::boxed::Box::new(MockWrapper::<T>(mock)));
}
}
}

impl RawHandler for MockHandler {
fn handle(&self, message_packet: &mut MessagePacket, callbacks: &dyn HostBackend, allocator: &dyn Allocator) -> Result<(), ErrorCode> {
for mock in &self.mocks {
let res = mock.handle(message_packet, callbacks, allocator);
match res {
Err(ErrorCode::SystemCode(SystemCode::MessageNotHandled)) => continue,
_ => return res
}
}
Err(ErrorCode::SystemCode(SystemCode::MessageNotHandled))
}
}

struct MockWrapper<T: RawHandler + ?Sized>(std::boxed::Box<T>);
impl <T: RawHandler + ?Sized> RawHandler for MockWrapper<T> {
fn handle(&self, message_packet: &mut MessagePacket, callbacks: &dyn HostBackend, allocator: &dyn Allocator) -> Result<(), ErrorCode> {
self.0.handle(message_packet, callbacks, allocator)
}
}
4 changes: 2 additions & 2 deletions rust/testing/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ impl NativeVM {
})))
}

pub fn register_handler<H: RawHandler + 'static>(&self, name: &str, handler: H) {
pub fn register_handler(&self, name: &str, handler: Box<dyn RawHandler>) {
let mut vm = self.0.write().unwrap();
vm.handlers.insert(name.to_string(), Box::new(handler));
vm.handlers.insert(name.to_string(), handler);
}
}

Expand Down

0 comments on commit 0722800

Please sign in to comment.