///|
fn verdict(attempts : Array[AttemptRecord], missing : Bool) -> Verdict {
if missing {
return Incomplete
}
guard attempts.last() is Some(last) else { return Incomplete }
let had_failure = attempts.any(record => {
record.observation.outcome is (Failure | Error)
})
match last.observation.outcome {
Pass => if had_failure { RecoveredOnRetry } else { Passed }
Failure => Failed
Error => Error
Skip => if had_failure { Unresolved } else { Skipped }
}
}
///|
fn observation(
index : Map[String, Map[CaseKey, ObservedCase]],
report_id : String,
key : CaseKey,
) -> ObservedCase? {
match index.get(report_id) {
None => None
Some(cases) => cases.get(key)
}
}
///|
/// Return a fresh evidence snapshot in canonical environment/shard/identity
/// order, regardless of the order documents were ingested.
pub fn Ledger::summary(self : Ledger) -> Summary {
let missing_reports : Array[String] = []
let missing_cases : Array[MissingCase] = []
let index : Map[String, Map[CaseKey, ObservedCase]] = Map([])
let expected_keys : Map[String, Set[CaseKey]] = Map([])
for id, document in self.documents {
let cases : Map[CaseKey, ObservedCase] = Map([])
for testcase in document.report.cases {
cases[testcase.key] = testcase
}
index[id] = cases
}
let groups : Map[(String, String), Array[ExpectedReport]] = Map([])
for expected in self.plan.reports {
let group = (expected.environment, expected.shard)
match groups.get(group) {
Some(attempts) => attempts.push(expected)
None => groups[group] = [expected]
}
if !self.documents.contains(expected.id) {
missing_reports.push(expected.id)
}
let keys = expected.cases.copy()
keys.sort()
let membership : Set[CaseKey] = Set([])
for key in keys {
membership.add(key)
if observation(index, expected.id, key) is None {
missing_cases.push({ report_id: expected.id, key, })
}
}
expected_keys[expected.id] = membership
}
let cases : Array[CaseSummary] = []
for (environment, shard), expected_attempts in groups {
let keys = expected_attempts[0].cases.copy()
keys.sort()
for key in keys {
let attempts : Array[AttemptRecord] = []
let mut missing = false
for expected in expected_attempts {
if expected_keys[expected.id].contains(key) {
match observation(index, expected.id, key) {
None => missing = true
Some(testcase) =>
attempts.push({
report_id: expected.id,
attempt: expected.attempt,
observation: copy_case(testcase),
})
}
}
}
cases.push({
environment,
shard,
key,
verdict: verdict(attempts, missing),
attempts,
})
}
}
{
run_id: self.plan.run_id,
revision: self.plan.revision,
complete: missing_reports.is_empty() && missing_cases.is_empty(),
cases,
missing_reports,
missing_cases,
}
}
///|
/// 0: complete passing/skipped/recovered; 1: failed/unresolved or denied
/// recovery; 3: incomplete inventory. Invalid input is reported separately.
pub fn Summary::exit_code(
self : Summary,
deny_recovered~ : Bool = false,
) -> Int32 {
if !self.complete {
return 3
}
for testcase in self.cases {
match testcase.verdict {
Failed | Error | Unresolved => return 1
Incomplete => return 3
RecoveredOnRetry if deny_recovered => return 1
Passed | Skipped | RecoveredOnRetry => ()
}
}
0
}