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, 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    /// Output frame identity and kind trace.
35    pub output_frames: Vec<OutputFrameTrace>,
36    /// Scope lifecycle events emitted by the transaction.
37    pub scope_events: Vec<ScopeLifecycleTrace>,
38    /// Audit log emitted by the transaction.
39    pub audit_log: Vec<AuditEntry>,
40    /// Phase trace emitted by the transaction.
41    pub phase_trace: Vec<TransactionPhase>,
42    /// Optional invariant results layered by testing support.
43    pub invariant_results: Vec<InvariantResultTrace>,
44}
45
46impl TransactionTrace {
47    /// Builds a deterministic trace from a transaction result.
48    pub fn from_result<C>(result: &TransactionResult<C>) -> Self {
49        Self {
50            transaction_id: result.transaction_id,
51            revision: result.revision,
52            staged_input_changes: result.staged_input_changes.clone(),
53            changed_inputs: result.changed_inputs.clone(),
54            dirty_roots: result.dirty_roots.clone(),
55            recomputed_derived_nodes: result.recomputed_derived_nodes.clone(),
56            changed_derived_nodes: result.changed_derived_nodes.clone(),
57            recomputed_collection_nodes: result.recomputed_collection_nodes.clone(),
58            changed_collection_nodes: result.changed_collection_nodes.clone(),
59            collection_diffs: result.collection_diffs.clone(),
60            resource_commands: result
61                .resource_plan
62                .commands()
63                .iter()
64                .map(ResourceCommandTrace::from_command)
65                .collect(),
66            output_frames: result
67                .output_frames
68                .iter()
69                .map(|frame| OutputFrameTrace {
70                    output_key: frame.output_key,
71                    scope: frame.scope,
72                    transaction_id: frame.transaction_id,
73                    revision: frame.revision,
74                    kind: OutputFrameKindTrace::from_kind(&frame.kind),
75                })
76                .collect(),
77            scope_events: result.scope_events.clone(),
78            audit_log: result.audit_log.clone(),
79            phase_trace: result.phase_trace.clone(),
80            invariant_results: result.invariant_results.clone(),
81        }
82    }
83}
84
85/// Payload-free resource command trace.
86#[derive(Clone, Debug, Eq, PartialEq)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88pub struct ResourceCommandTrace {
89    /// Resource identity.
90    pub key: ResourceKey,
91    /// Scope associated with the command.
92    pub scope: ScopeId,
93    /// Command operation.
94    pub kind: ResourceCommandKind,
95}
96
97impl ResourceCommandTrace {
98    fn from_command<C>(command: &ResourceCommand<C>) -> Self {
99        Self {
100            key: command.key().clone(),
101            scope: command.scope(),
102            kind: ResourceCommandKind::from_command(command),
103        }
104    }
105}
106
107/// Resource command operation without application payload.
108#[derive(Copy, Clone, Debug, Eq, PartialEq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub enum ResourceCommandKind {
111    /// Open a resource.
112    Open,
113    /// Close a resource.
114    Close,
115    /// Replace a resource.
116    Replace,
117    /// Refresh a resource.
118    Refresh,
119}
120
121impl ResourceCommandKind {
122    pub(crate) fn from_command<C>(command: &ResourceCommand<C>) -> Self {
123        match command {
124            ResourceCommand::Open { .. } => Self::Open,
125            ResourceCommand::Close { .. } => Self::Close,
126            ResourceCommand::Replace { .. } => Self::Replace,
127            ResourceCommand::Refresh { .. } => Self::Refresh,
128        }
129    }
130}
131
132/// Payload-free output frame trace.
133#[derive(Clone, Debug, Eq, PartialEq)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
135pub struct OutputFrameTrace {
136    /// Output identity.
137    pub output_key: OutputKey,
138    /// Scope that owns the output.
139    pub scope: ScopeId,
140    /// Transaction that emitted the frame.
141    pub transaction_id: TransactionId,
142    /// Revision carried by the frame.
143    pub revision: Revision,
144    /// Frame kind without materialized payload.
145    pub kind: OutputFrameKindTrace,
146}
147
148/// Output frame kind without materialized payload.
149#[derive(Copy, Clone, Debug, Eq, PartialEq)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151pub enum OutputFrameKindTrace {
152    /// Baseline frame.
153    Baseline,
154    /// Delta frame.
155    Delta,
156    /// Clear frame with reason.
157    Clear(ClearReason),
158    /// Rebaseline frame with reason.
159    Rebaseline(RebaselineReason),
160}
161
162impl OutputFrameKindTrace {
163    pub(crate) fn from_kind(kind: &OutputFrameKind) -> Self {
164        match kind {
165            OutputFrameKind::Baseline(_) => Self::Baseline,
166            OutputFrameKind::Delta(_) => Self::Delta,
167            OutputFrameKind::Clear(reason) => Self::Clear(*reason),
168            OutputFrameKind::Rebaseline(_, reason) => Self::Rebaseline(*reason),
169        }
170    }
171}
172
173impl<C> TransactionResult<C> {
174    /// Returns a deterministic payload-free projection of this result.
175    pub fn trace(&self) -> TransactionTrace {
176        TransactionTrace::from_result(self)
177    }
178}
179
180/// Difference between two replay trace sequences.
181#[derive(Clone, Debug, Eq, PartialEq)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
183pub struct TraceMismatch {
184    /// Expected transaction traces.
185    pub expected: Vec<TransactionTrace>,
186    /// Actual transaction traces.
187    pub actual: Vec<TransactionTrace>,
188}
189
190/// Compares two deterministic transaction trace sequences.
191pub fn assert_transaction_traces_match(
192    expected: &[TransactionTrace],
193    actual: &[TransactionTrace],
194) -> Result<(), TraceMismatch> {
195    if expected == actual {
196        Ok(())
197    } else {
198        Err(TraceMismatch {
199            expected: expected.to_vec(),
200            actual: actual.to_vec(),
201        })
202    }
203}