Skip to content

Commit

Permalink
hivesim-rs: add hivesim-rs a rust implementation of hivesim (#1126)
Browse files Browse the repository at this point in the history
  • Loading branch information
KolbyML authored Jun 21, 2024
1 parent 7ffba98 commit 62b3362
Show file tree
Hide file tree
Showing 11 changed files with 850 additions and 2 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ workflows:
jobs:
- path-filtering/filter:
mapping: |
hivesim-rs/.* hivesim-rs-ci true
simulators/portal/.* rust-ci true
base-revision: origin/master
32 changes: 31 additions & 1 deletion .circleci/continue_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ parameters:
rust-ci:
type: boolean
default: false
hivesim-rs-ci:
type: boolean
default: false

jobs:
# This job builds the hive executable and stores it in the workspace.
Expand Down Expand Up @@ -73,6 +76,29 @@ jobs:
name: "Compile Go simulators"
command: ".circleci/compile-simulators.sh"
# this makes sure the rust code is good
hivesim-rs:
docker:
- image: cimg/rust:1.75.0
steps:
- checkout
- run:
name: Install rustfmt
command: rustup component add rustfmt
- run:
name: Install Clippy
command: rustup component add clippy
- run:
name: Install Clang
command: sudo apt update && sudo apt-get install clang -y
- run:
name: "Lint"
command: "cd hivesim-rs && cargo fmt --all -- --check"
- run:
name: "Build"
command: "cd hivesim-rs && cargo clippy --all --all-targets --all-features --no-deps -- --deny warnings"
- run:
name: "Test hivesim-rs"
command: "cd hivesim-rs && cargo test --workspace -- --nocapture"
rust-simulators:
docker:
- image: cimg/rust:1.75.0
Expand Down Expand Up @@ -100,7 +126,11 @@ workflows:
requires: ["build"]
- smoke-tests-remote-docker:
requires: ["build"]
rust-jobs:
rust-simulator-jobs:
when: << pipeline.parameters.rust-ci >>
jobs:
- rust-simulators
hivesim-rs-jobs:
when: << pipeline.parameters.hivesim-rs-ci >>
jobs:
- hivesim-rs
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ workspace
# build output
/hive

# build output for rust simulators files
# build output for rust simulators and hivesim-rs files
simulators/**/Cargo.lock
simulators/**/target
hivesim-rs/Cargo.lock
hivesim-rs/target
16 changes: 16 additions & 0 deletions hivesim-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "hivesim"
version = "0.1.0-alpha.1"
authors = ["Kolby ML (Moroz Liebl) <kolbydml@gmail.com>", "Ognyan Genev <ognian.genev@gmail.com>"]
edition = "2021"


[dependencies]
async-trait = "0.1.59"
dyn-clone = "1.0.11"
jsonrpsee = {version="0.20.0", features = ["client"]}
regex = "1.10.5"
reqwest = { version = "0.11.12", default-features = false, features = ["json", "multipart"] }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0.87"
tokio = { version = "1", features = ["full"] }
12 changes: 12 additions & 0 deletions hivesim-rs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#![allow(dead_code)]
#![warn(clippy::unwrap_used)]
mod macros;
mod simulation;
mod testapi;
mod testmatch;
pub mod types;
pub mod utils;

pub use simulation::Simulation;
pub use testapi::{run_suite, Client, NClientTestSpec, Suite, Test, TestSpec};
pub use testmatch::TestMatcher;
20 changes: 20 additions & 0 deletions hivesim-rs/src/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#[macro_export]
macro_rules! dyn_async {(
$( #[$attr:meta] )* // includes doc strings
$pub:vis
async
fn $fname:ident<$lt:lifetime> ( $($args:tt)* ) $(-> $Ret:ty)?
{
$($body:tt)*
}
) => (
$( #[$attr] )*
#[allow(unused_parens)]
$pub
fn $fname<$lt> ( $($args)* ) -> ::std::pin::Pin<::std::boxed::Box<
dyn $lt + Send + ::std::future::Future<Output = ($($Ret)?)>
>>
{
Box::pin(async move { $($body)* })
}
)}
178 changes: 178 additions & 0 deletions hivesim-rs/src/simulation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use crate::types::{ClientDefinition, StartNodeResponse, SuiteID, TestID, TestRequest, TestResult};
use crate::TestMatcher;
use std::collections::HashMap;
use std::env;
use std::net::IpAddr;
use std::str::FromStr;

/// Wraps the simulation HTTP API provided by hive.
#[derive(Clone, Debug)]
pub struct Simulation {
pub url: String,
pub test_matcher: Option<TestMatcher>,
}

impl Default for Simulation {
fn default() -> Self {
Self::new()
}
}

// A struct in the structure of the JSON config shown in simulators.md
// it is used to pass information to the Hive Simulators
#[derive(serde::Serialize, serde::Deserialize)]
struct SimulatorConfig {
client: String,
environment: HashMap<String, String>,
}

impl SimulatorConfig {
pub fn new() -> Self {
Self {
client: "".to_string(),
environment: Default::default(),
}
}
}

impl Simulation {
/// New looks up the hive host URI using the HIVE_SIMULATOR environment variable
/// and connects to it. It will panic if HIVE_SIMULATOR is not set.
pub fn new() -> Self {
let url = env::var("HIVE_SIMULATOR").expect("HIVE_SIMULATOR environment variable not set");
let test_matcher = match env::var("HIVE_TEST_PATTERN") {
Ok(pattern) => {
if pattern.is_empty() {
None
} else {
Some(TestMatcher::new(&pattern))
}
}
Err(_) => None,
};

if url.is_empty() {
panic!("HIVE_SIMULATOR environment variable is empty")
}

Self { url, test_matcher }
}

pub async fn start_suite(
&self,
name: String,
description: String,
_sim_log: String,
) -> SuiteID {
let url = format!("{}/testsuite", self.url);
let client = reqwest::Client::new();
let body = TestRequest { name, description };

client
.post(url)
.json(&body)
.send()
.await
.expect("Failed to send start suite request")
.json::<SuiteID>()
.await
.expect("Failed to convert start suite response to json")
}

pub async fn end_suite(&self, test_suite: SuiteID) {
let url = format!("{}/testsuite/{}", self.url, test_suite);
let client = reqwest::Client::new();
client
.delete(url)
.send()
.await
.expect("Failed to send an end suite request");
}

/// Starts a new test case, returning the testcase id as a context identifier
pub async fn start_test(
&self,
test_suite: SuiteID,
name: String,
description: String,
) -> TestID {
let url = format!("{}/testsuite/{}/test", self.url, test_suite);
let client = reqwest::Client::new();
let body = TestRequest { name, description };

client
.post(url)
.json(&body)
.send()
.await
.expect("Failed to send start test request")
.json::<TestID>()
.await
.expect("Failed to convert start test response to json")
}

/// Finishes the test case, cleaning up everything, logging results, and returning
/// an error if the process could not be completed.
pub async fn end_test(&self, test_suite: SuiteID, test: TestID, test_result: TestResult) {
let url = format!("{}/testsuite/{}/test/{}", self.url, test_suite, test);
let client = reqwest::Client::new();

client
.post(url)
.json(&test_result)
.send()
.await
.expect("Failed to send end test request");
}

/// Starts a new node (or other container).
/// Returns container id and ip.
pub async fn start_client(
&self,
test_suite: SuiteID,
test: TestID,
client_type: String,
environment: Option<HashMap<String, String>>,
) -> (String, IpAddr) {
let url = format!("{}/testsuite/{}/test/{}/node", self.url, test_suite, test);
let client = reqwest::Client::new();

let mut config = SimulatorConfig::new();
config.client = client_type;
if let Some(environment) = environment {
config.environment = environment;
}

let config = serde_json::to_string(&config).expect("Failed to parse config to serde_json");
let form = reqwest::multipart::Form::new().text("config", config);

let resp = client
.post(url)
.multipart(form)
.send()
.await
.expect("Failed to send start client request")
.json::<StartNodeResponse>()
.await
.expect("Failed to convert start node response to json");

let ip = IpAddr::from_str(&resp.ip).expect("Failed to decode IpAddr from string");

(resp.id, ip)
}

/// Returns all client types available to this simulator run. This depends on
/// both the available client set and the command line filters.
pub async fn client_types(&self) -> Vec<ClientDefinition> {
let url = format!("{}/clients", self.url);
let client = reqwest::Client::new();
client
.get(&url)
.send()
.await
.expect("Failed to send get client types request")
.json::<Vec<ClientDefinition>>()
.await
.expect("Failed to convert client types response to json")
}
}
Loading

0 comments on commit 62b3362

Please sign in to comment.