forked from eigerco/ethereum-canister
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add e2e canister tests (eigerco#29)
* feat: Add e2e canister tests * take canister name as an argument * handle both candid types and argument encoders in call invocations * Add no arguments variant of call * Correct the result types in tests * make separate module for u256 and address in interface * make setup eth canister a standalone function * use wrap_err_with where needed * import more aggresively
- Loading branch information
Showing
8 changed files
with
370 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,5 @@ | ||
[build] | ||
target = "wasm32-unknown-unknown" | ||
|
||
[test] | ||
target = "x86_64-unknown-linux-gnu" |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
use candid::Nat; | ||
|
||
mod test_canister; | ||
|
||
use crate::test_canister::{call, setup_ethereum_canister}; | ||
|
||
#[test] | ||
fn get_block_number() { | ||
let canister = setup_ethereum_canister(); | ||
|
||
let block_num: (Nat,) = call!(canister, "get_block_number").unwrap(); | ||
assert!(block_num.0 > 17880732u128); | ||
} | ||
|
||
mod erc20 { | ||
use interface::{Erc20OwnerOfRequest, U256}; | ||
|
||
use super::*; | ||
|
||
#[test] | ||
fn balance_of() { | ||
let canister = setup_ethereum_canister(); | ||
|
||
let request = Erc20OwnerOfRequest { | ||
contract: "0xdAC17F958D2ee523a2206206994597C13D831ec7" // usdt | ||
.parse() | ||
.unwrap(), | ||
account: "0xF977814e90dA44bFA03b6295A0616a897441aceC" | ||
.parse() | ||
.unwrap(), | ||
}; | ||
let _: (U256,) = call!(canister, "erc20_balance_of", request).unwrap(); | ||
} | ||
} | ||
|
||
mod erc721 { | ||
use interface::{Address, Erc721OwnerOfRequest}; | ||
|
||
use super::*; | ||
|
||
#[test] | ||
fn owner_of() { | ||
let canister = setup_ethereum_canister(); | ||
|
||
let request = Erc721OwnerOfRequest { | ||
contract: "0x5Af0D9827E0c53E4799BB226655A1de152A425a5" // milady | ||
.parse() | ||
.unwrap(), | ||
token_id: 7773_u32.into(), | ||
}; | ||
|
||
let _: (Address,) = call!(canister, "erc721_owner_of", request).unwrap(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
use std::process::Command; | ||
use std::str; | ||
|
||
use candid::utils::{encode_args, ArgumentEncoder}; | ||
use candid::IDLArgs; | ||
use eyre::{ensure, Result, WrapErr}; | ||
use interface::SetupRequest; | ||
use temp_dir::TempDir; | ||
|
||
const DEFAULT_CONSENSUS_RPC: &str = "https://www.lightclientdata.org"; | ||
const DEFAULT_EXECUTION_RPC: &str = "https://ethereum.publicnode.com"; | ||
|
||
#[derive(Debug)] | ||
pub struct TestCanister { | ||
name: String, | ||
temp_dir: TempDir, | ||
} | ||
|
||
impl TestCanister { | ||
pub fn deploy(name: &str) -> Self { | ||
let temp_dir = TempDir::new().unwrap(); | ||
|
||
// setup the tempdir | ||
make_symlink(&temp_dir, "src"); | ||
make_symlink(&temp_dir, "target"); | ||
make_symlink(&temp_dir, "Cargo.toml"); | ||
make_symlink(&temp_dir, "Cargo.lock"); | ||
make_symlink(&temp_dir, "dfx.json"); | ||
|
||
// deploy | ||
let canister = TestCanister { | ||
temp_dir, | ||
name: name.to_owned(), | ||
}; | ||
canister.run_dfx(&["deploy", name]).unwrap(); | ||
|
||
canister | ||
} | ||
|
||
pub fn call(&self, method: &str, args: impl ArgumentEncoder) -> Result<Vec<u8>> { | ||
// convert arguments into format understood by `dfx` | ||
let args = encode_args(args).wrap_err("encoding args")?; | ||
let args = IDLArgs::from_bytes(&args).wrap_err("decoding dfx args")?; | ||
|
||
let stdout = self | ||
.run_dfx(&["canister", "call", &self.name, method, &args.to_string()]) | ||
.wrap_err_with(|| format!("calling '{method} {args}'"))?; | ||
|
||
// convert results from the format understood by `dfx` | ||
// to the candid binary representation | ||
// note: decoding here to the correct type is not possible as decoding | ||
// binds the result's lifetime to the lifetime of output | ||
// this is because decoding supports also reference types | ||
let stdout = str::from_utf8(&stdout).wrap_err("decoding output")?; | ||
let output: IDLArgs = stdout.parse().wrap_err("parsing output")?; | ||
output.to_bytes().wrap_err("encoding to candid") | ||
} | ||
|
||
fn remove(&self) { | ||
self.run_dfx(&["canister", "stop", &self.name]) | ||
.expect("Stopping failed"); | ||
self.run_dfx(&["canister", "delete", &self.name]) | ||
.expect("Deleting failed"); | ||
} | ||
|
||
fn run_dfx(&self, args: &[&str]) -> Result<Vec<u8>> { | ||
let output = Command::new("dfx") | ||
.args(args) | ||
.current_dir(self.temp_dir.path()) | ||
.output() | ||
.wrap_err_with(|| format!("executing dfx {args:?}"))?; | ||
ensure!( | ||
output.status.success(), | ||
"dfx {args:?} failed: {}", | ||
str::from_utf8(&output.stderr)? | ||
); | ||
Ok(output.stdout) | ||
} | ||
} | ||
|
||
impl Drop for TestCanister { | ||
fn drop(&mut self) { | ||
self.remove() | ||
} | ||
} | ||
|
||
pub fn setup_ethereum_canister() -> TestCanister { | ||
let canister = TestCanister::deploy("ethereum_canister"); | ||
let request = SetupRequest { | ||
consensus_rpc_url: DEFAULT_CONSENSUS_RPC.to_owned(), | ||
execution_rpc_url: DEFAULT_EXECUTION_RPC.to_owned(), | ||
}; | ||
let _: () = call!(canister, "setup", request).unwrap(); | ||
canister | ||
} | ||
|
||
/// A helper macro that allows calling canister methods with single and multiple arguments | ||
/// and decodes the results. | ||
macro_rules! call { | ||
($canister:expr, $method:expr, ($($arg:expr),*)) => {{ | ||
let result = $canister.call($method, ($($arg),*)); | ||
crate::test_canister::call!(@decode, result) | ||
}}; | ||
($canister:expr, $method:expr, $arg:expr) => {{ | ||
let result = $canister.call($method, ($arg,)); | ||
crate::test_canister::call!(@decode, result) | ||
}}; | ||
($canister:expr, $method:expr) => {{ | ||
let result = $canister.call($method, ()); | ||
crate::test_canister::call!(@decode, result) | ||
}}; | ||
(@decode, $result:expr) => { | ||
$result.and_then(|output| { | ||
::candid::utils::decode_args(&output).map_err(|err| ::eyre::eyre!(err)) | ||
}) | ||
} | ||
} | ||
|
||
fn make_symlink(temp_dir: &TempDir, name: &str) { | ||
#[cfg(not(target_family = "unix"))] | ||
{ | ||
_ = temp_dir; | ||
_ = name; | ||
panic!("unsupported test target") | ||
} | ||
#[cfg(target_family = "unix")] | ||
{ | ||
let path = format!("{}/../../{name}", env!("CARGO_MANIFEST_DIR")); | ||
std::os::unix::fs::symlink(path, temp_dir.child(name)).unwrap(); | ||
} | ||
} | ||
|
||
// this has to come after macro definitions | ||
pub(crate) use call; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
use std::str::FromStr; | ||
|
||
use candid::types::{Compound, Serializer, Type}; | ||
use candid::CandidType; | ||
use ethers_core::types::Address as EthersAddress; | ||
use serde::Deserialize; | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] | ||
pub struct Address(EthersAddress); | ||
|
||
impl CandidType for Address { | ||
fn _ty() -> Type { | ||
<String as CandidType>::ty() | ||
} | ||
|
||
fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error> | ||
where | ||
S: Serializer, | ||
{ | ||
let s = format!("{:?}", &self.0); | ||
let mut ser = serializer.serialize_struct()?; | ||
Compound::serialize_element(&mut ser, &s)?; | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl<'de> Deserialize<'de> for Address { | ||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||
where | ||
D: serde::Deserializer<'de>, | ||
{ | ||
let s = String::deserialize(deserializer)?; | ||
let addr = s | ||
.parse::<EthersAddress>() | ||
.map_err(serde::de::Error::custom)?; | ||
Ok(Address(addr)) | ||
} | ||
} | ||
|
||
impl From<EthersAddress> for Address { | ||
fn from(value: EthersAddress) -> Self { | ||
Address(value) | ||
} | ||
} | ||
|
||
impl From<Address> for EthersAddress { | ||
fn from(value: Address) -> Self { | ||
value.0 | ||
} | ||
} | ||
|
||
impl FromStr for Address { | ||
type Err = <EthersAddress as FromStr>::Err; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
let addr: EthersAddress = s.parse()?; | ||
Ok(Self(addr)) | ||
} | ||
} |
Oops, something went wrong.