use std::{
fs::File,
io::{self, BufWriter, Write},
path::Path,
};
use crate::record::{BenchmarkRecord, SchemaMode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Tsv,
Json,
JsonPretty,
}
impl OutputFormat {
#[must_use]
pub fn extension(self) -> &'static str {
match self {
Self::Tsv => "tsv",
Self::Json | Self::JsonPretty => "json",
}
}
}
pub fn write_to_path(
record: &BenchmarkRecord,
path: &Path,
format: OutputFormat,
mode: SchemaMode,
) -> io::Result<()> {
ensure_parent_dir(path)?;
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
write_to(record, &mut writer, format, mode)?;
writer.flush()
}
pub(crate) fn ensure_parent_dir(path: &Path) -> io::Result<()> {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)?;
}
Ok(())
}
pub fn write_to<W: Write>(
record: &BenchmarkRecord,
writer: &mut W,
format: OutputFormat,
mode: SchemaMode,
) -> io::Result<()> {
match format {
OutputFormat::Tsv => writer.write_all(record.to_tsv_document(mode).as_bytes()),
OutputFormat::Json => {
let json = record.to_json(mode).map_err(io::Error::other)?;
writer.write_all(json.as_bytes())
}
OutputFormat::JsonPretty => {
let json = record.to_json_pretty(mode).map_err(io::Error::other)?;
writer.write_all(json.as_bytes())?;
writer.write_all(b"\n")
}
}
}
pub fn write_markdown_to_path(
record: &BenchmarkRecord,
path: &Path,
mode: SchemaMode,
) -> io::Result<()> {
ensure_parent_dir(path)?;
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
writer.write_all(record.to_markdown_document(mode).as_bytes())?;
writer.flush()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_record() -> BenchmarkRecord {
BenchmarkRecord {
running_time: 0.5,
max_rss: Some(8.0),
max_vms: Some(64.0),
max_uss: Some(7.0),
max_pss: Some(7.5),
io_in: Some(1.0),
io_out: Some(0.25),
mean_load: 100.0,
cpu_time: 0.5,
major_page_faults: Some(3),
minor_page_faults: Some(120),
voluntary_ctx_switches: Some(40),
involuntary_ctx_switches: Some(5),
peak_n_threads: Some(6),
peak_n_procs: 2,
loadavg_1m_start: Some(0.75),
loadavg_1m_end: Some(1.10),
max_swap: Some(0.25),
page_cache_start: Some(256.0),
page_cache_end: Some(240.0),
data_collected: true,
}
}
#[test]
fn extension_matches_each_format() {
assert_eq!(OutputFormat::Tsv.extension(), "tsv");
assert_eq!(OutputFormat::Json.extension(), "json");
assert_eq!(OutputFormat::JsonPretty.extension(), "json");
}
#[test]
fn tsv_writer_full_mode_emits_full_header_and_data_row() {
let mut buf = Vec::new();
write_to(&sample_record(), &mut buf, OutputFormat::Tsv, SchemaMode::Full).unwrap();
let text = std::str::from_utf8(&buf).unwrap();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines.len(), 2);
assert!(lines[0].contains("\tmajor_page_faults\tminor_page_faults\t"));
assert!(lines[0].contains("\tvoluntary_ctx_switches\tinvoluntary_ctx_switches\t"));
assert!(lines[0].contains("\tpeak_n_threads\tpeak_n_procs\t"));
assert!(lines[0].contains("\tloadavg_1m_start\tloadavg_1m_end\t"));
assert!(lines[0].contains("\tmax_swap\t"));
assert!(lines[0].ends_with("\tpage_cache_start\tpage_cache_end"));
assert!(lines[1].starts_with("0.5000\t"));
assert!(
lines[1].ends_with("\t3\t120\t40\t5\t6\t2\t0.75\t1.10\t0.25\t256.00\t240.00"),
"data row: {}",
lines[1]
);
assert!(text.ends_with('\n'));
}
#[test]
fn tsv_writer_strict_mode_emits_snakemake_header() {
let mut buf = Vec::new();
write_to(&sample_record(), &mut buf, OutputFormat::Tsv, SchemaMode::SnakemakeStrict)
.unwrap();
let text = std::str::from_utf8(&buf).unwrap();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines[0], crate::record::TSV_HEADER);
assert_eq!(lines[1].split('\t').count(), 10);
}
#[test]
fn json_writer_full_mode_includes_tricord_fields() {
let mut buf = Vec::new();
write_to(&sample_record(), &mut buf, OutputFormat::Json, SchemaMode::Full).unwrap();
let text = std::str::from_utf8(&buf).unwrap().trim_end();
let value: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(value.is_object());
assert_eq!(value["data_collected"], true);
assert_eq!(value["major_page_faults"], 3);
assert_eq!(value["minor_page_faults"], 120);
}
#[test]
fn json_writer_strict_mode_omits_tricord_fields() {
let mut buf = Vec::new();
write_to(&sample_record(), &mut buf, OutputFormat::Json, SchemaMode::SnakemakeStrict)
.unwrap();
let text = std::str::from_utf8(&buf).unwrap().trim_end();
let value: serde_json::Value = serde_json::from_str(text).unwrap();
let obj = value.as_object().unwrap();
assert!(!obj.contains_key("major_page_faults"));
assert!(!obj.contains_key("minor_page_faults"));
}
#[test]
fn json_writer_emits_no_trailing_newline() {
let mut buf = Vec::new();
write_to(&sample_record(), &mut buf, OutputFormat::Json, SchemaMode::Full).unwrap();
let text = std::str::from_utf8(&buf).unwrap();
assert!(!text.ends_with('\n'), "unexpected trailing newline: {text:?}");
}
#[test]
fn json_pretty_writer_full_mode_includes_tricord_fields() {
let mut buf = Vec::new();
write_to(&sample_record(), &mut buf, OutputFormat::JsonPretty, SchemaMode::Full).unwrap();
let text = std::str::from_utf8(&buf).unwrap().trim_end();
assert_eq!(text.lines().count(), 23, "pretty output: {text}");
let value: serde_json::Value = serde_json::from_str(text).unwrap();
assert!(value.is_object());
assert_eq!(value["data_collected"], true);
assert_eq!(value["major_page_faults"], 3);
assert_eq!(value["minor_page_faults"], 120);
}
#[test]
fn json_pretty_writer_strict_mode_omits_tricord_fields() {
let mut buf = Vec::new();
write_to(&sample_record(), &mut buf, OutputFormat::JsonPretty, SchemaMode::SnakemakeStrict)
.unwrap();
let text = std::str::from_utf8(&buf).unwrap().trim_end();
let value: serde_json::Value = serde_json::from_str(text).unwrap();
let obj = value.as_object().unwrap();
assert!(!obj.contains_key("major_page_faults"));
assert!(!obj.contains_key("minor_page_faults"));
}
#[test]
fn json_pretty_writer_emits_trailing_newline() {
let mut buf = Vec::new();
write_to(&sample_record(), &mut buf, OutputFormat::JsonPretty, SchemaMode::Full).unwrap();
let text = std::str::from_utf8(&buf).unwrap();
assert!(text.ends_with('\n'), "missing trailing newline: {text:?}");
}
#[test]
fn write_to_path_creates_parent_directories() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("nested/deeper/timing.tsv");
write_to_path(&sample_record(), &path, OutputFormat::Tsv, SchemaMode::Full).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.starts_with("s\th:m:s"));
}
#[test]
fn write_markdown_to_path_emits_table_and_creates_parents() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("nested/deeper/timing.md");
write_markdown_to_path(&sample_record(), &path, SchemaMode::Full).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
let mut lines = text.lines();
assert!(lines.next().is_some_and(|line| line.starts_with("| metric")));
assert!(lines.next().is_some_and(|line| line.starts_with("|:")));
assert!(text.contains("major_page_faults"), "missing page-fault row in: {text}");
assert!(text.ends_with('\n'));
}
#[test]
fn write_markdown_to_path_strict_omits_page_faults() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("timing.md");
write_markdown_to_path(&sample_record(), &path, SchemaMode::SnakemakeStrict).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
assert!(!text.contains("major_page_faults"));
}
}