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

Quick start guide #77

Merged
merged 2 commits into from
Oct 31, 2023
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@ Check our helpful and verbose examples in the [examples](examples) directory.

`drink` library is continuously published to [crates.io](https://crates.io/crates/drink), so you can use it in your project with either `cargo add drink` or by adding the following line to your `Cargo.toml`:
```toml
drink = { version = "0.5" }
drink = { version = "0.6" }
```

Full library documentation is available at: https://docs.rs/drink.

**Quick start guide** is available [here](examples/quick-start-with-drink/README.md).

## As an alternative backend to ink!'s E2E testing framework

DRink! is already integrated with ink! and can be used as a drop-in replacement for the standard E2E testing environment.
Expand Down
1 change: 1 addition & 0 deletions drink/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{DrinkResult, Error};
/// - `deploy_bundle_and`
/// - `upload_bundle`
/// - `upload_bundle_and`
#[derive(Clone)]
pub struct ContractBundle {
/// WASM blob of the contract
pub wasm: Vec<u8>,
Expand Down
9 changes: 7 additions & 2 deletions drink/src/contract_api.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Contracts API.

use std::ops::Not;

use frame_support::weights::Weight;
use frame_system::Config;
use pallet_contracts::{CollectEvents, DebugInfo, Determinism};
Expand Down Expand Up @@ -191,10 +193,13 @@ impl<R: Runtime> ContractApi<R> for Sandbox<R> {
}
}

/// Converts bytes to a '\n'-split string.
/// Converts bytes to a '\n'-split string, ignoring empty lines.
pub fn decode_debug_buffer(buffer: &[u8]) -> Vec<String> {
let decoded = buffer.iter().map(|b| *b as char).collect::<String>();
decoded.split('\n').map(|s| s.to_string()).collect()
decoded
.split('\n')
.filter_map(|s| s.is_empty().not().then_some(s.to_string()))
.collect()
}

#[cfg(test)]
Expand Down
34 changes: 34 additions & 0 deletions examples/quick-start-with-drink/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "quick-start-with-drink"
authors = ["Cardinal"]
edition = "2021"
homepage = "https://alephzero.org"
repository = "https://github.com/Cardinal-Cryptography/drink"
version = "0.1.0"

[lib]
path = "lib.rs"

[dependencies]
# We use standard dependencies for an ink! smart-contract.

# For debugging from contract, we enable the `ink-debug` feature of `ink` crate.
ink = { version = "=4.2.1", default-features = false, features = ["ink-debug"] }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true }

[dev-dependencies]
# For testing purposes we bring the `drink` library.
drink = { path = "../../drink" }

[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
]
# If the current crate defines a smart contract that we want to test, we can't forget to have `ink-as-dependency`
# feature declared. This is how `#[drink::test]` and `#[drink::contract_bundle_provider]` discovers contracts to be
# built.
ink-as-dependency = []
85 changes: 85 additions & 0 deletions examples/quick-start-with-drink/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Quick start with `drink` library

This is a quick start guide introducing you to smart contract testing with `drink` library.
We will see how to write tests for a simple smart contract and make use of `drink`'s features.

## Prerequisites

