use trellis_core::{
Graph, NodeId, OutputFrameKind, OutputFrameKindTrace, OutputKey, ResourceCommandKind,
ResourceKey, Revision, ScopeId, TransactionId, TransactionResult,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResourceAuditContext {
pub key: ResourceKey,
pub scope: ScopeId,
pub transaction_id: TransactionId,
pub revision: Revision,
pub kind: ResourceCommandKind,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OutputAuditContext {
pub key: OutputKey,
pub scope: ScopeId,
pub transaction_id: TransactionId,
pub revision: Revision,
pub kind: OutputFrameKindTrace,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AuditAssertionError {
MissingResourceCommand {
context: ResourceAuditContext,
},
ResourceCommandMismatch {
context: ResourceAuditContext,
field: &'static str,
},
MissingOutputFrame {
context: OutputAuditContext,
},
OutputFrameMismatch {
context: OutputAuditContext,
field: &'static str,
},
MissingDependencyPath {
from: NodeId,
to: NodeId,
},
}
pub fn assert_no_unexplained_plan<C, O>(
graph: &Graph<C, O>,
result: &TransactionResult<C, O>,
) -> Result<(), AuditAssertionError> {
for command in result.resource_plan.commands() {
let context = resource_audit_context(command, result);
let key = command.key();
let explanation = graph.why_resource_command(key).ok_or_else(|| {
AuditAssertionError::MissingResourceCommand {
context: context.clone(),
}
})?;
if explanation.scope != command.scope() {
return Err(AuditAssertionError::ResourceCommandMismatch {
context,
field: "scope",
});
}
if explanation.transaction_id != result.transaction_id {
return Err(AuditAssertionError::ResourceCommandMismatch {
context,
field: "transaction_id",
});
}
if explanation.revision != result.revision {
return Err(AuditAssertionError::ResourceCommandMismatch {
context,
field: "revision",
});
}
if explanation.kind != resource_command_kind(command) {
return Err(AuditAssertionError::ResourceCommandMismatch {
context,
field: "kind",
});
}
if !resource_cause_is_explainable(explanation, command, result) {
return Err(AuditAssertionError::ResourceCommandMismatch {
context,
field: "cause",
});
}
}
Ok(())
}
pub fn assert_every_resource_command_has_cause<C, O>(
graph: &Graph<C, O>,
result: &TransactionResult<C, O>,
) -> Result<(), AuditAssertionError> {
assert_no_unexplained_plan(graph, result)
}
pub fn assert_no_unexplained_output_frame<C, O>(
graph: &Graph<C, O>,
result: &TransactionResult<C, O>,
) -> Result<(), AuditAssertionError> {
for frame in &result.output_frames {
let context = output_audit_context(frame);
let explanation = graph.why_output_frame(frame.output_key).ok_or(
AuditAssertionError::MissingOutputFrame {
context: context.clone(),
},
)?;
if explanation.scope != frame.scope {
return Err(AuditAssertionError::OutputFrameMismatch {
context,
field: "scope",
});
}
if explanation.transaction_id != frame.transaction_id {
return Err(AuditAssertionError::OutputFrameMismatch {
context,
field: "transaction_id",
});
}
if explanation.revision != frame.revision {
return Err(AuditAssertionError::OutputFrameMismatch {
context,
field: "revision",
});
}
if explanation.kind != output_frame_kind(&frame.kind) {
return Err(AuditAssertionError::OutputFrameMismatch {
context,
field: "kind",
});
}
if !output_frame_is_explainable(explanation, result) {
return Err(AuditAssertionError::OutputFrameMismatch {
context,
field: "input_causes",
});
}
}
Ok(())
}
pub fn assert_every_output_frame_has_revision<C, O>(
graph: &Graph<C, O>,
result: &TransactionResult<C, O>,
) -> Result<(), AuditAssertionError> {
assert_no_unexplained_output_frame(graph, result)
}
pub fn assert_every_output_frame_has_scope<C, O>(
graph: &Graph<C, O>,
result: &TransactionResult<C, O>,
) -> Result<(), AuditAssertionError> {
assert_no_unexplained_output_frame(graph, result)
}
pub fn assert_dependency_path_exists<C, O>(
graph: &Graph<C, O>,
from: NodeId,
to: NodeId,
) -> Result<(), AuditAssertionError> {
graph
.dependency_path(from, to)
.map(|_| ())
.ok_or(AuditAssertionError::MissingDependencyPath { from, to })
}
fn resource_command_kind<C>(command: &trellis_core::ResourceCommand<C>) -> ResourceCommandKind {
match command {
trellis_core::ResourceCommand::Open { .. } => ResourceCommandKind::Open,
trellis_core::ResourceCommand::Close { .. } => ResourceCommandKind::Close,
trellis_core::ResourceCommand::Replace { .. } => ResourceCommandKind::Replace,
trellis_core::ResourceCommand::Refresh { .. } => ResourceCommandKind::Refresh,
}
}
fn output_frame_kind<O>(kind: &OutputFrameKind<O>) -> OutputFrameKindTrace {
match kind {
OutputFrameKind::Baseline(_) => OutputFrameKindTrace::Baseline,
OutputFrameKind::Delta(_) => OutputFrameKindTrace::Delta,
OutputFrameKind::Clear(reason) => OutputFrameKindTrace::Clear(*reason),
OutputFrameKind::Rebaseline(_, reason) => OutputFrameKindTrace::Rebaseline(*reason),
}
}
fn resource_audit_context<C, O>(
command: &trellis_core::ResourceCommand<C>,
result: &TransactionResult<C, O>,
) -> ResourceAuditContext {
ResourceAuditContext {
key: command.key().clone(),
scope: command.scope(),
transaction_id: result.transaction_id,
revision: result.revision,
kind: resource_command_kind(command),
}
}
fn output_audit_context<O>(frame: &trellis_core::OutputFrame<O>) -> OutputAuditContext {
OutputAuditContext {
key: frame.output_key,
scope: frame.scope,
transaction_id: frame.transaction_id,
revision: frame.revision,
kind: output_frame_kind(&frame.kind),
}
}
fn resource_cause_is_explainable<C, O>(
explanation: &trellis_core::ResourceCommandExplanation,
command: &trellis_core::ResourceCommand<C>,
result: &TransactionResult<C, O>,
) -> bool {
match explanation.cause {
trellis_core::ResourceCommandCause::Planner { collection } => {
explanation.collection_diffs.contains(&collection)
&& (result.changed_inputs.is_empty()
|| (!explanation.input_causes.is_empty()
&& !explanation.dependency_paths.is_empty()))
}
trellis_core::ResourceCommandCause::ScopeClosed { scope } => scope == command.scope(),
}
}
fn output_frame_is_explainable<C, O>(
explanation: &trellis_core::OutputFrameExplanation,
result: &TransactionResult<C, O>,
) -> bool {
result.changed_inputs.is_empty()
|| explanation.changed_dependencies.is_empty()
|| (!explanation.input_causes.is_empty() && !explanation.dependency_paths.is_empty())
}