use std::fmt::{self, Display, Formatter};
use std::time::Duration;
use chrono::{DateTime, Utc};
use colored::*;
use serde::{Deserialize, Serialize};
use crate::TestType;
use crate::build_info::BuildInfo;
use crate::report::{REPORT_SCHEMA_VERSION, TestReport, write_build_info};
use crate::utils::env::Environment;
use crate::utils::format::format_bytes;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuiteReport {
pub schema_version: u32,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub build: BuildInfo,
pub server: String,
pub environment: Option<Environment>,
pub reports: Vec<NamedReport>,
#[serde(default)]
pub skipped: Vec<SkippedPhase>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NamedReport {
pub label: String,
pub params: PhaseParams,
pub report: TestReport,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseParams {
pub payload_size: Option<usize>,
pub io_unit: usize,
pub connections: usize,
pub duration: Duration,
pub test_type: TestType,
#[serde(default)]
pub deviations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkippedPhase {
pub label: String,
pub reason: String,
}
impl SuiteReport {
pub fn new(server: String) -> Self {
let now = Utc::now();
Self {
schema_version: REPORT_SCHEMA_VERSION,
start_time: now,
end_time: now,
build: BuildInfo::current(),
server,
environment: Some(Environment::capture()),
reports: Vec::new(),
skipped: Vec::new(),
}
}
pub fn record(&mut self, label: impl Into<String>, params: PhaseParams, report: TestReport) {
self.reports.push(NamedReport {
label: label.into(),
params,
report,
});
}
pub fn skip(&mut self, label: impl Into<String>, reason: impl Into<String>) {
self.skipped.push(SkippedPhase {
label: label.into(),
reason: reason.into(),
});
}
pub fn finalize(&mut self) {
self.end_time = Utc::now();
}
}
impl Display for PhaseParams {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let payload = match self.payload_size {
Some(n) => format_bytes(n),
None => "n/a".to_string(),
};
writeln!(
f,
" {}: payload={} io-unit={} connections={} duration={}s type={}",
"params".bright_white().bold(),
payload.yellow(),
format_bytes(self.io_unit).yellow(),
self.connections.to_string().yellow(),
self.duration.as_secs().to_string().yellow(),
self.test_type.to_string().yellow(),
)?;
for note in &self.deviations {
writeln!(f, " {} {}", "note:".bright_yellow().bold(), note.yellow())?;
}
Ok(())
}
}
impl Display for SuiteReport {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(
f,
"{}",
"═══ Speed CLI Suite Report ═══".bright_cyan().bold()
)?;
write_build_info(f, &self.build)?;
writeln!(
f,
"{}: {}",
"Server".bright_white().bold(),
self.server.cyan()
)?;
writeln!(
f,
"{}: {}",
"Start".bright_white().bold(),
self.start_time
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
.yellow()
)?;
writeln!(
f,
"{}: {}",
"End".bright_white().bold(),
self.end_time
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
.yellow()
)?;
if let Some(env) = &self.environment {
writeln!(f)?;
writeln!(f, "{}", "Environment:".bright_white().bold().underline())?;
write!(f, "{env}")?;
}
writeln!(f)?;
for nr in &self.reports {
writeln!(
f,
"{}",
format!("── Phase: {} ──", nr.label).bright_magenta().bold()
)?;
write!(f, "{}", nr.params)?;
write!(f, "{}", nr.report)?;
writeln!(f)?;
}
if !self.skipped.is_empty() {
writeln!(f, "{}", "Skipped phases:".bright_yellow().bold())?;
for s in &self.skipped {
writeln!(f, " {} — {}", s.label.yellow(), s.reason.red())?;
}
}
Ok(())
}
}