use std::collections::HashMap;
use std::fmt::{Display, Formatter, Write};
use std::sync::atomic::Ordering;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use tokio::runtime;
use crate::Arg;
use crate::statistics::Statistics;
#[derive(Debug, Deserialize, Serialize)]
pub struct Output {
pub avg_req_per_second: f64,
pub stdev_per_second: f64,
pub max_req_per_second: f64,
pub avg_req_used_time: Micros,
pub stdev_req_used_time: Micros,
pub max_req_used_time: Micros,
pub latencies: Vec<Latency>,
pub rsp1xx: u64,
pub rsp2xx: u64,
pub rsp3xx: u64,
pub rsp4xx: u64,
pub rsp5xx: u64,
pub rsp_others: u64,
pub errors: HashMap<String, u64>,
pub throughput: f64,
}
impl Output {
pub(crate) async fn from_statistics(s: &Statistics) -> Self {
Self {
avg_req_per_second: *(s.avg_req_per_second.lock().await),
stdev_per_second: *(s.stdev_per_second.lock().await),
max_req_per_second: *(s.max_req_per_second.lock().await),
avg_req_used_time: (*(s.avg_req_used_time.lock().await)).into(),
stdev_req_used_time: (*(s.stdev_req_used_time.lock().await)).into(),
max_req_used_time: (*(s.max_req_used_time.lock().await)).into(),
latencies: (*(s.latencies.lock().await).clone())
.to_owned()
.iter()
.map(|x| Latency::new(x.0, x.1.into()))
.collect(),
rsp1xx: s.rsp1xx.load(Ordering::Acquire),
rsp2xx: s.rsp2xx.load(Ordering::Acquire),
rsp3xx: s.rsp3xx.load(Ordering::Acquire),
rsp4xx: s.rsp4xx.load(Ordering::Acquire),
rsp5xx: s.rsp5xx.load(Ordering::Acquire),
rsp_others: s.rsp_others.load(Ordering::Acquire),
errors: ((s.errors.lock().await).clone().to_owned()).to_owned(),
throughput: *(s.throughput.lock().await),
}
}
pub(crate) fn sync_from_statistics(s: &Statistics) -> anyhow::Result<Self> {
runtime::Builder::new_current_thread()
.build()?
.block_on(async { Ok(Self::from_statistics(s).await) })
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub struct Latency {
pub percent: f32,
pub micros: Micros,
}
impl Latency {
pub fn new(percent: f32, micros: Micros) -> Self {
Self { percent, micros }
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub struct Micros(u64);
impl From<Duration> for Micros {
fn from(duration: Duration) -> Self {
Self(duration.as_micros() as u64)
}
}
impl From<&Duration> for Micros {
fn from(duration: &Duration) -> Self {
Self(duration.as_micros() as u64)
}
}
impl Display for Micros {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let duration = Duration::from_micros(self.0);
write!(f, "{duration:.2?}")
}
}
pub(crate) fn sync_text_output(
s: &Statistics,
arg: &Arg,
) -> anyhow::Result<String> {
runtime::Builder::new_current_thread()
.build()?
.block_on(text_output(s, arg))
}
pub(crate) async fn text_output(
s: &Statistics,
arg: &Arg,
) -> anyhow::Result<String> {
let mut output = String::new();
writeln!(
&mut output,
"{:<14}{:^14}{:^14}{:^14}
{:<12}{:^14.2}{:^14.2}{:^14.2}
{:<12}{:^14}{:^14}{:^14}",
"Statistics",
"Avg",
"Stdev",
"Max",
"Reqs/sec",
*(s.avg_req_per_second.lock().await),
*(s.stdev_per_second.lock().await),
*(s.max_req_per_second.lock().await),
"Latency",
format!("{:.2?}", *(s.avg_req_used_time.lock().await)),
format!("{:.2?}", *(s.stdev_req_used_time.lock().await)),
format!("{:.2?}", *(s.max_req_used_time.lock().await)),
)?;
if arg.latencies {
let latencies = &*(s.latencies.lock().await);
if !latencies.is_empty() {
writeln!(&mut output, " {:<20}", "Latency Distribution")?;
for (percent, duration) in latencies {
writeln!(
&mut output,
" {:^10}{:^10}",
format!("{:.0}%", *percent * 100f32),
format!("{:.2?}", *duration),
)?;
}
}
}
writeln!(&mut output, " {:<20}", "HTTP codes:")?;
writeln!(
&mut output,
" 1XX - {}, 2XX - {}, 3XX - {}, 4XX - {}, 5XX - {}",
s.rsp1xx.load(Ordering::Acquire),
s.rsp2xx.load(Ordering::Acquire),
s.rsp3xx.load(Ordering::Acquire),
s.rsp4xx.load(Ordering::Acquire),
s.rsp5xx.load(Ordering::Acquire),
)?;
writeln!(
&mut output,
" others - {}",
s.rsp_others.load(Ordering::Acquire)
)?;
let errors = s.errors.lock().await;
if !errors.is_empty() {
writeln!(&mut output, " {:<10}", "Errors:")?;
for (k, v) in &*errors {
writeln!(&mut output, " \"{k:>}\":{v:>8}")?;
}
}
write!(
&mut output,
" {:<12}{:>10.2}/s",
"Throughput:",
*(s.throughput.lock().await)
)?;
Ok(output)
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::arg::{Method, OutputFormat};
#[test]
fn test_micros_convert() {
let duration = Duration::from_micros(1);
let micros: Micros = duration.into();
assert_eq!("1.00µs", format!("{micros}"));
let duration = Duration::from_millis(1);
let micros: Micros = (&duration).into();
assert_eq!("1.00ms", format!("{micros}"));
let duration = Duration::from_millis(1);
let micros = Micros::from(duration);
assert_eq!("1.00ms", format!("{micros}"));
}
#[test]
fn test_latency_new() {
let latency = Latency::new(0.5, Micros(1000));
assert_eq!(latency.percent, 0.5);
assert_eq!(latency.micros.0, 1000);
}
#[test]
fn test_output_from_statistics() {
let stats = Statistics::new();
let rt = runtime::Builder::new_current_thread().build().unwrap();
rt.block_on(async {
let output = Output::from_statistics(&stats).await;
assert_eq!(output.rsp1xx, 0);
assert_eq!(output.rsp2xx, 0);
});
}
#[test]
fn test_sync_text_output() {
let stats = Statistics::new();
let arg = Arg {
url: Some("http://example.com".to_string()),
requests: Some(10),
connections: 1,
timeout: Duration::from_secs(30),
latencies: false,
percentiles: vec![],
method: Method::Get,
disable_keep_alive: false,
headers: vec![],
duration: None,
rate: None,
cert: None,
key: None,
insecure: false,
text_file: None,
text_body: None,
json_file: None,
json_body: None,
json_command: None,
form: vec![],
mp: vec![],
mp_file: vec![],
output_format: OutputFormat::Text,
completions: None,
};
let result = sync_text_output(&stats, &arg);
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("Statistics"));
}
#[test]
fn test_sync_text_output_with_latencies() {
let stats = Statistics::new();
let arg = Arg {
url: Some("http://example.com".to_string()),
requests: Some(10),
connections: 1,
timeout: Duration::from_secs(30),
latencies: true,
percentiles: vec![0.5, 0.9],
method: Method::Get,
disable_keep_alive: false,
headers: vec![],
duration: None,
rate: None,
cert: None,
key: None,
insecure: false,
text_file: None,
text_body: None,
json_file: None,
json_body: None,
json_command: None,
form: vec![],
mp: vec![],
mp_file: vec![],
output_format: OutputFormat::Text,
completions: None,
};
let result = sync_text_output(&stats, &arg);
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("Statistics"));
}
#[test]
fn test_sync_from_statistics() {
let stats = Statistics::new();
let result = Output::sync_from_statistics(&stats);
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output.rsp1xx, 0);
assert_eq!(output.rsp2xx, 0);
}
#[test]
fn test_output_serialization() {
let output = Output {
avg_req_per_second: 100.0,
stdev_per_second: 10.0,
max_req_per_second: 150.0,
avg_req_used_time: Micros(1000),
stdev_req_used_time: Micros(200),
max_req_used_time: Micros(5000),
latencies: vec![Latency::new(0.5, Micros(1500))],
rsp1xx: 0,
rsp2xx: 100,
rsp3xx: 0,
rsp4xx: 0,
rsp5xx: 0,
rsp_others: 0,
errors: std::collections::HashMap::new(),
throughput: 50.0,
};
let json = serde_json::to_string(&output);
assert!(json.is_ok());
let json_str = json.unwrap();
assert!(json_str.contains("avg_req_per_second"));
let deserialized: Result<Output, _> = serde_json::from_str(&json_str);
assert!(deserialized.is_ok());
let output_back = deserialized.unwrap();
assert_eq!(output.avg_req_per_second, output_back.avg_req_per_second);
}
#[test]
fn test_latency_display() {
let latency = Latency::new(0.5, Micros(1000));
assert_eq!(latency.percent, 0.5);
assert_eq!(latency.micros.0, 1000);
}
#[test]
fn test_micros_from_duration() {
let duration = Duration::from_secs(1);
let micros: Micros = duration.into();
assert_eq!(micros.0, 1_000_000);
let duration = Duration::from_millis(1);
let micros: Micros = duration.into();
assert_eq!(micros.0, 1_000);
let duration = Duration::from_micros(1);
let micros: Micros = duration.into();
assert_eq!(micros.0, 1);
}
#[test]
fn test_micros_display_various_formats() {
let micros = Micros(1);
assert_eq!("1.00µs", format!("{micros}"));
let micros = Micros(1_000);
assert_eq!("1.00ms", format!("{micros}"));
let micros = Micros(1_000_000);
assert_eq!("1.00s", format!("{micros}"));
let micros = Micros(1_500_000);
assert_eq!("1.50s", format!("{micros}"));
}
}