1use trellis_core::{
2 Graph, NodeId, OutputFrameKind, OutputFrameKindTrace, OutputKey, ResourceCommandKind,
3 ResourceKey, Revision, ScopeId, TransactionId, TransactionResult,
4};
5
6#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct ResourceAuditContext {
9 pub key: ResourceKey,
11 pub scope: ScopeId,
13 pub transaction_id: TransactionId,
15 pub revision: Revision,
17 pub kind: ResourceCommandKind,
19}
20
21#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct OutputAuditContext {
24 pub key: OutputKey,
26 pub scope: ScopeId,
28 pub transaction_id: TransactionId,
30 pub revision: Revision,
32 pub kind: OutputFrameKindTrace,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub enum AuditAssertionError {
39 MissingResourceCommand {
41 context: ResourceAuditContext,
43 },
44 ResourceCommandMismatch {
46 context: ResourceAuditContext,
48 field: &'static str,
50 },
51 MissingOutputFrame {
53 context: OutputAuditContext,
55 },
56 OutputFrameMismatch {
58 context: OutputAuditContext,
60 field: &'static str,
62 },
63 MissingDependencyPath {
65 from: NodeId,
67 to: NodeId,
69 },
70}
71
72pub fn assert_no_unexplained_plan<C, O>(
74 graph: &Graph<C, O>,
75 result: &TransactionResult<C, O>,
76) -> Result<(), AuditAssertionError> {
77 for command in result.resource_plan.commands() {
78 let context = resource_audit_context(command, result);
79 let key = command.key();
80 let explanation = graph.why_resource_command(key).ok_or_else(|| {
81 AuditAssertionError::MissingResourceCommand {
82 context: context.clone(),
83 }
84 })?;
85 if explanation.scope != command.scope() {
86 return Err(AuditAssertionError::ResourceCommandMismatch {
87 context,
88 field: "scope",
89 });
90 }
91 if explanation.transaction_id != result.transaction_id {
92 return Err(AuditAssertionError::ResourceCommandMismatch {
93 context,
94 field: "transaction_id",
95 });
96 }
97 if explanation.revision != result.revision {
98 return Err(AuditAssertionError::ResourceCommandMismatch {
99 context,
100 field: "revision",
101 });
102 }
103 if explanation.kind != resource_command_kind(command) {
104 return Err(AuditAssertionError::ResourceCommandMismatch {
105 context,
106 field: "kind",
107 });
108 }
109 if !resource_cause_is_explainable(explanation, command, result) {
110 return Err(AuditAssertionError::ResourceCommandMismatch {
111 context,
112 field: "cause",
113 });
114 }
115 }
116 Ok(())
117}
118
119pub fn assert_every_resource_command_has_cause<C, O>(
121 graph: &Graph<C, O>,
122 result: &TransactionResult<C, O>,
123) -> Result<(), AuditAssertionError> {
124 assert_no_unexplained_plan(graph, result)
125}
126
127pub fn assert_no_unexplained_output_frame<C, O>(
129 graph: &Graph<C, O>,
130 result: &TransactionResult<C, O>,
131) -> Result<(), AuditAssertionError> {
132 for frame in &result.output_frames {
133 let context = output_audit_context(frame);
134 let explanation = graph.why_output_frame(frame.output_key).ok_or(
135 AuditAssertionError::MissingOutputFrame {
136 context: context.clone(),
137 },
138 )?;
139 if explanation.scope != frame.scope {
140 return Err(AuditAssertionError::OutputFrameMismatch {
141 context,
142 field: "scope",
143 });
144 }
145 if explanation.transaction_id != frame.transaction_id {
146 return Err(AuditAssertionError::OutputFrameMismatch {
147 context,
148 field: "transaction_id",
149 });
150 }
151 if explanation.revision != frame.revision {
152 return Err(AuditAssertionError::OutputFrameMismatch {
153 context,
154 field: "revision",
155 });
156 }
157 if explanation.kind != output_frame_kind(&frame.kind) {
158 return Err(AuditAssertionError::OutputFrameMismatch {
159 context,
160 field: "kind",
161 });
162 }
163 if !output_frame_is_explainable(explanation, result) {
164 return Err(AuditAssertionError::OutputFrameMismatch {
165 context,
166 field: "input_causes",
167 });
168 }
169 }
170 Ok(())
171}
172
173pub fn assert_every_output_frame_has_revision<C, O>(
175 graph: &Graph<C, O>,
176 result: &TransactionResult<C, O>,
177) -> Result<(), AuditAssertionError> {
178 assert_no_unexplained_output_frame(graph, result)
179}
180
181pub fn assert_every_output_frame_has_scope<C, O>(
183 graph: &Graph<C, O>,
184 result: &TransactionResult<C, O>,
185) -> Result<(), AuditAssertionError> {
186 assert_no_unexplained_output_frame(graph, result)
187}
188
189pub fn assert_dependency_path_exists<C, O>(
191 graph: &Graph<C, O>,
192 from: NodeId,
193 to: NodeId,
194) -> Result<(), AuditAssertionError> {
195 graph
196 .dependency_path(from, to)
197 .map(|_| ())
198 .ok_or(AuditAssertionError::MissingDependencyPath { from, to })
199}
200
201fn resource_command_kind<C>(command: &trellis_core::ResourceCommand<C>) -> ResourceCommandKind {
202 match command {
203 trellis_core::ResourceCommand::Open { .. } => ResourceCommandKind::Open,
204 trellis_core::ResourceCommand::Close { .. } => ResourceCommandKind::Close,
205 trellis_core::ResourceCommand::Replace { .. } => ResourceCommandKind::Replace,
206 trellis_core::ResourceCommand::Refresh { .. } => ResourceCommandKind::Refresh,
207 }
208}
209
210fn output_frame_kind<O>(kind: &OutputFrameKind<O>) -> OutputFrameKindTrace {
211 match kind {
212 OutputFrameKind::Baseline(_) => OutputFrameKindTrace::Baseline,
213 OutputFrameKind::Delta(_) => OutputFrameKindTrace::Delta,
214 OutputFrameKind::Clear(reason) => OutputFrameKindTrace::Clear(*reason),
215 OutputFrameKind::Rebaseline(_, reason) => OutputFrameKindTrace::Rebaseline(*reason),
216 }
217}
218
219fn resource_audit_context<C, O>(
220 command: &trellis_core::ResourceCommand<C>,
221 result: &TransactionResult<C, O>,
222) -> ResourceAuditContext {
223 ResourceAuditContext {
224 key: command.key().clone(),
225 scope: command.scope(),
226 transaction_id: result.transaction_id,
227 revision: result.revision,
228 kind: resource_command_kind(command),
229 }
230}
231
232fn output_audit_context<O>(frame: &trellis_core::OutputFrame<O>) -> OutputAuditContext {
233 OutputAuditContext {
234 key: frame.output_key,
235 scope: frame.scope,
236 transaction_id: frame.transaction_id,
237 revision: frame.revision,
238 kind: output_frame_kind(&frame.kind),
239 }
240}
241
242fn resource_cause_is_explainable<C, O>(
243 explanation: &trellis_core::ResourceCommandExplanation,
244 command: &trellis_core::ResourceCommand<C>,
245 result: &TransactionResult<C, O>,
246) -> bool {
247 match explanation.cause {
248 trellis_core::ResourceCommandCause::Planner { collection } => {
249 explanation.collection_diffs.contains(&collection)
250 && (result.changed_inputs.is_empty()
251 || (!explanation.input_causes.is_empty()
252 && !explanation.dependency_paths.is_empty()))
253 }
254 trellis_core::ResourceCommandCause::ScopeClosed { scope } => scope == command.scope(),
255 }
256}
257
258fn output_frame_is_explainable<C, O>(
259 explanation: &trellis_core::OutputFrameExplanation,
260 result: &TransactionResult<C, O>,
261) -> bool {
262 result.changed_inputs.is_empty()
263 || explanation.changed_dependencies.is_empty()
264 || (!explanation.input_causes.is_empty() && !explanation.dependency_paths.is_empty())
265}