harmony/harmony_cli/src/lib.rs
Taha Hawa d307893f15 fix: small-fixes (#19)
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/19
Reviewed-by: johnride <jg@nationtech.io>
Co-authored-by: Taha Hawa <taha@taha.dev>
Co-committed-by: Taha Hawa <taha@taha.dev>
2025-04-24 18:47:47 +00:00

314 lines
7.9 KiB
Rust

use clap::Parser;
use clap::builder::ArgPredicate;
use harmony;
use harmony::{score::Score, topology::Topology};
use inquire::Confirm;
#[cfg(feature = "tui")]
use harmony_tui;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
/// Run score(s) without prompt
#[arg(short, long, default_value_t = false, conflicts_with = "interactive")]
yes: bool,
/// Filter query
#[arg(short, long, conflicts_with = "interactive")]
filter: Option<String>,
/// Run interactive TUI or not
#[arg(short, long, default_value_t = false)]
interactive: bool,
/// Run all or nth, defaults to all
#[arg(
short,
long,
default_value_t = true,
default_value_if("number", ArgPredicate::IsPresent, "false"),
conflicts_with = "number",
conflicts_with = "interactive"
)]
all: bool,
/// Run nth matching, zero indexed
#[arg(short, long, default_value_t = 0, conflicts_with = "interactive")]
number: usize,
/// list scores, will also be affected by run filter
#[arg(short, long, default_value_t = false, conflicts_with = "interactive")]
list: bool,
}
fn maestro_scores_filter<T: Topology>(
maestro: &harmony::maestro::Maestro<T>,
all: bool,
filter: Option<String>,
number: usize,
) -> Vec<Box<dyn Score<T>>> {
let scores = maestro.scores();
let scores_read = scores.read().expect("Should be able to read scores");
let mut scores_vec: Vec<Box<dyn Score<T>>> = match filter {
Some(f) => scores_read
.iter()
.filter(|s| s.name().contains(&f))
.map(|s| s.clone_box())
.collect(),
None => scores_read.iter().map(|s| s.clone_box()).collect(),
};
if !all {
let score = scores_vec.get(number);
match score {
Some(s) => scores_vec = vec![s.clone_box()],
None => return vec![],
}
};
return scores_vec;
}
// TODO: consider adding doctest for this function
fn list_scores_with_index<T: Topology>(scores_vec: &Vec<Box<dyn Score<T>>>) -> String {
let mut display_str = String::new();
for (i, s) in scores_vec.iter().enumerate() {
let name = s.name();
display_str.push_str(&format!("\n{i}: {name}"));
}
return display_str;
}
pub async fn init<T: Topology + Send + Sync + 'static>(
maestro: harmony::maestro::Maestro<T>,
args_struct: Option<Args>,
) -> Result<(), Box<dyn std::error::Error>> {
let args = match args_struct {
Some(args) => args,
None => Args::parse(),
};
#[cfg(feature = "tui")]
if args.interactive {
return harmony_tui::init(maestro).await;
}
#[cfg(not(feature = "tui"))]
if args.interactive {
return Err("Not compiled with interactive support".into());
}
env_logger::builder().init();
let scores_vec = maestro_scores_filter(&maestro, args.all, args.filter, args.number);
if scores_vec.len() == 0 {
return Err("No score found".into());
}
// if list option is specified, print filtered list and exit
if args.list {
println!("Available scores:");
println!("{}", list_scores_with_index(&scores_vec));
return Ok(());
}
// prompt user if --yes is not specified
if !args.yes {
let confirmation = Confirm::new(
format!(
"This will run the following scores: {}\n",
list_scores_with_index(&scores_vec)
)
.as_str(),
)
.with_default(true)
.prompt()
.expect("Unexpected prompt error");
if !confirmation {
return Ok(());
}
}
// Run filtered scores
for s in scores_vec {
println!("Running: {}", s.name());
maestro.interpret(s).await?;
}
Ok(())
}
#[cfg(test)]
mod test {
use harmony::{
inventory::Inventory,
maestro::Maestro,
modules::dummy::{ErrorScore, PanicScore, SuccessScore},
topology::HAClusterTopology,
};
fn init_test_maestro() -> Maestro<HAClusterTopology> {
let inventory = Inventory::autoload();
let topology = HAClusterTopology::autoload();
let mut maestro = Maestro::new(inventory, topology);
maestro.register_all(vec![
Box::new(SuccessScore {}),
Box::new(ErrorScore {}),
Box::new(PanicScore {}),
]);
maestro
}
#[tokio::test]
async fn test_init_success_score() {
let maestro = init_test_maestro();
let res = crate::init(
maestro,
Some(crate::Args {
yes: true,
filter: Some("SuccessScore".to_owned()),
interactive: false,
all: true,
number: 0,
list: false,
}),
)
.await;
assert!(res.is_ok());
}
#[tokio::test]
async fn test_init_error_score() {
let maestro = init_test_maestro();
let res = crate::init(
maestro,
Some(crate::Args {
yes: true,
filter: Some("ErrorScore".to_owned()),
interactive: false,
all: true,
number: 0,
list: false,
}),
)
.await;
assert!(res.is_err());
}
#[tokio::test]
async fn test_init_number_score() {
let maestro = init_test_maestro();
let res = crate::init(
maestro,
Some(crate::Args {
yes: true,
filter: None,
interactive: false,
all: false,
number: 0,
list: false,
}),
)
.await;
assert!(res.is_ok());
}
#[tokio::test]
async fn test_filter_fn_all() {
let maestro = init_test_maestro();
let res = crate::maestro_scores_filter(&maestro, true, None, 0);
assert!(res.len() == 3);
}
#[tokio::test]
async fn test_filter_fn_all_success() {
let maestro = init_test_maestro();
let res = crate::maestro_scores_filter(&maestro, true, Some("Success".to_owned()), 0);
assert!(res.len() == 1);
assert!(
maestro
.interpret(res.get(0).unwrap().clone_box())
.await
.is_ok()
);
}
#[tokio::test]
async fn test_filter_fn_all_error() {
let maestro = init_test_maestro();
let res = crate::maestro_scores_filter(&maestro, true, Some("Error".to_owned()), 0);
assert!(res.len() == 1);
assert!(
maestro
.interpret(res.get(0).unwrap().clone_box())
.await
.is_err()
);
}
#[tokio::test]
async fn test_filter_fn_all_score() {
let maestro = init_test_maestro();
let res = crate::maestro_scores_filter(&maestro, true, Some("Score".to_owned()), 0);
assert!(res.len() == 3);
assert!(
maestro
.interpret(res.get(0).unwrap().clone_box())
.await
.is_ok()
);
assert!(
maestro
.interpret(res.get(1).unwrap().clone_box())
.await
.is_err()
);
}
#[tokio::test]
async fn test_filter_fn_number() {
let maestro = init_test_maestro();
let res = crate::maestro_scores_filter(&maestro, false, None, 0);
assert!(res.len() == 1);
assert!(
maestro
.interpret(res.get(0).unwrap().clone_box())
.await
.is_ok()
);
}
#[tokio::test]
async fn test_filter_fn_number_invalid() {
let maestro = init_test_maestro();
let res = crate::maestro_scores_filter(&maestro, false, None, 11);
assert!(res.len() == 0);
}
}