use serde::Serialize;
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use uuid::Uuid;
use crate::output::RunReport;
pub const ENVELOPE_VERSION: u32 = 1;
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PublishSource {
Cli,
#[allow(dead_code)] Scheduled,
#[allow(dead_code)] Webhook,
}
#[derive(Serialize, Debug)]
pub struct PublishEnvelope<'a> {
pub envelope_version: u32,
pub run_id: Uuid,
pub cli_version: &'a str,
#[serde(serialize_with = "serialize_rfc3339")]
pub published_at: OffsetDateTime,
pub source: PublishSource,
pub report: &'a RunReport,
}
impl<'a> PublishEnvelope<'a> {
pub fn new(cli_version: &'a str, report: &'a RunReport) -> Self {
Self {
envelope_version: ENVELOPE_VERSION,
run_id: Uuid::now_v7(),
cli_version,
published_at: OffsetDateTime::now_utc(),
source: PublishSource::Cli,
report,
}
}
#[cfg(test)]
pub fn with_fixed_id_and_time(
cli_version: &'a str,
report: &'a RunReport,
run_id: Uuid,
published_at: OffsetDateTime,
) -> Self {
Self {
envelope_version: ENVELOPE_VERSION,
run_id,
cli_version,
published_at,
source: PublishSource::Cli,
report,
}
}
}
fn serialize_rfc3339<S: serde::Serializer>(ts: &OffsetDateTime, ser: S) -> Result<S::Ok, S::Error> {
let formatted = ts
.format(&Rfc3339)
.map_err(|e| serde::ser::Error::custom(format!("rfc3339 format: {e}")))?;
ser.serialize_str(&formatted)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::output::{LatencyStats, RequestSummary, RunMeta};
fn sample_report() -> RunReport {
RunReport {
version: 2,
run: RunMeta {
mode: "fixed".to_string(),
elapsed_ms: 1234.5,
curve_duration_ms: None,
template_generation_ms: None,
},
requests: RequestSummary {
total: 100,
ok: 99,
failed: 1,
skipped: 0,
error_rate: 0.01,
throughput_rps: 50.0,
},
latency: LatencyStats {
min_ms: 1.0,
p10_ms: 2.0,
p25_ms: 3.0,
p50_ms: 5.0,
p75_ms: 8.0,
p90_ms: 12.0,
p95_ms: 20.0,
p99_ms: 50.0,
max_ms: 100.0,
avg_ms: 6.5,
},
status_codes: {
let mut m = BTreeMap::new();
m.insert("200".to_string(), 99);
m.insert("500".to_string(), 1);
m
},
response_stats: None,
curve_stages: None,
scenarios: None,
thresholds: None,
}
}
#[test]
fn envelope_new_mints_uuidv7() {
let r = sample_report();
let env = PublishEnvelope::new("0.3.0", &r);
assert_eq!(env.run_id.get_version_num(), 7);
assert_eq!(env.envelope_version, ENVELOPE_VERSION);
assert_eq!(env.source, PublishSource::Cli);
}
#[test]
fn envelope_new_timestamps_are_recent() {
let r = sample_report();
let before = OffsetDateTime::now_utc();
let env = PublishEnvelope::new("0.3.0", &r);
let after = OffsetDateTime::now_utc();
assert!(env.published_at >= before);
assert!(env.published_at <= after);
}
#[test]
fn envelope_serializes_to_json_with_expected_fields() {
let r = sample_report();
let fixed_id = Uuid::parse_str("0190d8a3-9c00-7000-8000-000000000000").unwrap();
let fixed_ts =
OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid unix timestamp");
let env = PublishEnvelope::with_fixed_id_and_time("9.9.9", &r, fixed_id, fixed_ts);
let json = serde_json::to_value(&env).expect("serializes");
assert_eq!(json["envelope_version"], 1);
assert_eq!(json["run_id"], fixed_id.to_string());
assert_eq!(json["cli_version"], "9.9.9");
assert_eq!(json["source"], "cli");
assert_eq!(json["report"]["version"], 2);
assert_eq!(json["report"]["requests"]["total"], 100);
assert!(json["published_at"].as_str().unwrap().contains("2023-11-"));
}
#[test]
fn envelope_source_serializes_lowercase() {
assert_eq!(
serde_json::to_value(PublishSource::Cli).unwrap(),
serde_json::Value::String("cli".into())
);
assert_eq!(
serde_json::to_value(PublishSource::Scheduled).unwrap(),
serde_json::Value::String("scheduled".into())
);
}
#[test]
fn consecutive_envelope_ids_are_ordered() {
let r = sample_report();
let a = PublishEnvelope::new("x", &r).run_id;
let b = PublishEnvelope::new("x", &r).run_id;
assert!(a <= b, "UUIDv7 should be monotonically non-decreasing");
}
}