///|
/// Outcome assigned while replaying a protocol transaction stream.
pub enum ReplayOutcome {
ReplayAccepted
ReplayRejected
ReplayIncomplete
ReplayOutOfOrder
} derive(Debug, Eq)
///|
/// One replayed transaction with a state-machine sequence number.
pub struct ReplayStep {
index : Int
start : Int64
end : Int64
state_before : String
state_after : String
outcome : ReplayOutcome
payload : Value?
note : String
} derive(Debug)
///|
/// A replayed protocol state machine result.
pub struct ProtocolReplay {
channel : String
steps : Array[ReplayStep]
accepted : Int
rejected : Int
incomplete : Int
final_state : String
deterministic : Bool
} derive(Debug)
///|
/// List replay outcomes.
pub fn replay_outcome_catalog() -> Array[ReplayOutcome] {
[ReplayAccepted, ReplayRejected, ReplayIncomplete, ReplayOutOfOrder]
}
///|
/// Replay a decoded transaction stream as a small deterministic state machine.
pub fn replay_protocol(
channel : ProtocolChannel,
transactions : Array[ProtocolTransaction],
) -> ProtocolReplay {
let steps : Array[ReplayStep] = []
let mut state = "idle"
let mut accepted = 0
let mut rejected = 0
let mut incomplete = 0
let mut previous_end : Int64? = None
let mut deterministic = true
for transaction in transactions {
let order_bad = match previous_end {
Some(time) => transaction.start < time
None => false
}
let outcome = if order_bad {
ReplayOutOfOrder
} else if !transaction.completed {
ReplayIncomplete
} else if transaction.accepted {
ReplayAccepted
} else {
ReplayRejected
}
let after = match outcome {
ReplayAccepted => "accepted"
ReplayRejected => "rejected"
ReplayIncomplete => "incomplete"
ReplayOutOfOrder => "out-of-order"
}
steps.push({
index: transaction.index,
start: transaction.start,
end: transaction.end,
state_before: state,
state_after: after,
outcome,
payload: transaction.payload,
note: if order_bad {
"transaction starts before previous completion"
} else {
"replayed in source order"
},
})
state = after
match outcome {
ReplayAccepted => accepted += 1
ReplayRejected => rejected += 1
ReplayIncomplete => incomplete += 1
ReplayOutOfOrder => deterministic = false
}
previous_end = Some(transaction.end)
}
{
channel: channel.name,
steps,
accepted,
rejected,
incomplete,
final_state: state,
deterministic,
}
}
///|
/// Replay a trace directly through a protocol channel.
pub fn replay_protocol_trace(
trace : Trace,
channel : ProtocolChannel,
) -> ProtocolReplay {
replay_protocol(channel, protocol_transactions(trace, channel))
}
///|
/// Return whether the replay was deterministic and complete.
pub fn ProtocolReplay::is_success(self : ProtocolReplay) -> Bool {
self.deterministic && self.incomplete == 0
}
///|
/// Return replay steps with a particular outcome.
pub fn ProtocolReplay::steps_with(
self : ProtocolReplay,
outcome : ReplayOutcome,
) -> Array[ReplayStep] {
self.steps.filter(fn(item) { item.outcome == outcome })
}
///|
/// Export replay steps as CSV.
pub fn replay_steps_csv(steps : Array[ReplayStep]) -> String {
let out = StringBuilder()
out.write_string("index,start,end,before,after,outcome,payload,note\n")
for step in steps {
out.write_string(
step.index.to_string() +
"," +
step.start.to_string() +
"," +
step.end.to_string() +
"," +
step.state_before +
"," +
step.state_after +
"," +
replay_outcome_name(step.outcome) +
",",
)
out.write_string(
match step.payload {
Some(value) => escape_text(value.display())
None => ""
},
)
out.write_string("," + escape_text(step.note) + "\n")
}
out.to_string()
}
///|
/// Render replay metadata as JSON.
pub fn ProtocolReplay::to_json(self : ProtocolReplay) -> String {
"{\"channel\":\"" +
escape_text(self.channel) +
"\",\"accepted\":" +
self.accepted.to_string() +
",\"rejected\":" +
self.rejected.to_string() +
",\"incomplete\":" +
self.incomplete.to_string() +
",\"deterministic\":" +
self.deterministic.to_string() +
",\"steps\":[" +
self.steps.map(replay_step_json).join(",") +
"]}"
}
///|
/// Render replay metadata for a terminal.
pub fn ProtocolReplay::to_text(self : ProtocolReplay) -> String {
self.channel +
" accepted=" +
self.accepted.to_string() +
" rejected=" +
self.rejected.to_string() +
" incomplete=" +
self.incomplete.to_string() +
" deterministic=" +
self.deterministic.to_string()
}
///|
/// Name a replay outcome.
pub fn replay_outcome_name(value : ReplayOutcome) -> String {
match value {
ReplayAccepted => "accepted"
ReplayRejected => "rejected"
ReplayIncomplete => "incomplete"
ReplayOutOfOrder => "out-of-order"
}
}
///|
/// Encode a replay step as JSON.
fn replay_step_json(item : ReplayStep) -> String {
"{\"index\":" +
item.index.to_string() +
",\"start\":" +
item.start.to_string() +
",\"end\":" +
item.end.to_string() +
",\"before\":\"" +
item.state_before +
"\",\"after\":\"" +
item.state_after +
"\",\"outcome\":\"" +
replay_outcome_name(item.outcome) +
"\",\"note\":\"" +
escape_text(item.note) +
"\"}"
}