Skip to content

Commit

Permalink
Quick start guide (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmikolajczyk41 authored Oct 31, 2023
1 parent 7463076 commit eae811d
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 3 deletions.
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(())
}
}

0 comments on commit eae811d

Please sign in to comment.