Skip to main content

trellis_core/
trace.rs

1use crate::{
2    AuditEntry, ClearReason, CollectionDiffTrace, InvariantResultTrace, OutputFrameKind, OutputKey,
3    RebaselineReason, ResourceCommand, ResourceKey, ScopeId, ScopeLifecycleTrace,
4    StagedInputChange, TransactionId, TransactionPhase, TransactionResult,
5};
6use crate::{NodeId, ResourceCoalescedTrace, Revision};
7
8/// Deterministic payload-free projection of a committed transaction result.
9#[derive(Clone, Debug, Eq, PartialEq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct TransactionTrace {
12    /// Committed transaction id.
13    pub transaction_id: TransactionId,
14    /// Graph revision after commit.
15    pub revision: Revision,
16    /// Staged input writes in stable node-id order.
17    pub staged_input_changes: Vec<StagedInputChange>,
18    /// Input nodes changed by this transaction.
19    pub changed_inputs: Vec<NodeId>,
20    /// Initial dirty roots in stable node-id order.
21    pub dirty_roots: Vec<NodeId>,
22    /// Derived nodes recomputed in deterministic topological order.
23    pub recomputed_derived_nodes: Vec<NodeId>,
24    /// Derived nodes changed by this transaction.
25    pub changed_derived_nodes: Vec<NodeId>,
26    /// Collection nodes recomputed in deterministic topological order.
27    pub recomputed_collection_nodes: Vec<NodeId>,
28    /// Collection nodes changed by this transaction.
29    pub changed_collection_nodes: Vec<NodeId>,
30    /// Payload-neutral collection diff summaries.
31    pub collection_diffs: Vec<CollectionDiffTrace>,
32    /// Resource command identity and operation trace.
33    pub resource_commands: Vec<ResourceCommandTrace>,
34    /// Shared-key Open joins that produced no outgoing resource command.
35    #[cfg_attr(
36        feature = "serde",
37        serde(default, skip_serializing_if = "Vec::is_empty")
38    )]
39    pub resource_coalescences: Vec<ResourceCoalescedTrace>,
40    /// Output frame identity and kind trace.
41    pub output_frames: Vec<OutputFrameTrace>,
42    /// Scope lifecycle events emitted by the transaction.
43    pub scope_events: Vec<ScopeLifecycleTrace>,
44    /// Audit log emitted by the transaction.
45    pub audit_log: Vec<AuditEntry>,
46    /// Phase trace emitted by the transaction.
47    pub phase_trace: Vec<TransactionPhase>,
48    /// Optional invariant results layered by testing support.
49    pub invariant_results: Vec<InvariantResultTrace>,
50}
51
52impl TransactionTrace {
53    /// Builds a deterministic trace from a transaction result.
54    pub fn from_result<C>(result: &TransactionResult<C>) -> Self {
55        Self {
56            transaction_id: result.transaction_id,
57            revision: result.revision,
58            staged_input_changes: result.staged_input_changes.clone(),
59            changed_inputs: result.changed_inputs.clone(),
60            dirty_roots: result.dirty_roots.clone(),
61            recomputed_derived_nodes: result.recomputed_derived_nodes.clone(),
62            changed_derived_nodes: result.changed_derived_nodes.clone(),
63            recomputed_collection_nodes: result.recomputed_collection_nodes.clone(),
64            changed_collection_nodes: result.changed_collection_nodes.clone(),
65            collection_diffs: result.collection_diffs.clone(),
66            resource_commands: result
67                .resource_plan
68                .commands()
69                .iter()
70                .map(ResourceCommandTrace::from_command)
71                .collect(),
72            resource_coalescences: result.resource_coalescences.clone(),
73            output_frames: result
74                .output_frames
75                .iter()
76                .map(|frame| OutputFrameTrace {
77                    output_key: frame.output_key,
78                    scope: frame.scope,
79                    transaction_id: frame.transaction_id,
80                    revision: frame.revision,
81                    kind: OutputFrameKindTrace::from_kind(&frame.kind),
82                })
83                .collect(),
84            scope_events: result.scope_events.clone(),
85            audit_log: result.audit_log.clone(),
86            phase_trace: result.phase_trace.clone(),
87            invariant_results: result.invariant_results.clone(),
88        }
89    }
90}
91
92/// Payload-free resource command trace.
93#[derive(Clone, Debug, Eq, PartialEq)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95pub struct ResourceCommandTrace {
96    /// Resource identity.
97    pub key: ResourceKey,
98    /// Scope associated with the command.
99    pub scope: ScopeId,
100    /// Command operation.
101    pub kind: ResourceCommandKind,
102}
103
104impl ResourceCommandTrace {
105    fn from_command<C>(command: &ResourceCommand<C>) -> Self {
106        Self {
107            key: command.key().clone(),
108            scope: command.scope(),
109            kind: ResourceCommandKind::from_command(command),
110        }
111    }
112}
113
114/// Resource command operation without application payload.
115#[derive(Copy, Clone, Debug, Eq, PartialEq)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117pub enum ResourceCommandKind {
118    /// Open a resource.
119    Open,
120    /// Close a resource.
121    Close,
122    /// Replace a resource.
123    Replace,
124    /// Refresh a resource.
125    Refresh,
126}
127
128impl ResourceCommandKind {
129    pub(crate) fn from_command<C>(command: &ResourceCommand<C>) -> Self {
130        match command {
131            ResourceCommand::Open { .. } => Self::Open,
132            ResourceCommand::Close { .. } => Self::Close,
133            ResourceCommand::Replace { .. } => Self::Replace,
134            ResourceCommand::Refresh { .. } => Self::Refresh,
135        }
136    }
137}
138
139/// Payload-free output frame trace.
140#[derive(Clone, Debug, Eq, PartialEq)]
141#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
142pub struct OutputFrameTrace {
143    /// Output identity.
144    pub output_key: OutputKey,
145    /// Scope that owns the output.
146    pub scope: ScopeId,
147    /// Transaction that emitted the frame.
148    pub transaction_id: TransactionId,
149    /// Revision carried by the frame.
150    pub revision: Revision,
151    /// Frame kind without materialized payload.
152    pub kind: OutputFrameKindTrace,
153}
154
155/// Output frame kind without materialized payload.
156#[derive(Copy, Clone, Debug, Eq, PartialEq)]
157#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
158pub enum OutputFrameKindTrace {
159    /// Baseline frame.
160    Baseline,
161    /// Delta frame.
162    Delta,
163    /// Clear frame with reason.
164    Clear(ClearReason),
165    /// Rebaseline frame with reason.
166    Rebaseline(RebaselineReason),
167}
168
169impl OutputFrameKindTrace {
170    pub(crate) fn from_kind(kind: &OutputFrameKind) -> Self {
171        match kind {
172            OutputFrameKind::Baseline(_) => Self::Baseline,
173            OutputFrameKind::Delta(_) => Self::Delta,
174            OutputFrameKind::Clear(reason) => Self::Clear(*reason),
175            OutputFrameKind::Rebaseline(_, reason) => Self::Rebaseline(*reason),
176        }
177    }
178}
179
180impl<C> TransactionResult<C> {
181    /// Returns a deterministic payload-free projection of this result.
182    pub fn trace(&self) -> TransactionTrace {
183        TransactionTrace::from_result(self)
184    }
185}
186
187/// Difference between two replay trace sequences.
188#[derive(Clone, Debug, Eq, PartialEq)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
190pub struct TraceMismatch {
191    /// Expected transaction traces.
192    pub expected: Vec<TransactionTrace>,
193    /// Actual transaction traces.
194    pub actual: Vec<TransactionTrace>,
195}
196
197/// Compares two deterministic transaction trace sequences.
198pub fn assert_transaction_traces_match(
199    expected: &[TransactionTrace],
200    actual: &[TransactionTrace],
201) -> Result<(), TraceMismatch> {
202    if expected == actual {
203        Ok(())
204    } else {
205        Err(TraceMismatch {
206            expected: expected.to_vec(),
207            actual: actual.to_vec(),
208        })
209    }
210}