use std::collections::HashSet;
use std::{collections::HashMap, sync::Arc};
use wind_tunnel_summary_model::BuildInfo;
use crate::cli::ReporterOpt;
use crate::init::init;
use crate::{
cli::WindTunnelScenarioCli,
context::{AgentContext, RunnerContext, UserValuesConstraint},
};
pub type HookResult = anyhow::Result<()>;
pub type GlobalHookMut<RV> = fn(&mut RunnerContext<RV>) -> HookResult;
pub type GlobalHook<RV> = fn(Arc<RunnerContext<RV>>) -> HookResult;
pub type AgentHookMut<RV, V> = fn(&mut AgentContext<RV, V>) -> HookResult;
pub type BuildInfoFn<RV> = fn(Arc<RunnerContext<RV>>) -> anyhow::Result<Option<BuildInfo>>;
pub struct ScenarioDefinitionBuilder<RV: UserValuesConstraint, V: UserValuesConstraint> {
name: String,
#[doc(hidden)]
cli: WindTunnelScenarioCli,
default_agent_count: Option<usize>,
default_duration_s: Option<u64>,
capture_env: HashSet<String>,
build_info_fn: Option<BuildInfoFn<RV>>,
setup_fn: Option<GlobalHookMut<RV>>,
setup_agent_fn: Option<AgentHookMut<RV, V>>,
agent_behaviour: HashMap<String, AgentHookMut<RV, V>>,
teardown_agent_fn: Option<AgentHookMut<RV, V>>,
teardown_fn: Option<GlobalHook<RV>>,
}
pub struct AssignedBehaviour {
pub(crate) behaviour_name: String,
pub(crate) agent_count: usize,
}
pub struct ScenarioDefinition<RV: UserValuesConstraint, V: UserValuesConstraint> {
pub(crate) name: String,
pub(crate) assigned_behaviours: Vec<AssignedBehaviour>,
pub(crate) duration_s: Option<u64>,
pub(crate) connection_string: Option<String>,
pub(crate) capture_env: HashSet<String>,
pub(crate) no_progress: bool,
pub(crate) reporter: ReporterOpt,
pub(crate) build_info_fn: Option<BuildInfoFn<RV>>,
pub(crate) setup_fn: Option<GlobalHookMut<RV>>,
pub(crate) setup_agent_fn: Option<AgentHookMut<RV, V>>,
pub(crate) agent_behaviour: HashMap<String, AgentHookMut<RV, V>>,
pub(crate) teardown_agent_fn: Option<AgentHookMut<RV, V>>,
pub(crate) teardown_fn: Option<GlobalHook<RV>>,
pub(crate) run_id: String,
}
impl<RV: UserValuesConstraint, V: UserValuesConstraint> ScenarioDefinition<RV, V> {
pub(crate) fn assigned_behaviours_flat(&self) -> Vec<String> {
self.assigned_behaviours
.iter()
.flat_map(|b| std::iter::repeat_n(&b.behaviour_name, b.agent_count))
.cloned()
.collect()
}
}
impl<RV: UserValuesConstraint, V: UserValuesConstraint> ScenarioDefinitionBuilder<RV, V> {
pub fn new_with_init(name: &str) -> Self {
let cli = init();
ScenarioDefinitionBuilder::new(name, cli)
}
pub fn new(name: &str, cli: WindTunnelScenarioCli) -> Self {
Self {
name: name.to_string(),
cli,
default_agent_count: None,
default_duration_s: None,
capture_env: HashSet::with_capacity(0),
build_info_fn: None,
setup_fn: None,
setup_agent_fn: None,
agent_behaviour: HashMap::new(),
teardown_agent_fn: None,
teardown_fn: None,
}
}
pub fn with_default_agent_count(mut self, count: usize) -> Self {
self.default_agent_count = Some(count);
self
}
pub fn with_default_duration_s(mut self, duration: u64) -> Self {
self.default_duration_s = Some(duration);
self
}
pub fn add_capture_env(mut self, key: &str) -> Self {
self.capture_env.insert(key.to_string());
self
}
pub fn use_build_info(mut self, build_info_fn: BuildInfoFn<RV>) -> Self {
self.build_info_fn = Some(build_info_fn);
self
}
pub fn use_setup(mut self, setup_fn: GlobalHookMut<RV>) -> Self {
self.setup_fn = Some(setup_fn);
self
}
pub fn use_agent_setup(mut self, setup_agent_fn: AgentHookMut<RV, V>) -> Self {
self.setup_agent_fn = Some(setup_agent_fn);
self
}
pub fn use_agent_behaviour(self, behaviour: AgentHookMut<RV, V>) -> Self {
self.use_named_agent_behaviour("default", behaviour)
}
pub fn use_named_agent_behaviour(mut self, name: &str, behaviour: AgentHookMut<RV, V>) -> Self {
let previous = self.agent_behaviour.insert(name.to_string(), behaviour);
if previous.is_some() {
panic!("Behaviour [{name}] is already defined");
}
self
}
pub fn use_agent_teardown(mut self, teardown_agent_fn: AgentHookMut<RV, V>) -> Self {
self.teardown_agent_fn = Some(teardown_agent_fn);
self
}
pub fn use_teardown(mut self, teardown_fn: GlobalHook<RV>) -> Self {
self.teardown_fn = Some(teardown_fn);
self
}
pub(crate) fn build(self) -> anyhow::Result<ScenarioDefinition<RV, V>> {
let resolved_duration = if self.cli.soak {
None
} else {
self.cli.duration.or(self.default_duration_s)
};
let resolved_agent_count = self.cli.agents.or(self.default_agent_count).unwrap_or(1);
let registered_behaviours = self
.agent_behaviour
.keys()
.cloned()
.collect::<HashSet<String>>();
let requested_behaviours = self
.cli
.behaviour
.iter()
.map(|(name, _)| name.clone())
.collect::<HashSet<String>>();
let unknown_behaviours = requested_behaviours
.difference(®istered_behaviours)
.collect::<Vec<&String>>();
if !unknown_behaviours.is_empty() {
return Err(anyhow::anyhow!(
"Unknown behaviours requested: {unknown_behaviours:?}"
));
}
let run_id = self.cli.run_id.clone().unwrap_or_else(|| nanoid::nanoid!());
Ok(ScenarioDefinition {
name: self.name,
assigned_behaviours: build_assigned_behaviours(&self.cli, resolved_agent_count)?,
duration_s: resolved_duration,
connection_string: self.cli.connection_string,
capture_env: self.capture_env,
no_progress: self.cli.no_progress,
reporter: self.cli.reporter,
build_info_fn: self.build_info_fn,
setup_fn: self.setup_fn,
setup_agent_fn: self.setup_agent_fn,
agent_behaviour: self.agent_behaviour,
teardown_agent_fn: self.teardown_agent_fn,
teardown_fn: self.teardown_fn,
run_id,
})
}
}
fn build_assigned_behaviours(
cli: &WindTunnelScenarioCli,
resolved_agent_count: usize,
) -> anyhow::Result<Vec<AssignedBehaviour>> {
let mut resolved_agent_count = resolved_agent_count as i32; let mut assigned_behaviours = Vec::new();
for (behaviour_name, agent_count) in &cli.behaviour {
resolved_agent_count -= *agent_count as i32;
if resolved_agent_count < 0 {
return Err(anyhow::anyhow!("The number of agents assigned to behaviours must be less than or equal to the total number of agents"));
}
assigned_behaviours.push(AssignedBehaviour {
behaviour_name: behaviour_name.to_string(),
agent_count: *agent_count,
});
}
if resolved_agent_count > 0 {
assigned_behaviours.push(AssignedBehaviour {
behaviour_name: "default".to_string(),
agent_count: resolved_agent_count as usize, });
}
Ok(assigned_behaviours)
}
#[cfg(test)]
mod tests {
use crate::cli::ReporterOpt;
use crate::definition::build_assigned_behaviours;
#[test]
pub fn build_assigned_behaviours_default() {
let assigned = build_assigned_behaviours(
&crate::cli::WindTunnelScenarioCli {
connection_string: None,
agents: None,
behaviour: vec![],
duration: None,
soak: false,
no_progress: true,
reporter: ReporterOpt::Noop,
run_id: None,
},
5,
)
.unwrap();
assert_eq!(1, assigned.len());
assert_eq!("default", assigned[0].behaviour_name);
assert_eq!(5, assigned[0].agent_count);
}
#[test]
pub fn build_assigned_behaviours_exact() {
let assigned = build_assigned_behaviours(
&crate::cli::WindTunnelScenarioCli {
connection_string: None,
agents: None,
behaviour: vec![], duration: None,
soak: false,
no_progress: true,
reporter: ReporterOpt::Noop,
run_id: None,
},
5,
)
.unwrap();
assert_eq!(1, assigned.len());
assert_eq!("default", assigned[0].behaviour_name);
assert_eq!(5, assigned[0].agent_count);
}
#[test]
pub fn build_assigned_behaviours_partial() {
let assigned = build_assigned_behaviours(
&crate::cli::WindTunnelScenarioCli {
connection_string: None,
agents: None,
behaviour: vec![("login".to_string(), 3)], duration: None,
soak: false,
no_progress: true,
reporter: ReporterOpt::Noop,
run_id: None,
},
5,
)
.unwrap();
assert_eq!(2, assigned.len());
assert_eq!("login", assigned[0].behaviour_name);
assert_eq!(3, assigned[0].agent_count);
assert_eq!("default", assigned[1].behaviour_name);
assert_eq!(2, assigned[1].agent_count);
}
#[test]
pub fn build_assigned_behaviours_too_many() {
let result = build_assigned_behaviours(
&crate::cli::WindTunnelScenarioCli {
connection_string: None,
agents: None,
behaviour: vec![("login".to_string(), 30)], duration: None,
soak: false,
no_progress: true,
reporter: ReporterOpt::Noop,
run_id: None,
},
5,
);
assert!(result.is_err());
}
}