23 releases (stable)
1.6.2 | Oct 7, 2024 |
---|---|
1.5.0 | Aug 12, 2024 |
1.4.2 | Jul 2, 2024 |
1.1.1 | Feb 12, 2024 |
0.0.1 |
|
#31 in #gear
1,765 downloads per month
Used in 3 crates
1.5MB
27K
SLoC
Testing with gtest
gtest
simulates a real network by providing mockups of the user, program,
balances, mailbox, etc. Since it does not include parts of the actual
blockchain, it is fast and lightweight. But being a model of the blockchain
network, gtest
cannot be a complete reflection of the latter.
As we said earlier, gtest
is excellent for unit and integration testing.
It is also helpful for debugging Gear program logic. Nothing other than the
Rust compiler is required for running tests based on gtest
. It is
predictable and robust when used in continuous integration.
Main concepts
gtest
is a library that provides a set of tools for testing Gear programs.
The most important structures are:
System
— a structure that represents the environment of the Gear network. It contains the current block number, timestamp, and other parameters. It also stores the mailbox and the list of programs.Program
— a structure that represents a Gear program. It contains the information about program and allows sending messages to other programs.- [
Log
] — a structure that represents a message log. It allows checking the result of the program execution.
Let's take a closer look at how to write tests using gtest
.
Import gtest
lib
To use the gtest
library, you must import it into your Cargo.toml
file
in the [dev-dependencies]
block to fetch and compile it for tests only:
[package]
name = "my-gear-app"
version = "0.1.0"
authors = ["Your Name"]
edition = "2021"
[dependencies]
gstd = { git = "https://github.com/gear-tech/gear.git", tag = "v1.0.1" }
[build-dependencies]
gear-wasm-builder = { git = "https://github.com/gear-tech/gear.git", tag = "v1.0.1" }
[dev-dependencies]
gtest = { git = "https://github.com/gear-tech/gear.git", tag = "v1.0.1" }
Program example
Let's write a simple program that will receive a message and reply to it.
lib.rs
:
#![no_std]
use gstd::msg;
#[no_mangle]
extern "C" fn handle() {
let payload = msg::load_bytes().expect("Failed to load payload");
if payload == b"PING" {
msg::reply_bytes(b"PONG", 0).expect("Failed to send reply");
}
}
build.rs
:
fn main() {
gear_wasm_builder::build();
}
We will add a test that will check the program's behavior. To do this, we
will use the gtest
library.
Our test will consist of the following steps:
- Initialize the
System
structure. - Initialize the
Program
structure. - Send an init message to the program. Even though we don't have the
init
function in our program, the first message to the program sent viagtest
is always the init one. - Send a handle message to the program.
- Check the result of the program execution.
Add these lines to the bottom of the lib.rs
file:
#[cfg(test)]
mod tests {
use gtest::{Log, Program, System};
// Alternatively, you can use the default users from `gtest::constants`:
// `DEFAULT_USER_ALICE`, `DEFAULT_USER_BOB`, `DEFAULT_USER_CHARLIE`, `DEFAULT_USER_EVE`.
// The full list of default users can be obtained with `gtest::constants::default_users_list`.
const USER_ID: u64 = 100001;
#[test]
fn test_ping_pong() {
// Initialization of the common environment for running programs.
let sys = System::new();
// Initialization of the current program structure.
let prog = Program::current(&sys);
// Provide user with some balance.
sys.mint_to(USER_ID, EXISTENTIAL_DEPOSIT * 1000);
// Send an init message to the program.
let init_message_id = prog.send_bytes(USER_ID, b"Doesn't matter");
// Run execution of the block which will contain `init_message_id`
let block_run_result = sys.run_next_block();
// Check whether the program was initialized successfully.
assert!(block_run_result.succeed.contains(&init_message_id));
// Send a handle message to the program.
let handle_message_id = prog.send_bytes(USER_ID, b"PING");
let block_run_result = sys.run_next_block();
// Check the result of the program execution.
// 1. Create a log pattern with the expected result.
let log = Log::builder()
.source(prog.id())
.dest(USER_ID)
.payload_bytes(b"PONG");
// 2. Check whether the program was executed successfully.
assert!(block_run_result.succeed.contains(&handle_message_id));
// 3. Make sure the log entry is in the result.
assert!(block_run_result.contains(&log));
}
}
To run the test, use the following command:
cargo test
gtest
capabilities
Let's take a closer look at the gtest
capabilities.
Initialization of the network environment for running programs
let sys = System::new();
This emulates node's and chain's behavior. By default, the System::new
function sets the following parameters:
- current block equals
0
- current timestamp equals UNIX timestamp of your system
- starting message id equals
0x010000..
- starting program id equals
0x010000..
Program initialization
There are a few ways to initialize a program:
-
Initialize the current program using the
Program::current
function:let prog = Program::current(&sys);
-
Initialize a program from a Wasm-file with a default id using the
Program::from_file
function:let prog = Program::from_file( &sys, "./target/wasm32-unknown-unknown/release/demo_ping.wasm", );
-
Initialize a program via builder:
let prog = ProgramBuilder::from_file("your_gear_program.wasm") .with_id(105) .build(&sys);
Every place in this lib, where you need to specify some ids, it requires generic type
ID
, which implementsInto<ProgramIdWrapper>
.ProgramIdWrapper
may be built from:u64
[u8; 32]
String
&str
ProgramId
(fromgear_core
one's, not fromgstd
).
String
implementation means the input as hex (with or without "0x").
Getting the program from the system
If you initialize program not in this scope, in cycle, in other conditions, where you didn't save the structure, you may get the object from the system by id.
let prog = sys.get_program(105).unwrap();
Initialization of styled env_logger
Initialization of styled env_logger
to print logs (only from gwasm
by
default) into stdout:
sys.init_logger();
To specify printed logs, set the env variable RUST_LOG
:
RUST_LOG="target_1=logging_level,target_2=logging_level" cargo test
Pre-requisites for sending a message
Prior to sending a message, it is necessary to mint sufficient balance for the sender to ensure coverage of the existential deposit and gas costs.
let user_id = 42;
sys.mint_to(user_id, EXISTENTIAL_DEPOSIT * 1000);
Alternatively, you can use the default users from gtest::constants
, which
have a preallocated balance, as the message sender.
assert_eq!(
sys.balance_of(gtest::constants::DEFAULT_USER_ALICE),
DEFAULT_USERS_INITIAL_BALANCE
);
Sending messages
To send message to the program need to call one of two program's functions:
Program::send
(orProgram::send_with_value
if you need to send a message with attached funds).Program::send_bytes
(orProgram::send_bytes_with_value
if you need to send a message with attached funds).
Both of the methods require sender id as the first argument and the payload as second.
The difference between them is pretty simple and similar to gstd
functions msg::send
and msg::send_bytes
.
The first one requires payload to be CODEC Encodable, while the second
requires payload implement AsRef<[u8]>
, that means to be able to represent
as bytes.
Program::send
uses Program::send_bytes
under the hood with bytes
from payload.encode()
.
First message to the initialized program structure is always the init message.
let res = prog.send_bytes(100001, "INIT MESSAGE");
Processing the result of the program execution
Any sending functions in the lib returns an id of the sent message.
In order to actually get the result of the program execution the block
execution should be triggered (see "Block execution model" section).
Block execution function returns the result of the block run
(BlockRunResult
)
It contains the final result of the processing message and others, which were created during the execution.
It has 2 main functions:
BlockRunResult::log
— returns the reference to the Vec produced to users messages. You may assert them as you wish, iterating through them.BlockRunResult::contains
— returns bool which shows that logs contain a given log. Syntax sugar aroundres.log().iter().any(|v| v == arg)
.
Fields of the type are public, and some of them can be really useful:
- field
succeed
is a set of ids of messages that were successfully executed. - field
failed
is a set of ids of messages that failed during the execution.
To build a log for assertion you need to use [Log
] structure with its
builders. All fields here are optional. Assertion with Log
s from core are
made on the Some(..)
fields. You will run into panic if you try to set the
already specified field.
// Constructor for success log.
let log = Log::builder();
// Constructor for error reply log.
let log = Log::error_builder(ErrorReplyReason::InactiveActor);
// Other fields are set optionally by `dest()`, `source()`, `payload()`, `payload_bytes()`.
let log = Log::builder()
.source(prog.id())
.dest(100001)
.payload_bytes("PONG");
Log also has From
implementations from (ID, T)
and from (ID_1, ID_2, T)
, where ID: Into<ProgramIdWrapper>
, T: AsRef<[u8]>
.
let x = Log::builder().dest(5).payload_bytes("A");
let x_from: Log = (5, "A").into();
assert_eq!(x, x_from);
let y = Log::builder().dest(5).source(15).payload_bytes("A");
let y_from: Log = (15, 5, "A").into();
assert_eq!(y, y_from);
Blocks execution model
Block execution has 2 main step:
- tasks processing
- messages processing
Tasks processing is a step, when all scheduled for the current block number tasks are tried to be processed. This includes processing delayed dispatches, waking waited messages and etc.
Messages processing is a step, when messages from the queue are processed until either the queue is empty or the block gas allowance is not enough for the execution.
Blocks can't be "spent" without their execution except for use the
System::run_scheduled_tasks
method, which doesn't process the message
queue, but only processes scheduled tasks triggering blocks info
adjustments, which can be used to "spend" blocks.
Note, that for now 1 block in Gear-based network is 3 sec duration.
// Spend 150 blocks by running only the task pool (7.5 mins for 3 sec block).
sys.run_scheduled_tasks(150);
Note that processing messages (e.g. by using
Program::send
/Program::send_bytes
methods) doesn't spend blocks, nor
changes the timestamp. If you write time dependent logic, you should spend
blocks manually.
Balance
There are certain invariants in gtest
regarding user and program balances:
- For a user to successfully send a message to the program, they must have sufficient balance to cover the existential deposit and gas costs.
- The program charges the existential deposit from the user upon receiving the initial message.
As previously mentioned here, a balance for the user must be minted before sending a message. This balance should be sufficient to cover the following: the user's existential deposit, the existential deposit of the initialized program (the first message to the program charges the program's existential deposit from the sender), and the message's gas costs.
The System::mint_to
method can be utilized to allocate a balance to the
user or the program. The System::balance_of
method may be used to verify
the current balance.
// If you need to send a message with value you have to mint balance for the message sender:
let user_id = 42;
sys.mint_to(user_id, 5000);
assert_eq!(sys.balance_of(user_id), 5000);
// To give the balance to the program you should use [`System::transfer`] method:
let mut prog = Program::current(&sys);
sys.transfer(user_id, prog.id(), 1000, true);
assert_eq!(prog.balance(), 1000);
Dependencies
~70MB
~1M SLoC