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

hivesim-rs: add hivesim-rs a rust implementation of hivesim #1126

Merged
merged 1 commit into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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