-
Notifications
You must be signed in to change notification settings - Fork 353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add context to multitest execution errors #597
Changes from 9 commits
b4e8e67
582e47e
0291e82
2c90217
05cf444
3eb5e2d
ad155f5
708e572
ef50e4b
ede69a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -947,7 +947,7 @@ mod test { | |
}; | ||
|
||
use crate::error::Error; | ||
use crate::test_helpers::contracts::{echo, hackatom, payout, reflect}; | ||
use crate::test_helpers::contracts::{caller, echo, error, hackatom, payout, reflect}; | ||
use crate::test_helpers::{CustomMsg, EmptyMsg}; | ||
use crate::transactions::StorageTransaction; | ||
|
||
|
@@ -2580,4 +2580,154 @@ mod test { | |
assert_eq!(exec_res.data, Some(Binary::from(b"hello"))); | ||
} | ||
} | ||
|
||
mod errors { | ||
use super::*; | ||
|
||
#[test] | ||
fn simple_instantiation() { | ||
let owner = Addr::unchecked("owner"); | ||
let mut app = App::default(); | ||
|
||
// set up contract | ||
let code_id = app.store_code(error::contract(false)); | ||
let msg = EmptyMsg {}; | ||
let err = app | ||
.instantiate_contract(code_id, owner, &msg, &[], "error", None) | ||
.unwrap_err(); | ||
|
||
// we should be able to retrieve the original error by downcasting | ||
let source: &StdError = err.downcast_ref().unwrap(); | ||
if let StdError::GenericErr { msg } = source { | ||
assert_eq!(msg, "Init failed"); | ||
} else { | ||
panic!("wrong StdError variant"); | ||
} | ||
|
||
// we're expecting exactly 3 nested error types | ||
// (the original error, initiate msg context, WasmMsg context) | ||
assert_eq!(err.chain().count(), 3); | ||
} | ||
|
||
#[test] | ||
fn simple_call() { | ||
let owner = Addr::unchecked("owner"); | ||
let mut app = App::default(); | ||
|
||
// set up contract | ||
let code_id = app.store_code(error::contract(true)); | ||
let msg = EmptyMsg {}; | ||
let contract_addr = app | ||
.instantiate_contract(code_id, owner, &msg, &[], "error", None) | ||
.unwrap(); | ||
|
||
// execute should error | ||
let err = app | ||
.execute_contract(Addr::unchecked("random"), contract_addr, &msg, &[]) | ||
.unwrap_err(); | ||
|
||
// we should be able to retrieve the original error by downcasting | ||
let source: &StdError = err.downcast_ref().unwrap(); | ||
if let StdError::GenericErr { msg } = source { | ||
assert_eq!(msg, "Handle failed"); | ||
} else { | ||
panic!("wrong StdError variant"); | ||
} | ||
|
||
// we're expecting exactly 3 nested error types | ||
// (the original error, execute msg context, WasmMsg context) | ||
assert_eq!(err.chain().count(), 3); | ||
} | ||
|
||
#[test] | ||
fn nested_call() { | ||
let owner = Addr::unchecked("owner"); | ||
let mut app = App::default(); | ||
|
||
let error_code_id = app.store_code(error::contract(true)); | ||
let caller_code_id = app.store_code(caller::contract()); | ||
|
||
// set up contracts | ||
let msg = EmptyMsg {}; | ||
let caller_addr = app | ||
.instantiate_contract(caller_code_id, owner.clone(), &msg, &[], "caller", None) | ||
.unwrap(); | ||
let error_addr = app | ||
.instantiate_contract(error_code_id, owner, &msg, &[], "error", None) | ||
.unwrap(); | ||
|
||
// execute should error | ||
let msg = WasmMsg::Execute { | ||
contract_addr: error_addr.into(), | ||
msg: to_binary(&EmptyMsg {}).unwrap(), | ||
funds: vec![], | ||
}; | ||
let err = app | ||
.execute_contract(Addr::unchecked("random"), caller_addr, &msg, &[]) | ||
.unwrap_err(); | ||
|
||
// we can downcast to get the original error | ||
let source: &StdError = err.downcast_ref().unwrap(); | ||
if let StdError::GenericErr { msg } = source { | ||
assert_eq!(msg, "Handle failed"); | ||
} else { | ||
panic!("wrong StdError variant"); | ||
} | ||
|
||
// we're expecting exactly 4 nested error types | ||
// (the original error, execute msg context, 2 WasmMsg contexts) | ||
assert_eq!(err.chain().count(), 4); | ||
} | ||
|
||
#[test] | ||
fn double_nested_call() { | ||
let owner = Addr::unchecked("owner"); | ||
let mut app = App::default(); | ||
|
||
let error_code_id = app.store_code(error::contract(true)); | ||
let caller_code_id = app.store_code(caller::contract()); | ||
|
||
// set up contracts | ||
let msg = EmptyMsg {}; | ||
let caller_addr1 = app | ||
.instantiate_contract(caller_code_id, owner.clone(), &msg, &[], "caller", None) | ||
.unwrap(); | ||
let caller_addr2 = app | ||
.instantiate_contract(caller_code_id, owner.clone(), &msg, &[], "caller", None) | ||
.unwrap(); | ||
let error_addr = app | ||
.instantiate_contract(error_code_id, owner, &msg, &[], "error", None) | ||
.unwrap(); | ||
|
||
// caller1 calls caller2, caller2 calls error | ||
let msg = WasmMsg::Execute { | ||
contract_addr: caller_addr2.into(), | ||
msg: to_binary(&WasmMsg::Execute { | ||
contract_addr: error_addr.into(), | ||
msg: to_binary(&EmptyMsg {}).unwrap(), | ||
funds: vec![], | ||
}) | ||
.unwrap(), | ||
funds: vec![], | ||
}; | ||
let err = app | ||
.execute_contract(Addr::unchecked("random"), caller_addr1, &msg, &[]) | ||
.unwrap_err(); | ||
|
||
// uncomment to have the test fail and see how the error stringifies | ||
// panic!("{:?}", err); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A dirty way to take a peek at the debug formatted error with two levels of nesting. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a great output. Added CosmWasm/cosmwasm#1199 which could be an interesting follow-up sometime. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good! It's probably not super important that the binary stuff is readable here - it's usually going to be something predictable like |
||
|
||
// we can downcast to get the original error | ||
let source: &StdError = err.downcast_ref().unwrap(); | ||
if let StdError::GenericErr { msg } = source { | ||
assert_eq!(msg, "Handle failed"); | ||
} else { | ||
panic!("wrong StdError variant"); | ||
} | ||
|
||
// we're expecting exactly 5 nested error types | ||
// (the original error, execute msg context, 3 WasmMsg contexts) | ||
assert_eq!(err.chain().count(), 5); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,13 @@ | ||
use schemars::JsonSchema; | ||
use serde::de::DeserializeOwned; | ||
use std::error::Error; | ||
use std::fmt::{self, Debug, Display}; | ||
|
||
use cosmwasm_std::{ | ||
from_slice, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Reply, Response, SubMsg, | ||
}; | ||
|
||
use anyhow::{anyhow, bail, Result as AnyResult}; | ||
use anyhow::{anyhow, bail, Context, Result as AnyResult}; | ||
|
||
/// Interface to call into a Contract | ||
pub trait Contract<T> | ||
|
@@ -65,7 +66,7 @@ pub struct ContractWrapper< | |
T6 = Empty, | ||
E6 = anyhow::Error, | ||
> where | ||
T1: DeserializeOwned, | ||
T1: DeserializeOwned + Debug, | ||
T2: DeserializeOwned, | ||
T3: DeserializeOwned, | ||
T4: DeserializeOwned, | ||
|
@@ -88,7 +89,7 @@ pub struct ContractWrapper< | |
|
||
impl<T1, T2, T3, E1, E2, E3, C> ContractWrapper<T1, T2, T3, E1, E2, E3, C> | ||
where | ||
T1: DeserializeOwned + 'static, | ||
T1: DeserializeOwned + Debug + 'static, | ||
T2: DeserializeOwned + 'static, | ||
T3: DeserializeOwned + 'static, | ||
E1: Display + Debug + Send + Sync + 'static, | ||
|
@@ -132,7 +133,7 @@ where | |
impl<T1, T2, T3, E1, E2, E3, C, T4, E4, E5, T6, E6> | ||
ContractWrapper<T1, T2, T3, E1, E2, E3, C, T4, E4, E5, T6, E6> | ||
where | ||
T1: DeserializeOwned + 'static, | ||
T1: DeserializeOwned + Debug + 'static, | ||
T2: DeserializeOwned + 'static, | ||
T3: DeserializeOwned + 'static, | ||
T4: DeserializeOwned + 'static, | ||
|
@@ -317,14 +318,14 @@ where | |
impl<T1, T2, T3, E1, E2, E3, C, T4, E4, E5, T6, E6> Contract<C> | ||
for ContractWrapper<T1, T2, T3, E1, E2, E3, C, T4, E4, E5, T6, E6> | ||
where | ||
T1: DeserializeOwned, | ||
T2: DeserializeOwned, | ||
T3: DeserializeOwned, | ||
T1: DeserializeOwned + Debug + Clone, | ||
T2: DeserializeOwned + Debug + Clone, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would remove Clone from T1, T2, T3 |
||
T3: DeserializeOwned + Debug + Clone, | ||
T4: DeserializeOwned, | ||
T6: DeserializeOwned, | ||
E1: Display + Debug + Send + Sync + 'static, | ||
E2: Display + Debug + Send + Sync + 'static, | ||
E3: Display + Debug + Send + Sync + 'static, | ||
E1: Display + Debug + Send + Sync + Error + 'static, | ||
E2: Display + Debug + Send + Sync + Error + 'static, | ||
E3: Display + Debug + Send + Sync + Error + 'static, | ||
E4: Display + Debug + Send + Sync + 'static, | ||
E5: Display + Debug + Send + Sync + 'static, | ||
E6: Display + Debug + Send + Sync + 'static, | ||
|
@@ -337,8 +338,13 @@ where | |
info: MessageInfo, | ||
msg: Vec<u8>, | ||
) -> AnyResult<Response<C>> { | ||
let msg = from_slice(&msg)?; | ||
(self.execute_fn)(deps, env, info, msg).map_err(|err| anyhow!(err)) | ||
let msg: T1 = from_slice(&msg)?; | ||
(self.execute_fn)(deps, env, info, msg.clone()) | ||
.map_err(anyhow::Error::from) | ||
.context(format!( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ueco-jb This stuff takes the If you look at the failing test, it refers to places like this in code: https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw3-flex-multisig/src/contract.rs#L1089. Basically it looks like If you're happy with the direction though, I'll investigate when I'm back. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding context to the error does seem like a very nice way of handling it. I assume this context is print out when we do Question: if this is nested - execute calls execute in submsg, 2nd contract errors - this will show context for both contracts, right? A test showing the error message in such a case would be great to evaluate it. (I know... super hard to do CI there, maybe just something marked "#[skip]" that I can modify that line and run locally to evaluate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, although I haven't tested it with more levels of nesting I think. A test would be nice for that. I'll look at that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, the nested thing didn't quite work, but I fixed it. There are tests showing it works. There's a way to take a peek what the display looks like now: https://github.com/CosmWasm/cw-plus/pull/597/files#r778142632 |
||
"Contract returned an error on execute msg:\n{:?}", | ||
msg, | ||
)) | ||
} | ||
|
||
fn instantiate( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should do the same map error here and for query, right? I assume that will be a follow-up PR, but just want to double check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yeah, I guess I shouldn't close the issue until those are implemented. |
||
|
@@ -348,13 +354,23 @@ where | |
info: MessageInfo, | ||
msg: Vec<u8>, | ||
) -> AnyResult<Response<C>> { | ||
let msg = from_slice(&msg)?; | ||
(self.instantiate_fn)(deps, env, info, msg).map_err(|err| anyhow!(err)) | ||
let msg: T2 = from_slice(&msg)?; | ||
(self.instantiate_fn)(deps, env, info, msg.clone()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a thought, we could remove the let ctx = format!("Contract returned an error on instantiate msg:\n{:?}", msg);
(self.instantiate_fn)(deps, env, info, msg).map_err(anyhow::Error::from).context(ctx) Nothing really important, just like reducing required bounds and removing clones when possible. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, yeah, that looks much better. Will do 👍 |
||
.map_err(anyhow::Error::from) | ||
.context(format!( | ||
"Contract returned an error on instantiate msg:\n{:?}", | ||
msg, | ||
)) | ||
} | ||
|
||
fn query(&self, deps: Deps, env: Env, msg: Vec<u8>) -> AnyResult<Binary> { | ||
let msg = from_slice(&msg)?; | ||
(self.query_fn)(deps, env, msg).map_err(|err| anyhow!(err)) | ||
let msg: T3 = from_slice(&msg)?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks to add these two and the simple test. Looks good to me |
||
(self.query_fn)(deps, env, msg.clone()) | ||
.map_err(anyhow::Error::from) | ||
.context(format!( | ||
"Contract returned an error on query msg:\n{:?}", | ||
msg, | ||
)) | ||
} | ||
|
||
// this returns an error if the contract doesn't implement sudo | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
use std::fmt; | ||
|
||
use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, SubMsg, WasmMsg}; | ||
use schemars::JsonSchema; | ||
|
||
use crate::{test_helpers::EmptyMsg, Contract, ContractWrapper}; | ||
|
||
fn instantiate( | ||
_deps: DepsMut, | ||
_env: Env, | ||
_info: MessageInfo, | ||
_msg: EmptyMsg, | ||
) -> Result<Response, StdError> { | ||
Ok(Response::default()) | ||
} | ||
|
||
fn execute( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an even simpler form of reflect. Great example |
||
_deps: DepsMut, | ||
_env: Env, | ||
_info: MessageInfo, | ||
msg: WasmMsg, | ||
) -> Result<Response, StdError> { | ||
let message = SubMsg::new(msg); | ||
|
||
Ok(Response::new().add_submessage(message)) | ||
} | ||
|
||
fn query(_deps: Deps, _env: Env, _msg: EmptyMsg) -> Result<Binary, StdError> { | ||
Err(StdError::generic_err( | ||
"query not implemented for the `caller` contract", | ||
)) | ||
} | ||
|
||
pub fn contract<C>() -> Box<dyn Contract<C>> | ||
where | ||
C: Clone + fmt::Debug + PartialEq + JsonSchema + 'static, | ||
{ | ||
let contract = ContractWrapper::new_with_empty(execute, instantiate, query); | ||
Box::new(contract) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good demo