Skip to content

Commit

Permalink
feat: implement e2e-tests crate and kv-store example (#931)
Browse files Browse the repository at this point in the history
Co-authored-by: Sandi Fatic <[email protected]>
  • Loading branch information
fbozic and chefsale authored Nov 14, 2024
1 parent 4e8ba91 commit d303ad1
Show file tree
Hide file tree
Showing 16 changed files with 947 additions and 8 deletions.
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ members = [
"./apps/visited",
"./apps/blockchain",

"./contracts/context-config",
"./contracts/registry",
"./contracts/proxy-lib",
"./contracts/test-counter",
"./contracts/context-config",
"./contracts/registry",
"./contracts/proxy-lib",
"./contracts/test-counter",

"./e2e-tests",
]

[workspace.dependencies]
Expand Down
8 changes: 4 additions & 4 deletions crates/merod/src/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::str::FromStr;

use calimero_config::{ConfigFile, CONFIG_FILE};
use camino::Utf8PathBuf;
use clap::{value_parser, Parser};
use clap::Parser;
use eyre::{bail, eyre, Result as EyreResult};
use toml_edit::{Item, Value};
use tracing::info;
Expand All @@ -17,8 +17,8 @@ use crate::cli;
#[derive(Debug, Parser)]
pub struct ConfigCommand {
/// Key-value pairs to be added or updated in the TOML file
#[clap(short, long, value_parser = value_parser!(KeyValuePair))]
arg: Vec<KeyValuePair>,
#[clap(value_name = "ARGS")]
args: Vec<KeyValuePair>,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -58,7 +58,7 @@ impl ConfigCommand {
let mut doc = toml_str.parse::<toml_edit::DocumentMut>()?;

// Update the TOML document
for kv in self.arg.iter() {
for kv in self.args.iter() {
let key_parts: Vec<&str> = kv.key.split('.').collect();

let mut current = doc.as_item_mut();
Expand Down
23 changes: 23 additions & 0 deletions e2e-tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "e2e-tests"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
camino = { workspace = true, features = ["serde1"] }
clap = { workspace = true, features = ["env", "derive"] }
const_format.workspace = true
eyre.workspace = true
nix = { version = "0.29.0", features = ["signal"] }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
tokio = { workspace = true, features = ["fs", "io-util", "macros", "process", "rt", "rt-multi-thread", "time"] }

[lints]
workspace = true
15 changes: 15 additions & 0 deletions e2e-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# E2e tests

Binary crate which runs e2e tests for the merod node.

## Usage

Build the merod and meroctl binaries and run the e2e tests with the following
commands:

```bash
cargo build -p merod
cargo build -p meroctl

cargo run -p e2e-tests -- --input-dir ./e2e-tests/config --output-dir ./e2e-tests/corpus --merod-binary ./target/debug/merod --meroctl-binary ./target/debug/meroctl
```
10 changes: 10 additions & 0 deletions e2e-tests/config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"network": {
"nodeCount": 2,
"startSwarmPort": 2427,
"startServerPort": 2527
},
"merod": {
"args": []
}
}
28 changes: 28 additions & 0 deletions e2e-tests/config/scenarios/kv-store/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"steps": [
{
"contextCreate": {
"application": { "localFile": "./apps/kv-store/res/kv_store.wasm" }
}
},
{
"jsonRpcCall": {
"methodName": "set",
"argsJson": { "key": "foo", "value": "bar" },
"expectedResultJson": null,
"target": "inviter"
}
},
{
"jsonRpcCall": {
"methodName": "get",
"argsJson": { "key": "foo" },
"expectedResultJson": "bar",
"target": "inviter"
}
},
{
"contextInviteJoin": null
}
]
}
22 changes: 22 additions & 0 deletions e2e-tests/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub network: Network,
pub merod: MerodConfig,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Network {
pub node_count: u32,
pub start_swarm_port: u32,
pub start_server_port: u32,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MerodConfig {
pub args: Box<[String]>,
}
182 changes: 182 additions & 0 deletions e2e-tests/src/driver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;

use eyre::{bail, Result as EyreResult};
use rand::seq::SliceRandom;
use serde_json::from_slice;
use tokio::fs::{read, read_dir};
use tokio::time::sleep;

use crate::config::Config;
use crate::meroctl::Meroctl;
use crate::merod::Merod;
use crate::steps::{TestScenario, TestStep};
use crate::TestEnvironment;

pub struct TestContext<'a> {
pub inviter: String,
pub invitees: Vec<String>,
pub meroctl: &'a Meroctl,
pub context_id: Option<String>,
pub inviter_public_key: Option<String>,
pub invitees_public_keys: HashMap<String, String>,
}

pub trait Test {
async fn run_assert(&self, ctx: &mut TestContext<'_>) -> EyreResult<()>;
}

impl<'a> TestContext<'a> {
pub fn new(inviter: String, invitees: Vec<String>, meroctl: &'a Meroctl) -> Self {
Self {
inviter,
invitees,
meroctl,
context_id: None,
inviter_public_key: None,
invitees_public_keys: HashMap::new(),
}
}
}

pub struct Driver {
environment: TestEnvironment,
config: Config,
meroctl: Meroctl,
merods: RefCell<HashMap<String, Merod>>,
}

impl Driver {
pub fn new(environment: TestEnvironment, config: Config) -> Self {
let meroctl = Meroctl::new(&environment);
Self {
environment,
config,
meroctl,
merods: RefCell::new(HashMap::new()),
}
}

pub async fn run(&self) -> EyreResult<()> {
self.environment.init().await?;

let result = self.run_tests().await;

self.stop_nodes().await;

result
}

async fn run_tests(&self) -> EyreResult<()> {
self.boot_nodes().await?;

let scenarios_dir = self.environment.input_dir.join("scenarios");
let mut entries = read_dir(scenarios_dir).await?;

while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.is_dir() {
let test_file_path = path.join("test.json");
if test_file_path.exists() {
self.run_scenario(test_file_path).await?;
}
}
}

Ok(())
}

async fn run_scenario(&self, file_path: PathBuf) -> EyreResult<()> {
println!("================= Setting up scenario and context ==================");
let scenario: TestScenario = from_slice(&read(&file_path).await?)?;

println!(
"Loaded test scenario from file: {:?}\n{:?}",
file_path, scenario
);

let (inviter, invitees) = match self.pick_inviter_node() {
Some((inviter, invitees)) => (inviter, invitees),
None => bail!("Not enough nodes to run the test"),
};

println!("Picked inviter: {}", inviter);
println!("Picked invitees: {:?}", invitees);

let mut ctx = TestContext::new(inviter, invitees, &self.meroctl);

println!("====================================================================");

for step in scenario.steps.iter() {
println!("======================== Starting step =============================");
println!("Step: {:?}", step);
match step {
TestStep::ContextCreate(step) => step.run_assert(&mut ctx).await?,
TestStep::ContextInviteJoin(step) => step.run_assert(&mut ctx).await?,
TestStep::JsonRpcCall(step) => step.run_assert(&mut ctx).await?,
};
println!("====================================================================");
}

Ok(())
}

fn pick_inviter_node(&self) -> Option<(String, Vec<String>)> {
let merods = self.merods.borrow();
let mut node_names: Vec<String> = merods.keys().cloned().collect();
if node_names.len() < 1 {
None
} else {
let mut rng = rand::thread_rng();
node_names.shuffle(&mut rng);
let picked_node = node_names.remove(0);
Some((picked_node, node_names))
}
}

async fn boot_nodes(&self) -> EyreResult<()> {
println!("========================= Starting nodes ===========================");

for i in 0..self.config.network.node_count {
let node_name = format!("node{}", i + 1);
let mut merods = self.merods.borrow_mut();
if !merods.contains_key(&node_name) {
let merod = Merod::new(node_name.clone(), &self.environment);
let args: Vec<&str> = self.config.merod.args.iter().map(|s| s.as_str()).collect();

merod
.init(
self.config.network.start_swarm_port + i,
self.config.network.start_server_port + i,
&args,
)
.await?;

merod.run().await?;

drop(merods.insert(node_name, merod));
}
}

// TODO: Implement health check?
sleep(Duration::from_secs(20)).await;

println!("====================================================================");

Ok(())
}

async fn stop_nodes(&self) {
let mut merods = self.merods.borrow_mut();

for (_, merod) in merods.iter_mut() {
if let Err(err) = merod.stop().await {
eprintln!("Error stopping merod: {:?}", err);
}
}

merods.clear();
}
}
Loading

0 comments on commit d303ad1

Please sign in to comment.