///|
/// Accepted documents are immutable snapshots. A conflicting re-import cannot
/// replace earlier evidence, and rejected input leaves this ledger unchanged.
struct Ledger {
plan : RunPlan
limits : Limits
documents : Map[String, Document]
mut source_units : Int64
mut observation_count : Int32
mut problem_count : Int32
}
///|
fn copy_case(testcase : ObservedCase) -> ObservedCase {
{ ..testcase, problems: testcase.problems.copy(), }
}
///|
fn copy_document(document : Document) -> Document {
{
..document,
report: {
source: document.report.source,
cases: document.report.cases.map(copy_case),
},
}
}
///|
pub fn Ledger::new(
plan : RunPlan,
limits~ : Limits = Limits::default(),
) -> Ledger raise InputError {
let plan = RunPlan::new(plan.run_id, plan.revision, plan.reports, limits~)
{
plan,
limits,
documents: Map([]),
source_units: 0L,
observation_count: 0,
problem_count: 0,
}
}
///|
fn validate_observation(
testcase : ObservedCase,
id : String,
) -> Unit raise InputError {
let failure = testcase.problems.any(p => p.kind == ProblemKind::Failure)
let error = testcase.problems.any(p => p.kind == ProblemKind::Error)
let coherent = match testcase.outcome {
Pass => !failure && !error && testcase.skipped_reason is None
Skip => !failure && !error
Failure => failure && !error && testcase.skipped_reason is None
Error => error && testcase.skipped_reason is None
}
if !coherent {
raise invalid(
"InconsistentOutcome",
id,
testcase.path,
"Outcome does not agree with the retained failure/error/skipped evidence",
)
}
}
///|
pub fn Ledger::ingest(
self : Ledger,
document : Document,
) -> Unit raise InputError {
let binding = document.binding
let expected = match
self.plan.reports.iter().find_first(report => report.id == binding.id) {
Some(expected) => expected
None =>
raise invalid(
"UnexpectedReport",
binding.id,
"/binding/id",
"Report ID was not declared in this run plan",
)
}
if binding.run_id != self.plan.run_id ||
binding.revision != self.plan.revision ||
binding.environment != expected.environment ||
binding.shard != expected.shard ||
binding.attempt != expected.attempt {
raise invalid(
"BindingMismatch",
binding.id,
"/binding",
"Run, revision, environment, shard and attempt must exactly match the plan",
)
}
match self.documents.get(binding.id) {
Some(accepted) => {
if accepted == document {
return
}
raise invalid(
"ConflictingReport",
binding.id,
"/",
"This report identity already has different accepted content; prior evidence was retained",
)
}
None => ()
}
if document.report.source.length() > self.limits.max_source_units ||
self.source_units + document.report.source.length().to_int64() >
self.limits.max_total_source_units.to_int64() {
raise invalid(
"SourceLimit",
binding.id,
"/",
"Per-document or total retained-source budget exceeded",
)
}
if document.report.cases.length() >
self.limits.max_cases - self.observation_count {
raise invalid(
"CaseLimit",
binding.id,
"/cases",
"Accepted observation budget exceeded",
)
}
let seen : Set[CaseKey] = Set([])
let expected_keys : Set[CaseKey] = Set([])
for key in expected.cases {
expected_keys.add(key)
}
let mut problems = 0
for testcase in document.report.cases {
if seen.contains(testcase.key) {
raise invalid(
"DuplicateCase",
binding.id,
testcase.path,
"A testcase identity appears more than once in this report",
)
}
seen.add(testcase.key)
if !expected_keys.contains(testcase.key) {
raise invalid(
"UnexpectedCase",
binding.id,
testcase.path,
"Testcase identity was not expected in this report and attempt",
)
}
validate_observation(testcase, binding.id)
if testcase.problems.length() >
self.limits.max_problems - self.problem_count - problems {
raise invalid(
"ProblemLimit",
binding.id,
testcase.path,
"Accepted problem evidence budget exceeded",
)
}
problems += testcase.problems.length()
}
// All possible refusals occur before mutation. Retained arrays belong to us.
let accepted = copy_document(document)
self.documents[binding.id] = accepted
self.source_units += accepted.report.source.length().to_int64()
self.observation_count += accepted.report.cases.length()
self.problem_count += problems
}
///|
pub fn Ledger::source(self : Ledger, report_id : String) -> String? {
self.documents.get(report_id).map(document => document.report.source)
}
///|
/// Export a fresh snapshot, ordered by the plan. Mutating its arrays cannot
/// alter the ledger; absent expected reports remain absent in the document list.
pub fn Ledger::snapshot(self : Ledger) -> Snapshot {
let documents : Array[Document] = []
for report in self.plan.reports {
if self.documents.get(report.id) is Some(document) {
documents.push(copy_document(document))
}
}
{
plan: { ..self.plan, reports: self.plan.reports.map(copy_expected), },
documents,
}
}