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 #![allow(
492 clippy::pedantic,
493 clippy::nursery,
494 clippy::expect_fun_call,
495 clippy::map_unwrap_or,
496 clippy::cast_possible_wrap,
497 clippy::future_not_send
498 )]
499 use super::*;
500 use crate::trace::event::{TraceData, TraceEvent, TraceEventKind};
501 use crate::types::Time;
502
503 #[test]
504 fn format_empty_trace() {
505 let buffer = TraceBuffer::new(10);
506 let output = trace_to_string(&buffer);
507 assert!(output.contains("0 events"));
508 }
509
510 #[test]
511 fn format_with_events() {
512 let mut buffer = TraceBuffer::new(10);
513 buffer.push(TraceEvent::new(
514 1,
515 Time::from_millis(100),
516 TraceEventKind::UserTrace,
517 TraceData::Message("test".to_string()),
518 ));
519 let output = trace_to_string(&buffer);
520 assert!(output.contains("1 events"));
521 assert!(output.contains("test"));
522 }
523
524 #[test]
527 fn golden_trace_config_debug_clone_eq() {
528 let cfg = GoldenTraceConfig {
529 seed: 42,
530 entropy_seed: 7,
531 worker_count: 4,
532 trace_capacity: 1000,
533 max_steps: Some(500),
534 canonical_prefix_layers: 10,
535 canonical_prefix_events: 100,
536 };
537 let dbg = format!("{cfg:?}");
538 assert!(dbg.contains("GoldenTraceConfig"));
539
540 let cloned = cfg.clone();
541 assert_eq!(cfg, cloned);
542 }
543
544 #[test]
545 fn golden_trace_config_ne() {
546 let a = GoldenTraceConfig {
547 seed: 1,
548 entropy_seed: 0,
549 worker_count: 1,
550 trace_capacity: 10,
551 max_steps: None,
552 canonical_prefix_layers: 1,
553 canonical_prefix_events: 1,
554 };
555 let mut b = a.clone();
556 b.seed = 2;
557 assert_ne!(a, b);
558 }
559
560 #[test]
561 fn golden_trace_oracle_summary_debug_clone_eq() {
562 let summary = GoldenTraceOracleSummary {
563 violations: vec!["leak".to_string()],
564 };
565 let dbg = format!("{summary:?}");
566 assert!(dbg.contains("GoldenTraceOracleSummary"));
567
568 let cloned = summary.clone();
569 assert_eq!(summary, cloned);
570 }
571
572 #[test]
573 fn golden_trace_oracle_summary_empty() {
574 let summary = GoldenTraceOracleSummary { violations: vec![] };
575 assert!(summary.violations.is_empty());
576 }
577
578 #[test]
579 fn golden_trace_diff_default_is_empty() {
580 let diff = GoldenTraceDiff::default();
581 assert!(diff.is_empty());
582 }
583
584 #[test]
585 fn golden_trace_diff_debug() {
586 let diff = GoldenTraceDiff::default();
587 let dbg = format!("{diff:?}");
588 assert!(dbg.contains("GoldenTraceDiff"));
589 }
590
591 #[test]
592 fn golden_trace_diff_display_empty() {
593 let diff = GoldenTraceDiff::default();
594 let display = diff.to_string();
595 assert!(display.is_empty());
596 }
597
598 #[test]
599 fn golden_trace_diff_error_trait() {
600 let diff = GoldenTraceDiff::default();
601 let err: &dyn std::error::Error = &diff;
602 assert!(err.source().is_none());
603 }
604
605 #[test]
606 fn golden_trace_mismatch_display_all_variants() {
607 let m = GoldenTraceMismatch::SchemaVersion {
608 expected: 1,
609 actual: 2,
610 };
611 assert!(m.to_string().contains("schema_version"));
612
613 let m = GoldenTraceMismatch::Fingerprint {
614 expected: 0xAB,
615 actual: 0xCD,
616 };
617 assert!(m.to_string().contains("fingerprint"));
618
619 let m = GoldenTraceMismatch::EventCount {
620 expected: 10,
621 actual: 20,
622 };
623 assert!(m.to_string().contains("event_count"));
624
625 let m = GoldenTraceMismatch::CanonicalPrefix {
626 expected_layers: 3,
627 actual_layers: 5,
628 first_mismatch: Some((1, 2)),
629 };
630 let s = m.to_string();
631 assert!(s.contains("canonical_prefix"));
632 assert!(s.contains("layer 1"));
633
634 let m = GoldenTraceMismatch::CanonicalPrefix {
635 expected_layers: 3,
636 actual_layers: 5,
637 first_mismatch: None,
638 };
639 assert!(m.to_string().contains("expected_layers=3"));
640
641 let m = GoldenTraceMismatch::OracleViolations {
642 expected: vec!["a".into()],
643 actual: vec!["b".into()],
644 };
645 assert!(m.to_string().contains("oracle violations"));
646 }
647
648 #[test]
649 fn golden_trace_mismatch_config_variant() {
650 let cfg1 = GoldenTraceConfig {
651 seed: 1,
652 entropy_seed: 0,
653 worker_count: 1,
654 trace_capacity: 10,
655 max_steps: None,
656 canonical_prefix_layers: 1,
657 canonical_prefix_events: 1,
658 };
659 let cfg2 = GoldenTraceConfig { seed: 2, ..cfg1 };
660 let m = GoldenTraceMismatch::Config {
661 expected: cfg1,
662 actual: cfg2,
663 };
664 assert!(m.to_string().contains("config changed"));
665 }
666
667 #[test]
668 fn golden_trace_mismatch_debug() {
669 let m = GoldenTraceMismatch::SchemaVersion {
670 expected: 1,
671 actual: 2,
672 };
673 let dbg = format!("{m:?}");
674 assert!(dbg.contains("SchemaVersion"));
675 }
676
677 #[test]
678 fn schema_version_constant() {
679 assert_eq!(GOLDEN_TRACE_SCHEMA_VERSION, 1);
680 }
681
682 #[test]
683 fn first_prefix_mismatch_identical() {
684 let a: Vec<Vec<TraceEventKey>> = vec![];
685 assert!(first_prefix_mismatch(&a, &a).is_none());
686 }
687
688 #[test]
689 fn first_prefix_mismatch_different_lengths() {
690 let a: Vec<Vec<TraceEventKey>> = vec![vec![]];
691 let b: Vec<Vec<TraceEventKey>> = vec![];
692 let m = first_prefix_mismatch(&a, &b);
693 assert!(m.is_some());
694 }
695
696 #[test]
697 fn golden_trace_delta_report_clean_when_equal() {
698 let config = GoldenTraceConfig {
699 seed: 1,
700 entropy_seed: 1,
701 worker_count: 1,
702 trace_capacity: 32,
703 max_steps: Some(128),
704 canonical_prefix_layers: 2,
705 canonical_prefix_events: 8,
706 };
707 let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
708 let report = expected.delta_report(&expected);
709 assert!(report.is_clean());
710 assert!(!report.config_drift);
711 assert!(!report.semantic_drift);
712 assert!(!report.timing_drift);
713 assert!(!report.observability_drift);
714 assert!(report.to_json().expect("json").contains("\"deltas\""));
715 }
716
717 #[test]
718 fn golden_trace_delta_report_detects_drift_classes() {
719 let config = GoldenTraceConfig {
720 seed: 1,
721 entropy_seed: 1,
722 worker_count: 1,
723 trace_capacity: 32,
724 max_steps: Some(128),
725 canonical_prefix_layers: 2,
726 canonical_prefix_events: 8,
727 };
728 let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
729 let mut actual = expected.clone();
730 actual.config.seed = 2;
731 actual.fingerprint ^= 0xA5A5;
732 actual.event_count = actual.event_count.saturating_add(1);
733 actual.oracle_summary.violations = vec!["TaskLeak".to_string()];
734
735 let report = expected.delta_report(&actual);
736 assert!(!report.is_clean());
737 assert!(report.config_drift);
738 assert!(report.semantic_drift);
739 assert!(report.timing_drift);
740 assert!(report.observability_drift);
741 assert!(report.deltas.iter().any(|d| d.field == "config"));
742 assert!(report.deltas.iter().any(|d| d.field == "fingerprint"));
743 assert!(report.deltas.iter().any(|d| d.field == "event_count"));
744 assert!(report.deltas.iter().any(|d| d.field == "oracle_violations"));
745 }
746}