use chrono::prelude::*;
use gumdrop::Options;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::str::FromStr;
use std::time;
use crate::config::GooseConfiguration;
use crate::util;
use crate::{AttackPhase, GooseAttack, GooseAttackRunState, GooseError};
#[derive(Options, Debug, Clone, Serialize, Deserialize)]
pub struct TestPlan {
pub steps: Vec<(usize, usize)>,
pub current: usize,
}
impl Default for TestPlan {
fn default() -> Self {
Self::new()
}
}
impl TestPlan {
pub fn new() -> TestPlan {
TestPlan {
steps: Vec::new(),
current: 0,
}
}
pub(crate) fn build(configuration: &GooseConfiguration) -> TestPlan {
if let Some(test_plan) = configuration.test_plan.as_ref() {
test_plan.clone()
} else {
let mut steps: Vec<(usize, usize)> = Vec::new();
if let Some(users) = configuration.users {
if configuration.startup_time != "0" {
steps.push((
users,
util::parse_timespan(&configuration.startup_time) * 1_000,
));
} else {
let hatch_rate = if let Some(hatch_rate) = configuration.hatch_rate.as_ref() {
util::get_hatch_rate(Some(hatch_rate.to_string()))
} else {
util::get_hatch_rate(None)
};
let ms_hatch_rate = 1.0 / hatch_rate * 1_000.0;
let total_time = ms_hatch_rate * users as f32;
steps.push((users, total_time as usize));
}
if configuration.run_time != "0" {
steps.push((users, util::parse_timespan(&configuration.run_time) * 1_000));
steps.push((0, 0));
}
}
TestPlan { steps, current: 0 }
}
}
pub fn total_users(&self) -> usize {
let mut total_users: usize = 0;
let mut previous: usize = 0;
for step in &self.steps {
if step.0 > previous {
total_users += step.0 - previous;
}
previous = step.0
}
total_users
}
}
impl FromStr for TestPlan {
type Err = GooseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut steps: Vec<(usize, usize)> = Vec::new();
let re = Regex::new(r"^\s*(\d+)\s*,\s*(\d+|((\d+?)h)?((\d+?)m)?((\d+?)s)?)\s*$").unwrap();
let lines = s.split(';');
for line in lines {
if let Some(cap) = re.captures(line) {
let left = cap[1]
.parse::<usize>()
.expect("failed to convert \\d to usize");
let right = util::parse_timespan(&cap[2]) * 1_000;
steps.push((left, right));
} else {
eprintln!("ERROR: invalid `configuration.test_plan` value: '{line}'");
eprintln!(" Expected format: --test-plan \"{{users}},{{timespan}};{{users}},{{timespan}}\"");
eprintln!(" {{users}} must be an integer, ie \"100\"");
eprintln!(" {{timespan}} can be integer seconds or \"30s\", \"20m\", \"3h\", \"1h30m\", etc");
return Err(GooseError::InvalidOption {
option: "`configuration.test_plan".to_string(),
value: line.to_string(),
detail: "invalid `configuration.test_plan` value.".to_string(),
});
}
}
Ok(TestPlan { steps, current: 0 })
}
}
#[derive(Clone, Debug)]
pub enum TestPlanStepAction {
Increasing,
Maintaining,
Decreasing,
Canceling,
Finished,
}
#[derive(Clone, Debug)]
pub struct TestPlanHistory {
pub action: TestPlanStepAction,
pub timestamp: DateTime<Utc>,
pub users: usize,
}
impl TestPlanHistory {
pub(crate) fn step(action: TestPlanStepAction, users: usize) -> TestPlanHistory {
TestPlanHistory {
action,
timestamp: Utc::now(),
users,
}
}
}
impl GooseAttack {
pub(crate) fn advance_test_plan(&mut self, goose_attack_run_state: &mut GooseAttackRunState) {
self.step_started = Some(time::Instant::now());
let action = if self.test_plan.current == self.test_plan.steps.len() - 1 {
if self.test_plan.steps[self.test_plan.current].0 == 0 {
self.set_attack_phase(goose_attack_run_state, AttackPhase::Shutdown);
TestPlanStepAction::Finished
}
else {
self.set_attack_phase(goose_attack_run_state, AttackPhase::Maintain);
TestPlanStepAction::Maintaining
}
} else if self.test_plan.current < self.test_plan.steps.len() {
match self.test_plan.steps[self.test_plan.current]
.0
.cmp(&self.test_plan.steps[self.test_plan.current + 1].0)
{
Ordering::Less => {
self.set_attack_phase(goose_attack_run_state, AttackPhase::Increase);
TestPlanStepAction::Increasing
}
Ordering::Greater => {
self.set_attack_phase(goose_attack_run_state, AttackPhase::Decrease);
TestPlanStepAction::Decreasing
}
Ordering::Equal => {
self.set_attack_phase(goose_attack_run_state, AttackPhase::Maintain);
TestPlanStepAction::Maintaining
}
}
} else {
unreachable!("Advanced 2 steps beyond the end of the TestPlan.")
};
self.metrics.history.push(TestPlanHistory::step(
action,
self.test_plan.steps[self.test_plan.current].0,
));
self.test_plan.current += 1;
}
}