You only need Rust installed (see [here](https://www.rust-lang.org/tools/install) for help).
Drink is developed and tested with stable Rust 1.70 (see [toolchain file](../../rust-toolchain.toml)).

## Dependencies

You only need the `drink` library brought into your project:
```toml
drink = { version = "0.6" }
```

See [Cargo.toml](Cargo.toml) for a typical cargo setup of a single-contract project.

## Writing tests

### Preparing contracts

For every contract that you want to interact with from your tests, you need to create a _contract bundle_, which includes:
- built contract artifact (`.wasm` file),
- contract transcoder (object based on the `.json` file, responsible for translating message arguments and results).

The recommended way is to use `drink::contract_bundle_provider` macro, which will discover all the contract dependencies (including the current crate, if that is the case) and gather all contract bundles into a single registry.

However, if needed, you can do it manually, by running `cargo contract build` for every such contract, and then, bring the artifacts into your tests.
For this, you might want to use `drink::ContractBundle` API, which includes `ContractBundle::load` and `local_contract_file!` utilities.

### `drink` test macros

`drink` provides a few macros to write tests for smart contracts:
- `#[drink::test]` - which marks a function as a test function (similar to `#[test]`).
- `#[drink::contract_bundle_provider]` - which gathers all contract artifacts into a single registry.

While neither is required to write `drink` tests, they make it easier to write and maintain them.

### Writing tests

Your typical test module will look like:
```rust
#[cfg(test)]
mod tests {
#[drink::contract_bundle_provider]
enum BundleProvider {}

#[drink::test]
fn deploy_and_call_a_contract() -> Result<(), Box<dyn Error>> {
let result: bool = Session::<MinimalRuntime>::new()?
.deploy_bundle_and(BundleProvider::local(), "new", &["true"], vec![], None)?
.call_and("flip", NO_ARGS, None)?
.call_and("flip", NO_ARGS, None)?
.call_and("flip", NO_ARGS, None)?
.call("get", NO_ARGS, None)??;
assert_eq!(result, false);
}
}
```

So, firstly, you declare a bundle provider like:
```rust
#[drink::contract_bundle_provider]
enum BundleProvider {}
```

It will take care of building all contract dependencies in the compilation phase and gather all contract bundles into a single registry.
Then, you will be able to get a contract bundle by calling:
```rust
let bundle = BundleProvider::local()?; // for the contract from the current crate
let bundle = BundleProvider::Flipper.bundle()?; // for the contract from the `flipper` crate
```

We mark each testcase with `#[drink::test]` attribute and declare return type as `Result` so that we can use the `?` operator:
```rust
#[drink::test]
fn testcase() -> Result<(), Box<dyn Error>> {
// ...
}
```

Then, we can use the `Session` API to interact with both contracts and the whole runtime.
For details, check out testcases in [lib.rs](lib.rs).
175 changes: 175 additions & 0 deletions examples/quick-start-with-drink/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#![cfg_attr(not(feature = "std"), no_std, no_main)]

/// This is the classical flipper contract. It stores a single `bool` value in its storage. The
/// contract exposes:
/// - a constructor (`new`) that initializes the `bool` value to the given value,
/// - a message `flip` that flips the stored `bool` value from `true` to `false` or vice versa,
/// - a getter message `get` that returns the current `bool` value.
///
/// Additionally, we use the `debug_println` macro from the `ink_env` crate to produce some debug
/// logs from the contract.
#[ink::contract]
mod flipper {
use ink::env::debug_println;

#[ink(storage)]
pub struct Flipper {
value: bool,
}

impl Flipper {
#[ink(constructor)]
pub fn new(init: bool) -> Self {
debug_println!("Initializing contract with: `{init}`");
Self { value: init }
}

#[ink(message)]
pub fn flip(&mut self) {
debug_println!("Previous value: `{}`", self.value);
self.value = !self.value;
debug_println!("Flipped to: `{}`", self.value);
}

#[ink(message)]
pub fn get(&self) -> bool {
debug_println!("Reading value from storage");
self.value
}
}
}

/// We put `drink`-based tests as usual unit tests, into a test module.
#[cfg(test)]
mod tests {
use drink::{
contract_api::decode_debug_buffer,
runtime::MinimalRuntime,
session::{Session, NO_ARGS},
};

/// `drink` automatically discovers all the contract projects that your tests will need. For
/// every such dependency (including the contract from the current crate), it will generate a
/// [`ContractBundle`](drink::ContractBundle) object that contains the compiled contract's code
/// and a special transcoder, which is used to encode and decode the contract's message
/// arguments. Such a bundle will be useful when deploying a contract.
///
/// To get a convenient way for obtaining such bundles, we can define an empty enum and mark
/// it with the [`drink::contract_bundle_provider`](drink::contract_bundle_provider) attribute.
/// From now on, we can use it in all testcases in this module.
#[drink::contract_bundle_provider]
enum BundleProvider {}

/// Now we write the simplest contract test, that will:
/// 1. Deploy the contract.
/// 2. Call its `flip` method.
/// 3. Call its `get` method and ensure that the stored value has been flipped.
///
/// We can use the [`drink::test`](drink::test) attribute to mark a function as a `drink` test.
/// This way we ensure that all the required contracts are compiled and built, so that we don't
/// have to run `cargo contract build` manually for every contract dependency.
///
/// For convenience of using `?` operator, we mark the test function as returning a `Result`.
#[drink::test]
fn deploy_and_call_a_contract() -> Result<(), Box<dyn std::error::Error>> {
// Firstly, we create a `Session` object. It is a wrapper around a runtime and it exposes a
// broad API for interacting with it.
//
// It is generic over the runtime type, but usually, it is sufficient to use
// `MinimalRuntime`, which is a minimalistic runtime that allows using smart contracts.
let mut session = Session::<MinimalRuntime>::new()?;

// Now we get the contract bundle from the `BundleProvider` enum. Since the current crate
// comes with a contract, we can use the `local` method to get the bundle for it.
let contract_bundle = BundleProvider::local()?;

// We can now deploy the contract.
let _contract_address = session.deploy_bundle(
// The bundle that we want to deploy.
contract_bundle,
// The constructor that we want to call.
"new",
// The constructor arguments (as stringish objects).
&["true"],
// Salt for the contract address derivation.
vec![],
// Initial endowment (the amount of tokens that we want to transfer to the contract).
None,
)?;

// Once the contract is instantiated, we can call the `flip` method on the contract.
session.call(
// The message that we want to call.
"flip",
// The message arguments (as stringish objects). If none, then we can use the `NO_ARGS`
// constant, which spares us from typing `&[]`.
NO_ARGS,
// Endowment (the amount of tokens that we want to transfer to the contract).
None,
)??;

// Finally, we can call the `get` method on the contract and ensure that the value has been
// flipped.
//
// `Session::call` returns a `Result<MessageResult<T>, SessionError>`, where `T` is the
// type of the message result. In this case, the `get` message returns a `bool`, and we have
// to explicitly hint the compiler about it.
let result: bool = session.call("get", NO_ARGS, None)??;
assert_eq!(result, false);

Ok(())
}

/// In this testcase we will see how to get and read debug logs from the contract.
#[drink::test]
fn get_debug_logs() -> Result<(), Box<dyn std::error::Error>> {
// We create a session object as usual and deploy the contract bundle.
let mut session = Session::<MinimalRuntime>::new()?;
session.deploy_bundle(BundleProvider::local()?, "new", &["true"], vec![], None)?;

// `deploy_bundle` returns just a contract address. If we are interested in more details
// about last operation (either deploy or call), we can use the `last_deploy_result`
// (or analogously `last_call_result`) method, which will provide us with a full report
// from the last contract interaction.
//
// In particular, we can get the decoded debug buffer from the contract. The buffer is
// just a vector of bytes, which we can decode using the `decode_debug_buffer` function.
let decoded_buffer = &session
.last_deploy_result()
.expect("The deployment succeeded, so there should be a result available")
.debug_message;
let encoded_buffer = decode_debug_buffer(decoded_buffer);

assert_eq!(encoded_buffer, vec!["Initializing contract with: `true`"]);

Ok(())
}

/// In this testcase we will see how to work with multiple contracts.
#[drink::test]
fn work_with_multiple_contracts() -> Result<(), Box<dyn std::error::Error>> {
let mut session = Session::<MinimalRuntime>::new()?;
let bundle = BundleProvider::local()?;

// We can deploy the same contract multiple times. However, we have to ensure that the
// derived contract addresses are different. We can do this by providing using different
// arguments for the constructor or by providing a different salt.
let first_address =
session.deploy_bundle(bundle.clone(), "new", &["true"], vec![], None)?;
let _second_address =
session.deploy_bundle(bundle.clone(), "new", &["true"], vec![0], None)?;
let _third_address = session.deploy_bundle(bundle, "new", &["false"], vec![], None)?;

// By default, when we run `session.call`, `drink` will interact with the last deployed
// contract.
let value_at_third_contract: bool = session.call("get", NO_ARGS, None)??;
assert_eq!(value_at_third_contract, false);

// However, we can also call a specific contract by providing its address.
let value_at_first_contract: bool =
session.call_with_address(first_address, "get", NO_ARGS, None)??;
assert_eq!(value_at_first_contract, true);

Ok(())
}
}
Loading