Skip to content

Commit

Permalink
build: bump motsu v0.4.0 (#39)
Browse files Browse the repository at this point in the history
bump motsu to `v0.4.0` and update docs
  • Loading branch information
qalisander authored Feb 11, 2025
1 parent 75c139a commit 862770f
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 62 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed (Breaking)


## [0.4.0] - 2025-02-06
## [0.4.0] - 2025-02-11

### Added

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ alloy-sol-types = { version = "=0.8.14", default-features = false }

# members
motsu = { path = "crates/motsu" }
motsu-proc = { path = "crates/motsu-proc", version = ">=0.3.0, <=0.4.0" }
motsu-proc = { path = "crates/motsu-proc", version = "0.4.0" }

[profile.release]
codegen-units = 1
Expand Down
53 changes: 17 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Stylus Test Helpers

Testing in Stylus is limited today. While developing [OpenZeppelin Contracts for Stylus](https://github.com/OpenZeppelin/rust-contracts-stylus) we created a few helpers to test our contracts, and we've decided to open source them and publish them as a separate crate for the community, at least until a more comprehensive testing framework is available.
Testing in Stylus is limited today. While
developing [OpenZeppelin Contracts for Stylus](https://github.com/OpenZeppelin/rust-contracts-stylus) we created a few
helpers to test our contracts, and we've decided to open source them and publish them as a separate crate for the
community, at least until a more comprehensive testing framework is available.

This crate is a work in progress, and we'll be adding more features and improving the ergonomics as we go. We encourage projects that find this useful to contribute by opening issues and pull requests.
This crate is a work in progress, and we'll be adding more features and improving the ergonomics as we go. We encourage
projects that find this useful to contribute by opening issues and pull requests.

## Motsu (持つ) - Unit Testing for Stylus

Expand All @@ -15,49 +19,26 @@ Japanese -- we hold a stylus in our hand.

### Usage

You can import `motsu` from crates.io by adding the following line to your `Cargo.toml`:
Add motsu to your project's dependencies:

```toml
[dev-dependencies]
motsu = "0.3.0"
```

Then, when writing tests, use `#[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`
or `stylus_proc::storage` macros.
motsu = "0.4.0"

```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);
}
}
[dependencies]
stylus-sdk = { version = "0.7.0", default-features = false, features = [
"mini-alloc",
] }
```

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]`.
// ...
}
}
```
Important: When using motsu, the stylus-sdk must be configured without
the default `hostio-caching` feature to prevent conflicts.
E.g., you can use `stylus-sdk` with `mini-alloc` feature
without including defaults as shown above.

## Contribute

If you're interested in contributing please check our [contribution guidelines].
If you're interested in contributing, please check our [contribution guidelines].

[contribution guidelines]: ./CONTRIBUTING.md

Expand Down
2 changes: 1 addition & 1 deletion crates/motsu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ categories = ["development-tools::testing", "cryptography::cryptocurrencies"]
keywords = ["arbitrum", "ethereum", "stylus", "unit-tests", "tests"]
license.workspace = true
repository.workspace = true
version = "0.3.0"
version = "0.4.0"

[dependencies]
const-hex.workspace = true
Expand Down
122 changes: 109 additions & 13 deletions crates/motsu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,135 @@ Japanese -- we hold a stylus in our hand.

## Usage

Annotate tests with `#[motsu::test]` instead of `#[test]` to get access to VM
affordances.
This crate enables unit-testing for Stylus contracts. It abstracts away the
machinery necessary for writing tests behind a `#[motsu::test]` procedural macro.

Note that we require contracts to implement `stylus_sdk::prelude::StorageType`.
This trait is typically implemented by default with `stylus_proc::sol_storage`
or `stylus_proc::storage` macros.
Annotate tests with `#[motsu::test]` instead of `#[test]`
to get access to VM affordances:

```rust
#[cfg(test)]
mod tests {
use contracts::token::erc20::Erc20;
use openzeppelin_stylus::token::erc20::{Erc20, IErc20};
use motsu::prelude::*;

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

Annotating a test function that accepts no parameters will make `#[motsu::test]`
behave the same as `#[test]`.
Function `Contract::sender()` is necessary to trigger call
to a contract, and should accept an `Account` or `Address` as an argument.

Alternatively `Contract::sender_and_value()` can be used to
pass additional value to the contract.
To make a payable call work, user should be funded with
`Account::fund` method (there is no funding by default),
like in example below:

```rust
use motsu::prelude::*;

#[motsu::test]
fn pay_three_proxies(proxy: Contract<Proxy>, alice: Account) {
let one = uint!(1_U256);
let ten = uint!(10_U256);

// Initialize the proxy contract.
proxy.sender(alice).init(Address::ZERO);

// Fund alice.
alice.fund(ten);

// Call some contract with value.
proxy.sender_and_value(alice, one).pay_proxy();

// Assert that alice lost one wei and the proxy gained one wei.
assert_eq!(alice.balance(), ten - one);
assert_eq!(proxy.balance(), one);
}
```

Multiple external calls are supported in Motsu.
Assuming `Proxy` is a contract that exposes (`#[public]`) function `call_proxy`,
where it adds `one` to the passed argument and calls next `Proxy` contract
at the address provided during initialization.
The following test case can emulate a call chain of three `Proxy` contracts:

```rust
use motsu::prelude::*;

#[motsu::test]
fn call_three_proxies(
proxy1: Contract<Proxy>,
proxy2: Contract<Proxy>,
proxy3: Contract<Proxy>,
alice: Account,
) {
let one = uint!(1_U256);
let ten = uint!(10_U256);

// Set up a chain of three proxies.
// With the given call chain: proxy1 -> proxy2 -> proxy3.
proxy1.sender(alice).init(proxy2.address());
proxy2.sender(alice).init(proxy3.address());
proxy3.sender(alice).init(Address::ZERO);

// Call the first proxy.
let result = proxy1.sender(alice).call_proxy(ten);

// The value is incremented by 1 for each proxy.
assert_eq!(result, ten + one + one + one);
}
```

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]`.
// ...
#[motsu::test] // Equivalent to #[test]
fn test_fn() {
...
}
}
```

**Important:** To use a contract in tests, you must ensure it implements the
unsafe trait `stylus_sdk::prelude::TopLevelStorage`.
While this trait is automatically derived for contracts marked with`#[entrypoint]`,
you'll need to implement it manually for any contract without this attribute:

```rust
use stylus_sdk::{
storage::{StorageMap, StorageU256, StorageAddress},
prelude::*,
alloy_primitives::Address,
};

// Entry point is not implemented, so we should implement `TopLevelStorage` ourselves.
// #[entrypoint]
#[storage]
pub struct Erc20 {
balances: StorageMap<Address, StorageU256>,
allowances: StorageMap<Address, StorageMap<Address, StorageU256>>,
total_supply: StorageU256,
}

unsafe impl TopLevelStorage for Erc20 {}
```

**Important:** For `motsu` to work correctly, `stylus-sdk` should **not** have
a default `hostio-caching` feature enabled.

### Notice

We maintain this crate on a best-effort basis. We use it extensively on our own
Expand Down
110 changes: 101 additions & 9 deletions crates/motsu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,94 @@
//! ## Usage
//!
//! Annotate tests with [`#[motsu::test]`][test_attribute] 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` or `stylus_proc::storage` macros.
//! to get access to VM affordances:
//!
//! ```rust
//! #[cfg(test)]
//! mod tests {
//! use openzeppelin_stylus::token::erc20::Erc20;
//! use motsu::prelude::{Account, Contract};
//! use motsu::prelude::*;
//! use stylus_sdk::alloy_primitives::{Address, U256};
//!
//! #[motsu::test]
//! fn reads_balance(
//! contract: Contract<Erc20>,
//! alice: Account,
//! ) {
//! let balance = contract.sender(alice).balance_of(Address::ZERO); // Access storage.
//! ) {
//! // Access storage.
//! let balance = contract.sender(alice).balance_of(Address::ZERO);
//! assert_eq!(balance, U256::ZERO);
//! }
//! }
//! ```
//!
//! Function [`crate::prelude::Contract::sender`] is necessary to trigger call
//! to a contract, and should accept an [`crate::prelude::Account`] or an
//! [`stylus_sdk::alloy_primitives::Address`] as an argument.
//!
//! Alternatively [`crate::prelude::Contract::sender_and_value`] can be used to
//! pass additional value to the contract.
//! To make a payable call work, user should be funded with
//! [`crate::prelude::Account::fund`] method (there is no funding by default),

Check warning on line 40 in crates/motsu/src/lib.rs

View workflow job for this annotation

GitHub Actions / nightly / doc

unresolved link to `crate::prelude::Account::fund`
//! like in example below:
//!
//! ```rust
//! use motsu::prelude::*;
//! use stylus_sdk::alloy_primitives::{Address, U256, ruint::uint};
//!
//! #[motsu::test]
//! fn pay_three_proxies(proxy: Contract<Proxy>, alice: Account) {
//! let one = uint!(1_U256);
//! let ten = uint!(10_U256);
//!
//! // Initialize the proxy contract.
//! proxy.sender(alice).init(Address::ZERO);
//!
//! // Fund alice.
//! alice.fund(ten);
//!
//! // Call the contract.
//! proxy.sender_and_value(alice, one).pay_proxy();
//!
//! // Assert that alice lost one wei and the proxy gained one wei.
//! assert_eq!(alice.balance(), ten - one);
//! assert_eq!(proxy.balance(), one);
//! }
//! ```
//!
//! Multiple external calls are supported in Motsu.
//! Assuming `Proxy` is a contract that exposes `#[public]` function
//! `Proxy::call_proxy`, where it adds `one` to the passed argument and calls
//! next `Proxy` contract at the address provided during initialization.
//! The following test case can emulate a call chain of three `Proxy` contracts:
//!
//! ```rust
//! use motsu::prelude::*;
//! use stylus_sdk::alloy_primitives::{Address, U256, ruint::uint};
//!
//! #[motsu::test]
//! fn call_three_proxies(
//! proxy1: Contract<Proxy>,
//! proxy2: Contract<Proxy>,
//! proxy3: Contract<Proxy>,
//! alice: Account,
//! ) {
//! let one = uint!(1_U256);
//! let ten = uint!(10_U256);
//!
//! // Set up a chain of three proxies.
//! // With the given call chain: proxy1 -> proxy2 -> proxy3.
//! proxy1.sender(alice).init(proxy2.address());
//! proxy2.sender(alice).init(proxy3.address());
//! proxy3.sender(alice).init(Address::ZERO);
//!
//! // Call the first proxy.
//! let result = proxy1.sender(alice).call_proxy(ten);
//!
//! // The value is incremented by 1 for each proxy.
//! assert_eq!(result, ten + one + one + one);
//! }
//! ```
//!
//! Annotating a test function that accepts no parameters will make
//! [`#[motsu::test]`][test_attribute] behave the same as `#[test]`.
//!
Expand All @@ -47,6 +111,34 @@
//! }
//! ```
//!
//! **Important:** To use a contract in tests, you must ensure it implements the
//! unsafe trait [`stylus_sdk::prelude::TopLevelStorage`]. While this trait is
//! automatically derived for contracts marked with
//! [`stylus_sdk::prelude::entrypoint`], you'll need to implement it manually
//! for any contract without this attribute:
//!
//! ```rust
//! use stylus_sdk::{
//! storage::{StorageMap, StorageU256, StorageAddress},
//! prelude::*,
//! alloy_primitives::Address,
//! };
//!
//! // Entry point is not implemented, so we should implement `TopLevelStorage` ourselves.
//! // #[entrypoint]
//! #[storage]
//! pub struct Erc20 {
//! balances: StorageMap<Address, StorageU256>,
//! allowances: StorageMap<Address, StorageMap<Address, StorageU256>>,
//! total_supply: StorageU256,
//! }
//!
//! unsafe impl TopLevelStorage for Erc20 {}
//! ```
//!
//! **Important:** For `motsu` to work correctly, `stylus-sdk` should **not**
//! have a default `hostio-caching` feature enabled.
//!
//! [test_attribute]: crate::test
#[cfg(test)]
extern crate alloc;
Expand Down

0 comments on commit 862770f

Please sign in to comment.