1use super::buffer::TraceBuffer;
6use super::canonicalize::{TraceEventKey, canonicalize, trace_event_key, trace_fingerprint};
7use super::event::TraceEvent;
8use serde::{Deserialize, Serialize};
9use std::io::{self, Write};
10
11pub const GOLDEN_TRACE_SCHEMA_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct GoldenTraceConfig {
17 pub seed: u64,
19 pub entropy_seed: u64,
21 pub worker_count: usize,
23 pub trace_capacity: usize,
25 pub max_steps: Option<u64>,
27 pub canonical_prefix_layers: usize,
29 pub canonical_prefix_events: usize,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct GoldenTraceOracleSummary {
36 pub violations: Vec<String>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct GoldenTraceFixture {
43 pub schema_version: u32,
45 pub config: GoldenTraceConfig,
47 pub fingerprint: u64,
49 pub event_count: u64,
51 pub canonical_prefix: Vec<Vec<TraceEventKey>>,
53 pub oracle_summary: GoldenTraceOracleSummary,
55}
56
57impl GoldenTraceFixture {
58 #[must_use]
60 pub fn from_events(
61 config: GoldenTraceConfig,
62 events: &[TraceEvent],
63 oracle_violations: impl IntoIterator<Item = impl Into<String>>,
64 ) -> Self {
65 let canonical_prefix = canonical_prefix(
66 events,
67 config.canonical_prefix_layers,
68 config.canonical_prefix_events,
69 );
70 let mut violations: Vec<String> = oracle_violations.into_iter().map(Into::into).collect();
71 violations.sort();
72 violations.dedup();
73
74 Self {
75 schema_version: GOLDEN_TRACE_SCHEMA_VERSION,
76 fingerprint: trace_fingerprint(events),
77 event_count: u64::try_from(events.len()).unwrap_or(u64::MAX),
78 canonical_prefix,
79 oracle_summary: GoldenTraceOracleSummary { violations },
80 config,
81 }
82 }
83
84 pub fn verify(&self, actual: &Self) -> Result<(), GoldenTraceDiff> {
86 GoldenTraceDiff::from_fixtures(self, actual).into_result()
87 }
88
89 #[must_use]
91 pub fn delta_report(&self, actual: &Self) -> GoldenTraceDeltaReport {
92 GoldenTraceDiff::from_fixtures(self, actual).to_delta_report(self, actual)
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum GoldenTraceDeltaClass {
100 Config,
102 Timing,
104 Semantic,
106 Observability,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum GoldenTraceDeltaSeverity {
114 Info,
116 Warning,
118 Error,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct GoldenTraceDelta {
125 pub class: GoldenTraceDeltaClass,
127 pub severity: GoldenTraceDeltaSeverity,
129 pub field: String,
131 pub message: String,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[allow(clippy::struct_excessive_bools)]
138pub struct GoldenTraceDeltaReport {
139 pub expected_fingerprint: u64,
141 pub actual_fingerprint: u64,
143 pub expected_event_count: u64,
145 pub actual_event_count: u64,
147 pub config_drift: bool,
149 pub semantic_drift: bool,
151 pub timing_drift: bool,
153 pub observability_drift: bool,
155 pub deltas: Vec<GoldenTraceDelta>,
157}
158
159impl GoldenTraceDeltaReport {
160 #[must_use]
162 pub fn is_clean(&self) -> bool {
163 self.deltas.is_empty()
164 }
165
166 pub fn to_json(&self) -> Result<String, serde_json::Error> {
172 serde_json::to_string_pretty(self)
173 }
174}
175
176#[derive(Debug, Default)]
178pub struct GoldenTraceDiff {
179 mismatches: Vec<GoldenTraceMismatch>,
180}
181
182impl GoldenTraceDiff {
183 #[must_use]
185 pub fn is_empty(&self) -> bool {
186 self.mismatches.is_empty()
187 }
188
189 fn push(&mut self, mismatch: GoldenTraceMismatch) {
190 self.mismatches.push(mismatch);
191 }
192
193 fn from_fixtures(expected: &GoldenTraceFixture, actual: &GoldenTraceFixture) -> Self {
194 let mut diff = Self::default();
195 if expected.schema_version != actual.schema_version {
196 diff.push(GoldenTraceMismatch::SchemaVersion {
197 expected: expected.schema_version,
198 actual: actual.schema_version,
199 });
200 }
201 if expected.config != actual.config {
202 diff.push(GoldenTraceMismatch::Config {
203 expected: expected.config.clone(),
204 actual: actual.config.clone(),
205 });
206 }
207 if expected.fingerprint != actual.fingerprint {
208 diff.push(GoldenTraceMismatch::Fingerprint {
209 expected: expected.fingerprint,
210 actual: actual.fingerprint,
211 });
212 }
213 if expected.event_count != actual.event_count {
214 diff.push(GoldenTraceMismatch::EventCount {
215 expected: expected.event_count,
216 actual: actual.event_count,
217 });
218 }
219 if expected.canonical_prefix != actual.canonical_prefix {
220 diff.push(GoldenTraceMismatch::CanonicalPrefix {
221 expected_layers: expected.canonical_prefix.len(),
222 actual_layers: actual.canonical_prefix.len(),
223 first_mismatch: first_prefix_mismatch(
224 &expected.canonical_prefix,
225 &actual.canonical_prefix,
226 ),
227 });
228 }
229 if expected.oracle_summary != actual.oracle_summary {
230 diff.push(GoldenTraceMismatch::OracleViolations {
231 expected: expected.oracle_summary.violations.clone(),
232 actual: actual.oracle_summary.violations.clone(),
233 });
234 }
235 diff
236 }
237
238 fn into_result(self) -> Result<(), Self> {
239 if self.is_empty() { Ok(()) } else { Err(self) }
240 }
241
242 #[must_use]
244 pub fn to_delta_report(
245 &self,
246 expected: &GoldenTraceFixture,
247 actual: &GoldenTraceFixture,
248 ) -> GoldenTraceDeltaReport {
249 let mut config_drift = false;
250 let mut semantic_drift = false;
251 let mut timing_drift = false;
252 let mut observability_drift = false;
253 let mut deltas = Vec::with_capacity(self.mismatches.len());
254
255 for mismatch in &self.mismatches {
256 let (class, severity, field) = classify_delta(mismatch);
257 match class {
258 GoldenTraceDeltaClass::Config => config_drift = true,
259 GoldenTraceDeltaClass::Timing => timing_drift = true,
260 GoldenTraceDeltaClass::Semantic => semantic_drift = true,
261 GoldenTraceDeltaClass::Observability => observability_drift = true,
262 }
263 deltas.push(GoldenTraceDelta {
264 class,
265 severity,
266 field: field.to_string(),
267 message: mismatch.to_string(),
268 });
269 }
270
271 GoldenTraceDeltaReport {
272 expected_fingerprint: expected.fingerprint,
273 actual_fingerprint: actual.fingerprint,
274 expected_event_count: expected.event_count,
275 actual_event_count: actual.event_count,
276 config_drift,
277 semantic_drift,
278 timing_drift,
279 observability_drift,
280 deltas,
281 }
282 }
283}
284
285fn classify_delta(
286 mismatch: &GoldenTraceMismatch,
287) -> (
288 GoldenTraceDeltaClass,
289 GoldenTraceDeltaSeverity,
290 &'static str,
291) {
292 match mismatch {
293 GoldenTraceMismatch::SchemaVersion { .. } => (
294 GoldenTraceDeltaClass::Config,
295 GoldenTraceDeltaSeverity::Error,
296 "schema_version",
297 ),
298 GoldenTraceMismatch::Config { .. } => (
299 GoldenTraceDeltaClass::Config,
300 GoldenTraceDeltaSeverity::Error,
301 "config",
302 ),
303 GoldenTraceMismatch::Fingerprint { .. } => (
304 GoldenTraceDeltaClass::Semantic,
305 GoldenTraceDeltaSeverity::Error,
306 "fingerprint",
307 ),
308 GoldenTraceMismatch::EventCount { .. } => (
309 GoldenTraceDeltaClass::Timing,
310 GoldenTraceDeltaSeverity::Warning,
311 "event_count",
312 ),
313 GoldenTraceMismatch::CanonicalPrefix { .. } => (
314 GoldenTraceDeltaClass::Semantic,
315 GoldenTraceDeltaSeverity::Error,
316 "canonical_prefix",
317 ),
318 GoldenTraceMismatch::OracleViolations { .. } => (
319 GoldenTraceDeltaClass::Observability,
320 GoldenTraceDeltaSeverity::Warning,
321 "oracle_violations",
322 ),
323 }
324}
325
326impl std::fmt::Display for GoldenTraceDiff {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 for mismatch in &self.mismatches {
329 writeln!(f, "{mismatch}")?;
330 }
331 Ok(())
332 }
333}
334
335impl std::error::Error for GoldenTraceDiff {}
336
337#[derive(Debug)]
338enum GoldenTraceMismatch {
339 SchemaVersion {
340 expected: u32,
341 actual: u32,
342 },
343 Config {
344 expected: GoldenTraceConfig,
345 actual: GoldenTraceConfig,
346 },
347 Fingerprint {
348 expected: u64,
349 actual: u64,
350 },
351 EventCount {
352 expected: u64,
353 actual: u64,
354 },
355 CanonicalPrefix {
356 expected_layers: usize,
357 actual_layers: usize,
358 first_mismatch: Option<(usize, usize)>,
359 },
360 OracleViolations {
361 expected: Vec<String>,
362 actual: Vec<String>,
363 },
364}
365
366impl std::fmt::Display for GoldenTraceMismatch {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 match self {
369 Self::SchemaVersion { expected, actual } => {
370 write!(
371 f,
372 "schema_version changed (expected {expected}, actual {actual})"
373 )
374 }
375 Self::Config { expected, actual } => {
376 write!(
377 f,
378 "config changed (expected {expected:?}, actual {actual:?})"
379 )
380 }
381 Self::Fingerprint { expected, actual } => {
382 write!(
383 f,
384 "fingerprint changed (expected 0x{expected:016X}, actual 0x{actual:016X})"
385 )
386 }
387 Self::EventCount { expected, actual } => write!(
388 f,
389 "event_count changed (expected {expected}, actual {actual})"
390 ),
391 Self::CanonicalPrefix {
392 expected_layers,
393 actual_layers,
394 first_mismatch,
395 } => {
396 if let Some((layer, index)) = first_mismatch {
397 write!(
398 f,
399 "canonical_prefix mismatch (layer {layer}, index {index}; expected_layers={expected_layers}, actual_layers={actual_layers})"
400 )
401 } else {
402 write!(
403 f,
404 "canonical_prefix mismatch (expected_layers={expected_layers}, actual_layers={actual_layers})"
405 )
406 }
407 }
408 Self::OracleViolations { expected, actual } => {
409 write!(
410 f,
411 "oracle violations changed (expected {expected:?}, actual {actual:?})"
412 )
413 }
414 }
415 }
416}
417
418fn canonical_prefix(
419 events: &[TraceEvent],
420 max_layers: usize,
421 max_events: usize,
422) -> Vec<Vec<TraceEventKey>> {
423 let foata = canonicalize(events);
424 let mut remaining = max_events;
425 let mut prefix = Vec::new();
426
427 for layer in foata.layers().iter().take(max_layers) {
428 if remaining == 0 {
429 break;
430 }
431 let mut keys = Vec::new();
432 for event in layer {
433 if remaining == 0 {
434 break;
435 }
436 keys.push(trace_event_key(event));
437 remaining = remaining.saturating_sub(1);
438 }
439 if !keys.is_empty() {
440 prefix.push(keys);
441 }
442 }
443
444 prefix
445}
446
447fn first_prefix_mismatch(
448 expected: &[Vec<TraceEventKey>],
449 actual: &[Vec<TraceEventKey>],
450) -> Option<(usize, usize)> {
451 let layers = expected.len().min(actual.len());
452 for layer_idx in 0..layers {
453 let expected_layer = &expected[layer_idx];
454 let actual_layer = &actual[layer_idx];
455 let events = expected_layer.len().min(actual_layer.len());
456 for event_idx in 0..events {
457 if expected_layer[event_idx] != actual_layer[event_idx] {
458 return Some((layer_idx, event_idx));
459 }
460 }
461 if expected_layer.len() != actual_layer.len() {
462 return Some((layer_idx, events));
463 }
464 }
465 if expected.len() != actual.len() {
466 return Some((layers, 0));
467 }
468 None
469}
470
471pub fn format_trace(buffer: &TraceBuffer, w: &mut impl Write) -> io::Result<()> {
473 writeln!(w, "=== Trace ({} events) ===", buffer.len())?;
474 for event in buffer.iter() {
475 writeln!(w, "{event}")?;
476 }
477 writeln!(w, "=== End Trace ===")?;
478 Ok(())
479}
480
481#[must_use]
483pub fn trace_to_string(buffer: &TraceBuffer) -> String {
484 let mut s = Vec::new();
485 format_trace(buffer, &mut s).expect("writing to Vec should not fail");
486 String::from_utf8(s).expect("trace should be valid UTF-8")
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::trace::event::{TraceData, TraceEvent, TraceEventKind};
493 use crate::types::Time;
494
495 #[test]
496 fn format_empty_trace() {
497 let buffer = TraceBuffer::new(10);
498 let output = trace_to_string(&buffer);
499 assert!(output.contains("0 events"));
500 }
501
502 #[test]
503 fn format_with_events() {
504 let mut buffer = TraceBuffer::new(10);
505 buffer.push(TraceEvent::new(
506 1,
507 Time::from_millis(100),
508 TraceEventKind::UserTrace,
509 TraceData::Message("test".to_string()),
510 ));
511 let output = trace_to_string(&buffer);
512 assert!(output.contains("1 events"));
513 assert!(output.contains("test"));
514 }
515
516 #[test]
519 fn golden_trace_config_debug_clone_eq() {
520 let cfg = GoldenTraceConfig {
521 seed: 42,
522 entropy_seed: 7,
523 worker_count: 4,
524 trace_capacity: 1000,
525 max_steps: Some(500),
526 canonical_prefix_layers: 10,
527 canonical_prefix_events: 100,
528 };
529 let dbg = format!("{cfg:?}");
530 assert!(dbg.contains("GoldenTraceConfig"));
531
532 let cloned = cfg.clone();
533 assert_eq!(cfg, cloned);
534 }
535
536 #[test]
537 fn golden_trace_config_ne() {
538 let a = GoldenTraceConfig {
539 seed: 1,
540 entropy_seed: 0,
541 worker_count: 1,
542 trace_capacity: 10,
543 max_steps: None,
544 canonical_prefix_layers: 1,
545 canonical_prefix_events: 1,
546 };
547 let mut b = a.clone();
548 b.seed = 2;
549 assert_ne!(a, b);
550 }
551
552 #[test]
553 fn golden_trace_oracle_summary_debug_clone_eq() {
554 let summary = GoldenTraceOracleSummary {
555 violations: vec!["leak".to_string()],
556 };
557 let dbg = format!("{summary:?}");
558 assert!(dbg.contains("GoldenTraceOracleSummary"));
559
560 let cloned = summary.clone();
561 assert_eq!(summary, cloned);
562 }
563
564 #[test]
565 fn golden_trace_oracle_summary_empty() {
566 let summary = GoldenTraceOracleSummary { violations: vec![] };
567 assert!(summary.violations.is_empty());
568 }
569
570 #[test]
571 fn golden_trace_diff_default_is_empty() {
572 let diff = GoldenTraceDiff::default();
573 assert!(diff.is_empty());
574 }
575
576 #[test]
577 fn golden_trace_diff_debug() {
578 let diff = GoldenTraceDiff::default();
579 let dbg = format!("{diff:?}");
580 assert!(dbg.contains("GoldenTraceDiff"));
581 }
582
583 #[test]
584 fn golden_trace_diff_display_empty() {
585 let diff = GoldenTraceDiff::default();
586 let display = diff.to_string();
587 assert!(display.is_empty());
588 }
589
590 #[test]
591 fn golden_trace_diff_error_trait() {
592 let diff = GoldenTraceDiff::default();
593 let err: &dyn std::error::Error = &diff;
594 assert!(err.source().is_none());
595 }
596
597 #[test]
598 fn golden_trace_mismatch_display_all_variants() {
599 let m = GoldenTraceMismatch::SchemaVersion {
600 expected: 1,
601 actual: 2,
602 };
603 assert!(m.to_string().contains("schema_version"));
604
605 let m = GoldenTraceMismatch::Fingerprint {
606 expected: 0xAB,
607 actual: 0xCD,
608 };
609 assert!(m.to_string().contains("fingerprint"));
610
611 let m = GoldenTraceMismatch::EventCount {
612 expected: 10,
613 actual: 20,
614 };
615 assert!(m.to_string().contains("event_count"));
616
617 let m = GoldenTraceMismatch::CanonicalPrefix {
618 expected_layers: 3,
619 actual_layers: 5,
620 first_mismatch: Some((1, 2)),
621 };
622 let s = m.to_string();
623 assert!(s.contains("canonical_prefix"));
624 assert!(s.contains("layer 1"));
625
626 let m = GoldenTraceMismatch::CanonicalPrefix {
627 expected_layers: 3,
628 actual_layers: 5,
629 first_mismatch: None,
630 };
631 assert!(m.to_string().contains("expected_layers=3"));
632
633 let m = GoldenTraceMismatch::OracleViolations {
634 expected: vec!["a".into()],
635 actual: vec!["b".into()],
636 };
637 assert!(m.to_string().contains("oracle violations"));
638 }
639
640 #[test]
641 fn golden_trace_mismatch_config_variant() {
642 let cfg1 = GoldenTraceConfig {
643 seed: 1,
644 entropy_seed: 0,
645 worker_count: 1,
646 trace_capacity: 10,
647 max_steps: None,
648 canonical_prefix_layers: 1,
649 canonical_prefix_events: 1,
650 };
651 let cfg2 = GoldenTraceConfig { seed: 2, ..cfg1 };
652 let m = GoldenTraceMismatch::Config {
653 expected: cfg1,
654 actual: cfg2,
655 };
656 assert!(m.to_string().contains("config changed"));
657 }
658
659 #[test]
660 fn golden_trace_mismatch_debug() {
661 let m = GoldenTraceMismatch::SchemaVersion {
662 expected: 1,
663 actual: 2,
664 };
665 let dbg = format!("{m:?}");
666 assert!(dbg.contains("SchemaVersion"));
667 }
668
669 #[test]
670 fn schema_version_constant() {
671 assert_eq!(GOLDEN_TRACE_SCHEMA_VERSION, 1);
672 }
673
674 #[test]
675 fn first_prefix_mismatch_identical() {
676 let a: Vec<Vec<TraceEventKey>> = vec![];
677 assert!(first_prefix_mismatch(&a, &a).is_none());
678 }
679
680 #[test]
681 fn first_prefix_mismatch_different_lengths() {
682 let a: Vec<Vec<TraceEventKey>> = vec![vec![]];
683 let b: Vec<Vec<TraceEventKey>> = vec![];
684 let m = first_prefix_mismatch(&a, &b);
685 assert!(m.is_some());
686 }
687
688 #[test]
689 fn golden_trace_delta_report_clean_when_equal() {
690 let config = GoldenTraceConfig {
691 seed: 1,
692 entropy_seed: 1,
693 worker_count: 1,
694 trace_capacity: 32,
695 max_steps: Some(128),
696 canonical_prefix_layers: 2,
697 canonical_prefix_events: 8,
698 };
699 let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
700 let report = expected.delta_report(&expected);
701 assert!(report.is_clean());
702 assert!(!report.config_drift);
703 assert!(!report.semantic_drift);
704 assert!(!report.timing_drift);
705 assert!(!report.observability_drift);
706 assert!(report.to_json().expect("json").contains("\"deltas\""));
707 }
708
709 #[test]
710 fn golden_trace_delta_report_detects_drift_classes() {
711 let config = GoldenTraceConfig {
712 seed: 1,
713 entropy_seed: 1,
714 worker_count: 1,
715 trace_capacity: 32,
716 max_steps: Some(128),
717 canonical_prefix_layers: 2,
718 canonical_prefix_events: 8,
719 };
720 let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
721 let mut actual = expected.clone();
722 actual.config.seed = 2;
723 actual.fingerprint ^= 0xA5A5;
724 actual.event_count = actual.event_count.saturating_add(1);
725 actual.oracle_summary.violations = vec!["TaskLeak".to_string()];
726
727 let report = expected.delta_report(&actual);
728 assert!(!report.is_clean());
729 assert!(report.config_drift);
730 assert!(report.semantic_drift);
731 assert!(report.timing_drift);
732 assert!(report.observability_drift);
733 assert!(report.deltas.iter().any(|d| d.field == "config"));
734 assert!(report.deltas.iter().any(|d| d.field == "fingerprint"));
735 assert!(report.deltas.iter().any(|d| d.field == "event_count"));
736 assert!(report.deltas.iter().any(|d| d.field == "oracle_violations"));
737 }
738}