Skip to main content

asupersync/trace/
format.rs

1//! Formatting utilities for trace output.
2//!
3//! Provides human-readable and machine-readable formatting for traces.
4
5use super::buffer::TraceBuffer;
6use super::canonicalize::{canonicalize, trace_event_key, trace_fingerprint, TraceEventKey};
7use super::event::TraceEvent;
8use serde::{Deserialize, Serialize};
9use std::io::{self, Write};
10
11/// Schema version for golden trace fixtures.
12pub const GOLDEN_TRACE_SCHEMA_VERSION: u32 = 1;
13
14/// Minimal configuration snapshot for golden trace fixtures.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct GoldenTraceConfig {
17    /// Deterministic seed used to run the workload.
18    pub seed: u64,
19    /// Entropy seed used for capability randomness.
20    pub entropy_seed: u64,
21    /// Virtual worker count.
22    pub worker_count: usize,
23    /// Trace buffer capacity.
24    pub trace_capacity: usize,
25    /// Maximum steps before termination (if set).
26    pub max_steps: Option<u64>,
27    /// Maximum number of Foata layers to keep in the canonical prefix.
28    pub canonical_prefix_layers: usize,
29    /// Maximum number of events to keep in the canonical prefix.
30    pub canonical_prefix_events: usize,
31}
32
33/// Summary of oracle results for a golden trace run.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct GoldenTraceOracleSummary {
36    /// Sorted list of oracle violation tags (empty if all invariants held).
37    pub violations: Vec<String>,
38}
39
40/// Golden trace fixture for deterministic verification.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct GoldenTraceFixture {
43    /// Fixture schema version.
44    pub schema_version: u32,
45    /// Configuration snapshot.
46    pub config: GoldenTraceConfig,
47    /// Canonical trace fingerprint.
48    pub fingerprint: u64,
49    /// Number of events in the trace.
50    pub event_count: u64,
51    /// Canonicalized prefix (Foata layers of stable event keys).
52    pub canonical_prefix: Vec<Vec<TraceEventKey>>,
53    /// Oracle summary captured at end of the run.
54    pub oracle_summary: GoldenTraceOracleSummary,
55}
56
57impl GoldenTraceFixture {
58    /// Build a golden trace fixture from a trace event slice.
59    #[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    /// Compare two fixtures and return a diff if any field changed.
85    pub fn verify(&self, actual: &Self) -> Result<(), GoldenTraceDiff> {
86        GoldenTraceDiff::from_fixtures(self, actual).into_result()
87    }
88}
89
90/// Diff between two golden trace fixtures.
91#[derive(Debug, Default)]
92pub struct GoldenTraceDiff {
93    mismatches: Vec<GoldenTraceMismatch>,
94}
95
96impl GoldenTraceDiff {
97    /// Returns true if no mismatches were recorded.
98    #[must_use]
99    pub fn is_empty(&self) -> bool {
100        self.mismatches.is_empty()
101    }
102
103    fn push(&mut self, mismatch: GoldenTraceMismatch) {
104        self.mismatches.push(mismatch);
105    }
106
107    fn from_fixtures(expected: &GoldenTraceFixture, actual: &GoldenTraceFixture) -> Self {
108        let mut diff = Self::default();
109        if expected.schema_version != actual.schema_version {
110            diff.push(GoldenTraceMismatch::SchemaVersion {
111                expected: expected.schema_version,
112                actual: actual.schema_version,
113            });
114        }
115        if expected.config != actual.config {
116            diff.push(GoldenTraceMismatch::Config {
117                expected: expected.config.clone(),
118                actual: actual.config.clone(),
119            });
120        }
121        if expected.fingerprint != actual.fingerprint {
122            diff.push(GoldenTraceMismatch::Fingerprint {
123                expected: expected.fingerprint,
124                actual: actual.fingerprint,
125            });
126        }
127        if expected.event_count != actual.event_count {
128            diff.push(GoldenTraceMismatch::EventCount {
129                expected: expected.event_count,
130                actual: actual.event_count,
131            });
132        }
133        if expected.canonical_prefix != actual.canonical_prefix {
134            diff.push(GoldenTraceMismatch::CanonicalPrefix {
135                expected_layers: expected.canonical_prefix.len(),
136                actual_layers: actual.canonical_prefix.len(),
137                first_mismatch: first_prefix_mismatch(
138                    &expected.canonical_prefix,
139                    &actual.canonical_prefix,
140                ),
141            });
142        }
143        if expected.oracle_summary != actual.oracle_summary {
144            diff.push(GoldenTraceMismatch::OracleViolations {
145                expected: expected.oracle_summary.violations.clone(),
146                actual: actual.oracle_summary.violations.clone(),
147            });
148        }
149        diff
150    }
151
152    fn into_result(self) -> Result<(), Self> {
153        if self.is_empty() {
154            Ok(())
155        } else {
156            Err(self)
157        }
158    }
159}
160
161impl std::fmt::Display for GoldenTraceDiff {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        for mismatch in &self.mismatches {
164            writeln!(f, "{mismatch}")?;
165        }
166        Ok(())
167    }
168}
169
170impl std::error::Error for GoldenTraceDiff {}
171
172#[derive(Debug)]
173enum GoldenTraceMismatch {
174    SchemaVersion {
175        expected: u32,
176        actual: u32,
177    },
178    Config {
179        expected: GoldenTraceConfig,
180        actual: GoldenTraceConfig,
181    },
182    Fingerprint {
183        expected: u64,
184        actual: u64,
185    },
186    EventCount {
187        expected: u64,
188        actual: u64,
189    },
190    CanonicalPrefix {
191        expected_layers: usize,
192        actual_layers: usize,
193        first_mismatch: Option<(usize, usize)>,
194    },
195    OracleViolations {
196        expected: Vec<String>,
197        actual: Vec<String>,
198    },
199}
200
201impl std::fmt::Display for GoldenTraceMismatch {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        match self {
204            Self::SchemaVersion { expected, actual } => {
205                write!(
206                    f,
207                    "schema_version changed (expected {expected}, actual {actual})"
208                )
209            }
210            Self::Config { expected, actual } => {
211                write!(
212                    f,
213                    "config changed (expected {expected:?}, actual {actual:?})"
214                )
215            }
216            Self::Fingerprint { expected, actual } => {
217                write!(
218                    f,
219                    "fingerprint changed (expected 0x{expected:016X}, actual 0x{actual:016X})"
220                )
221            }
222            Self::EventCount { expected, actual } => write!(
223                f,
224                "event_count changed (expected {expected}, actual {actual})"
225            ),
226            Self::CanonicalPrefix {
227                expected_layers,
228                actual_layers,
229                first_mismatch,
230            } => {
231                if let Some((layer, index)) = first_mismatch {
232                    write!(
233                        f,
234                        "canonical_prefix mismatch (layer {layer}, index {index}; expected_layers={expected_layers}, actual_layers={actual_layers})"
235                    )
236                } else {
237                    write!(
238                        f,
239                        "canonical_prefix mismatch (expected_layers={expected_layers}, actual_layers={actual_layers})"
240                    )
241                }
242            }
243            Self::OracleViolations { expected, actual } => {
244                write!(
245                    f,
246                    "oracle violations changed (expected {expected:?}, actual {actual:?})"
247                )
248            }
249        }
250    }
251}
252
253fn canonical_prefix(
254    events: &[TraceEvent],
255    max_layers: usize,
256    max_events: usize,
257) -> Vec<Vec<TraceEventKey>> {
258    let foata = canonicalize(events);
259    let mut remaining = max_events;
260    let mut prefix = Vec::new();
261
262    for layer in foata.layers().iter().take(max_layers) {
263        if remaining == 0 {
264            break;
265        }
266        let mut keys = Vec::new();
267        for event in layer {
268            if remaining == 0 {
269                break;
270            }
271            keys.push(trace_event_key(event));
272            remaining = remaining.saturating_sub(1);
273        }
274        if !keys.is_empty() {
275            prefix.push(keys);
276        }
277    }
278
279    prefix
280}
281
282fn first_prefix_mismatch(
283    expected: &[Vec<TraceEventKey>],
284    actual: &[Vec<TraceEventKey>],
285) -> Option<(usize, usize)> {
286    let layers = expected.len().min(actual.len());
287    for layer_idx in 0..layers {
288        let expected_layer = &expected[layer_idx];
289        let actual_layer = &actual[layer_idx];
290        let events = expected_layer.len().min(actual_layer.len());
291        for event_idx in 0..events {
292            if expected_layer[event_idx] != actual_layer[event_idx] {
293                return Some((layer_idx, event_idx));
294            }
295        }
296        if expected_layer.len() != actual_layer.len() {
297            return Some((layer_idx, events));
298        }
299    }
300    if expected.len() != actual.len() {
301        return Some((layers, 0));
302    }
303    None
304}
305
306/// Formats a trace buffer as human-readable text.
307pub fn format_trace(buffer: &TraceBuffer, w: &mut impl Write) -> io::Result<()> {
308    writeln!(w, "=== Trace ({} events) ===", buffer.len())?;
309    for event in buffer.iter() {
310        writeln!(w, "{event}")?;
311    }
312    writeln!(w, "=== End Trace ===")?;
313    Ok(())
314}
315
316/// Formats a trace buffer as a string.
317#[must_use]
318pub fn trace_to_string(buffer: &TraceBuffer) -> String {
319    let mut s = Vec::new();
320    format_trace(buffer, &mut s).expect("writing to Vec should not fail");
321    String::from_utf8(s).expect("trace should be valid UTF-8")
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::trace::event::{TraceData, TraceEvent, TraceEventKind};
328    use crate::types::Time;
329
330    #[test]
331    fn format_empty_trace() {
332        let buffer = TraceBuffer::new(10);
333        let output = trace_to_string(&buffer);
334        assert!(output.contains("0 events"));
335    }
336
337    #[test]
338    fn format_with_events() {
339        let mut buffer = TraceBuffer::new(10);
340        buffer.push(TraceEvent::new(
341            1,
342            Time::from_millis(100),
343            TraceEventKind::UserTrace,
344            TraceData::Message("test".to_string()),
345        ));
346        let output = trace_to_string(&buffer);
347        assert!(output.contains("1 events"));
348        assert!(output.contains("test"));
349    }
350}