use crate::test_plan::{TestPlanHistory, TestPlanStepAction};
use chrono::prelude::*;
use itertools::Itertools;
use serde::Serialize;
use serde_json::json;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::Write;
use std::marker::PhantomData;
pub(crate) struct GraphData {
requests_per_second: HashMap<String, TimeSeries<u32, u32>>,
errors_per_second: HashMap<String, TimeSeries<u32, u32>>,
average_response_time_per_second: HashMap<String, TimeSeries<MovingAverage, f32>>,
transactions_per_second: TimeSeries<usize, usize>,
scenarios_per_second: TimeSeries<usize, usize>,
users_per_second: TimeSeries<usize, usize>,
}
impl GraphData {
pub(crate) fn new() -> Self {
trace!("new graph");
GraphData {
requests_per_second: HashMap::new(),
errors_per_second: HashMap::new(),
average_response_time_per_second: HashMap::new(),
transactions_per_second: TimeSeries::new(),
scenarios_per_second: TimeSeries::new(),
users_per_second: TimeSeries::new(),
}
}
pub(crate) fn record_requests_per_second(&mut self, key: &str, second: usize) {
if !self.requests_per_second.contains_key(key) {
self.requests_per_second
.insert(key.to_string(), TimeSeries::new());
}
let data = self.requests_per_second.get_mut(key).unwrap();
data.increase_value(second, 1);
debug!(
"incremented second {} for requests per second counter: {}",
second,
data.get(second)
);
}
pub(crate) fn record_errors_per_second(&mut self, key: &str, second: usize) {
if !self.errors_per_second.contains_key(key) {
self.errors_per_second
.insert(key.to_string(), TimeSeries::new());
}
let data = self.errors_per_second.get_mut(key).unwrap();
data.increase_value(second, 1);
debug!(
"incremented second {} for errors per second counter: {}",
second,
data.get(second)
);
}
pub(crate) fn record_average_response_time_per_second(
&mut self,
key: String,
second: usize,
response_time: u64,
) {
if !self.average_response_time_per_second.contains_key(&key) {
self.average_response_time_per_second
.insert(key.clone(), TimeSeries::new());
}
let data = self.average_response_time_per_second.get_mut(&key).unwrap();
data.increase_value(second, response_time as f32);
debug!(
"updated second {} for average response time per second: {}",
second,
data.get(second).average
);
}
pub(crate) fn record_transactions_per_second(&mut self, second: usize) {
self.transactions_per_second.increase_value(second, 1);
debug!(
"incremented second {} for transactions per second counter: {}",
second,
self.transactions_per_second.get(second)
);
}
pub(crate) fn record_scenarios_per_second(&mut self, second: usize) {
self.scenarios_per_second.increase_value(second, 1);
debug!(
"incremented second {} for scenarios per second counter: {}",
second,
self.scenarios_per_second.get(second)
);
}
pub(crate) fn record_users_per_second(&mut self, users: usize, second: usize) {
self.users_per_second.set_and_maintain_last(second, users);
}
pub(crate) fn get_active_users_graph(&self, granular_data: bool) -> Graph<usize, usize> {
self.create_graph_from_single_data(
"graph-active-users",
"Active users #",
granular_data,
self.users_per_second.clone(),
)
}
pub(crate) fn get_requests_per_second_graph(&self, granular_data: bool) -> Graph<u32, u32> {
self.create_graph_from_data(
"graph-rps",
"Requests #",
granular_data,
self.requests_per_second.clone(),
)
}
pub(crate) fn get_average_response_time_graph(
&self,
granular_data: bool,
) -> Graph<MovingAverage, f32> {
self.create_graph_from_data(
"graph-avg-response-time",
"Response time [ms]",
granular_data,
self.average_response_time_per_second.clone(),
)
}
pub(crate) fn get_transactions_per_second_graph(
&self,
granular_data: bool,
) -> Graph<usize, usize> {
self.create_graph_from_single_data(
"graph-tps",
"Transactions #",
granular_data,
self.transactions_per_second.clone(),
)
}
pub(crate) fn get_scenarios_per_second_graph(
&self,
granular_data: bool,
) -> Graph<usize, usize> {
self.create_graph_from_single_data(
"graph-sps",
"Scenarios #",
granular_data,
self.scenarios_per_second.clone(),
)
}
pub(crate) fn get_errors_per_second_graph(&self, granular_data: bool) -> Graph<u32, u32> {
self.create_graph_from_data(
"graph-eps",
"Errors #",
granular_data,
self.errors_per_second.clone(),
)
}
fn create_graph_from_data<
'a,
T: Clone + TimeSeriesValue<T, U>,
U: Serialize + Copy + PartialEq + PartialOrd,
>(
&self,
html_id: &'a str,
y_axis_label: &'a str,
granular_data: bool,
data: HashMap<String, TimeSeries<T, U>>,
) -> Graph<'a, T, U> {
Graph::new(html_id, y_axis_label, granular_data, data)
}
fn create_graph_from_single_data<
'a,
T: Clone + TimeSeriesValue<T, U>,
U: Serialize + Copy + PartialEq + PartialOrd,
>(
&self,
html_id: &'a str,
y_axis_label: &'a str,
granular_data: bool,
data: TimeSeries<T, U>,
) -> Graph<'a, T, U> {
let mut hash_map_data = HashMap::new();
hash_map_data.insert("Total".to_string(), data);
Graph::new(html_id, y_axis_label, granular_data, hash_map_data)
}
}
#[derive(Debug)]
pub(crate) struct Graph<'a, T: Clone + TimeSeriesValue<T, U>, U: Serialize + Copy> {
html_id: &'a str,
y_axis_label: &'a str,
granular_data: bool,
data: HashMap<String, TimeSeries<T, U>>,
}
impl<'a, T: Clone + TimeSeriesValue<T, U>, U: Serialize + Copy + PartialEq + PartialOrd>
Graph<'a, T, U>
{
#[allow(clippy::too_many_arguments)]
fn new(
html_id: &'a str,
y_axis_label: &'a str,
granular_data: bool,
data: HashMap<String, TimeSeries<T, U>>,
) -> Graph<'a, T, U> {
Graph {
html_id,
y_axis_label,
granular_data,
data,
}
}
pub(crate) fn get_markup(
self,
history: &[TestPlanHistory],
test_started_time: DateTime<Utc>,
) -> String {
let mut steps = String::new();
for step in history.windows(2) {
let started = Local
.timestamp_opt(step[0].timestamp.timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string();
let stopped = Local
.timestamp_opt(step[1].timestamp.timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string();
match &step[0].action {
TestPlanStepAction::Increasing => {
let _ = write!(
steps,
r#"[
{{
xAxis: '{started}',
itemStyle: {{ borderColor: 'rgba(44, 102, 79, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{started}'
}}
],
[
{{
xAxis: '{started}',
itemStyle: {{ color: 'rgba(44, 102, 79, 0.05)' }},
}},
{{
xAxis: '{stopped}'
}}
],
[
{{
xAxis: '{stopped}',
itemStyle: {{ borderColor: 'rgba(44, 102, 79, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{stopped}'
}}
],"#,
started = started,
stopped = stopped,
);
}
TestPlanStepAction::Decreasing | TestPlanStepAction::Canceling => {
let _ = write!(
steps,
r#"[
{{
xAxis: '{started}',
itemStyle: {{ borderColor: 'rgba(179, 65, 65, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{started}'
}}
],
[
{{
xAxis: '{started}',
itemStyle: {{ color: 'rgba(179, 65, 65, 0.05)' }},
}},
{{
xAxis: '{stopped}'
}}
],
[
{{
xAxis: '{stopped}',
itemStyle: {{ borderColor: 'rgba(179, 65, 65, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{stopped}'
}}
],"#,
started = started,
stopped = stopped,
);
}
_ => {}
}
}
let mut total_values: TimeSeries<T, U> = TimeSeries::new();
let (legend, main_label, main_values, other_values) = if self.data.len() > 1 {
for (_, single_data) in self.data.iter() {
total_values.add_time_series(single_data);
}
let mut legend = vec!["Total"];
let mut other_values = String::new();
if self.granular_data {
for (sub_data, label) in self
.data
.iter()
.map(|(label, sub_data)| (sub_data, label))
.sorted()
.rev()
{
legend.push(label);
let formatted_line = format!(
r#"{{
name: '{label}',
type: 'line',
symbol: 'none',
sampling: 'lttb',
data: {values},
}},
"#,
label = label,
values = json!(self.add_timestamp_to_html_graph_data(
&sub_data.get_graph_data(),
test_started_time
))
);
other_values += formatted_line.as_str();
}
(
format!(
r#"legend: {{
type: '{legend_type}',
width: '75%',
data: {data},
}},"#,
legend_type = if self.data.len() > 4 {
"scroll"
} else {
"plain"
},
data = json!(legend)
),
"Total",
&total_values,
other_values,
)
} else {
("".to_string(), "Total", &total_values, "".to_string())
}
} else {
(
"".to_string(),
self.data.keys().next().unwrap().as_str(),
self.data.values().next().unwrap(),
"".to_string(),
)
};
format!(
r#"<div class="graph">
<div id="{html_id}" style="width: 1000px; height:500px; background: white;"></div>
<script type="text/javascript">
var chartDom = document.getElementById('{html_id}');
var myChart = echarts.init(chartDom);
myChart.setOption({{
color: ['#2c664f', '#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
tooltip: {{ trigger: 'axis' }},
toolbox: {{
feature: {{
dataZoom: {{ yAxisIndex: 'none' }},
restore: {{}},
saveAsImage: {{}}
}}
}},
dataZoom: [
{{
type: 'inside',
start: 0,
end: 100,
fillerColor: 'rgba(34, 80, 61, 0.25)',
selectedDataBackground: {{
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }}
}}
}},
{{
start: 0,
end: 100,
fillerColor: 'rgba(34, 80, 61, 0.25)',
selectedDataBackground: {{
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }}
}}
}},
],
xAxis: {{ type: 'time' }},
yAxis: {{
name: '{y_axis_label}',
nameLocation: 'center',
nameRotate: 90,
nameGap: 45,
type: 'value'
}},
{legend}
series: [
{{
name: '{main_label}',
type: 'line',
symbol: 'none',
sampling: 'lttb',
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }},
markArea: {{
data: [
{steps}
]
}},
data: {main_values},
}},
{other_values}
]
}});
</script>
</div>"#,
html_id = self.html_id,
main_values = json!(self.add_timestamp_to_html_graph_data(
&main_values.get_graph_data(),
test_started_time
)),
y_axis_label = self.y_axis_label,
main_label = main_label,
legend = legend,
other_values = other_values,
steps = steps,
)
}
fn add_timestamp_to_html_graph_data(
&self,
data: &[U],
started: DateTime<Utc>,
) -> Vec<(String, U)> {
data.iter()
.enumerate()
.map(|(second, value)| {
(
Local
.timestamp_opt(second as i64 + started.timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
*value,
)
})
.collect::<Vec<_>>()
}
}
#[derive(Debug, Clone)]
struct TimeSeries<T: TimeSeriesValue<T, U>, U> {
data: Vec<T>,
total: T,
phantom: PhantomData<U>,
}
impl<T: Clone + TimeSeriesValue<T, U>, U: PartialEq + PartialOrd> Ord for TimeSeries<T, U> {
fn cmp(&self, other: &Self) -> Ordering {
let self_total = self.total();
let other_total = other.total();
if self_total > other_total {
Ordering::Greater
} else if self_total < other_total {
Ordering::Less
} else {
Ordering::Equal
}
}
}
impl<T: Clone + TimeSeriesValue<T, U>, U: PartialEq + PartialOrd> PartialOrd for TimeSeries<T, U> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let self_total = self.total();
let other_total = other.total();
if self_total > other_total {
Some(Ordering::Greater)
} else if self_total < other_total {
Some(Ordering::Less)
} else {
Some(Ordering::Equal)
}
}
}
impl<T: Clone + TimeSeriesValue<T, U>, U: PartialEq> Eq for TimeSeries<T, U> {}
impl<T: Clone + TimeSeriesValue<T, U>, U: PartialEq> PartialEq for TimeSeries<T, U> {
fn eq(&self, other: &Self) -> bool {
self.total() == other.total()
}
}
impl<T: Clone + TimeSeriesValue<T, U>, U> TimeSeries<T, U> {
fn new() -> TimeSeries<T, U> {
TimeSeries {
data: Vec::new(),
phantom: PhantomData,
total: T::initial_value(),
}
}
fn increase_value(&mut self, second: usize, value: U) {
self.expand(second, T::initial_value());
self.data[second].increase_value(&value);
self.total.increase_value(&value);
}
fn add_time_series(&mut self, other: &TimeSeries<T, U>) {
for (second, other_item) in other.data.iter().enumerate() {
self.expand(second, T::initial_value());
self.data.get_mut(second).unwrap().merge(other_item);
self.total.merge(other_item);
}
}
fn set_and_maintain_last(&mut self, second: usize, value: U) {
self.expand(second, self.last());
self.total.increase_value(&value);
self.data[second].set_value(value);
}
fn get(&self, second: usize) -> T {
match self.data.get(second) {
Some(value) => value.clone(),
None => T::initial_value(),
}
}
fn last(&self) -> T {
match self.data.last() {
Some(last) => last.clone(),
None => T::initial_value(),
}
}
fn get_graph_data(&self) -> Vec<U> {
self.data
.iter()
.map(|value| value.get_graph_value())
.collect::<Vec<_>>()
}
fn expand(&mut self, second: usize, initial: T) {
if self.data.len() <= second {
for _ in 0..(second - self.data.len() + 1) {
self.data.push(initial.clone());
self.total.merge(&initial);
}
};
}
fn total(&self) -> U {
self.total.get_graph_value()
}
}
pub trait TimeSeriesValue<T, U> {
fn initial_value() -> T;
fn increase_value(&mut self, value: &U);
fn set_value(&mut self, value: U);
fn merge(&mut self, other: &T);
fn get_graph_value(&self) -> U;
}
impl TimeSeriesValue<usize, usize> for usize {
fn initial_value() -> usize {
0
}
fn increase_value(&mut self, value: &usize) {
*self += *value;
}
fn set_value(&mut self, value: usize) {
*self = value;
}
fn merge(&mut self, other: &usize) {
*self += *other;
}
fn get_graph_value(&self) -> usize {
*self
}
}
impl TimeSeriesValue<u32, u32> for u32 {
fn initial_value() -> u32 {
0
}
fn increase_value(&mut self, value: &u32) {
*self += *value;
}
fn set_value(&mut self, value: u32) {
*self = value;
}
fn merge(&mut self, other: &u32) {
*self += *other;
}
fn get_graph_value(&self) -> u32 {
*self
}
}
impl TimeSeriesValue<MovingAverage, f32> for MovingAverage {
fn initial_value() -> MovingAverage {
MovingAverage::new()
}
fn increase_value(&mut self, value: &f32) {
self.add_item(value);
}
fn set_value(&mut self, _: f32) {
panic!("TimeSeriesValue::set() is not supported for MovingAverage");
}
fn merge(&mut self, other: &MovingAverage) {
let total_count = self.count + other.count;
if total_count == 0 {
self.average = 0.;
} else {
self.average = self.average * (self.count as f32 / total_count as f32)
+ other.average * (other.count as f32 / total_count as f32);
};
self.count = total_count;
}
fn get_graph_value(&self) -> f32 {
self.average
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct MovingAverage {
count: u32,
average: f32,
}
impl MovingAverage {
fn new() -> Self {
MovingAverage {
count: 0,
average: 0.,
}
}
fn add_item(&mut self, item: &f32) {
self.count += 1;
self.average += (*item - self.average) / self.count as f32;
}
}
impl Default for MovingAverage {
fn default() -> MovingAverage {
MovingAverage::new()
}
}
impl Ord for MovingAverage {
fn cmp(&self, other: &Self) -> Ordering {
if self.average > other.average {
Ordering::Greater
} else if self.average < other.average {
Ordering::Less
} else {
Ordering::Equal
}
}
}
impl PartialOrd for MovingAverage {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for MovingAverage {
fn eq(&self, other: &Self) -> bool {
self.average == other.average
}
}
impl Eq for MovingAverage {}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_graph_setters() {
let mut graph = GraphData::new();
graph.requests_per_second.insert(
"GET /".to_string(),
TimeSeries {
data: vec![123, 234, 345, 456, 567],
phantom: PhantomData,
total: 0,
},
);
graph.users_per_second = TimeSeries {
data: vec![345, 456, 567, 123, 234],
phantom: PhantomData,
total: 0,
};
graph.average_response_time_per_second.insert(
"GET /".to_string(),
TimeSeries {
data: vec![
MovingAverage {
count: 123,
average: 1.23,
},
MovingAverage {
count: 234,
average: 2.34,
},
MovingAverage {
count: 345,
average: 3.45,
},
MovingAverage {
count: 456,
average: 4.56,
},
MovingAverage {
count: 567,
average: 5.67,
},
],
phantom: PhantomData,
total: MovingAverage {
count: 0,
average: 0.,
},
},
);
graph.transactions_per_second = TimeSeries {
data: vec![345, 123, 234, 456, 567],
phantom: PhantomData,
total: 0,
};
graph.scenarios_per_second = TimeSeries {
data: vec![345, 123, 234, 456, 567],
phantom: PhantomData,
total: 0,
};
graph.errors_per_second.insert(
"GET /".to_string(),
TimeSeries {
data: vec![567, 123, 234, 345, 456],
phantom: PhantomData,
total: 0,
},
);
let rps_graph = graph.get_requests_per_second_graph(true);
let expected_time_series: TimeSeries<u32, u32> = TimeSeries {
data: vec![123, 234, 345, 456, 567],
phantom: PhantomData,
total: 0,
};
assert_eq!(
rps_graph.data.get("GET /").unwrap().clone(),
expected_time_series
);
assert_eq!(rps_graph.html_id, "graph-rps");
assert_eq!(rps_graph.y_axis_label, "Requests #");
let users_graph = graph.get_active_users_graph(true);
let expected_time_series: TimeSeries<usize, usize> = TimeSeries {
data: vec![345, 456, 567, 123, 234],
phantom: PhantomData,
total: 0,
};
assert_eq!(
users_graph.data.get("Total").unwrap().clone(),
expected_time_series
);
assert_eq!(users_graph.html_id, "graph-active-users");
assert_eq!(users_graph.y_axis_label, "Active users #");
let avg_rt_graph = graph.get_average_response_time_graph(true);
let expected_time_series: TimeSeries<MovingAverage, f32> = TimeSeries {
data: vec![
MovingAverage {
count: 123,
average: 1.23,
},
MovingAverage {
count: 234,
average: 2.34,
},
MovingAverage {
count: 345,
average: 3.45,
},
MovingAverage {
count: 456,
average: 4.56,
},
MovingAverage {
count: 567,
average: 5.67,
},
],
phantom: PhantomData,
total: MovingAverage {
count: 0,
average: 0.,
},
};
assert_eq!(
avg_rt_graph.data.get("GET /").unwrap().clone(),
expected_time_series
);
assert_eq!(avg_rt_graph.html_id, "graph-avg-response-time");
assert_eq!(avg_rt_graph.y_axis_label, "Response time [ms]");
let transactions_graph = graph.get_transactions_per_second_graph(true);
let expected_time_series: TimeSeries<usize, usize> = TimeSeries {
data: vec![345, 123, 234, 456, 567],
phantom: PhantomData,
total: 0,
};
assert_eq!(
transactions_graph.data.get("Total").unwrap().clone(),
expected_time_series
);
assert_eq!(transactions_graph.html_id, "graph-tps");
assert_eq!(transactions_graph.y_axis_label, "Transactions #");
let scenarios_graph = graph.get_scenarios_per_second_graph(true);
let expected_time_series: TimeSeries<usize, usize> = TimeSeries {
data: vec![345, 123, 234, 456, 567],
phantom: PhantomData,
total: 0,
};
assert_eq!(
scenarios_graph.data.get("Total").unwrap().clone(),
expected_time_series
);
assert_eq!(scenarios_graph.html_id, "graph-sps");
assert_eq!(scenarios_graph.y_axis_label, "Scenarios #");
let errors_graph = graph.get_errors_per_second_graph(true);
let expected_time_series: TimeSeries<u32, u32> = TimeSeries {
data: vec![567, 123, 234, 345, 456],
phantom: PhantomData,
total: 0,
};
assert_eq!(
errors_graph.data.get("GET /").unwrap().clone(),
expected_time_series
);
assert_eq!(errors_graph.html_id, "graph-eps");
assert_eq!(errors_graph.y_axis_label, "Errors #");
}
#[test]
fn test_record_requests_per_second() {
let mut graph = GraphData::new();
assert_eq!(graph.requests_per_second.len(), 0);
graph.record_requests_per_second("GET /", 0);
graph.record_requests_per_second("GET /", 0);
graph.record_requests_per_second("GET /", 0);
graph.record_requests_per_second("GET /", 1);
graph.record_requests_per_second("GET /", 2);
graph.record_requests_per_second("GET /", 2);
graph.record_requests_per_second("GET /", 2);
graph.record_requests_per_second("GET /", 2);
graph.record_requests_per_second("GET /", 2);
assert_eq!(
graph.requests_per_second.get("GET /").unwrap().data.len(),
3
);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[0], 3);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[1], 1);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[2], 5);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().total(), 9);
graph.record_requests_per_second("GET /", 100);
graph.record_requests_per_second("GET /", 100);
graph.record_requests_per_second("GET /", 100);
graph.record_requests_per_second("GET /", 0);
graph.record_requests_per_second("GET /", 1);
graph.record_requests_per_second("GET /", 2);
graph.record_requests_per_second("GET /", 5);
assert_eq!(
graph.requests_per_second.get("GET /").unwrap().data.len(),
101
);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[0], 4);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[1], 2);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[2], 6);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[3], 0);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[4], 0);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[5], 1);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[100], 3);
for second in 6..100 {
assert_eq!(
graph.requests_per_second.get("GET /").unwrap().data[second],
0
);
}
assert_eq!(graph.requests_per_second.get("GET /").unwrap().total(), 16);
graph.record_requests_per_second("GET /user", 0);
graph.record_requests_per_second("GET /user", 1);
graph.record_requests_per_second("GET /user", 1);
graph.record_requests_per_second("GET /", 2);
graph.record_requests_per_second("GET /", 5);
assert_eq!(
graph
.requests_per_second
.get("GET /user")
.unwrap()
.data
.len(),
2
);
assert_eq!(
graph.requests_per_second.get("GET /user").unwrap().data[0],
1
);
assert_eq!(
graph.requests_per_second.get("GET /user").unwrap().data[1],
2
);
assert_eq!(
graph.requests_per_second.get("GET /").unwrap().data.len(),
101
);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[0], 4);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[1], 2);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[2], 7);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[3], 0);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[4], 0);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[5], 2);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[100], 3);
for second in 6..100 {
assert_eq!(
graph.requests_per_second.get("GET /").unwrap().data[second],
0
);
}
assert_eq!(graph.requests_per_second.get("GET /").unwrap().total(), 18);
assert_eq!(
graph.requests_per_second.get("GET /user").unwrap().total(),
3
);
graph.record_requests_per_second("GET /user", 100);
graph.record_requests_per_second("GET /user", 0);
graph.record_requests_per_second("GET /user", 1);
graph.record_requests_per_second("GET /", 0);
graph.record_requests_per_second("GET /", 1);
assert_eq!(
graph
.requests_per_second
.get("GET /user")
.unwrap()
.data
.len(),
101
);
assert_eq!(
graph.requests_per_second.get("GET /user").unwrap().data[0],
2
);
assert_eq!(
graph.requests_per_second.get("GET /user").unwrap().data[1],
3
);
assert_eq!(
graph.requests_per_second.get("GET /user").unwrap().data[100],
1
);
for second in 6..100 {
assert_eq!(
graph.requests_per_second.get("GET /user").unwrap().data[second],
0
);
}
assert_eq!(
graph.requests_per_second.get("GET /").unwrap().data.len(),
101
);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[0], 5);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[1], 3);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[2], 7);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[3], 0);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[4], 0);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[5], 2);
assert_eq!(graph.requests_per_second.get("GET /").unwrap().data[100], 3);
for second in 6..100 {
assert_eq!(
graph.requests_per_second.get("GET /").unwrap().data[second],
0
);
}
assert_eq!(graph.requests_per_second.get("GET /").unwrap().total(), 20);
assert_eq!(
graph.requests_per_second.get("GET /user").unwrap().total(),
6
);
}
#[test]
fn test_record_errors_per_second() {
let mut graph = GraphData::new();
assert_eq!(graph.errors_per_second.len(), 0);
graph.record_errors_per_second("GET /", 0);
graph.record_errors_per_second("GET /", 0);
graph.record_errors_per_second("GET /", 0);
graph.record_errors_per_second("GET /", 1);
graph.record_errors_per_second("GET /", 2);
graph.record_errors_per_second("GET /", 2);
graph.record_errors_per_second("GET /", 2);
graph.record_errors_per_second("GET /", 2);
graph.record_errors_per_second("GET /", 2);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data.len(), 3);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[0], 3);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[1], 1);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[2], 5);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().total(), 9);
graph.record_errors_per_second("GET /", 100);
graph.record_errors_per_second("GET /", 100);
graph.record_errors_per_second("GET /", 100);
graph.record_errors_per_second("GET /", 0);
graph.record_errors_per_second("GET /", 1);
graph.record_errors_per_second("GET /", 2);
graph.record_errors_per_second("GET /", 5);
assert_eq!(
graph.errors_per_second.get("GET /").unwrap().data.len(),
101
);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[0], 4);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[1], 2);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[2], 6);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[3], 0);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[4], 0);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[5], 1);
assert_eq!(graph.errors_per_second.get("GET /").unwrap().data[100], 3);
for second in 6..100 {
assert_eq!(
graph.errors_per_second.get("GET /").unwrap().data[second],
0
);
}
assert_eq!(graph.errors_per_second.get("GET /").unwrap().total(), 16);
}
#[test]
#[allow(clippy::float_cmp)]
fn test_record_average_response_time_per_second() {
let mut graph = GraphData::new();
assert_eq!(graph.average_response_time_per_second.len(), 0);
graph.record_average_response_time_per_second("GET /".to_string(), 0, 5);
graph.record_average_response_time_per_second("GET /".to_string(), 0, 4);
graph.record_average_response_time_per_second("GET /".to_string(), 0, 3);
graph.record_average_response_time_per_second("GET /".to_string(), 1, 1);
graph.record_average_response_time_per_second("GET /".to_string(), 2, 4);
graph.record_average_response_time_per_second("GET /".to_string(), 2, 8);
graph.record_average_response_time_per_second("GET /".to_string(), 2, 12);
graph.record_average_response_time_per_second("GET /".to_string(), 2, 4);
graph.record_average_response_time_per_second("GET /".to_string(), 2, 4);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data
.len(),
3
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[0]
.average,
4.
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[1]
.average,
1.
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[2]
.average,
6.4
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.total(),
5.0000005
);
graph.record_average_response_time_per_second("GET /".to_string(), 100, 5);
graph.record_average_response_time_per_second("GET /".to_string(), 100, 9);
graph.record_average_response_time_per_second("GET /".to_string(), 100, 7);
graph.record_average_response_time_per_second("GET /".to_string(), 0, 2);
graph.record_average_response_time_per_second("GET /".to_string(), 1, 2);
graph.record_average_response_time_per_second("GET /".to_string(), 2, 5);
graph.record_average_response_time_per_second("GET /".to_string(), 5, 2);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data
.len(),
101
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[0]
.average,
3.5
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[1]
.average,
1.5
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[2]
.average,
6.166667
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[3]
.average,
0.
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[4]
.average,
0.
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[5]
.average,
2.
);
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[100]
.average,
7.
);
for second in 6..100 {
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.data[second]
.average,
0.
);
}
assert_eq!(
graph
.average_response_time_per_second
.get("GET /")
.unwrap()
.total(),
4.8125005
);
}
#[test]
fn test_record_transactions_per_second() {
let mut graph = GraphData::new();
assert_eq!(graph.transactions_per_second.data.len(), 0);
graph.record_transactions_per_second(0);
graph.record_transactions_per_second(0);
graph.record_transactions_per_second(0);
graph.record_transactions_per_second(1);
graph.record_transactions_per_second(2);
graph.record_transactions_per_second(2);
graph.record_transactions_per_second(2);
graph.record_transactions_per_second(2);
graph.record_transactions_per_second(2);
assert_eq!(graph.transactions_per_second.data.len(), 3);
assert_eq!(graph.transactions_per_second.data[0], 3);
assert_eq!(graph.transactions_per_second.data[1], 1);
assert_eq!(graph.transactions_per_second.data[2], 5);
assert_eq!(graph.transactions_per_second.total(), 9);
graph.record_transactions_per_second(100);
graph.record_transactions_per_second(100);
graph.record_transactions_per_second(100);
graph.record_transactions_per_second(0);
graph.record_transactions_per_second(1);
graph.record_transactions_per_second(2);
graph.record_transactions_per_second(5);
assert_eq!(graph.transactions_per_second.data.len(), 101);
assert_eq!(graph.transactions_per_second.data[0], 4);
assert_eq!(graph.transactions_per_second.data[1], 2);
assert_eq!(graph.transactions_per_second.data[2], 6);
assert_eq!(graph.transactions_per_second.data[3], 0);
assert_eq!(graph.transactions_per_second.data[4], 0);
assert_eq!(graph.transactions_per_second.data[5], 1);
assert_eq!(graph.transactions_per_second.data[100], 3);
for second in 6..100 {
assert_eq!(graph.transactions_per_second.data[second], 0);
}
assert_eq!(graph.transactions_per_second.total(), 16);
}
#[test]
fn test_record_users_per_second() {
let mut graph = GraphData::new();
assert_eq!(graph.users_per_second.data.len(), 0);
graph.record_users_per_second(1, 0);
graph.record_users_per_second(1, 1);
graph.record_users_per_second(2, 2);
assert_eq!(graph.users_per_second.data.len(), 3);
assert_eq!(graph.users_per_second.data[0], 1);
assert_eq!(graph.users_per_second.data[1], 1);
assert_eq!(graph.users_per_second.data[2], 2);
assert_eq!(graph.users_per_second.total(), 6);
graph.record_users_per_second(5, 5);
graph.record_users_per_second(10, 37);
assert_eq!(graph.users_per_second.data.len(), 38);
assert_eq!(graph.users_per_second.data[0], 1);
assert_eq!(graph.users_per_second.data[1], 1);
assert_eq!(graph.users_per_second.data[2], 2);
assert_eq!(graph.users_per_second.data[3], 2);
assert_eq!(graph.users_per_second.data[4], 2);
assert_eq!(graph.users_per_second.data[5], 5);
assert_eq!(graph.users_per_second.data[37], 10);
for second in 6..36 {
assert_eq!(graph.users_per_second.data[second], 5);
}
assert_eq!(graph.users_per_second.total(), 187);
}
#[test]
fn test_moving_average() {
let mut moving_average = MovingAverage::new();
assert_eq!(
moving_average,
MovingAverage {
count: 0,
average: 0.
}
);
moving_average.add_item(&1.23);
assert_eq!(
moving_average,
MovingAverage {
count: 1,
average: 1.23
}
);
moving_average.add_item(&2.34);
assert_eq!(
moving_average,
MovingAverage {
count: 2,
average: 1.785
}
);
moving_average.add_item(&89.23);
assert_eq!(
moving_average,
MovingAverage {
count: 3,
average: 30.933332
}
);
moving_average.add_item(&12.34);
assert_eq!(
moving_average,
MovingAverage {
count: 4,
average: 26.285
}
);
}
#[test]
fn test_moving_average_cmp() {
assert!(
MovingAverage {
count: 0,
average: 0.
} < MovingAverage {
count: 0,
average: 0.1,
}
);
assert!(
MovingAverage {
count: 0,
average: 1.1,
} > MovingAverage {
count: 0,
average: 0.1,
}
);
assert_eq!(
MovingAverage {
count: 1,
average: 1.1,
},
MovingAverage {
count: 2,
average: 1.1,
}
);
assert!(
MovingAverage {
count: 0,
average: 1.1,
} != MovingAverage {
count: 0,
average: 1.0,
}
);
}
#[test]
fn test_add_timestamp_to_html_graph_data() {
let data = vec![123, 234, 345, 456, 567];
let graph: Graph<usize, usize> = Graph::new("html_id", "Label", true, HashMap::new());
assert_eq!(
graph.add_timestamp_to_html_graph_data(
&data,
Utc.with_ymd_and_hms(2021, 12, 14, 15, 12, 23).unwrap()
),
vec![
(
Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 12, 14, 15, 12, 23)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
123
),
(
Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 12, 14, 15, 12, 24)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
234
),
(
Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 12, 14, 15, 12, 25)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
345
),
(
Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 12, 14, 15, 12, 26)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
456
),
(
Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 12, 14, 15, 12, 27)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
567
)
]
);
}
fn expected_graph_html_prefix(
html_id: &str,
y_axis_label: &str,
main_series_label: &str,
) -> String {
format!(
r#"<div class="graph">
<div id="{html_id}" style="width: 1000px; height:500px; background: white;"></div>
<script type="text/javascript">
var chartDom = document.getElementById('{html_id}');
var myChart = echarts.init(chartDom);
myChart.setOption({{
color: ['#2c664f', '#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
tooltip: {{ trigger: 'axis' }},
toolbox: {{
feature: {{
dataZoom: {{ yAxisIndex: 'none' }},
restore: {{}},
saveAsImage: {{}}
}}
}},
dataZoom: [
{{
type: 'inside',
start: 0,
end: 100,
fillerColor: 'rgba(34, 80, 61, 0.25)',
selectedDataBackground: {{
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }}
}}
}},
{{
start: 0,
end: 100,
fillerColor: 'rgba(34, 80, 61, 0.25)',
selectedDataBackground: {{
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }}
}}
}},
],
xAxis: {{ type: 'time' }},
yAxis: {{
name: '{y_axis_label}',
nameLocation: 'center',
nameRotate: 90,
nameGap: 45,
type: 'value'
}},
series: [
{{
name: '{main_series_label}',
type: 'line',
symbol: 'none',
sampling: 'lttb',
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }},
markArea: {{
"#,
html_id = html_id,
y_axis_label = y_axis_label,
main_series_label = main_series_label
)
}
#[test]
fn test_graph_markup() {
let expected_prefix = expected_graph_html_prefix("graph-rps", "Requests #", "GET /");
let data: TimeSeries<usize, usize> = TimeSeries {
data: vec![123, 111, 99, 134],
phantom: PhantomData,
total: 0,
};
let mut graph = HashMap::new();
graph.insert("GET /".to_string(), data);
let mut expected = expected_prefix.to_owned();
expected += format!(
r#" data: [
]
}},
data: [["{data_series_prefix}:32",123],["{data_series_prefix}:33",111],["{data_series_prefix}:34",99],["{data_series_prefix}:35",134]],
}},
]
}});
</script>
</div>"#,
data_series_prefix = Local
.timestamp_opt(Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap().timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M")
).as_str();
assert_eq!(
Graph::new("graph-rps", "Requests #", true, graph.clone(),).get_markup(
&Vec::new(),
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap()
),
expected
);
assert_eq!(
Graph::new("graph-rps", "Requests #", false, graph.clone(),).get_markup(
&Vec::new(),
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap()
),
expected
);
let mut expected = expected_prefix;
expected += format!(
r#" data: [
[
{{
xAxis: '{increasing}',
itemStyle: {{ borderColor: 'rgba(44, 102, 79, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{increasing}'
}}
],
[
{{
xAxis: '{increasing}',
itemStyle: {{ color: 'rgba(44, 102, 79, 0.05)' }},
}},
{{
xAxis: '{decreasing}'
}}
],
[
{{
xAxis: '{decreasing}',
itemStyle: {{ borderColor: 'rgba(44, 102, 79, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{decreasing}'
}}
],[
{{
xAxis: '{decreasing}',
itemStyle: {{ borderColor: 'rgba(179, 65, 65, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{decreasing}'
}}
],
[
{{
xAxis: '{decreasing}',
itemStyle: {{ color: 'rgba(179, 65, 65, 0.05)' }},
}},
{{
xAxis: '{cancelling}'
}}
],
[
{{
xAxis: '{cancelling}',
itemStyle: {{ borderColor: 'rgba(179, 65, 65, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{cancelling}'
}}
],[
{{
xAxis: '{cancelling}',
itemStyle: {{ borderColor: 'rgba(179, 65, 65, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{cancelling}'
}}
],
[
{{
xAxis: '{cancelling}',
itemStyle: {{ color: 'rgba(179, 65, 65, 0.05)' }},
}},
{{
xAxis: '{finishing}'
}}
],
[
{{
xAxis: '{finishing}',
itemStyle: {{ borderColor: 'rgba(179, 65, 65, 0.25)', borderWidth: 1 }},
}},
{{
xAxis: '{finishing}'
}}
],
]
}},
data: [["{data_series_prefix}:32",123],["{data_series_prefix}:33",111],["{data_series_prefix}:34",99],["{data_series_prefix}:35",134]],
}},
]
}});
</script>
</div>"#,
data_series_prefix = Local
.timestamp_opt(Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap().timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M"),
increasing = Local
.timestamp_opt(Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap().timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M:%S"),
decreasing = Local
.timestamp_opt(Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 33).unwrap().timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M:%S"),
cancelling = Local
.timestamp_opt(Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 34).unwrap().timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M:%S"),
finishing = Local
.timestamp_opt(Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 35).unwrap().timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M:%S"),
).as_str();
let steps = vec![
TestPlanHistory {
action: TestPlanStepAction::Increasing,
timestamp: Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap(),
users: 123,
},
TestPlanHistory {
action: TestPlanStepAction::Decreasing,
timestamp: Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 33).unwrap(),
users: 123,
},
TestPlanHistory {
action: TestPlanStepAction::Canceling,
timestamp: Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 34).unwrap(),
users: 123,
},
TestPlanHistory {
action: TestPlanStepAction::Finished,
timestamp: Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 35).unwrap(),
users: 123,
},
];
assert_eq!(
Graph::new("graph-rps", "Requests #", true, graph.clone(),).get_markup(
&steps,
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap()
),
expected
);
assert_eq!(
Graph::new("graph-rps", "Requests #", false, graph.clone(),).get_markup(
&steps,
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap()
),
expected
);
let user_data: TimeSeries<usize, usize> = TimeSeries {
data: vec![23, 12, 44, 22],
phantom: PhantomData,
total: 0,
};
graph.insert("GET /user".to_string(), user_data);
let markup = Graph::new("graph-rps", "Requests #", true, graph.clone()).get_markup(
&Vec::new(),
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap(),
);
let expected_legend = r#"
legend: {
type: 'plain',
width: '75%',
data: ["Total","GET /"#;
assert!(
markup.contains(expected_legend),
"legend {} not found in {}",
expected_legend,
markup
);
let expected_line = format!(
r#"{{
name: 'Total',
type: 'line',
symbol: 'none',
sampling: 'lttb',
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }},
markArea: {{
data: [
]
}},
data: [["{data_series_prefix}:32",146],["{data_series_prefix}:33",123],["{data_series_prefix}:34",143],["{data_series_prefix}:35",156]],
}},"#,
data_series_prefix = Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M"),
);
assert!(
markup.contains(expected_line.as_str()),
"line {} not found in {}",
expected_line,
markup
);
let expected_line = format!(
r#"{{
name: 'GET /',
type: 'line',
symbol: 'none',
sampling: 'lttb',
data: [["{data_series_prefix}:32",123],["{data_series_prefix}:33",111],["{data_series_prefix}:34",99],["{data_series_prefix}:35",134]],
}},"#,
data_series_prefix = Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M"),
);
assert!(
markup.contains(expected_line.as_str()),
"line {} not found in {}",
expected_line,
markup
);
let expected_line = format!(
r#"{{
name: 'GET /user',
type: 'line',
symbol: 'none',
sampling: 'lttb',
data: [["{data_series_prefix}:32",23],["{data_series_prefix}:33",12],["{data_series_prefix}:34",44],["{data_series_prefix}:35",22]],
}},"#,
data_series_prefix = Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M"),
);
assert!(
markup.contains(expected_line.as_str()),
"line {} not found in {}",
expected_line,
markup
);
let expected_nongranular_prefix =
expected_graph_html_prefix("graph-rps", "Requests #", "Total");
let mut expected = expected_nongranular_prefix.to_owned();
expected += format!(
r#" data: [
]
}},
data: [["{data_series_prefix}:32",146],["{data_series_prefix}:33",123],["{data_series_prefix}:34",143],["{data_series_prefix}:35",156]],
}},
]
}});
</script>
</div>"#,
data_series_prefix = Local
.timestamp_opt(Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap().timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M")
).as_str();
assert_eq!(
Graph::new("graph-rps", "Requests #", false, graph.clone(),).get_markup(
&Vec::new(),
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap()
),
expected
);
let more_data: TimeSeries<usize, usize> = TimeSeries {
data: vec![1, 1, 1, 1],
phantom: PhantomData,
total: 0,
};
graph.insert("GET /one".to_string(), more_data.clone());
graph.insert("GET /two".to_string(), more_data.clone());
graph.insert("GET /three".to_string(), more_data);
let markup = Graph::new("graph-rps", "Requests #", true, graph.clone()).get_markup(
&Vec::new(),
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap(),
);
let expected_legend = r#"
legend: {
type: 'scroll',
width: '75%',
data: ["Total","GET /"#;
assert!(
markup.contains(expected_legend),
"legend {} not found in {}",
expected_legend,
markup
);
let expected_line = format!(
r#"{{
name: 'Total',
type: 'line',
symbol: 'none',
sampling: 'lttb',
lineStyle: {{ color: '#2c664f' }},
areaStyle: {{ color: '#378063' }},
markArea: {{
data: [
]
}},
data: [["{data_series_prefix}:32",149],["{data_series_prefix}:33",126],["{data_series_prefix}:34",146],["{data_series_prefix}:35",159]],
}},"#,
data_series_prefix = Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M"),
);
assert!(
markup.contains(expected_line.as_str()),
"line {} not found in {}",
expected_line,
markup
);
let expected_line = format!(
r#"{{
name: 'GET /',
type: 'line',
symbol: 'none',
sampling: 'lttb',
data: [["{data_series_prefix}:32",123],["{data_series_prefix}:33",111],["{data_series_prefix}:34",99],["{data_series_prefix}:35",134]],
}},"#,
data_series_prefix = Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M"),
);
assert!(
markup.contains(expected_line.as_str()),
"line {} not found in {}",
expected_line,
markup
);
let expected_line = format!(
r#"{{
name: 'GET /user',
type: 'line',
symbol: 'none',
sampling: 'lttb',
data: [["{data_series_prefix}:32",23],["{data_series_prefix}:33",12],["{data_series_prefix}:34",44],["{data_series_prefix}:35",22]],
}},"#,
data_series_prefix = Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M"),
);
assert!(
markup.contains(expected_line.as_str()),
"line {} not found in {}",
expected_line,
markup
);
let expected_line = format!(
r#"{{
name: 'GET /one',
type: 'line',
symbol: 'none',
sampling: 'lttb',
data: [["{data_series_prefix}:32",1],["{data_series_prefix}:33",1],["{data_series_prefix}:34",1],["{data_series_prefix}:35",1]],
}},"#,
data_series_prefix = Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M"),
);
assert!(
markup.contains(expected_line.as_str()),
"line {} not found in {}",
expected_line,
markup
);
let expected_line = format!(
r#"{{
name: 'GET /two',
type: 'line',
symbol: 'none',
sampling: 'lttb',
data: [["{data_series_prefix}:32",1],["{data_series_prefix}:33",1],["{data_series_prefix}:34",1],["{data_series_prefix}:35",1]],
}},"#,
data_series_prefix = Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M"),
);
assert!(
markup.contains(expected_line.as_str()),
"line {} not found in {}",
expected_line,
markup
);
let expected_line = format!(
r#"{{
name: 'GET /three',
type: 'line',
symbol: 'none',
sampling: 'lttb',
data: [["{data_series_prefix}:32",1],["{data_series_prefix}:33",1],["{data_series_prefix}:34",1],["{data_series_prefix}:35",1]],
}},"#,
data_series_prefix = Local
.timestamp_opt(
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32)
.unwrap()
.timestamp(),
0
)
.unwrap()
.format("%Y-%m-%d %H:%M"),
);
assert!(
markup.contains(expected_line.as_str()),
"line {} not found in {}",
expected_line,
markup
);
let mut expected = expected_nongranular_prefix;
expected += format!(
r#" data: [
]
}},
data: [["{data_series_prefix}:32",149],["{data_series_prefix}:33",126],["{data_series_prefix}:34",146],["{data_series_prefix}:35",159]],
}},
]
}});
</script>
</div>"#,
data_series_prefix = Local
.timestamp_opt(Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap().timestamp(), 0)
.unwrap()
.format("%Y-%m-%d %H:%M")
).as_str();
assert_eq!(
Graph::new("graph-rps", "Requests #", false, graph.clone(),).get_markup(
&Vec::new(),
Utc.with_ymd_and_hms(2021, 11, 21, 21, 20, 32).unwrap()
),
expected
);
}
}