Skip to content

Commit

Permalink
feat: add tests for all CLI commands shown in docs
Browse files Browse the repository at this point in the history
This makes use of the newly-published
[soroban-test](https://docs.rs/soroban-test/latest/soroban_test/) crate
and the newly-reorganized [soroban-cli](https://docs.rs/soroban-cli)
library to test all commands shown in the docs.

This gives us confidence that all of these commands work, though it does
come with some downsides:

- these tests live far from the thing they are meant to test
  - could we generate the docs from this repository, rather than keeping
    them in sync with each other?
  - maybe, but it's a little unclear how that would work
  - this is a good starting point

Another shortcoming of the approach here: it still makes use of
`assert_cmd` in some tests, which runs the version of `soroban-cli`
installed on the host system, making it quite brittle. This is necessary
because some commands (`Cmd`s) from `soroban-cli` do not currently
operate in a test-friendly way. This can be seen in a secondary way,
too: not all `Cmd`s can be run the same way. Some use `run`, some use
`run_in_sandbox`, some take arguments, some do not. These concerns can
all be addressed together by refactoring `soroban-cli` to make all
`Cmd`s implement an asynchronous `run` method with the same signature,
and which will return a struct containing both `stdout` and `stderr`.

However, even with these shortcomings and downsides, it is still worth
merging these tests as-is, since it will give us higher confidence that
everything works as expected with the current code. We can refactor
these tests later, and potentially move them to a different repo.

Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com>
  • Loading branch information
willemneal and chadoh committed Apr 6, 2023
1 parent 9613590 commit 2de3f37
Show file tree
Hide file tree
Showing 13 changed files with 2,385 additions and 236 deletions.
2,240 changes: 2,006 additions & 234 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[workspace]
resolver = "2"

members = [
members = [
"hello_world",
"increment",
"auth",
Expand All @@ -15,14 +15,18 @@ members = [
"events",
"token",
"logging",
"errors",
"errors",
"timelock",
"atomic_swap",
"atomic_multiswap",
"account",
"alloc",
"test/doc-tests", # include in members for Rust Analyzer
]

# exclude from `cargo build`
exclude = ["test/doc-tests"]

[profile.release-with-logs]
inherits = "release"
debug-assertions = true
Expand Down
17 changes: 17 additions & 0 deletions test/doc-tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "cli-tests"
version = "0.0.0"
authors = ["Stellar Development Foundation <info@stellar.org>"]
license = "Apache-2.0"
edition = "2021"
publish = false


[lib]
crate-type = ["cdylib"]
doctest = false

[dev-dependencies]
predicates = "2.1.5"
soroban-test = "0.7.1"
soroban-cli = "0.7.1"
9 changes: 9 additions & 0 deletions test/doc-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Test CLI commands shown in the docs
===================================

This directory contains tests for all CLI commands currently shown in
https://soroban.stellar.org/docs/category/how-to-guides.

Ideally, the docs themselves would have executable tests on all of their CLI
examples. Perhaps in the future this can be facilitated by co-locating docs and
examples in the same repo with git submodules.
1 change: 1 addition & 0 deletions test/doc-tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// empty crate to collect all workspace tests into one binary
100 changes: 100 additions & 0 deletions test/doc-tests/tests/it/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Tests CLI commands from https://soroban.stellar.org/docs/how-to-guides/auth

use soroban_cli::commands::config::identity::address;
use soroban_test::{TestEnv, Wasm};

const WASM: &Wasm = &Wasm::Release("soroban_auth_contract");

const INCREMENT_1: &str = "2";
const INCREMENT_2: &str = "5";

fn account_pk(workspace: &TestEnv, hd_path: u8) -> String {
workspace
.cmd_arr::<address::Cmd>(&["--hd-path", &hd_path.to_string()])
.public_key()
.unwrap()
.to_string()
}

#[test]
fn invoke_and_read() {
TestEnv::with_default(|workspace| {
let account_0_pk = account_pk(workspace, 0);
let account_1_pk = account_pk(workspace, 1);

assert_eq!(
format!("{INCREMENT_1}"),
workspace
.invoke(&[
"--wasm",
&WASM.path().to_string_lossy(),
"--id",
"1",
"--",
"increment",
"--user",
&account_0_pk,
"--value",
&INCREMENT_1,
])
.unwrap(),
);
assert_eq!(
format!("{INCREMENT_2}"),
workspace
.invoke(&[
"--hd-path",
"1",
"--wasm",
&WASM.path().to_string_lossy(),
"--id",
"1",
"--",
"increment",
"--user",
&account_1_pk,
"--value",
&INCREMENT_2,
])
.unwrap(),
);

// `read::Cmd`'s `run` returns `()` & dumps right to STDOUT; need to use assert_cmd
workspace
.new_assert_cmd("contract")
.arg("read")
.args(["--id", "1"])
.assert()
.stderr("")
.stdout(format!(
r#""[""Counter"",""{account_0_pk}""]",{INCREMENT_1}
"[""Counter"",""{account_1_pk}""]",{INCREMENT_2}
"#
));
});
}

#[test]
fn invoke_auth_preview() {
TestEnv::with_default(|workspace| {
let account_0_pk = account_pk(workspace, 0);

// `invoke::Cmd`'s `auth` option prints directly to STDERR; need to test with assert_cmd
workspace.new_assert_cmd("contract")
.arg("invoke")
.arg("--auth")
.arg("--wasm")
.arg(&WASM.path())
.args(["--id", "1"])
.args(["--"])
.arg("increment")
.args(["--user", &account_0_pk])
.args(["--value", &INCREMENT_1])
.assert()
.stdout(format!("{INCREMENT_1}\n"))
.stderr(
r#"Contract auth: [{"address_with_nonce":null,"root_invocation":{"contract_id":"0000000000000000000000000000000000000000000000000000000000000001","function_name":"increment","args":[{"address":{"account":{"public_key_type_ed25519":"d18f0210ff6cc1f2dcf1301fbbd4c30ee11a075820684d471df89d0f1011ea28"}}},{"u32":"#.to_string() + &format!("{INCREMENT_1}") + r#"}],"sub_invocations":[]},"signature_args":[]}]
"#
);
});
}
49 changes: 49 additions & 0 deletions test/doc-tests/tests/it/cross_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! Tests CLI commands from https://soroban.stellar.org/docs/how-to-guides/cross-contract-call

use soroban_cli::commands::contract::{deploy, install};
use soroban_test::{TestEnv, Wasm};

const WASM_A: &Wasm = &Wasm::Release("soroban_cross_contract_a_contract");
const WASM_B: &Wasm = &Wasm::Release("soroban_cross_contract_b_contract");

#[test]
fn invoke() {
TestEnv::with_default(|workspace| {
let hash_a = WASM_A.hash().unwrap();
workspace
.cmd_arr::<install::Cmd>(&["--wasm", &WASM_A.path().to_string_lossy()])
.run_in_sandbox(WASM_A.bytes())
.unwrap();
workspace
.cmd_arr::<deploy::Cmd>(&["--id", "a", "--wasm-hash", &format!("{hash_a}")])
.run_in_sandbox(hash_a)
.unwrap();

let hash_b = WASM_B.hash().unwrap();
workspace
.cmd_arr::<install::Cmd>(&["--wasm", &WASM_B.path().to_string_lossy()])
.run_in_sandbox(WASM_B.bytes())
.unwrap();
workspace
.cmd_arr::<deploy::Cmd>(&["--id", "b", "--wasm-hash", &format!("{hash_b}")])
.run_in_sandbox(hash_b)
.unwrap();

let res = workspace
.invoke(&[
"--id",
"b",
"--",
"add_with",
"--contract_id",
"a",
"--x",
"5",
"--y",
"7",
])
.unwrap();

assert_eq!(res, "12");
});
}
47 changes: 47 additions & 0 deletions test/doc-tests/tests/it/custom_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Tests CLI commands from https://soroban.stellar.org/docs/how-to-guides/custom-types

use soroban_test::{TestEnv, Wasm};

const WASM: &Wasm = &Wasm::Release("soroban_custom_types_contract");

fn increment_by_5(workspace: &TestEnv) -> String {
workspace
.invoke(&[
"--wasm",
&WASM.path().to_string_lossy(),
"--id",
"1",
"--",
"increment",
"--incr",
"5",
])
.unwrap()
}

#[test]
fn invoke() {
TestEnv::with_default(|workspace| {
assert_eq!("5", increment_by_5(workspace));
});
}

const EXPECTED_READ: &str = r#"STATE,"{""count"":5,""last_incr"":5}"
"#;

#[test]
fn read() {
TestEnv::with_default(|workspace| {
increment_by_5(workspace);

// `read::Cmd`'s `run` returns `()` & dumps right to STDOUT; need to use assert_cmd
workspace
.new_assert_cmd("contract")
.arg("read")
.args(["--id", "1"])
.args(["--key", "STATE"])
.assert()
.stderr("")
.stdout(EXPECTED_READ);
});
}
52 changes: 52 additions & 0 deletions test/doc-tests/tests/it/deployer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Tests CLI commands from https://soroban.stellar.org/docs/how-to-guides/deployer

use soroban_cli::commands::contract::install;
use soroban_test::{TestEnv, Wasm};

const WASM_DEPLOYER_TEST: &Wasm = &Wasm::Release("soroban_deployer_test_contract");
const WASM_DEPLOYER: &Wasm = &Wasm::Release("soroban_deployer_contract");

const INIT_VALUE: &str = "5";

#[test]
fn invoke() {
TestEnv::with_default(|workspace| {
let hash = WASM_DEPLOYER_TEST.hash().unwrap();

// install (aka upload) the bytes
let install_ret = workspace
.cmd_arr::<install::Cmd>(&["--wasm", &WASM_DEPLOYER_TEST.path().to_string_lossy()])
.run_in_sandbox(WASM_DEPLOYER_TEST.bytes())
.unwrap();
assert_eq!(hash, install_ret);

// now invoke a 2nd contract, which deploys an instance of the previously-uploaded bytes
let new_contract_info_array = workspace
.invoke(&[
"--wasm",
&WASM_DEPLOYER.path().to_string_lossy(),
"--id",
"0",
"--",
"deploy",
"--salt",
"0000000000000000000000000000000000000000000000000000000000000000",
"--wasm_hash",
&hash.to_string(),
"--init_fn",
"init",
"--init_args",
&format!("[{{\"u32\":{INIT_VALUE}}}]"),
])
.unwrap();

let contract_id = new_contract_info_array.split('"').nth(1).unwrap();

assert_eq!(
format!("{INIT_VALUE}"),
workspace
.invoke(&["--id", contract_id, "--", "value",])
.unwrap()
);
});
}
39 changes: 39 additions & 0 deletions test/doc-tests/tests/it/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! Tests CLI commands from https://soroban.stellar.org/docs/how-to-guides/errors

use soroban_cli::commands::contract::invoke;
use soroban_test::{TestEnv, Wasm};

const WASM: &Wasm = &Wasm::Release("soroban_errors_contract");

const EXPECTED_ERROR_START: &str = r#"HostError
Value: Status(ContractError(1))
"#;

#[test]
fn invoke() {
TestEnv::with_default(|sandbox| {
let increment = || {
sandbox.invoke(&[
"--wasm",
&WASM.path().to_string_lossy(),
"--id",
"1",
"--",
"increment",
])
};

// works the first five times
assert_eq!(increment().unwrap(), "1");
assert_eq!(increment().unwrap(), "2");
assert_eq!(increment().unwrap(), "3");
assert_eq!(increment().unwrap(), "4");
assert_eq!(increment().unwrap(), "5");

// then errors
let res = increment();

assert!(matches!(res, Err(invoke::Error::Host(_))));
assert!(format!("{res:?}").contains(EXPECTED_ERROR_START));
});
}
25 changes: 25 additions & 0 deletions test/doc-tests/tests/it/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! Tests CLI commands from https://soroban.stellar.org/docs/how-to-guides/events

use soroban_test::{TestEnv, Wasm};

const WASM: &Wasm = &Wasm::Release("soroban_events_contract");

const EXPECTED_EVENT: &str = r#"#0: event: {"ext":"v0","contract_id":"0000000000000000000000000000000000000000000000000000000000000001","type_":"contract","body":{"v0":{"topics":[{"symbol":"COUNTER"},{"symbol":"increment"}],"data":{"u32":1}}}}
"#;

#[test]
fn invoke() {
TestEnv::with_default(|workspace| {
// events get dumped right to STDERR using eprintln; need to test with assert_cmd
workspace
.new_assert_cmd("contract")
.arg("invoke")
.arg("--wasm")
.arg(&WASM.path())
.args(["--id", "1"])
.args(["--", "increment"])
.assert()
.stdout("1\n")
.stderr(EXPECTED_EVENT);
});
}
Loading

0 comments on commit 2de3f37

Please sign in to comment.