diff --git a/src/metrics.rs b/src/metrics.rs index 77966b01..3716dc19 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -3360,7 +3360,15 @@ fn determine_precision(value: f32) -> usize { /// Format large number in locale appropriate style. pub(crate) fn format_number(number: usize) -> String { - (number).to_formatted_string(&Locale::en) + number.to_formatted_string(&Locale::en) +} + +/// Format large value in locale appropriate style. +pub(crate) fn format_value(value: &Value) -> String +where + T: DeltaValue + ToFormattedString, +{ + value.to_formatted_number(&Locale::en) } /// A helper function that merges together times. diff --git a/src/metrics/delta.rs b/src/metrics/delta.rs index 24a52ce9..ac608b17 100644 --- a/src/metrics/delta.rs +++ b/src/metrics/delta.rs @@ -1,9 +1,14 @@ -use num_format::ToFormattedStr; +use num_format::{Format, ToFormattedString}; use std::fmt::{Debug, Display, Formatter, Write}; +/// A value that can be used to provide a delta +/// +/// As the actual value can be an unsigned type, we require an associated type which defines the +/// type of the delta. pub trait DeltaValue: Copy + Debug + Display { type Delta: Copy + Display; + /// Generate the delta between this and the provided value fn delta(self, value: Self) -> Self::Delta; /// It's positive if it's not negative or zero @@ -23,7 +28,7 @@ impl DeltaValue for usize { if delta > 9223372036854775808 /* the absolute value of isize::MIN as usize */ { - // ... which is too big to fix into the negative space of isize, so we limit to isize::MIN + // ... which is too big to fit into the negative space of isize, so we limit to isize::MIN isize::MIN } else { // ... which fits, so we return the negative value @@ -49,6 +54,7 @@ impl DeltaValue for f32 { } } +/// A value, being either a plain value of a value with delta to a baseline #[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] #[serde(untagged)] pub(crate) enum Value { @@ -81,6 +87,29 @@ impl Value { } } +impl Value +where + T: DeltaValue + ToFormattedString, +{ + pub fn to_formatted_number(&self, format: &impl Format) -> String { + match self { + Self::Plain(value) => value.to_formatted_string(format), + Self::Delta { value, delta } => { + let s = if T::is_delta_positive(*delta) { + "+" + } else { + "" + }; + format!( + "{} ({s}{})", + value.to_formatted_string(format), + delta.to_formatted_string(format) + ) + } + } + } +} + impl DeltaEval for Value { fn eval(&mut self, other: Self) { self.diff(other.value()) @@ -141,22 +170,11 @@ pub trait DeltaTo { fn delta_to(&mut self, other: &Self); } -pub struct Formatted(pub T); - -impl Display for Formatted { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - use num_format::{Locale, ToFormattedString}; - - f.write_str(&self.0.to_formatted_string(&Locale::en))?; - - Ok(()) - } -} - #[cfg(test)] mod test { use super::*; use crate::metrics::Value; + use num_format::Locale; #[test] fn eval_optional() { @@ -181,4 +199,54 @@ mod test { None ); } + + #[test] + fn delta_to_string() { + assert_eq!(format!("{}", 0.delta(10)), "-10"); + assert_eq!(format!("{}", 10.delta(10)), "0"); + assert_eq!(format!("{}", 10.delta(0)), "10"); + } + + #[test] + fn value_to_string() { + fn value(value: T, baseline: T) -> Value { + let mut result = Value::from(value); + result.diff(baseline); + result + } + + assert_eq!(format!("{}", value(0, 1000)), "0 (-1000)"); + assert_eq!(format!("{}", value(1000, 1000)), "1000 (0)"); + assert_eq!(format!("{}", value(1000, 0)), "1000 (+1000)"); + } + + #[test] + fn value_with_delta_to_string_num() { + fn value(value: T, baseline: T) -> Value { + let mut result = Value::from(value); + result.diff(baseline); + result + } + + assert_eq!( + format!("{}", value(0, 1000).to_formatted_number(&Locale::en)), + "0 (-1,000)" + ); + assert_eq!( + format!("{}", value(1000, 1000).to_formatted_number(&Locale::en)), + "1,000 (0)" + ); + assert_eq!( + format!("{}", value(1000, 0).to_formatted_number(&Locale::en)), + "1,000 (+1,000)" + ); + } + + #[test] + fn value_to_string_num() { + assert_eq!( + format!("{}", Value::from(1000).to_formatted_number(&Locale::en)), + "1,000" + ); + } } diff --git a/src/report.rs b/src/report.rs index b3e07175..bd21a91d 100644 --- a/src/report.rs +++ b/src/report.rs @@ -4,9 +4,9 @@ mod markdown; pub(crate) use markdown::write_markdown_report; -use crate::goose::GooseMethod; use crate::{ - metrics::{self, DeltaEval, DeltaTo, Value}, + goose::GooseMethod, + metrics::{self, format_value, DeltaEval, DeltaTo, Value}, report::common::OrEmpty, }; use serde::{Deserialize, Serialize}; @@ -255,14 +255,14 @@ pub(crate) fn response_metrics_row(metric: ResponseMetric) -> String { "#, method = metric.method, name = metric.name, - percentile_50 = metric.percentile_50, - percentile_60 = metric.percentile_60, - percentile_70 = metric.percentile_70, - percentile_80 = metric.percentile_80, - percentile_90 = metric.percentile_90, - percentile_95 = metric.percentile_95, - percentile_99 = metric.percentile_99, - percentile_100 = metric.percentile_100, + percentile_50 = format_value(&metric.percentile_50), + percentile_60 = format_value(&metric.percentile_60), + percentile_70 = format_value(&metric.percentile_70), + percentile_80 = format_value(&metric.percentile_80), + percentile_90 = format_value(&metric.percentile_90), + percentile_95 = format_value(&metric.percentile_95), + percentile_99 = format_value(&metric.percentile_99), + percentile_100 = format_value(&metric.percentile_100), ) } @@ -358,14 +358,14 @@ pub(crate) fn coordinated_omission_response_metrics_row(metric: ResponseMetric) "#, method = metric.method, name = metric.name, - percentile_50 = metric.percentile_50, - percentile_60 = metric.percentile_60, - percentile_70 = metric.percentile_70, - percentile_80 = metric.percentile_80, - percentile_90 = metric.percentile_90, - percentile_95 = metric.percentile_95, - percentile_99 = metric.percentile_99, - percentile_100 = metric.percentile_100, + percentile_50 = format_value(&metric.percentile_50), + percentile_60 = format_value(&metric.percentile_60), + percentile_70 = format_value(&metric.percentile_70), + percentile_80 = format_value(&metric.percentile_80), + percentile_90 = format_value(&metric.percentile_90), + percentile_95 = format_value(&metric.percentile_95), + percentile_99 = format_value(&metric.percentile_99), + percentile_100 = format_value(&metric.percentile_100), ) } @@ -460,8 +460,8 @@ pub(crate) fn transaction_metrics_row(metric: TransactionMetric) -> String { "#, transaction = metric.transaction, name = metric.name, - number_of_requests = metric.number_of_requests, - number_of_failures = metric.number_of_failures, + number_of_requests = format_value(&metric.number_of_requests), + number_of_failures = format_value(&metric.number_of_failures), response_time_average = OrEmpty(metric.response_time_average), response_time_minimum = metric.response_time_minimum, response_time_maximum = metric.response_time_maximum, @@ -516,8 +516,8 @@ pub(crate) fn scenario_metrics_row(metric: ScenarioMetric) -> String { {iterations:.2} "#, name = metric.name, - users = metric.users, - count = metric.count, + users = format_value(&metric.users), + count = format_value(&metric.count), response_time_average = metric.response_time_average, response_time_minimum = metric.response_time_minimum, response_time_maximum = metric.response_time_maximum, diff --git a/src/report/common.rs b/src/report/common.rs index ede83130..2ae30502 100644 --- a/src/report/common.rs +++ b/src/report/common.rs @@ -1,3 +1,4 @@ +use num_format::{Locale, ToFormattedString}; use std::fmt::{Display, Formatter}; #[derive(Clone, Debug, PartialEq, Eq)] @@ -12,14 +13,34 @@ impl Display for OrEmpty { } } +pub struct FormattedNumber(pub T) +where + T: ToFormattedString; + +impl Display for FormattedNumber +where + T: ToFormattedString, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0.to_formatted_string(&Locale::en)) + } +} + #[cfg(test)] mod test { - use crate::report::common::OrEmpty; + use crate::report::common::{FormattedNumber, OrEmpty}; #[test] - pub fn format() { + pub fn format_or_empty() { assert_eq!("1.23", format!("{:.2}", OrEmpty(Some(1.23456)))); assert_eq!("1", format!("{:.0}", OrEmpty(Some(1.23456)))); assert_eq!("", format!("{:.2}", OrEmpty::(None))); } + + #[test] + pub fn format_number_format() { + assert_eq!("1", format!("{:.2}", FormattedNumber(1))); + assert_eq!("1,000", format!("{:.2}", FormattedNumber(1000))); + assert_eq!("1,000,000", format!("{:.2}", FormattedNumber(1000000))); + } } diff --git a/src/report/markdown.rs b/src/report/markdown.rs index 7d1b6478..bb42e21d 100644 --- a/src/report/markdown.rs +++ b/src/report/markdown.rs @@ -1,3 +1,4 @@ +use crate::metrics::format_value; use crate::{ metrics::ReportData, report::{ @@ -161,6 +162,14 @@ impl<'m, 'w, W: Write> Markdown<'m, 'w, W> { writeln!( self.w, r#"| {method} | {name} | {percentile_50} | {percentile_60 } | {percentile_70 } | {percentile_80} | {percentile_90} | {percentile_95} | {percentile_99} | {percentile_100} |"#, + percentile_50 = format_value(percentile_50), + percentile_60 = format_value(percentile_60), + percentile_70 = format_value(percentile_70), + percentile_80 = format_value(percentile_80), + percentile_90 = format_value(percentile_90), + percentile_95 = format_value(percentile_95), + percentile_99 = format_value(percentile_99), + percentile_100 = format_value(percentile_100), )?; }