gear-tech / gear

Web3 Ultimate Execution Engine
https://gear-tech.io
GNU General Public License v3.0
231 stars 104 forks source link

Consider a new approach to test programs #2127

Closed shamilsan closed 1 year ago

shamilsan commented 1 year ago

A new approach to test programs

This approach is mostly inspired by the Lunatic project.

Main concerns with gtest and gclient

gtest

gclient

New approach details

We don't need to build native binaries for tests but use Wasms with the external runner instead.

Step 1. Configure the program to build to wasm32-unknown-unknown target

Create config.toml in .cargo subdirectory:

[build]
target = "wasm32-unknown-unknown"

[target.wasm32-unknown-unknown]
runner = "gear run"

Step 2. Configure the program to be a dynamic library

[lib]
crate-type = ["rlib", "cdylib"]

Step 3. Implement custom #[test] attribute-like macro that exports test functions

We implement our custom #[test] attribute-like macro that is to be used instead of the default #[test] attribute.

use gtest2::test;

#[test]
fn my_test() {
    assert!(true);
}

See this macro implementation example in lunatic-test crate.

Step 4. Implement gear run subcommand that finds test functions and runs them

We need to add a subcommand in gear binary that opens Wasm binary, finds test functions inside it and runs them. cargo test will execute the runner with the test Wasm as an argument.

The good news is that the test binary also contains smart contract exports (init, handle and so on). Therefore, we can use it both as a contract binary and as a test binary. This is true for unit tests but not true (by default) for integration tests. To include contract exports to the integration tests binary we are to include the contract's rlib to the tests module:

use my_program as _;

#[test]
fn my_intgration_test() {
   assert!(true);
}

Also, there is a good implementation of such kind of runner in lunatic runtime.

Step 5. Implement a super lightweight library that use minimal number of imports

The good news is we can reuse many functions from gstd for testing purposes. For example, we can use msg::send etc. for sending messages, exec::block_height to get the current block number, prog::create_program to create a new program.

We don't need to rewrite tons of methods that are already declared.

The only thing we are to add in our new gtest2 crate is the minimal count of extrinsics that are not present in the "standard" library (gstd and mates).

Let's imagine what the test can look like.

use gstd::{exec, msg, prog};
use gtest2::test;
use my_program as _;

#[test]
fn my_new_test() {
    let code_id = gtest2::upload_self_code();
    let (_, prog_id) = prog::create_program(code_id, b"salt", "INIT", 0).expect("Failed to create program");
    let msg_id = msg::send(prog_id, "PING", 0).expect("Failed to send");
    let reply = gtest2::get_from_mailbox(msg_id).expect("No message in mailbox");
    assert_eq!(reply.dest, msg::source());
    assert_eq!(reply.payload, "PONG");
}

Pros

Cons

NikVolf commented 1 year ago

Please avoid this vague issues that don't have straight implementation path and touch multiple components