Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 96 additions & 6 deletions src/export/csv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ impl Exporter for CsvExporter {
) -> Result<Vec<u8>> {
let mut writer = WriterBuilder::new().from_writer(vec![]);

// Collect parameter names from the first result that has any parameters.
// The reference command (if present) has no parameters, so using .first()
// would produce a header with no parameter columns, causing a field-count
// mismatch when the parameterised rows are written.
let param_names: Vec<&str> = results
.iter()
.find(|r| !r.parameters.is_empty())
.map(|r| r.parameters.keys().map(String::as_str).collect())
.unwrap_or_default();

{
let mut headers: Vec<Cow<[u8]>> = [
// The list of times and exit codes cannot be exported to the CSV file - omit them.
Expand All @@ -29,10 +39,8 @@ impl Exporter for CsvExporter {
.iter()
.map(|x| Cow::Borrowed(x.as_bytes()))
.collect();
if let Some(res) = results.first() {
for param_name in res.parameters.keys() {
headers.push(Cow::Owned(format!("parameter_{param_name}").into_bytes()));
}
for param_name in &param_names {
headers.push(Cow::Owned(format!("parameter_{param_name}").into_bytes()));
}
writer.write_record(headers)?;
}
Expand All @@ -50,8 +58,11 @@ impl Exporter for CsvExporter {
] {
fields.push(Cow::Owned(f.to_string().into_bytes()))
}
for v in res.parameters.values() {
fields.push(Cow::Borrowed(v.as_bytes()))
// Emit each parameter value in order, or empty string when the result
// has no parameters (e.g. the reference command).
for name in &param_names {
let value = res.parameters.get(*name).map(String::as_str).unwrap_or("");
fields.push(Cow::Owned(value.as_bytes().to_vec()));
}
writer.write_record(fields)?;
}
Expand Down Expand Up @@ -121,3 +132,82 @@ fn test_csv() {
command_b,11,12,11,13,14,15,16.5,seven,one
"#);
}

#[test]
fn test_csv_with_reference() {
use std::collections::BTreeMap;
let exporter = CsvExporter::default();

// Reference command has no parameters; parameterised runs follow.
// The CSV header must include parameter columns and the reference row
// must emit empty strings for those columns.
let results = vec![
BenchmarkResult {
command: String::from("sleep 1"),
command_with_unused_parameters: String::from("sleep 1"),
mean: 1.0,
stddev: Some(0.0),
median: 1.0,
user: 0.0,
system: 0.0,
min: 1.0,
max: 1.0,
times: Some(vec![1.0]),
memory_usage_byte: None,
exit_codes: vec![Some(0)],
parameters: BTreeMap::new(),
},
BenchmarkResult {
command: String::from("sleep 2"),
command_with_unused_parameters: String::from("sleep {secs}"),
mean: 2.0,
stddev: Some(0.0),
median: 2.0,
user: 0.0,
system: 0.0,
min: 2.0,
max: 2.0,
times: Some(vec![2.0]),
memory_usage_byte: None,
exit_codes: vec![Some(0)],
parameters: {
let mut params = BTreeMap::new();
params.insert("secs".into(), "2".into());
params
},
},
BenchmarkResult {
command: String::from("sleep 3"),
command_with_unused_parameters: String::from("sleep {secs}"),
mean: 3.0,
stddev: Some(0.0),
median: 3.0,
user: 0.0,
system: 0.0,
min: 3.0,
max: 3.0,
times: Some(vec![3.0]),
memory_usage_byte: None,
exit_codes: vec![Some(0)],
parameters: {
let mut params = BTreeMap::new();
params.insert("secs".into(), "3".into());
params
},
},
];

let actual = String::from_utf8(
exporter
.serialize(&results, Some(Unit::Second), SortOrder::Command)
.unwrap(),
)
.unwrap();

insta::assert_snapshot!(actual, @r#"
command,mean,stddev,median,user,system,min,max,parameter_secs
sleep 1,1,0,1,0,0,1,1,
sleep 2,2,0,2,0,0,2,2,2
sleep 3,3,0,3,0,0,3,3,3
"#);
}