///|
pub(all) struct Mismatch {
event_id : String
expected : String
actual : String
expected_codes : Array[String]
actual_codes : Array[String]
} derive(Eq, ToJson)
///|
pub(all) struct CaseResult {
name : String
passed : Bool
calls : Int32
mismatches : Array[Mismatch]
} derive(Eq, ToJson)
///|
pub(all) struct SuiteReport {
schema_version : Int32
policy : String
cases : Int32
passed : Int32
failed : Int32
results : Array[CaseResult]
} derive(Eq, ToJson)
///|
priv struct Expectation {
event_id : String
decision : String
codes : Array[String]
}
///|
fn same_codes(a : Array[String], b : Array[String]) -> Bool {
a.length() == b.length() && a.all(item => b.contains(item))
}
///|
fn expectations(
value : Json,
path : String,
) -> Map[String, Expectation] raise InputError {
let result : Map[String, Expectation] = Map([])
match value {
Array(items) => {
if items.length() > 10000 {
invalid(path, "at most 10000 expectations")
}
for i, item in items {
let path = "\{path}[\{i}]"
let f = object_at(item, path)
known_fields(f, ["event_id", "decision", "codes"], path)
let id = text_field(f, "event_id", path)
check_text(id, "\{path}.event_id")
if result.contains(id) {
invalid(path, "duplicate expectation")
}
let decision = text_field(f, "decision", path)
if decision != "allow" && decision != "deny" {
invalid(path, "expected allow or deny")
}
let codes = string_list(f, "codes", path)
unique_strings(codes, "\{path}.codes")
if decision == "allow" && !codes.is_empty() {
invalid(path, "allow expectation cannot contain violation codes")
}
result[id] = { event_id: id, decision, codes, }
}
}
_ => invalid(path, "expected expectation array")
}
result
}
///|
fn run_case(
policy : Policy,
value : Json,
path : String,
) -> CaseResult raise InputError {
let f = object_at(value, path)
known_fields(f, ["name", "trace", "expect"], path)
let name = text_field(f, "name", path)
check_text(name, "\{path}.name")
let trace_value = required(f, "trace", path)
guard trace_value is Array(_) else {
return invalid(path, "trace must be a JSON array")
}
let trace = parse_trace(trace_value.stringify())
let expected = expectations(required(f, "expect", path), "\{path}.expect")
let report = evaluate(policy, trace)
let actual_ids : Map[String, Bool] = Map([])
let mismatches : Array[Mismatch] = []
for decision in report.decisions {
actual_ids[decision.event_id] = true
let actual_codes : Array[String] = []
append_unique(actual_codes, decision.findings.map(f => f.code))
match expected.get(decision.event_id) {
None =>
mismatches.push({
event_id: decision.event_id,
expected: "expectation_required",
actual: decision.decision,
expected_codes: [],
actual_codes,
})
Some(expectation) =>
if expectation.decision != decision.decision ||
!same_codes(expectation.codes, actual_codes) {
mismatches.push({
event_id: decision.event_id,
expected: expectation.decision,
actual: decision.decision,
expected_codes: expectation.codes,
actual_codes,
})
}
}
}
for id, expectation in expected {
if !actual_ids.contains(id) {
mismatches.push({
event_id: expectation.event_id,
expected: expectation.decision,
actual: "call_not_found",
expected_codes: expectation.codes,
actual_codes: [],
})
}
}
{ name, passed: mismatches.is_empty(), calls: report.calls, mismatches, }
}
///|
/// Every observed call needs exactly one expectation. Missing assertions fail.
pub fn run_suite(source : String) -> SuiteReport raise InputError {
let f = object_at(parse_json(source, "suite"), "suite")
known_fields(f, ["schema_version", "policy", "cases"], "suite")
if int_at(required(f, "schema_version", "suite"), "suite.schema_version") != 1 {
invalid("suite.schema_version", "only version 1 is supported")
}
let policy = parse_policy(required(f, "policy", "suite").stringify())
let results = []
let mut passed = 0
let names : Map[String, Bool] = Map([])
match required(f, "cases", "suite") {
Array(cases) => {
if cases.is_empty() || cases.length() > 64 {
invalid("suite.cases", "expected 1 to 64 cases")
}
for i, case in cases {
let result = run_case(policy, case, "suite.cases[\{i}]")
if names.contains(result.name) {
invalid("suite.cases", "duplicate case name")
}
names[result.name] = true
if result.passed {
passed += 1
}
results.push(result)
}
}
_ => invalid("suite.cases", "expected case array")
}
{
schema_version: 1,
policy: policy.name,
cases: results.length(),
passed,
failed: results.length() - passed,
results,
}
}
///|
pub fn run_suite_json(source : String) -> String raise InputError {
@json.to_json(run_suite(source)).stringify(indent=2)
}