Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement e2e-tests crate and kv-store example #931

Merged
merged 11 commits into from
Nov 14, 2024
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 @@ -32,10 +32,12 @@ members = [
"./apps/only-peers",
"./apps/gen-ext",

"./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
2 changes: 1 addition & 1 deletion crates/meroctl/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub struct RootArgs {
#[arg(short, long, value_name = "NAME")]
pub node_name: String,

#[arg(long, value_name = "FORMAT")]
#[arg(long, value_name = "FORMAT", default_value_t, value_enum)]
pub output_format: Format,
}

Expand Down
5 changes: 0 additions & 5 deletions crates/meroctl/src/cli/jsonrpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,6 @@ impl CallCommand {
payload,
);

match serde_json::to_string_pretty(&request) {
Ok(json) => println!("Request JSON:\n{json}"),
Err(e) => println!("Error serializing request to JSON: {e}"),
}

let client = reqwest::Client::new();
let response: Response = do_request(
&client,
Expand Down
24 changes: 24 additions & 0 deletions e2e-tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[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"] }
# color-eyre.workspace = true
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
```
7 changes: 7 additions & 0 deletions e2e-tests/config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"networkLayout": {
"nodeCount": 2,
"startSwarmPort": 2427,
"startServerPort": 2527
}
}
26 changes: 26 additions & 0 deletions e2e-tests/config/scenarios/kv-store/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"steps": [
{
"contextCreate": {
"application": { "localFile": "./apps/kv-store/res/kv_store.wasm" }
}
},
{
"jsonRpcExecute": {
"methodName": "set",
"argsJson": { "key": "foo", "value": "bar" },
"expectedResultJson": null
}
},
{
"jsonRpcExecute": {
"methodName": "get",
"argsJson": { "key": "foo" },
"expectedResultJson": "bar"
}
},
{
"contextInviteJoin": null
}
]
}
15 changes: 15 additions & 0 deletions e2e-tests/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub network_layout: NetworkLayout,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkLayout {
pub node_count: u32,
pub start_swarm_port: u32,
pub start_server_port: u32,
}
183 changes: 183 additions & 0 deletions e2e-tests/src/driver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::time::Duration;

use eyre::{bail, Result as EyreResult};
use rand::seq::SliceRandom;
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 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?;

Ok(())
}

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

use serde_json::from_slice;

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() {
let scenario = from_slice(&read(&test_file_path).await?)?;

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

self.run_scenario(scenario).await?;
}
}
}

Ok(())
}

async fn run_scenario(&self, scenario: TestScenario) -> EyreResult<()> {
let (inviter_node, invitee_node) = match self.pick_two_nodes() {
Some((inviter_node, invitee_node)) => (inviter_node, invitee_node),
None => bail!("Not enough nodes to run the test"),
};

let ctx = TestContext::new(inviter_node, invitee_node, &self.meroctl);

for step in scenario.steps.iter() {
println!("Running step: {:?}", step);
match step {
TestStep::ContextCreate(step) => step.run_assert(&ctx).await?,
TestStep::ContextInviteJoin(step) => step.run_assert(&ctx).await?,
TestStep::JsonRpcExecute(step) => step.run_assert(&ctx).await?,
fbozic marked this conversation as resolved.
Show resolved Hide resolved
};
}

Ok(())
}

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

async fn boot_nodes(&self) -> EyreResult<()> {
for i in 0..self.config.network_layout.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);

merod
.init(
self.config.network_layout.start_swarm_port + i,
self.config.network_layout.start_server_port + i,
)
.await?;

merod.run().await?;

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

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

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();
}
}

pub struct TestContext<'a> {
pub inviter_node: String,
pub invitee_node: String,
pub meroctl: &'a Meroctl,
context_id: RefCell<Option<String>>,
inviter_public_key: RefCell<Option<String>>,
}

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

impl<'a> TestContext<'a> {
pub fn new(inviter_node: String, invitee_node: String, meroctl: &'a Meroctl) -> Self {
Self {
inviter_node,
invitee_node,
meroctl,
context_id: RefCell::new(None),
inviter_public_key: RefCell::new(None),
}
}

pub fn set_context_id(&self, context_id: String) {
fbozic marked this conversation as resolved.
Show resolved Hide resolved
*self.context_id.borrow_mut() = Some(context_id);
}

pub fn get_context_id(&self) -> Option<String> {
self.context_id.borrow().clone()
}

pub fn set_inviter_public_key(&self, inviter_public_key: String) {
*self.inviter_public_key.borrow_mut() = Some(inviter_public_key);
}

pub fn get_inviter_public_key(&self) -> Option<String> {
self.inviter_public_key.borrow().clone()
}
}
Loading
Loading