use super::buffer::TraceBuffer;
use super::canonicalize::{TraceEventKey, canonicalize, trace_event_key, trace_fingerprint};
use super::event::TraceEvent;
use serde::{Deserialize, Serialize};
use std::io::{self, Write};
pub const GOLDEN_TRACE_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoldenTraceConfig {
pub seed: u64,
pub entropy_seed: u64,
pub worker_count: usize,
pub trace_capacity: usize,
pub max_steps: Option<u64>,
pub canonical_prefix_layers: usize,
pub canonical_prefix_events: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoldenTraceOracleSummary {
pub violations: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoldenTraceFixture {
pub schema_version: u32,
pub config: GoldenTraceConfig,
pub fingerprint: u64,
pub event_count: u64,
pub canonical_prefix: Vec<Vec<TraceEventKey>>,
pub oracle_summary: GoldenTraceOracleSummary,
}
impl GoldenTraceFixture {
#[must_use]
pub fn from_events(
config: GoldenTraceConfig,
events: &[TraceEvent],
oracle_violations: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
let canonical_prefix = canonical_prefix(
events,
config.canonical_prefix_layers,
config.canonical_prefix_events,
);
let mut violations: Vec<String> = oracle_violations.into_iter().map(Into::into).collect();
violations.sort();
violations.dedup();
Self {
schema_version: GOLDEN_TRACE_SCHEMA_VERSION,
fingerprint: trace_fingerprint(events),
event_count: u64::try_from(events.len()).unwrap_or(u64::MAX),
canonical_prefix,
oracle_summary: GoldenTraceOracleSummary { violations },
config,
}
}
pub fn verify(&self, actual: &Self) -> Result<(), GoldenTraceDiff> {
GoldenTraceDiff::from_fixtures(self, actual).into_result()
}
#[must_use]
pub fn delta_report(&self, actual: &Self) -> GoldenTraceDeltaReport {
GoldenTraceDiff::from_fixtures(self, actual).to_delta_report(self, actual)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GoldenTraceDeltaClass {
Config,
Timing,
Semantic,
Observability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GoldenTraceDeltaSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoldenTraceDelta {
pub class: GoldenTraceDeltaClass,
pub severity: GoldenTraceDeltaSeverity,
pub field: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct GoldenTraceDeltaReport {
pub expected_fingerprint: u64,
pub actual_fingerprint: u64,
pub expected_event_count: u64,
pub actual_event_count: u64,
pub config_drift: bool,
pub semantic_drift: bool,
pub timing_drift: bool,
pub observability_drift: bool,
pub deltas: Vec<GoldenTraceDelta>,
}
impl GoldenTraceDeltaReport {
#[must_use]
pub fn is_clean(&self) -> bool {
self.deltas.is_empty()
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
#[derive(Debug, Default)]
pub struct GoldenTraceDiff {
mismatches: Vec<GoldenTraceMismatch>,
}
impl GoldenTraceDiff {
#[must_use]
pub fn is_empty(&self) -> bool {
self.mismatches.is_empty()
}
fn push(&mut self, mismatch: GoldenTraceMismatch) {
self.mismatches.push(mismatch);
}
fn from_fixtures(expected: &GoldenTraceFixture, actual: &GoldenTraceFixture) -> Self {
let mut diff = Self::default();
if expected.schema_version != actual.schema_version {
diff.push(GoldenTraceMismatch::SchemaVersion {
expected: expected.schema_version,
actual: actual.schema_version,
});
}
if expected.config != actual.config {
diff.push(GoldenTraceMismatch::Config {
expected: expected.config.clone(),
actual: actual.config.clone(),
});
}
if expected.fingerprint != actual.fingerprint {
diff.push(GoldenTraceMismatch::Fingerprint {
expected: expected.fingerprint,
actual: actual.fingerprint,
});
}
if expected.event_count != actual.event_count {
diff.push(GoldenTraceMismatch::EventCount {
expected: expected.event_count,
actual: actual.event_count,
});
}
if expected.canonical_prefix != actual.canonical_prefix {
diff.push(GoldenTraceMismatch::CanonicalPrefix {
expected_layers: expected.canonical_prefix.len(),
actual_layers: actual.canonical_prefix.len(),
first_mismatch: first_prefix_mismatch(
&expected.canonical_prefix,
&actual.canonical_prefix,
),
});
}
if expected.oracle_summary != actual.oracle_summary {
diff.push(GoldenTraceMismatch::OracleViolations {
expected: expected.oracle_summary.violations.clone(),
actual: actual.oracle_summary.violations.clone(),
});
}
diff
}
fn into_result(self) -> Result<(), Self> {
if self.is_empty() { Ok(()) } else { Err(self) }
}
#[must_use]
pub fn to_delta_report(
&self,
expected: &GoldenTraceFixture,
actual: &GoldenTraceFixture,
) -> GoldenTraceDeltaReport {
let mut config_drift = false;
let mut semantic_drift = false;
let mut timing_drift = false;
let mut observability_drift = false;
let mut deltas = Vec::with_capacity(self.mismatches.len());
for mismatch in &self.mismatches {
let (class, severity, field) = classify_delta(mismatch);
match class {
GoldenTraceDeltaClass::Config => config_drift = true,
GoldenTraceDeltaClass::Timing => timing_drift = true,
GoldenTraceDeltaClass::Semantic => semantic_drift = true,
GoldenTraceDeltaClass::Observability => observability_drift = true,
}
deltas.push(GoldenTraceDelta {
class,
severity,
field: field.to_string(),
message: mismatch.to_string(),
});
}
GoldenTraceDeltaReport {
expected_fingerprint: expected.fingerprint,
actual_fingerprint: actual.fingerprint,
expected_event_count: expected.event_count,
actual_event_count: actual.event_count,
config_drift,
semantic_drift,
timing_drift,
observability_drift,
deltas,
}
}
}
fn classify_delta(
mismatch: &GoldenTraceMismatch,
) -> (
GoldenTraceDeltaClass,
GoldenTraceDeltaSeverity,
&'static str,
) {
match mismatch {
GoldenTraceMismatch::SchemaVersion { .. } => (
GoldenTraceDeltaClass::Config,
GoldenTraceDeltaSeverity::Error,
"schema_version",
),
GoldenTraceMismatch::Config { .. } => (
GoldenTraceDeltaClass::Config,
GoldenTraceDeltaSeverity::Error,
"config",
),
GoldenTraceMismatch::Fingerprint { .. } => (
GoldenTraceDeltaClass::Semantic,
GoldenTraceDeltaSeverity::Error,
"fingerprint",
),
GoldenTraceMismatch::EventCount { .. } => (
GoldenTraceDeltaClass::Timing,
GoldenTraceDeltaSeverity::Warning,
"event_count",
),
GoldenTraceMismatch::CanonicalPrefix { .. } => (
GoldenTraceDeltaClass::Semantic,
GoldenTraceDeltaSeverity::Error,
"canonical_prefix",
),
GoldenTraceMismatch::OracleViolations { .. } => (
GoldenTraceDeltaClass::Observability,
GoldenTraceDeltaSeverity::Warning,
"oracle_violations",
),
}
}
impl std::fmt::Display for GoldenTraceDiff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for mismatch in &self.mismatches {
writeln!(f, "{mismatch}")?;
}
Ok(())
}
}
impl std::error::Error for GoldenTraceDiff {}
#[derive(Debug)]
enum GoldenTraceMismatch {
SchemaVersion {
expected: u32,
actual: u32,
},
Config {
expected: GoldenTraceConfig,
actual: GoldenTraceConfig,
},
Fingerprint {
expected: u64,
actual: u64,
},
EventCount {
expected: u64,
actual: u64,
},
CanonicalPrefix {
expected_layers: usize,
actual_layers: usize,
first_mismatch: Option<(usize, usize)>,
},
OracleViolations {
expected: Vec<String>,
actual: Vec<String>,
},
}
impl std::fmt::Display for GoldenTraceMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SchemaVersion { expected, actual } => {
write!(
f,
"schema_version changed (expected {expected}, actual {actual})"
)
}
Self::Config { expected, actual } => {
write!(
f,
"config changed (expected {expected:?}, actual {actual:?})"
)
}
Self::Fingerprint { expected, actual } => {
write!(
f,
"fingerprint changed (expected 0x{expected:016X}, actual 0x{actual:016X})"
)
}
Self::EventCount { expected, actual } => write!(
f,
"event_count changed (expected {expected}, actual {actual})"
),
Self::CanonicalPrefix {
expected_layers,
actual_layers,
first_mismatch,
} => {
if let Some((layer, index)) = first_mismatch {
write!(
f,
"canonical_prefix mismatch (layer {layer}, index {index}; expected_layers={expected_layers}, actual_layers={actual_layers})"
)
} else {
write!(
f,
"canonical_prefix mismatch (expected_layers={expected_layers}, actual_layers={actual_layers})"
)
}
}
Self::OracleViolations { expected, actual } => {
write!(
f,
"oracle violations changed (expected {expected:?}, actual {actual:?})"
)
}
}
}
}
fn canonical_prefix(
events: &[TraceEvent],
max_layers: usize,
max_events: usize,
) -> Vec<Vec<TraceEventKey>> {
let foata = canonicalize(events);
let mut remaining = max_events;
let mut prefix = Vec::new();
for layer in foata.layers().iter().take(max_layers) {
if remaining == 0 {
break;
}
let mut keys = Vec::new();
for event in layer {
if remaining == 0 {
break;
}
keys.push(trace_event_key(event));
remaining = remaining.saturating_sub(1);
}
if !keys.is_empty() {
prefix.push(keys);
}
}
prefix
}
fn first_prefix_mismatch(
expected: &[Vec<TraceEventKey>],
actual: &[Vec<TraceEventKey>],
) -> Option<(usize, usize)> {
let layers = expected.len().min(actual.len());
for layer_idx in 0..layers {
let expected_layer = &expected[layer_idx];
let actual_layer = &actual[layer_idx];
let events = expected_layer.len().min(actual_layer.len());
for event_idx in 0..events {
if expected_layer[event_idx] != actual_layer[event_idx] {
return Some((layer_idx, event_idx));
}
}
if expected_layer.len() != actual_layer.len() {
return Some((layer_idx, events));
}
}
if expected.len() != actual.len() {
return Some((layers, 0));
}
None
}
pub fn format_trace(buffer: &TraceBuffer, w: &mut impl Write) -> io::Result<()> {
writeln!(w, "=== Trace ({} events) ===", buffer.len())?;
for event in buffer.iter() {
writeln!(w, "{event}")?;
}
writeln!(w, "=== End Trace ===")?;
Ok(())
}
#[must_use]
pub fn trace_to_string(buffer: &TraceBuffer) -> String {
let mut s = Vec::new();
format_trace(buffer, &mut s).expect("writing to Vec should not fail");
String::from_utf8(s).expect("trace should be valid UTF-8")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::trace::event::{TraceData, TraceEvent, TraceEventKind};
use crate::types::Time;
#[test]
fn format_empty_trace() {
let buffer = TraceBuffer::new(10);
let output = trace_to_string(&buffer);
assert!(output.contains("0 events"));
}
#[test]
fn format_with_events() {
let mut buffer = TraceBuffer::new(10);
buffer.push(TraceEvent::new(
1,
Time::from_millis(100),
TraceEventKind::UserTrace,
TraceData::Message("test".to_string()),
));
let output = trace_to_string(&buffer);
assert!(output.contains("1 events"));
assert!(output.contains("test"));
}
#[test]
fn golden_trace_config_debug_clone_eq() {
let cfg = GoldenTraceConfig {
seed: 42,
entropy_seed: 7,
worker_count: 4,
trace_capacity: 1000,
max_steps: Some(500),
canonical_prefix_layers: 10,
canonical_prefix_events: 100,
};
let dbg = format!("{cfg:?}");
assert!(dbg.contains("GoldenTraceConfig"));
let cloned = cfg.clone();
assert_eq!(cfg, cloned);
}
#[test]
fn golden_trace_config_ne() {
let a = GoldenTraceConfig {
seed: 1,
entropy_seed: 0,
worker_count: 1,
trace_capacity: 10,
max_steps: None,
canonical_prefix_layers: 1,
canonical_prefix_events: 1,
};
let mut b = a.clone();
b.seed = 2;
assert_ne!(a, b);
}
#[test]
fn golden_trace_oracle_summary_debug_clone_eq() {
let summary = GoldenTraceOracleSummary {
violations: vec!["leak".to_string()],
};
let dbg = format!("{summary:?}");
assert!(dbg.contains("GoldenTraceOracleSummary"));
let cloned = summary.clone();
assert_eq!(summary, cloned);
}
#[test]
fn golden_trace_oracle_summary_empty() {
let summary = GoldenTraceOracleSummary { violations: vec![] };
assert!(summary.violations.is_empty());
}
#[test]
fn golden_trace_diff_default_is_empty() {
let diff = GoldenTraceDiff::default();
assert!(diff.is_empty());
}
#[test]
fn golden_trace_diff_debug() {
let diff = GoldenTraceDiff::default();
let dbg = format!("{diff:?}");
assert!(dbg.contains("GoldenTraceDiff"));
}
#[test]
fn golden_trace_diff_display_empty() {
let diff = GoldenTraceDiff::default();
let display = diff.to_string();
assert!(display.is_empty());
}
#[test]
fn golden_trace_diff_error_trait() {
let diff = GoldenTraceDiff::default();
let err: &dyn std::error::Error = &diff;
assert!(err.source().is_none());
}
#[test]
fn golden_trace_mismatch_display_all_variants() {
let m = GoldenTraceMismatch::SchemaVersion {
expected: 1,
actual: 2,
};
assert!(m.to_string().contains("schema_version"));
let m = GoldenTraceMismatch::Fingerprint {
expected: 0xAB,
actual: 0xCD,
};
assert!(m.to_string().contains("fingerprint"));
let m = GoldenTraceMismatch::EventCount {
expected: 10,
actual: 20,
};
assert!(m.to_string().contains("event_count"));
let m = GoldenTraceMismatch::CanonicalPrefix {
expected_layers: 3,
actual_layers: 5,
first_mismatch: Some((1, 2)),
};
let s = m.to_string();
assert!(s.contains("canonical_prefix"));
assert!(s.contains("layer 1"));
let m = GoldenTraceMismatch::CanonicalPrefix {
expected_layers: 3,
actual_layers: 5,
first_mismatch: None,
};
assert!(m.to_string().contains("expected_layers=3"));
let m = GoldenTraceMismatch::OracleViolations {
expected: vec!["a".into()],
actual: vec!["b".into()],
};
assert!(m.to_string().contains("oracle violations"));
}
#[test]
fn golden_trace_mismatch_config_variant() {
let cfg1 = GoldenTraceConfig {
seed: 1,
entropy_seed: 0,
worker_count: 1,
trace_capacity: 10,
max_steps: None,
canonical_prefix_layers: 1,
canonical_prefix_events: 1,
};
let cfg2 = GoldenTraceConfig { seed: 2, ..cfg1 };
let m = GoldenTraceMismatch::Config {
expected: cfg1,
actual: cfg2,
};
assert!(m.to_string().contains("config changed"));
}
#[test]
fn golden_trace_mismatch_debug() {
let m = GoldenTraceMismatch::SchemaVersion {
expected: 1,
actual: 2,
};
let dbg = format!("{m:?}");
assert!(dbg.contains("SchemaVersion"));
}
#[test]
fn schema_version_constant() {
assert_eq!(GOLDEN_TRACE_SCHEMA_VERSION, 1);
}
#[test]
fn first_prefix_mismatch_identical() {
let a: Vec<Vec<TraceEventKey>> = vec![];
assert!(first_prefix_mismatch(&a, &a).is_none());
}
#[test]
fn first_prefix_mismatch_different_lengths() {
let a: Vec<Vec<TraceEventKey>> = vec![vec![]];
let b: Vec<Vec<TraceEventKey>> = vec![];
let m = first_prefix_mismatch(&a, &b);
assert!(m.is_some());
}
#[test]
fn golden_trace_delta_report_clean_when_equal() {
let config = GoldenTraceConfig {
seed: 1,
entropy_seed: 1,
worker_count: 1,
trace_capacity: 32,
max_steps: Some(128),
canonical_prefix_layers: 2,
canonical_prefix_events: 8,
};
let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
let report = expected.delta_report(&expected);
assert!(report.is_clean());
assert!(!report.config_drift);
assert!(!report.semantic_drift);
assert!(!report.timing_drift);
assert!(!report.observability_drift);
assert!(report.to_json().expect("json").contains("\"deltas\""));
}
#[test]
fn golden_trace_delta_report_detects_drift_classes() {
let config = GoldenTraceConfig {
seed: 1,
entropy_seed: 1,
worker_count: 1,
trace_capacity: 32,
max_steps: Some(128),
canonical_prefix_layers: 2,
canonical_prefix_events: 8,
};
let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
let mut actual = expected.clone();
actual.config.seed = 2;
actual.fingerprint ^= 0xA5A5;
actual.event_count = actual.event_count.saturating_add(1);
actual.oracle_summary.violations = vec!["TaskLeak".to_string()];
let report = expected.delta_report(&actual);
assert!(!report.is_clean());
assert!(report.config_drift);
assert!(report.semantic_drift);
assert!(report.timing_drift);
assert!(report.observability_drift);
assert!(report.deltas.iter().any(|d| d.field == "config"));
assert!(report.deltas.iter().any(|d| d.field == "fingerprint"));
assert!(report.deltas.iter().any(|d| d.field == "event_count"));
assert!(report.deltas.iter().any(|d| d.field == "oracle_violations"));
}
}