///|
/// A replay step kind derived from one witness case.
pub(all) enum ReplayStepKind {
ExpectSourceAccept
ExpectTargetReject
PreserveRegression
} derive(Eq, Debug)
///|
/// One deterministic action a downstream test runner can perform.
pub(all) struct ReplayStep {
id : String
case_id : String
kind : ReplayStepKind
contract_version : String
expected_valid : Bool
path : String
payload : String
assertion : String
note : String
} derive(Eq, Debug)
///|
/// A portable replay plan for turning witness cases into negative tests.
pub(all) struct ReplaySuite {
contract_name : String
old_version : String
new_version : String
mode : CompatibilityMode
case_count : Int
steps : Array[ReplayStep]
} derive(Eq, Debug)
///|
pub fn ReplayStepKind::render(self : ReplayStepKind) -> String {
match self {
ExpectSourceAccept => "expect-source-accept"
ExpectTargetReject => "expect-target-reject"
PreserveRegression => "preserve-regression"
}
}
///|
pub fn ReplayStepKind::title(self : ReplayStepKind) -> String {
match self {
ExpectSourceAccept => "Accept source side"
ExpectTargetReject => "Reject target side"
PreserveRegression => "Keep regression guard"
}
}
///|
pub fn WitnessPack::to_replay_suite(self : WitnessPack) -> ReplaySuite {
let steps : Array[ReplayStep] = []
for case in self.cases {
append_case_replay_steps(case, steps)
}
{
contract_name: self.contract_name,
old_version: self.old_version,
new_version: self.new_version,
mode: self.mode,
case_count: self.cases.length(),
steps,
}
}
///|
pub fn AnalysisReport::to_replay_suite(self : AnalysisReport) -> ReplaySuite {
self.to_witness_pack().to_replay_suite()
}
///|
pub fn ReplaySuite::accept_step_count(self : ReplaySuite) -> Int {
self.steps.count_if(step => step.kind == ExpectSourceAccept)
}
///|
pub fn ReplaySuite::reject_step_count(self : ReplaySuite) -> Int {
self.steps.count_if(step => step.kind == ExpectTargetReject)
}
///|
pub fn ReplaySuite::guard_step_count(self : ReplaySuite) -> Int {
self.steps.count_if(step => step.kind == PreserveRegression)
}
///|
pub fn ReplaySuite::to_json(self : ReplaySuite) -> String {
let steps = self.steps.map(replay_step_to_json)
"{" +
"\"schemaVersion\":\"1.0\"," +
"\"kind\":\"evowitness-replay-suite\"," +
"\"contract\":" +
json_string(self.contract_name) +
"," +
"\"oldVersion\":" +
json_string(self.old_version) +
"," +
"\"newVersion\":" +
json_string(self.new_version) +
"," +
"\"mode\":" +
json_string(self.mode.render()) +
"," +
"\"summary\":{" +
"\"cases\":" +
self.case_count.to_string() +
"," +
"\"steps\":" +
self.steps.length().to_string() +
"," +
"\"acceptSteps\":" +
self.accept_step_count().to_string() +
"," +
"\"rejectSteps\":" +
self.reject_step_count().to_string() +
"," +
"\"guardSteps\":" +
self.guard_step_count().to_string() +
"}," +
"\"steps\":[" +
steps.join(",") +
"]}"
}
///|
pub fn ReplaySuite::to_markdown(self : ReplaySuite) -> String {
let out = StringBuilder()
out.write_string("# EvoWitness replay suite\n\n")
out.write_string(
"- Contract: `" + markdown_escape(self.contract_name) + "`\n",
)
out.write_string(
"- Versions: `" +
markdown_escape(self.old_version) +
"` -> `" +
markdown_escape(self.new_version) +
"`\n",
)
out.write_string("- Mode: `" + self.mode.render() + "`\n")
out.write_string("- Witness cases: " + self.case_count.to_string() + "\n")
out.write_string(
"- Replay steps: " + self.steps.length().to_string() + "\n\n",
)
if self.steps.is_empty() {
out.write_string("No replay steps were generated.\n")
return out.to_string()
}
out.write_string("| Step | Kind | Contract | Expected | Case | Path |\n")
out.write_string("|---|---|---|---|---|---|\n")
for step in self.steps {
out.write_string("| `" + step.id + "` | ")
out.write_string(step.kind.render() + " | `")
out.write_string(markdown_escape(step.contract_version) + "` | ")
out.write_string(expected_word(step.expected_valid) + " | `")
out.write_string(markdown_escape(step.case_id) + "` | `")
out.write_string(markdown_escape(step.path) + "` |\n")
}
out.write_string("\n## Replay details\n\n")
for step in self.steps {
out.write_string("### " + step.id + "\n\n")
out.write_string("- Kind: `" + step.kind.render() + "`\n")
out.write_string(
"- Contract: `" + markdown_escape(step.contract_version) + "`\n",
)
out.write_string(
"- Expected: `" + expected_word(step.expected_valid) + "`\n",
)
out.write_string("- Assertion: " + markdown_escape(step.assertion) + "\n")
out.write_string("- Note: " + markdown_escape(step.note) + "\n\n")
out.write_string("```json\n")
out.write_string(step.payload + "\n")
out.write_string("```\n\n")
}
out.to_string()
}
///|
pub fn ReplaySuite::to_checklist(self : ReplaySuite) -> String {
let out = StringBuilder()
out.write_string("# Replay checklist\n\n")
for step in self.steps {
out.write_string("- [ ] `" + step.id + "` ")
out.write_string(step.kind.title() + " with `")
out.write_string(markdown_escape(step.contract_version) + "` should be `")
out.write_string(expected_word(step.expected_valid) + "` for `")
out.write_string(markdown_escape(step.case_id) + "` at `")
out.write_string(markdown_escape(step.path) + "`.\n")
}
if self.steps.is_empty() {
out.write_string("- [ ] No witness cases to replay.\n")
}
out.to_string()
}
///|
pub fn AnalysisReport::to_replay_json(self : AnalysisReport) -> String {
self.to_replay_suite().to_json()
}
///|
pub fn AnalysisReport::to_replay_markdown(self : AnalysisReport) -> String {
self.to_replay_suite().to_markdown()
}
///|
pub fn AnalysisReport::to_replay_checklist(self : AnalysisReport) -> String {
self.to_replay_suite().to_checklist()
}
///|
fn append_case_replay_steps(
case : WitnessCase,
steps : Array[ReplayStep],
) -> Unit {
steps.push(
replay_step(
case,
"accept",
ExpectSourceAccept,
case.accepted_by,
true,
"Replay the payload against the contract version that originally accepted it.",
),
)
steps.push(
replay_step(
case,
"reject",
ExpectTargetReject,
case.rejected_by,
false,
"Replay the same payload against the contract version that should reject it.",
),
)
steps.push(
replay_step(
case,
"guard",
PreserveRegression,
case.rejected_by,
false,
"Keep this case as a regression guard until the compatibility break is retired.",
),
)
}
///|
fn replay_step(
case : WitnessCase,
suffix : String,
kind : ReplayStepKind,
contract_version : String,
expected_valid : Bool,
note : String,
) -> ReplayStep {
{
id: case.id + "-" + suffix,
case_id: case.id,
kind,
contract_version,
expected_valid,
path: case.path,
payload: case.payload,
assertion: case.assertion,
note,
}
}
///|
fn replay_step_to_json(step : ReplayStep) -> String {
"{" +
"\"id\":" +
json_string(step.id) +
"," +
"\"caseId\":" +
json_string(step.case_id) +
"," +
"\"kind\":" +
json_string(step.kind.render()) +
"," +
"\"contractVersion\":" +
json_string(step.contract_version) +
"," +
"\"expectedValid\":" +
bool_json(step.expected_valid) +
"," +
"\"path\":" +
json_string(step.path) +
"," +
"\"payload\":" +
json_string(step.payload) +
"," +
"\"assertion\":" +
json_string(step.assertion) +
"," +
"\"note\":" +
json_string(step.note) +
"}"
}
///|
fn expected_word(value : Bool) -> String {
if value {
"valid"
} else {
"invalid"
}
}