1use std::collections::{BTreeMap, BTreeSet};
2
3use trellis_core::{
4 OutputFrame, OutputFrameKind, OutputFrameKindTrace, OutputFrameTrace, OutputKey, OutputPayload,
5 Revision, ScopeId, TransactionId, TransactionResult,
6};
7
8#[derive(Clone, Debug, PartialEq)]
10pub struct OutputSnapshot {
11 pub scope: ScopeId,
13 pub transaction_id: TransactionId,
15 pub revision: Revision,
17 pub state: Option<OutputPayload>,
19 pub cleared: bool,
21 pub frame: OutputFrameTrace,
23}
24
25impl OutputSnapshot {
26 pub fn state_as<T>(&self) -> Option<&T>
28 where
29 T: Clone + PartialEq + Send + Sync + 'static,
30 {
31 self.state.as_ref().and_then(OutputPayload::get::<T>)
32 }
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum OutputLedgerError {
38 RevisionRegression {
40 context: OutputFrameTrace,
42 previous: Revision,
44 },
45 NotCleared {
47 key: OutputKey,
49 context: Option<OutputFrameTrace>,
51 },
52 FrameAfterClosedScope {
54 context: OutputFrameTrace,
56 },
57 ClosedScopeNotCleared {
59 scope: ScopeId,
61 outputs: Vec<OutputKey>,
63 contexts: Vec<OutputFrameTrace>,
65 },
66 StateMismatch {
68 key: OutputKey,
70 context: Option<OutputFrameTrace>,
72 },
73}
74
75#[derive(Clone, Debug, Default, PartialEq)]
77pub struct OutputLedger {
78 pub(crate) outputs: BTreeMap<OutputKey, OutputSnapshot>,
79 pub(crate) closed_scopes: BTreeSet<ScopeId>,
80 pub(crate) frames: Vec<OutputFrameTrace>,
81 pub(crate) frame_records: Vec<OutputFrame>,
82 pub(crate) errors: Vec<OutputLedgerError>,
83}
84
85impl OutputLedger {
86 pub fn new() -> Self {
88 Self {
89 outputs: BTreeMap::new(),
90 closed_scopes: BTreeSet::new(),
91 frames: Vec::new(),
92 frame_records: Vec::new(),
93 errors: Vec::new(),
94 }
95 }
96
97 pub fn close_scope(&mut self, scope: ScopeId) {
99 self.closed_scopes.insert(scope);
100 }
101
102 pub fn apply_result<C>(&mut self, result: &TransactionResult<C>) {
104 for frame in &result.output_frames {
105 self.apply_frame(frame);
106 }
107 }
108
109 pub fn apply_frame(&mut self, frame: &OutputFrame) {
111 let trace = output_frame_trace(frame);
112 self.frames.push(trace.clone());
113 self.frame_records.push(frame.clone());
114 if self.closed_scopes.contains(&frame.scope)
115 && !matches!(frame.kind, OutputFrameKind::Clear(_))
116 {
117 self.errors
118 .push(OutputLedgerError::FrameAfterClosedScope { context: trace });
119 return;
120 }
121
122 if let Some(previous) = self.outputs.get(&frame.output_key)
123 && frame.revision < previous.revision
124 {
125 self.errors.push(OutputLedgerError::RevisionRegression {
126 context: trace.clone(),
127 previous: previous.revision,
128 });
129 }
130
131 let state = match &frame.kind {
132 OutputFrameKind::Baseline(value)
133 | OutputFrameKind::Delta(value)
134 | OutputFrameKind::Rebaseline(value, _) => Some(value.clone()),
135 OutputFrameKind::Clear(_) => None,
136 };
137 self.outputs.insert(
138 frame.output_key,
139 OutputSnapshot {
140 scope: frame.scope,
141 transaction_id: frame.transaction_id,
142 revision: frame.revision,
143 state,
144 cleared: matches!(frame.kind, OutputFrameKind::Clear(_)),
145 frame: trace,
146 },
147 );
148 }
149
150 pub fn snapshot(&self, key: OutputKey) -> Option<&OutputSnapshot> {
152 self.outputs.get(&key)
153 }
154
155 pub fn errors(&self) -> &[OutputLedgerError] {
157 &self.errors
158 }
159
160 pub fn frame_trace(&self) -> &[OutputFrameTrace] {
162 &self.frames
163 }
164
165 pub fn frame_records(&self) -> &[OutputFrame] {
167 &self.frame_records
168 }
169
170 pub fn assert_revision_monotonic(&self) -> Result<(), OutputLedgerError> {
172 self.errors
173 .iter()
174 .find(|error| matches!(error, OutputLedgerError::RevisionRegression { .. }))
175 .cloned()
176 .map_or(Ok(()), Err)
177 }
178
179 pub fn assert_no_frame_for_closed_scope_except_terminal(
181 &self,
182 ) -> Result<(), OutputLedgerError> {
183 self.errors
184 .iter()
185 .find(|error| matches!(error, OutputLedgerError::FrameAfterClosedScope { .. }))
186 .cloned()
187 .map_or(Ok(()), Err)
188 }
189
190 pub fn assert_closed_scope_cleared(&self, scope: ScopeId) -> Result<(), OutputLedgerError> {
192 let uncleared = self
193 .outputs
194 .iter()
195 .filter(|(_, snapshot)| snapshot.scope == scope && !snapshot.cleared)
196 .map(|(key, _)| *key)
197 .collect::<Vec<_>>();
198 if uncleared.is_empty() {
199 Ok(())
200 } else {
201 let contexts = uncleared
202 .iter()
203 .filter_map(|key| self.outputs.get(key).map(|snapshot| snapshot.frame.clone()))
204 .collect();
205 Err(OutputLedgerError::ClosedScopeNotCleared {
206 scope,
207 outputs: uncleared,
208 contexts,
209 })
210 }
211 }
212
213 pub fn assert_cleared(&self, key: OutputKey) -> Result<(), OutputLedgerError> {
215 if self
216 .outputs
217 .get(&key)
218 .is_some_and(|snapshot| snapshot.cleared)
219 {
220 Ok(())
221 } else {
222 Err(OutputLedgerError::NotCleared {
223 key,
224 context: self
225 .outputs
226 .get(&key)
227 .map(|snapshot| snapshot.frame.clone()),
228 })
229 }
230 }
231
232 pub fn assert_current_equals(
234 &self,
235 key: OutputKey,
236 expected: &(impl Clone + PartialEq + Send + Sync + 'static),
237 ) -> Result<(), OutputLedgerError> {
238 if self.outputs.get(&key).and_then(OutputSnapshot::state_as) == Some(expected) {
239 Ok(())
240 } else {
241 Err(OutputLedgerError::StateMismatch {
242 key,
243 context: self
244 .outputs
245 .get(&key)
246 .map(|snapshot| snapshot.frame.clone()),
247 })
248 }
249 }
250
251 pub fn assert_consumer_needs_no_hidden_graph_state(&self) -> Result<(), OutputLedgerError> {
253 self.errors.first().cloned().map_or(Ok(()), Err)
254 }
255}
256
257fn output_frame_trace(frame: &OutputFrame) -> OutputFrameTrace {
258 OutputFrameTrace {
259 output_key: frame.output_key,
260 scope: frame.scope,
261 transaction_id: frame.transaction_id,
262 revision: frame.revision,
263 kind: output_frame_kind(&frame.kind),
264 }
265}
266
267fn output_frame_kind(kind: &OutputFrameKind) -> OutputFrameKindTrace {
268 match kind {
269 OutputFrameKind::Baseline(_) => OutputFrameKindTrace::Baseline,
270 OutputFrameKind::Delta(_) => OutputFrameKindTrace::Delta,
271 OutputFrameKind::Clear(reason) => OutputFrameKindTrace::Clear(*reason),
272 OutputFrameKind::Rebaseline(_, reason) => OutputFrameKindTrace::Rebaseline(*reason),
273 }
274}