Skip to content

Commit

Permalink
feat: allow writing multiple reports, also in JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
ctron committed Jul 29, 2024
1 parent 3cbad85 commit ecdc0e3
Show file tree
Hide file tree
Showing 6 changed files with 478 additions and 377 deletions.
63 changes: 40 additions & 23 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ pub struct GooseConfiguration {
/// Doesn't display an error summary
#[options(no_short)]
pub no_error_summary: bool,
/// Create an html-formatted report
/// Create a report file
#[options(no_short, meta = "NAME")]
pub report_file: String,
pub report_file: Vec<String>,
/// Disable granular graphs in report file
#[options(no_short)]
pub no_granular_report: bool,
Expand Down Expand Up @@ -282,7 +282,7 @@ pub(crate) struct GooseDefaults {
/// An optional default for not displaying an error summary.
pub no_error_summary: Option<bool>,
/// An optional default for the html-formatted report file name.
pub report_file: Option<String>,
pub report_file: Option<Vec<String>>,
/// An optional default for the flag that disables granular data in HTML report graphs.
pub no_granular_report: Option<bool>,
/// An optional default for the requests log file name.
Expand Down Expand Up @@ -569,7 +569,7 @@ impl GooseDefaultType<&str> for GooseAttack {
Some(value.to_string())
}
}
GooseDefault::ReportFile => self.defaults.report_file = Some(value.to_string()),
GooseDefault::ReportFile => self.defaults.report_file = Some(vec![value.to_string()]),
GooseDefault::RequestLog => self.defaults.request_log = Some(value.to_string()),
GooseDefault::ScenarioLog => self.defaults.scenario_log = Some(value.to_string()),
GooseDefault::Scenarios => {
Expand Down Expand Up @@ -1161,6 +1161,24 @@ impl GooseConfigure<String> for GooseConfiguration {
None
}
}
impl GooseConfigure<Vec<String>> for GooseConfiguration {
/// Use [`GooseValue`] to set a [`String`] value.
fn get_value(&self, values: Vec<GooseValue<Vec<String>>>) -> Option<Vec<String>> {
for value in values {
if let Some(v) = value.value {
if value.filter {
continue;
} else {
if !value.message.is_empty() {
info!("{} = {:?}", value.message, v)
}
return Some(v);
}
}
}
None
}
}
impl GooseConfigure<bool> for GooseConfiguration {
/// Use [`GooseValue`] to set a [`bool`] value.
fn get_value(&self, values: Vec<GooseValue<bool>>) -> Option<bool> {
Expand Down Expand Up @@ -1563,23 +1581,22 @@ impl GooseConfiguration {
.unwrap_or(false);

// Configure `report_file`.
self.report_file = match self.get_value(vec![
// Use --report-file if set.
GooseValue {
value: Some(self.report_file.to_string()),
filter: self.report_file.is_empty(),
message: "report_file",
},
// Otherwise use GooseDefault if set.
GooseValue {
value: defaults.report_file.clone(),
filter: defaults.report_file.is_none(),
message: "report_file",
},
]) {
Some(v) => v,
None => "".to_string(),
};
self.report_file = self
.get_value(vec![
// Use --report-file if set.
GooseValue {
value: Some(self.report_file.clone()),
filter: self.report_file.is_empty(),
message: "report_file",
},
// Otherwise use GooseDefault if set.
GooseValue {
value: defaults.report_file.clone(),
filter: defaults.report_file.is_none(),
message: "report_file",
},
])
.unwrap_or_default();

// Configure `no_granular_report`.
self.no_debug_body = self
Expand Down Expand Up @@ -2013,7 +2030,7 @@ impl GooseConfiguration {
} else if !self.report_file.is_empty() {
return Err(GooseError::InvalidOption {
option: "`configuration.report_file`".to_string(),
value: self.report_file.to_string(),
value: format!("{:?}", self.report_file),
detail:
"`configuration.report_file` can not be set with `configuration.no_metrics`."
.to_string(),
Expand Down Expand Up @@ -2273,7 +2290,7 @@ mod test {
assert!(goose_attack.defaults.no_autostart == Some(true));
assert!(goose_attack.defaults.timeout == Some(timeout));
assert!(goose_attack.defaults.no_gzip == Some(true));
assert!(goose_attack.defaults.report_file == Some(report_file));
assert!(goose_attack.defaults.report_file == Some(vec![report_file]));
assert!(goose_attack.defaults.request_log == Some(request_log));
assert!(goose_attack.defaults.request_format == Some(GooseLogFormat::Raw));
assert!(goose_attack.defaults.error_log == Some(error_log));
Expand Down
55 changes: 22 additions & 33 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ pub enum GooseError {
Reqwest(reqwest::Error),
/// Wraps a ['tokio::task::JoinError'](https://tokio-rs.github.io/tokio/doc/tokio/task/struct.JoinError.html).
TokioJoin(tokio::task::JoinError),
/// Wraps a ['serde_json::Error'](https://docs.rs/serde_json/*/serde_json/struct.Error.html).
Serde(serde_json::Error),
/// Failed attempt to use code that requires a compile-time feature be enabled.
FeatureNotEnabled {
/// The missing compile-time feature.
Expand Down Expand Up @@ -160,6 +162,7 @@ impl GooseError {
match *self {
GooseError::Io(_) => "io::Error",
GooseError::Reqwest(_) => "reqwest::Error",
GooseError::Serde(_) => "serde_json::Error",
GooseError::TokioJoin(_) => "tokio::task::JoinError",
GooseError::FeatureNotEnabled { .. } => "required compile-time feature not enabled",
GooseError::InvalidHost { .. } => "failed to parse hostname",
Expand Down Expand Up @@ -228,6 +231,13 @@ impl From<tokio::task::JoinError> for GooseError {
}
}

/// Auto-convert serde_json errors.
impl From<serde_json::Error> for GooseError {
fn from(err: serde_json::Error) -> GooseError {
GooseError::Serde(err)
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
/// A [`GooseAttack`](./struct.GooseAttack.html) load test operates in one (and only one)
/// of the following modes.
Expand Down Expand Up @@ -803,17 +813,6 @@ impl GooseAttack {
self.attack_phase = phase;
}

// If enabled, returns the path of the report_file, otherwise returns None.
fn get_report_file_path(&mut self) -> Option<String> {
// Return if enabled.
if !self.configuration.report_file.is_empty() {
Some(self.configuration.report_file.to_string())
// Otherwise there is no report file.
} else {
None
}
}

// Display all scenarios (sorted by machine name).
fn print_scenarios(&self) {
let mut scenarios = BTreeMap::new();
Expand Down Expand Up @@ -951,8 +950,8 @@ impl GooseAttack {
);
print!("{}", self.metrics);

// Write an html report, if enabled.
self.write_html_report().await?;
// Write reports
self.write_reports().await?;
}

Ok(self.metrics)
Expand Down Expand Up @@ -1164,15 +1163,6 @@ impl GooseAttack {
Some(controller_request_rx)
}

// Prepare an asynchronous file writer for `report_file` (if enabled).
async fn prepare_report_file(&mut self) -> Result<Option<File>, GooseError> {
if let Some(report_file_path) = self.get_report_file_path() {
Ok(Some(File::create(&report_file_path).await?))
} else {
Ok(None)
}
}

// Invoke `test_start` transactions if existing.
async fn run_test_start(&self) -> Result<(), GooseError> {
// First run global test_start_transaction, if defined.
Expand Down Expand Up @@ -1548,8 +1538,8 @@ impl GooseAttack {
if !self.configuration.no_metrics {
println!("{}", self.metrics);
}
// Write an html report, if enabled.
self.write_html_report().await?;
// Write reports, if enabled.
self.write_reports().await?;
// Return to an Idle state.
self.set_attack_phase(goose_attack_run_state, AttackPhase::Idle);
}
Expand Down Expand Up @@ -1734,16 +1724,15 @@ impl GooseAttack {
goose_attack_run_state.parent_to_throttle_tx = parent_to_throttle_tx;

// If enabled, try to create the report file to confirm access.
let _report_file = match self.prepare_report_file().await {
Ok(f) => f,
Err(e) => {
return Err(GooseError::InvalidOption {
for file in &self.configuration.report_file {
let _ = File::create(&file)
.await
.map_err(|err| GooseError::InvalidOption {
option: "--report-file".to_string(),
value: self.get_report_file_path().unwrap(),
detail: format!("Failed to create report file: {}", e),
})
}
};
value: file.clone(),
detail: format!("Failed to create report file: {err}"),
})?;
}

// Record when the GooseAttack officially started.
self.started = Some(time::Instant::now());
Expand Down
Loading

0 comments on commit ecdc0e3

Please sign in to comment.