//// Deterministic protocol contract verification and fuzz harness for M43.
import gleam/int
import gleam/list
import gleam/string
import lightspeed/agent/session
import lightspeed/diff
import lightspeed/pipeline/checkpoint
import lightspeed/protocol
import lightspeed/transport/contract
import lightspeed/transport/matrix
pub const snapshot_version = 1
/// M43 conformance scenarios.
pub type Scenario {
ProtocolConformanceMatrixExpansion
EnvelopeFuzzCorpusExpansion
FuzzRegressionReplayCertification
UnifiedProtocolEvidenceSurface
}
/// One M43 scenario outcome.
pub type ScenarioOutcome {
ScenarioOutcome(
scenario: Scenario,
passed: Bool,
deterministic: Bool,
signature: String,
)
}
/// Full M43 report.
pub type Report {
Report(
outcomes: List(ScenarioOutcome),
failed_scenarios: Int,
nondeterministic_failures: Int,
)
}
type EnvelopeTrack {
TransportEnvelope
EventEnvelope
DiffEnvelope
PipelineEnvelope
}
type CorpusCase {
CorpusCase(
id: String,
track: EnvelopeTrack,
payload: String,
expected: String,
)
}
/// Run all M43 scenarios.
pub fn run_matrix() -> Report {
let outcomes =
[
ProtocolConformanceMatrixExpansion,
EnvelopeFuzzCorpusExpansion,
FuzzRegressionReplayCertification,
UnifiedProtocolEvidenceSurface,
]
|> list.map(run_scenario)
Report(
outcomes: outcomes,
failed_scenarios: count_failed(outcomes),
nondeterministic_failures: count_nondeterministic(outcomes),
)
}
/// Run one M43 scenario twice and require deterministic parity.
pub fn run_scenario(scenario: Scenario) -> ScenarioOutcome {
let #(first_passed, first_signature) = evaluate(scenario)
let #(second_passed, second_signature) = evaluate(scenario)
let deterministic =
first_passed == second_passed && first_signature == second_signature
let passed = first_passed && second_passed && deterministic
ScenarioOutcome(
scenario: scenario,
passed: passed,
deterministic: deterministic,
signature: first_signature,
)
}
/// Scenario label.
pub fn scenario_label(scenario: Scenario) -> String {
case scenario {
ProtocolConformanceMatrixExpansion ->
"protocol_conformance_matrix_expansion"
EnvelopeFuzzCorpusExpansion -> "envelope_fuzz_corpus_expansion"
FuzzRegressionReplayCertification -> "fuzz_regression_replay_certification"
UnifiedProtocolEvidenceSurface -> "unified_protocol_evidence_surface"
}
}
/// Stable pass/fail label.
pub fn pass_fail_label(outcome: ScenarioOutcome) -> String {
case outcome.passed {
True -> "pass"
False -> "fail"
}
}
/// Scenario signature accessor.
pub fn signature(outcome: ScenarioOutcome) -> String {
outcome.signature
}
/// Scenario accessor.
pub fn scenario(outcome: ScenarioOutcome) -> Scenario {
outcome.scenario
}
/// Determinism accessor.
pub fn deterministic(outcome: ScenarioOutcome) -> Bool {
outcome.deterministic
}
/// Report outcomes accessor.
pub fn outcomes(report: Report) -> List(ScenarioOutcome) {
report.outcomes
}
/// Failed scenario count.
pub fn failed_scenarios(report: Report) -> Int {
report.failed_scenarios
}
/// Nondeterministic scenario count.
pub fn nondeterministic_failures(report: Report) -> Int {
report.nondeterministic_failures
}
/// Stable report signature.
pub fn report_signature(report: Report) -> String {
let entries =
list.map(report.outcomes, fn(outcome) {
scenario_label(outcome.scenario)
<> "="
<> pass_fail_label(outcome)
<> ":deterministic="
<> bool_label(outcome.deterministic)
<> ":"
<> outcome.signature
})
join_with(";", entries)
}
/// Deterministic snapshot signature for M43 fixture drift gates.
pub fn snapshot_signature() -> String {
"m43.snapshot.v"
<> int.to_string(snapshot_version)
<> "|"
<> report_signature(run_matrix())
}
/// Deterministic markdown report for M43 fixture scripts.
pub fn snapshot_report_markdown() -> String {
let report = run_matrix()
let failed = failed_scenarios(report)
let nondeterministic = nondeterministic_failures(report)
let status = case failed == 0 && nondeterministic == 0 {
True -> "OK"
False -> "FAIL"
}
"# Protocol Contract Fixture Report\n\n"
<> "snapshot_version: "
<> int.to_string(snapshot_version)
<> "\n"
<> "status: "
<> status
<> "\n"
<> "failed_scenarios: "
<> int.to_string(failed)
<> "\n"
<> "nondeterministic_failures: "
<> int.to_string(nondeterministic)
<> "\n\n"
<> "snapshot_signature: "
<> snapshot_signature()
<> "\n\n"
<> "report_signature: "
<> report_signature(report)
<> "\n"
}
fn evaluate(scenario: Scenario) -> #(Bool, String) {
case scenario {
ProtocolConformanceMatrixExpansion ->
evaluate_protocol_conformance_matrix_expansion()
EnvelopeFuzzCorpusExpansion -> evaluate_envelope_fuzz_corpus_expansion()
FuzzRegressionReplayCertification ->
evaluate_fuzz_regression_replay_certification()
UnifiedProtocolEvidenceSurface ->
evaluate_unified_protocol_evidence_surface()
}
}
fn evaluate_protocol_conformance_matrix_expansion() -> #(Bool, String) {
let #(mismatches, outcomes) =
evaluate_protocol_cases(protocol_matrix_cases(), 1, 0, [])
let passed =
mismatches == 0
&& protocol.protocol_version == 1
&& diff.patch_stream_version == 1
#(
passed,
"mismatches="
<> int.to_string(mismatches)
<> "|protocol_version="
<> int.to_string(protocol.protocol_version)
<> "|patch_stream_version="
<> int.to_string(diff.patch_stream_version)
<> "|cases="
<> join_with(",", outcomes),
)
}
fn evaluate_envelope_fuzz_corpus_expansion() -> #(Bool, String) {
let transport_cases =
cases_for_track(expanded_corpus(), TransportEnvelope, [])
let event_cases = cases_for_track(expanded_corpus(), EventEnvelope, [])
let diff_cases = cases_for_track(expanded_corpus(), DiffEnvelope, [])
let pipeline_cases = cases_for_track(expanded_corpus(), PipelineEnvelope, [])
let #(transport_mismatches, transport_outcomes) =
evaluate_corpus_cases(transport_cases, 1, 0, [])
let #(event_mismatches, event_outcomes) =
evaluate_corpus_cases(event_cases, 1, 0, [])
let #(diff_mismatches, diff_outcomes) =
evaluate_corpus_cases(diff_cases, 1, 0, [])
let #(pipeline_mismatches, pipeline_outcomes) =
evaluate_corpus_cases(pipeline_cases, 1, 0, [])
let total_mismatches =
transport_mismatches
+ event_mismatches
+ diff_mismatches
+ pipeline_mismatches
let all_outcomes =
append_labels(
append_labels(transport_outcomes, event_outcomes),
append_labels(diff_outcomes, pipeline_outcomes),
)
let passed =
total_mismatches == 0
&& list.length(transport_cases) == 5
&& list.length(event_cases) == 7
&& list.length(diff_cases) == 6
&& list.length(pipeline_cases) == 5
#(
passed,
"mismatches="
<> int.to_string(total_mismatches)
<> "|transport="
<> int.to_string(list.length(transport_cases))
<> "/"
<> int.to_string(transport_mismatches)
<> "|event="
<> int.to_string(list.length(event_cases))
<> "/"
<> int.to_string(event_mismatches)
<> "|diff="
<> int.to_string(list.length(diff_cases))
<> "/"
<> int.to_string(diff_mismatches)
<> "|pipeline="
<> int.to_string(list.length(pipeline_cases))
<> "/"
<> int.to_string(pipeline_mismatches)
<> "|cases="
<> join_with(",", all_outcomes),
)
}
fn evaluate_fuzz_regression_replay_certification() -> #(Bool, String) {
let regressions = error_cases(expanded_corpus(), [])
let #(replay_mismatches, replay_outcomes) =
replay_regression_cases(regressions, 1, 0, [])
let passed =
replay_mismatches == 0
&& list.length(regressions) >= 10
&& replay_outcomes != []
#(
passed,
"replay_mismatches="
<> int.to_string(replay_mismatches)
<> "|regressions="
<> int.to_string(list.length(regressions))
<> "|replay="
<> join_with(",", replay_outcomes),
)
}
fn evaluate_unified_protocol_evidence_surface() -> #(Bool, String) {
let #(protocol_passed, protocol_signature) =
evaluate_protocol_conformance_matrix_expansion()
let #(envelope_passed, envelope_signature) =
evaluate_envelope_fuzz_corpus_expansion()
let #(replay_passed, replay_signature) =
evaluate_fuzz_regression_replay_certification()
let drift_classes =
drift_classes_from_cases(error_cases(expanded_corpus(), []), [])
let passed =
protocol_passed
&& envelope_passed
&& replay_passed
&& list.length(drift_classes) >= 6
#(
passed,
"protocol="
<> bool_label(protocol_passed)
<> "|envelopes="
<> bool_label(envelope_passed)
<> "|replay="
<> bool_label(replay_passed)
<> "|drift_classes="
<> join_with(",", drift_classes)
<> "|protocol_signature="
<> protocol_signature
<> "|envelope_signature="
<> envelope_signature
<> "|replay_signature="
<> replay_signature,
)
}
fn protocol_matrix_cases() -> List(#(String, String)) {
let escaped_event_payload =
protocol.encode(protocol.Event(
ref: "r|1",
name: "save",
payload: "{\"path\":\"a|b\"}",
))
[
#("hello|lightspeed|1", "ok:hello:lightspeed:1"),
#("hello|lightspeed|0", "error:unsupported_version:0"),
#("hello|lightspeed|2", "error:unsupported_version:2"),
#("hello|lightspeed|abc", "error:invalid_version:abc"),
#("hello|other|1", "error:unsupported_protocol:other"),
#(escaped_event_payload, "ok:event:r|1:save"),
#("ack|r-1", "ok:ack:r-1"),
#("failure|f-1|fault", "ok:failure:f-1"),
#("noop|x", "error:unknown_frame_tag:noop"),
]
}
fn expanded_corpus() -> List(CorpusCase) {
[
CorpusCase(
id: "transport_hello_rejected",
track: TransportEnvelope,
payload: protocol.encode(protocol.hello()),
expected: "error:unsupported_client_frame:hello",
),
CorpusCase(
id: "transport_diff_rejected",
track: TransportEnvelope,
payload: protocol.encode(protocol.Diff(ref: "d-1", html: "<p>x</p>")),
expected: "error:unsupported_client_frame:diff",
),
CorpusCase(
id: "transport_decode_error",
track: TransportEnvelope,
payload: "event|e-1|increment|bad\\",
expected: "error:protocol_decode_failed:invalid_escape_sequence",
),
CorpusCase(
id: "transport_payload_too_large",
track: TransportEnvelope,
payload: protocol.encode(protocol.Event(
ref: "e-oversize",
name: "increment",
payload: "{\"blob\":\"1234567890123456789012345678901234567890\"}",
)),
expected: "error:unsupported_client_frame:payload_too_large",
),
CorpusCase(
id: "transport_unknown_event",
track: TransportEnvelope,
payload: protocol.encode(protocol.Event(
ref: "e-unknown",
name: "unknown",
payload: "{}",
)),
expected: "error:unsupported_client_event:unknown",
),
CorpusCase(
id: "event_increment_ok",
track: EventEnvelope,
payload: protocol.encode(protocol.Event(
ref: "evt-1",
name: "increment",
payload: "{}",
)),
expected: "ok:event:increment",
),
CorpusCase(
id: "event_decrement_ok",
track: EventEnvelope,
payload: protocol.encode(protocol.Event(
ref: "evt-2",
name: "decrement",
payload: "{}",
)),
expected: "ok:event:decrement",
),
CorpusCase(
id: "event_unknown_name",
track: EventEnvelope,
payload: protocol.encode(protocol.Event(
ref: "evt-3",
name: "unknown",
payload: "{}",
)),
expected: "error:unsupported_event:unknown",
),
CorpusCase(
id: "event_bad_payload_shape",
track: EventEnvelope,
payload: protocol.encode(protocol.Event(
ref: "evt-4",
name: "increment",
payload: "not-json",
)),
expected: "error:malformed_event_payload",
),
CorpusCase(
id: "event_missing_ref",
track: EventEnvelope,
payload: protocol.encode(protocol.Event(
ref: "",
name: "increment",
payload: "{}",
)),
expected: "error:missing_event_ref",
),
CorpusCase(
id: "event_wrong_frame_kind",
track: EventEnvelope,
payload: protocol.encode(protocol.Ack(ref: "ack-1")),
expected: "error:unsupported_event_envelope:ack",
),
CorpusCase(
id: "event_decode_error",
track: EventEnvelope,
payload: "event|evt-5|increment|bad\\",
expected: "error:protocol_decode_failed:invalid_escape_sequence",
),
CorpusCase(
id: "diff_replace_ok",
track: DiffEnvelope,
payload: diff.encode_stream([
diff.Replace(target: "#app", html: "<div>ok</div>"),
]),
expected: "ok_ops:1:replace",
),
CorpusCase(
id: "diff_reorder_ok",
track: DiffEnvelope,
payload: diff.encode_stream([
diff.ReorderKeyed(target: "#items", keys: ["k1", "k2"]),
]),
expected: "ok_ops:1:reorder_keyed",
),
CorpusCase(
id: "diff_unsupported_version",
track: DiffEnvelope,
payload: "ps|2|0|0",
expected: "error:unsupported_version:2",
),
CorpusCase(
id: "diff_missing_dictionary_entry",
track: DiffEnvelope,
payload: "ps|1|0|1|r,0,1",
expected: "error:missing_dictionary_entry:0",
),
CorpusCase(
id: "diff_malformed_operation",
track: DiffEnvelope,
payload: "ps|1|1|#app|1|z,0",
expected: "error:malformed_operation:z,0",
),
CorpusCase(
id: "diff_bad_dynamic_slots",
track: DiffEnvelope,
payload: "ps|1|3|#app|fp|slot|1|u,0,1,1,2",
expected: "error:bad_field_count:dynamic_slots:2:1",
),
CorpusCase(
id: "pipeline_checkpoint_ok",
track: PipelineEnvelope,
payload: "checkpoint|run-1|extract_orders|1|10|100|source-a|101",
expected: "ok:run=run-1|stage=extract_orders|sequence=1",
),
CorpusCase(
id: "pipeline_bad_field_count",
track: PipelineEnvelope,
payload: "checkpoint|run-1|extract_orders|1|10|100|source-a",
expected: "error:bad_field_count:pipeline_checkpoint:8:7",
),
CorpusCase(
id: "pipeline_invalid_integer",
track: PipelineEnvelope,
payload: "checkpoint|run-1|extract_orders|a|10|100|source-a|101",
expected: "error:invalid_integer:sequence:a",
),
CorpusCase(
id: "pipeline_invalid_checkpoint",
track: PipelineEnvelope,
payload: "checkpoint|run-1|extract_orders|1|-1|100|source-a|101",
expected: "error:invalid_checkpoint",
),
CorpusCase(
id: "pipeline_empty_stage",
track: PipelineEnvelope,
payload: "checkpoint|run-1||1|10|100|source-a|101",
expected: "error:invalid_checkpoint",
),
]
}
fn evaluate_protocol_cases(
cases: List(#(String, String)),
index: Int,
mismatches: Int,
outcomes_rev: List(String),
) -> #(Int, List(String)) {
case cases {
[] -> #(mismatches, list.reverse(outcomes_rev))
[entry, ..rest] -> {
let #(payload, expected) = entry
let actual = protocol_matrix_case_label(payload)
let next_mismatches = case actual == expected {
True -> mismatches
False -> mismatches + 1
}
evaluate_protocol_cases(rest, index + 1, next_mismatches, [
int.to_string(index) <> ":" <> actual,
..outcomes_rev
])
}
}
}
fn evaluate_corpus_cases(
cases: List(CorpusCase),
index: Int,
mismatches: Int,
outcomes_rev: List(String),
) -> #(Int, List(String)) {
case cases {
[] -> #(mismatches, list.reverse(outcomes_rev))
[entry, ..rest] -> {
let actual = corpus_case_label(entry)
let next_mismatches = case actual == entry.expected {
True -> mismatches
False -> mismatches + 1
}
evaluate_corpus_cases(rest, index + 1, next_mismatches, [
int.to_string(index) <> ":" <> entry.id <> ":" <> actual,
..outcomes_rev
])
}
}
}
fn replay_regression_cases(
cases: List(CorpusCase),
index: Int,
mismatches: Int,
outcomes_rev: List(String),
) -> #(Int, List(String)) {
case cases {
[] -> #(mismatches, list.reverse(outcomes_rev))
[entry, ..rest] -> {
let first = corpus_case_label(entry)
let replayed = corpus_case_label(entry)
let next_mismatches = case
first == entry.expected
&& replayed == entry.expected
&& first == replayed
{
True -> mismatches
False -> mismatches + 1
}
replay_regression_cases(rest, index + 1, next_mismatches, [
int.to_string(index)
<> ":"
<> entry.id
<> ":"
<> first
<> ":replay="
<> replayed,
..outcomes_rev
])
}
}
}
fn protocol_matrix_case_label(payload: String) -> String {
case protocol.decode(payload) {
Ok(protocol.Hello(protocol_name, version)) ->
"ok:hello:" <> protocol_name <> ":" <> int.to_string(version)
Ok(protocol.Event(ref, name, _)) -> "ok:event:" <> ref <> ":" <> name
Ok(protocol.Diff(ref, _)) -> "ok:diff:" <> ref
Ok(protocol.Ack(ref)) -> "ok:ack:" <> ref
Ok(protocol.Failure(ref, _)) -> "ok:failure:" <> ref
Error(error) -> "error:" <> protocol.decode_error_to_string(error)
}
}
fn corpus_case_label(entry: CorpusCase) -> String {
case entry.track {
TransportEnvelope -> transport_envelope_case_label(entry.payload)
EventEnvelope -> event_envelope_case_label(entry.payload)
DiffEnvelope -> diff_envelope_case_label(entry.payload)
PipelineEnvelope -> pipeline_envelope_case_label(entry.payload)
}
}
fn transport_envelope_case_label(payload: String) -> String {
let payload_limit = case string.contains(payload, "e-oversize") {
True -> 16
False -> 64
}
case connect_transport(payload_limit) {
Error(reason) -> "error:connect_failed:" <> reason
Ok(state) -> {
let result = matrix.receive(state, payload, 1, 1)
case first_failure_reason(result.outbound_frames) {
"" -> "ok:" <> frame_labels(result.outbound_frames)
reason -> "error:" <> reason
}
}
}
}
fn event_envelope_case_label(payload: String) -> String {
case protocol.decode(payload) {
Ok(protocol.Event(ref, name, event_payload)) ->
case ref == "" {
True -> "error:missing_event_ref"
False ->
case is_supported_event(name) {
False -> "error:unsupported_event:" <> name
True ->
case is_structured_payload(event_payload) {
True -> "ok:event:" <> name
False -> "error:malformed_event_payload"
}
}
}
Ok(frame) -> "error:unsupported_event_envelope:" <> frame_tag(frame)
Error(error) ->
"error:protocol_decode_failed:" <> protocol.decode_error_to_string(error)
}
}
fn diff_envelope_case_label(payload: String) -> String {
case diff.decode_stream(payload) {
Ok(patches) ->
"ok_ops:"
<> int.to_string(list.length(patches))
<> ":"
<> join_with(",", list.map(patches, diff.operation))
Error(error) -> "error:" <> diff.decode_error_to_string(error)
}
}
fn pipeline_envelope_case_label(payload: String) -> String {
case string.split(payload, "|") {
[
"checkpoint",
run_id,
stage,
sequence_text,
offset_text,
event_time_text,
idempotency_key,
at_ms_text,
] ->
case parse_int_field("sequence", sequence_text) {
Error(reason) -> reason
Ok(sequence) ->
case parse_int_field("offset", offset_text) {
Error(reason) -> reason
Ok(offset) ->
case parse_int_field("event_time_ms", event_time_text) {
Error(reason) -> reason
Ok(event_time_ms) ->
case parse_int_field("at_ms", at_ms_text) {
Error(reason) -> reason
Ok(at_ms) -> {
let entry =
checkpoint.checkpoint(
run_id,
stage,
sequence,
checkpoint.watermark(offset, event_time_ms),
idempotency_key,
at_ms,
)
case checkpoint.valid(entry) {
True ->
"ok:run="
<> entry.run_id
<> "|stage="
<> entry.stage
<> "|sequence="
<> int.to_string(entry.sequence)
False -> "error:invalid_checkpoint"
}
}
}
}
}
}
tokens ->
"error:bad_field_count:pipeline_checkpoint:8:"
<> int.to_string(list.length(tokens))
}
}
fn parse_int_field(field: String, value: String) -> Result(Int, String) {
case int.parse(value) {
Ok(parsed) -> Ok(parsed)
Error(_) -> Error("error:invalid_integer:" <> field <> ":" <> value)
}
}
fn connect_transport(
max_payload_bytes: Int,
) -> Result(matrix.AdapterState, String) {
case
matrix.connect_with_policies(
session.start("m43-transport", "proc-a", session.Rehydrate, 0, 100),
matrix.ConnectRequest(
route: "/counter",
csrf_token: "csrf",
origin: "https://example.test",
now_ms: 0,
prefer_fallback: False,
),
matrix.WebSocketOnly,
contract.allow_all("proc-a"),
contract.allow_protection(),
matrix.SecurityPolicy(
require_https_origin: True,
require_csrf: True,
max_payload_bytes: max_payload_bytes,
),
matrix.TimeoutPolicy(
heartbeat_timeout_ms: 30_000,
reconnect_grace_ms: 60_000,
),
)
{
matrix.Connected(state, _) -> Ok(state)
matrix.Rejected(error) -> Error(contract.error_to_string(error))
}
}
fn first_failure_reason(frames: List(String)) -> String {
case frames {
[] -> ""
[frame, ..rest] ->
case protocol.decode(frame) {
Ok(protocol.Failure(_, reason)) -> reason
_ -> first_failure_reason(rest)
}
}
}
fn frame_labels(frames: List(String)) -> String {
join_with(
",",
list.map(frames, fn(frame) {
case protocol.decode(frame) {
Ok(decoded) -> frame_tag(decoded)
Error(error) ->
"decode_error:" <> protocol.decode_error_to_string(error)
}
}),
)
}
fn frame_tag(frame: protocol.Frame) -> String {
case frame {
protocol.Hello(_, _) -> "hello"
protocol.Event(_, _, _) -> "event"
protocol.Diff(_, _) -> "diff"
protocol.Ack(_) -> "ack"
protocol.Failure(_, _) -> "failure"
}
}
fn is_supported_event(name: String) -> Bool {
name == "increment" || name == "decrement"
}
fn is_structured_payload(payload: String) -> Bool {
string.starts_with(payload, "{") && string.ends_with(payload, "}")
}
fn cases_for_track(
cases: List(CorpusCase),
target: EnvelopeTrack,
acc_rev: List(CorpusCase),
) -> List(CorpusCase) {
case cases {
[] -> list.reverse(acc_rev)
[entry, ..rest] ->
case entry.track == target {
True -> cases_for_track(rest, target, [entry, ..acc_rev])
False -> cases_for_track(rest, target, acc_rev)
}
}
}
fn error_cases(
cases: List(CorpusCase),
acc_rev: List(CorpusCase),
) -> List(CorpusCase) {
case cases {
[] -> list.reverse(acc_rev)
[entry, ..rest] ->
case string.starts_with(entry.expected, "error:") {
True -> error_cases(rest, [entry, ..acc_rev])
False -> error_cases(rest, acc_rev)
}
}
}
fn drift_classes_from_cases(
cases: List(CorpusCase),
classes_rev: List(String),
) -> List(String) {
case cases {
[] -> list.reverse(classes_rev)
[entry, ..rest] -> {
let next = drift_class(entry.expected)
let next_classes = case contains(classes_rev, next) {
True -> classes_rev
False -> [next, ..classes_rev]
}
drift_classes_from_cases(rest, next_classes)
}
}
}
fn drift_class(expected: String) -> String {
case string.split(expected, ":") {
["error", class, ..] -> class
_ -> "unknown"
}
}
fn append_labels(left: List(String), right: List(String)) -> List(String) {
case left {
[] -> right
[value, ..rest] -> [value, ..append_labels(rest, right)]
}
}
fn contains(values: List(String), target: String) -> Bool {
case values {
[] -> False
[value, ..rest] ->
case value == target {
True -> True
False -> contains(rest, target)
}
}
}
fn count_failed(outcomes: List(ScenarioOutcome)) -> Int {
case outcomes {
[] -> 0
[outcome, ..rest] ->
case outcome.passed {
True -> count_failed(rest)
False -> 1 + count_failed(rest)
}
}
}
fn count_nondeterministic(outcomes: List(ScenarioOutcome)) -> Int {
case outcomes {
[] -> 0
[outcome, ..rest] ->
case outcome.deterministic {
True -> count_nondeterministic(rest)
False -> 1 + count_nondeterministic(rest)
}
}
}
fn join_with(separator: String, values: List(String)) -> String {
case values {
[] -> ""
[value] -> value
[value, ..rest] -> value <> separator <> join_with(separator, rest)
}
}
fn bool_label(value: Bool) -> String {
case value {
True -> "true"
False -> "false"
}
}