Skip to content
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

feat: add event checks to motsu #13

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

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

56 changes: 19 additions & 37 deletions crates/motsu-proc/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, FnArg};

/// Defines a unit test that provides access to Stylus execution context.
/// Defines a unit test that provides access to Stylus' execution context.
///
/// For more information see [`crate::test`].
pub(crate) fn test(_attr: &TokenStream, input: TokenStream) -> TokenStream {
Expand All @@ -21,49 +21,31 @@ pub(crate) fn test(_attr: &TokenStream, input: TokenStream) -> TokenStream {
}

// Whether 1 or none contracts will be declared.
let arg_binding_and_ty = match fn_args
.into_iter()
.map(|arg| {
let FnArg::Typed(arg) = arg else {
error!(@arg, "unexpected receiver argument in test signature");
};
let contract_arg_binding = &arg.pat;
let contract_ty = &arg.ty;
Ok((contract_arg_binding, contract_ty))
})
.collect::<Result<Vec<_>, _>>()
{
Ok(res) => res,
Err(err) => return err.to_compile_error().into(),
};
let contract_declarations = fn_args.into_iter().map(|arg| {
let FnArg::Typed(arg) = arg else {
error!(arg, "unexpected receiver argument in test signature");
};
let contract_arg_binding = &arg.pat;
let contract_ty = &arg.ty;

let contract_arg_defs =
arg_binding_and_ty.iter().map(|(arg_binding, contract_ty)| {
// Test case assumes, that contract's variable has `&mut` reference
// to contract's type.
quote! {
#arg_binding: &mut #contract_ty
}
});

let contract_args =
arg_binding_and_ty.iter().map(|(_arg_binding, contract_ty)| {
// Pass mutable reference to the contract.
quote! {
&mut <#contract_ty>::default()
}
});
// Test case assumes, that contract's variable has `&mut` reference
// to contract's type.
quote! {
let mut #contract_arg_binding = <#contract_ty>::default();
let #contract_arg_binding = &mut #contract_arg_binding;
}
});

// Declare test case closure.
// Pass mut ref to the test closure and call it.
// Reset storage for the test context and return test's output.
// Output full testcase function.
// Declare contract.
// And in the end, reset storage for test context.
quote! {
#( #attrs )*
#[test]
fn #fn_name() #fn_return_type {
use ::motsu::prelude::DefaultStorage;
let test = | #( #contract_arg_defs ),* | #fn_block;
let res = test( #( #contract_args ),* );
#( #contract_declarations )*
let res = #fn_block;
::motsu::prelude::Context::current().reset_storage();
res
}
Expand Down
1 change: 1 addition & 0 deletions crates/motsu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repository.workspace = true
version = "0.2.1"

[dependencies]
alloy-sol-types.workspace = true
const-hex.workspace = true
once_cell.workspace = true
tiny-keccak.workspace = true
Expand Down
62 changes: 62 additions & 0 deletions crates/motsu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Motsu (持つ) - Unit Testing for Stylus

This crate enables unit-testing for Stylus contracts. It abstracts away the
machinery necessary for writing tests behind a `#[motsu::test]` procedural
macro.

`motsu` means ["to hold"](https://jisho.org/word/%E6%8C%81%E3%81%A4) in
Japanese -- we hold a stylus in our hand.

## Usage

Annotate tests with `#[motsu::test]` instead of `#[test]` to get access to VM
affordances.

Note that we require contracts to implement `stylus_sdk::prelude::StorageType`.
This trait is typically implemented by default with `stylus_proc::sol_storage` macro.

```rust
#[cfg(test)]
mod tests {
use contracts::token::erc20::Erc20;

#[motsu::test]
fn reads_balance(contract: Erc20) {
let balance = contract.balance_of(Address::ZERO); // Access storage.
assert_eq!(balance, U256::ZERO);
}
}
```

Annotating a test function that accepts no parameters will make `#[motsu::test]`
behave the same as `#[test]`.

```rust,ignore
#[cfg(test)]
mod tests {
#[motsu::test]
fn t() { // If no params, it expands to a `#[test]`.
// ...
}
}
```

Note that currently, test suites using `motsu::test` will run serially because
of global access to storage.

### Notice

We maintain this crate on a best-effort basis. We use it extensively on our own
tests, so we will add here any symbols we may need. However, since we expect
this to be a temporary solution, don't expect us to address all requests.

That being said, please do open an issue to start a discussion, keeping in mind
our [code of conduct] and [contribution guidelines].

[code of conduct]: ../../CODE_OF_CONDUCT.md

[contribution guidelines]: ../../CONTRIBUTING.md

## Security

Refer to our [Security Policy](../../SECURITY.md) for more details.
109 changes: 0 additions & 109 deletions crates/motsu/src/context.rs

This file was deleted.

83 changes: 83 additions & 0 deletions crates/motsu/src/context/environment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! Module with unit test EVM environment for Stylus contracts.

/// Block Timestamp - Epoch timestamp: 1st January 2025 `00::00::00`.
const BLOCK_TIMESTAMP: u64 = 1_735_689_600;
/// Arbitrum's CHAID ID.
const CHAIN_ID: u64 = 42161;

/// Dummy contract address set for tests.
const CONTRACT_ADDRESS: &[u8; 42] =
b"0xdCE82b5f92C98F27F116F70491a487EFFDb6a2a9";

/// Externally Owned Account (EOA) code hash.
const EOA_CODEHASH: &[u8; 66] =
b"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470";

/// Dummy msg sender set for tests.
const MSG_SENDER: &[u8; 42] = b"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF";

pub(crate) struct Environment {
account_codehash: [u8; 66],
block_timestamp: u64,
chain_id: u64,
contract_address: [u8; 42],
events: Vec<Vec<u8>>,
msg_sender: [u8; 42],
}

impl Default for Environment {
/// Creates default environment for a test case.
fn default() -> Environment {
Self {
account_codehash: *EOA_CODEHASH,
block_timestamp: BLOCK_TIMESTAMP,
chain_id: CHAIN_ID,
contract_address: *CONTRACT_ADDRESS,
events: Vec::new(),
msg_sender: *MSG_SENDER,
}
}
}

impl Environment {
/// Gets the code hash of the account at the given address.
pub(crate) fn account_codehash(&self) -> [u8; 66] {
self.account_codehash
}

/// Gets a bounded estimate of the Unix timestamp at which the Sequencer
/// sequenced the transaction.
pub(crate) fn block_timestamp(&self) -> u64 {
self.block_timestamp
}

/// Gets the chain ID of the current chain.
pub(crate) fn chain_id(&self) -> u64 {
self.chain_id
}

/// Gets the address of the current program.
pub(crate) fn contract_address(&self) -> [u8; 42] {
self.contract_address
}

/// Gets the address of the account that called the program.
pub(crate) fn msg_sender(&self) -> [u8; 42] {
self.msg_sender
}

/// Stores emitted event.
pub(crate) fn store_event(&mut self, event: &[u8]) {
self.events.push(Vec::from(event));
}

/// Removes all the stored events.
pub(crate) fn clear_events(&mut self) {
self.events.clear();
}

/// Gets all emitted events.
pub(crate) fn events(&self) -> Vec<Vec<u8>> {
self.events.clone()
}
}
Loading
Loading