#[cfg(test)]
use crate::types::Severity;
#[cfg(test)]
use crate::types::policy::AggregateDecision;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Law {
SeverityOrder,
SeverityBounded,
JoinCommutativity,
JoinAssociativity,
JoinIdempotency,
JoinUnit,
JoinAbsorb,
JoinWorst,
JoinAllWorst,
RaceCommutativity,
RaceNeverIdentity,
RaceDrain,
RaceJoinDistributivity,
TimeoutMin,
TimeoutIdentity,
TimeoutCommutativity,
TimeoutTighten,
BudgetAssociativity,
BudgetCommutativity,
BudgetUnit,
BudgetAbsorb,
BudgetDeadlineMin,
BudgetQuotaMin,
BudgetPriorityMax,
CancelIdempotency,
CancelAssociativity,
CancelMonotonicity,
CancelMax,
QuorumJoinDegeneracy,
QuorumRaceDegeneracy,
QuorumZero,
HedgeFast,
HedgeSlow,
HedgeDrain,
PipelineSequential,
PipelineShortCircuit,
PipelineAssociativity,
FirstOkFound,
FirstOkAllFail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LawClassification {
Unconditional,
SeverityLevelOnly,
ConditionalOnPolicy,
ConditionalOnTiming,
}
#[derive(Debug, Clone)]
pub struct LawEntry {
pub law: Law,
pub classification: LawClassification,
pub statement: &'static str,
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn law_sheet() -> Vec<LawEntry> {
vec![
LawEntry {
law: Law::SeverityOrder,
classification: LawClassification::Unconditional,
statement: "Ok < Err < Cancelled < Panicked",
},
LawEntry {
law: Law::SeverityBounded,
classification: LawClassification::Unconditional,
statement: "For all outcomes o: Ok <= severity(o) <= Panicked",
},
LawEntry {
law: Law::JoinCommutativity,
classification: LawClassification::Unconditional,
statement: "severity(join(a,b)) = severity(join(b,a))",
},
LawEntry {
law: Law::JoinAssociativity,
classification: LawClassification::Unconditional,
statement: "severity(join(join(a,b),c)) = severity(join(a,join(b,c)))",
},
LawEntry {
law: Law::JoinIdempotency,
classification: LawClassification::Unconditional,
statement: "severity(join(a,a)) = severity(a)",
},
LawEntry {
law: Law::JoinUnit,
classification: LawClassification::Unconditional,
statement: "severity(join(Ok,a)) = severity(a) — Ok is the identity",
},
LawEntry {
law: Law::JoinAbsorb,
classification: LawClassification::Unconditional,
statement: "severity(join(Panicked,a)) = Panicked — Panicked is absorbing",
},
LawEntry {
law: Law::JoinWorst,
classification: LawClassification::Unconditional,
statement: "severity(join(a,b)) >= max(severity(a), severity(b))",
},
LawEntry {
law: Law::JoinAllWorst,
classification: LawClassification::Unconditional,
statement: "severity(join_all(os)) = max(severity(o) for o in os)",
},
LawEntry {
law: Law::RaceCommutativity,
classification: LawClassification::Unconditional,
statement: "Swapping inputs + flipping winner preserves severity",
},
LawEntry {
law: Law::RaceNeverIdentity,
classification: LawClassification::Unconditional,
statement: "race(f, never) ~= f where never always loses with RaceLost",
},
LawEntry {
law: Law::RaceDrain,
classification: LawClassification::Unconditional,
statement: "Losers are always cancelled and awaited (structural invariant)",
},
LawEntry {
law: Law::RaceJoinDistributivity,
classification: LawClassification::SeverityLevelOnly,
statement: "race(join(a,b), join(a,c)) ~= join(a, race(b,c)) at severity level only",
},
LawEntry {
law: Law::TimeoutMin,
classification: LawClassification::Unconditional,
statement: "timeout(d1, timeout(d2, f)) ~= timeout(min(d1,d2), f)",
},
LawEntry {
law: Law::TimeoutIdentity,
classification: LawClassification::Unconditional,
statement: "timeout(None, f) ~= f — no deadline is identity",
},
LawEntry {
law: Law::TimeoutCommutativity,
classification: LawClassification::Unconditional,
statement: "effective(a, Some(b)) = effective(b, Some(a))",
},
LawEntry {
law: Law::TimeoutTighten,
classification: LawClassification::Unconditional,
statement: "effective(a, Some(b)) <= a",
},
LawEntry {
law: Law::BudgetAssociativity,
classification: LawClassification::Unconditional,
statement: "(a + b) + c = a + (b + c) for budget combine",
},
LawEntry {
law: Law::BudgetCommutativity,
classification: LawClassification::Unconditional,
statement: "a + b = b + a for budget combine",
},
LawEntry {
law: Law::BudgetUnit,
classification: LawClassification::Unconditional,
statement: "a + INFINITE = a (deadline and quotas)",
},
LawEntry {
law: Law::BudgetAbsorb,
classification: LawClassification::Unconditional,
statement: "a + ZERO -> 0 (quotas are absorbed)",
},
LawEntry {
law: Law::BudgetDeadlineMin,
classification: LawClassification::Unconditional,
statement: "Combined deadline = min of input deadlines",
},
LawEntry {
law: Law::BudgetQuotaMin,
classification: LawClassification::Unconditional,
statement: "Combined poll/cost quota = min of inputs",
},
LawEntry {
law: Law::BudgetPriorityMax,
classification: LawClassification::Unconditional,
statement: "Combined priority = max of inputs",
},
LawEntry {
law: Law::CancelIdempotency,
classification: LawClassification::Unconditional,
statement: "a.strengthen(a) is a no-op",
},
LawEntry {
law: Law::CancelAssociativity,
classification: LawClassification::Unconditional,
statement: "strengthen(strengthen(a,b),c) = strengthen(a,strengthen(b,c))",
},
LawEntry {
law: Law::CancelMonotonicity,
classification: LawClassification::Unconditional,
statement: "severity(a.strengthen(b)) >= severity(a)",
},
LawEntry {
law: Law::CancelMax,
classification: LawClassification::Unconditional,
statement: "a.strengthen(b).kind = max(a.kind, b.kind) by PartialOrd",
},
LawEntry {
law: Law::QuorumJoinDegeneracy,
classification: LawClassification::Unconditional,
statement: "quorum(N,N,[f1..fN]) ~= join_all([f1..fN])",
},
LawEntry {
law: Law::QuorumRaceDegeneracy,
classification: LawClassification::Unconditional,
statement: "quorum(1,N,[f1..fN]) ~= race_all — first Ok wins",
},
LawEntry {
law: Law::QuorumZero,
classification: LawClassification::Unconditional,
statement: "quorum(0,N,[..]) -> Ok([]) immediately",
},
LawEntry {
law: Law::HedgeFast,
classification: LawClassification::ConditionalOnTiming,
statement: "If primary beats deadline: hedge(p,b,d) ~= p",
},
LawEntry {
law: Law::HedgeSlow,
classification: LawClassification::ConditionalOnTiming,
statement: "If primary exceeds deadline: hedge(p,b,d) ~= race(p,b)",
},
LawEntry {
law: Law::HedgeDrain,
classification: LawClassification::Unconditional,
statement: "Hedge loser is always cancelled and drained",
},
LawEntry {
law: Law::PipelineSequential,
classification: LawClassification::Unconditional,
statement: "output(stage N) = input(stage N+1)",
},
LawEntry {
law: Law::PipelineShortCircuit,
classification: LawClassification::ConditionalOnPolicy,
statement: "First non-Ok terminates pipeline (when continue_on_error=false)",
},
LawEntry {
law: Law::PipelineAssociativity,
classification: LawClassification::ConditionalOnPolicy,
statement: "pipeline(a, pipeline(b,c)) ~= pipeline(pipeline(a,b), c) under short-circuit",
},
LawEntry {
law: Law::FirstOkFound,
classification: LawClassification::Unconditional,
statement: "First Ok result wins; remaining stages never evaluated",
},
LawEntry {
law: Law::FirstOkAllFail,
classification: LawClassification::Unconditional,
statement: "All non-Ok -> worst severity returned",
},
]
}
#[must_use]
pub fn unconditional_laws() -> Vec<LawEntry> {
law_sheet()
.into_iter()
.filter(|e| e.classification == LawClassification::Unconditional)
.collect()
}
#[must_use]
pub fn conditional_laws() -> Vec<LawEntry> {
law_sheet()
.into_iter()
.filter(|e| e.classification != LawClassification::Unconditional)
.collect()
}
#[cfg(test)]
fn decision_severity<E>(d: &AggregateDecision<E>) -> Severity {
match d {
AggregateDecision::AllOk => Severity::Ok,
AggregateDecision::FirstError(_) => Severity::Err,
AggregateDecision::Cancelled(_) => Severity::Cancelled,
AggregateDecision::Panicked { .. } => Severity::Panicked,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::combinator::first_ok::first_ok_outcomes;
use crate::combinator::hedge::{HedgeResult, HedgeWinner};
use crate::combinator::join::{join_all_outcomes, join2_outcomes};
use crate::combinator::pipeline::{PipelineResult, pipeline_n_outcomes};
use crate::combinator::quorum::quorum_outcomes;
use crate::combinator::race::{RaceWinner, race2_outcomes};
use crate::combinator::timeout::{effective_deadline, make_timed_result};
use crate::types::Outcome;
use crate::types::Time;
use crate::types::cancel::{CancelKind, CancelReason};
use crate::types::outcome::{PanicPayload, join_outcomes};
fn ok(v: i32) -> Outcome<i32, i32> {
Outcome::Ok(v)
}
fn err(e: i32) -> Outcome<i32, i32> {
Outcome::Err(e)
}
fn cancelled() -> Outcome<i32, i32> {
Outcome::Cancelled(CancelReason::timeout())
}
fn panicked() -> Outcome<i32, i32> {
Outcome::Panicked(PanicPayload::new("boom"))
}
fn race_lost() -> Outcome<i32, i32> {
Outcome::Cancelled(CancelReason::race_loser())
}
#[test]
fn law_sheet_is_nonempty() {
let sheet = law_sheet();
assert!(!sheet.is_empty());
let unique: std::collections::HashSet<Law> = sheet.iter().map(|e| e.law).collect();
assert_eq!(unique.len(), sheet.len(), "duplicate law entries in sheet");
}
#[test]
fn law_sheet_has_all_classifications() {
let sheet = law_sheet();
let classifications: std::collections::HashSet<LawClassification> =
sheet.iter().map(|e| e.classification).collect();
assert!(classifications.contains(&LawClassification::Unconditional));
assert!(classifications.contains(&LawClassification::ConditionalOnPolicy));
assert!(classifications.contains(&LawClassification::ConditionalOnTiming));
assert!(classifications.contains(&LawClassification::SeverityLevelOnly));
}
#[test]
fn unconditional_laws_count() {
let uncond = unconditional_laws();
assert!(
uncond.len() >= 25,
"expected at least 25 unconditional laws, got {}",
uncond.len()
);
}
#[test]
fn conditional_laws_count() {
let cond = conditional_laws();
assert!(
cond.len() >= 5,
"expected at least 5 conditional laws, got {}",
cond.len()
);
}
#[test]
fn quorum_n_of_n_matches_join_all() {
let outcomes = vec![ok(1), ok(2), ok(3)];
let n = outcomes.len();
let (join_decision, _) = join_all_outcomes(outcomes.clone());
let join_sev = decision_severity(&join_decision);
let quorum_result = quorum_outcomes(n, outcomes);
assert_eq!(join_sev, Severity::Ok);
assert!(quorum_result.quorum_met, "quorum(3,3) all-ok should be met");
let outcomes = vec![ok(1), err(2), ok(3)];
let n = outcomes.len();
let (join_decision, _) = join_all_outcomes(outcomes.clone());
let join_sev = decision_severity(&join_decision);
let quorum_result = quorum_outcomes(n, outcomes);
assert_eq!(join_sev, Severity::Err);
assert!(
!quorum_result.quorum_met,
"quorum(3,3) with error should not be met"
);
let outcomes = vec![ok(1), panicked(), ok(3)];
let n = outcomes.len();
let (join_decision, _) = join_all_outcomes(outcomes.clone());
let join_sev = decision_severity(&join_decision);
let quorum_result = quorum_outcomes(n, outcomes);
assert_eq!(join_sev, Severity::Panicked);
assert!(
!quorum_result.quorum_met,
"quorum(3,3) with panic should not be met"
);
}
#[test]
fn quorum_1_of_n_first_ok_wins() {
let outcomes = vec![err(1), ok(42), err(3)];
let result = quorum_outcomes(1, outcomes);
assert!(result.quorum_met, "quorum(1,N) should succeed with one Ok");
assert_eq!(result.success_count(), 1);
let outcomes = vec![err(1), err(2), err(3)];
let result = quorum_outcomes(1, outcomes);
assert!(!result.quorum_met, "quorum(1,N) should fail when all fail");
}
#[test]
fn quorum_zero_succeeds_immediately() {
let outcomes = vec![err(1), panicked(), cancelled()];
let result = quorum_outcomes(0, outcomes);
assert!(result.quorum_met, "quorum(0,N) should always succeed");
assert_eq!(
result.success_count(),
0,
"quorum(0,N) should have no successes"
);
}
#[test]
fn join_ok_is_identity() {
let inputs: Vec<Outcome<i32, i32>> = vec![ok(1), err(2), cancelled(), panicked()];
for input in inputs {
let sev = input.severity();
let joined = join_outcomes(ok(0), input);
assert_eq!(
joined.severity(),
sev,
"join(Ok, x) should have severity of x"
);
}
}
#[test]
fn join_panicked_absorbs() {
let inputs: Vec<Outcome<i32, i32>> = vec![ok(1), err(2), cancelled(), panicked()];
for input in inputs {
let joined = join_outcomes(panicked(), input);
assert_eq!(
joined.severity(),
Severity::Panicked,
"join(Panicked, x) should always be Panicked"
);
}
}
#[test]
fn pipeline_short_circuits_on_error() {
let result = pipeline_n_outcomes(vec![ok(1), err(99)], 2);
match &result {
PipelineResult::Failed { failed_at, .. } => {
assert_eq!(failed_at.index, 1, "should fail at stage index 1");
}
other => panic!("expected PipelineResult::Failed, got {other:?}"),
}
let result = pipeline_n_outcomes(vec![err(1)], 2);
match &result {
PipelineResult::Failed { failed_at, .. } => {
assert_eq!(failed_at.index, 0, "should fail at stage index 0");
}
other => panic!("expected PipelineResult::Failed, got {other:?}"),
}
}
#[test]
fn pipeline_all_ok_succeeds() {
let result = pipeline_n_outcomes(vec![ok(1), ok(2), ok(3)], 3);
match result {
PipelineResult::Completed {
stages_completed, ..
} => {
assert_eq!(stages_completed, 3);
}
other => panic!("expected PipelineResult::Completed, got {other:?}"),
}
}
#[test]
fn first_ok_returns_first_success() {
let result = first_ok_outcomes(vec![err(1), err(2), ok(42), ok(99)]);
assert!(result.is_success());
let success = result.success.as_ref().unwrap();
assert_eq!(success.value, 42, "should return first Ok value");
assert_eq!(success.index, 2, "first Ok was at index 2");
}
#[test]
fn first_ok_all_fail_returns_worst() {
let result = first_ok_outcomes(vec![err(1), err(2), cancelled()]);
assert!(!result.is_success());
assert_eq!(result.failures.len(), 3);
assert!(result.was_cancelled, "should record cancellation");
}
#[test]
fn race_symmetry_exhaustive() {
let outcomes: Vec<Outcome<i32, i32>> = vec![ok(1), err(2), cancelled(), panicked()];
for a in &outcomes {
for b in &outcomes {
let (w_ab, _, l_ab) = race2_outcomes(RaceWinner::First, a.clone(), b.clone());
let (w_ba, _, l_ba) = race2_outcomes(RaceWinner::Second, b.clone(), a.clone());
assert_eq!(
w_ab.severity(),
w_ba.severity(),
"winner severity mismatch for a={a:?}, b={b:?}"
);
assert_eq!(
l_ab.severity(),
l_ba.severity(),
"loser severity mismatch for a={a:?}, b={b:?}"
);
}
}
}
#[test]
fn race_never_identity_exhaustive() {
let outcomes: Vec<Outcome<i32, i32>> = vec![ok(1), err(2), cancelled(), panicked()];
for f in &outcomes {
let (winner, _, loser) = race2_outcomes(RaceWinner::First, f.clone(), race_lost());
assert_eq!(
winner.severity(),
f.severity(),
"race(f, never) winner should match f"
);
assert!(
matches!(loser, Outcome::Cancelled(ref r) if r.kind == CancelKind::RaceLost),
"race(f, never) loser should be RaceLost"
);
}
}
#[test]
fn timeout_identity_preserves_effective_deadline_and_outcome() {
let deadlines = [
Time::ZERO,
Time::from_nanos(1),
Time::from_nanos(1_000),
Time::from_nanos(1_000_000),
];
let outcomes: Vec<Outcome<i32, i32>> = vec![ok(1), err(2), cancelled(), panicked()];
for requested in deadlines {
let effective = effective_deadline(requested, None);
assert_eq!(
effective, requested,
"timeout(None, f) should preserve requested deadline"
);
for outcome in &outcomes {
let wrapped = make_timed_result(outcome.clone(), effective, true).into_outcome();
assert_eq!(
wrapped.severity(),
outcome.severity(),
"identity timeout wrapper should preserve outcome severity"
);
}
}
}
#[test]
fn severity_lattice_complete() {
assert!(Severity::Ok < Severity::Err);
assert!(Severity::Err < Severity::Cancelled);
assert!(Severity::Cancelled < Severity::Panicked);
for s in [
Severity::Ok,
Severity::Err,
Severity::Cancelled,
Severity::Panicked,
] {
assert!(s >= Severity::Ok);
assert!(s <= Severity::Panicked);
}
}
#[test]
fn hedge_fast_no_backup() {
use crate::combinator::hedge::hedge_outcomes;
let outcomes: Vec<Outcome<i32, i32>> = vec![ok(1), err(2), cancelled(), panicked()];
for primary in &outcomes {
let result: HedgeResult<i32, i32> = hedge_outcomes(primary.clone(), false, None, None);
match result {
HedgeResult::PrimaryFast(o) => {
assert_eq!(
o.severity(),
primary.severity(),
"hedge fast path should return primary severity"
);
}
other @ HedgeResult::Raced { .. } => {
panic!("expected PrimaryFast, got {other:?}");
}
}
}
}
#[test]
fn hedge_slow_acts_like_race() {
use crate::combinator::hedge::hedge_outcomes;
let result: HedgeResult<i32, i32> =
hedge_outcomes(ok(1), true, Some(err(2)), Some(HedgeWinner::Primary));
match result {
HedgeResult::Raced { winner_outcome, .. } => {
assert_eq!(winner_outcome.severity(), Severity::Ok);
}
other @ HedgeResult::PrimaryFast(_) => {
panic!("expected Raced, got {other:?}");
}
}
let result: HedgeResult<i32, i32> =
hedge_outcomes(err(1), true, Some(ok(2)), Some(HedgeWinner::Backup));
match result {
HedgeResult::Raced { winner_outcome, .. } => {
assert_eq!(winner_outcome.severity(), Severity::Ok);
}
other @ HedgeResult::PrimaryFast(_) => {
panic!("expected Raced, got {other:?}");
}
}
}
#[test]
fn join2_worst_decision() {
type JoinCase = (Outcome<i32, i32>, Outcome<i32, i32>, Severity);
let cases: Vec<JoinCase> = vec![
(ok(1), ok(2), Severity::Ok),
(ok(1), err(2), Severity::Err),
(ok(1), cancelled(), Severity::Cancelled),
(ok(1), panicked(), Severity::Panicked),
(err(1), cancelled(), Severity::Cancelled),
(err(1), panicked(), Severity::Panicked),
(cancelled(), panicked(), Severity::Panicked),
];
for (a, b, expected) in cases {
let (result, _, _): (Outcome<(i32, i32), i32>, _, _) =
join2_outcomes(a.clone(), b.clone());
assert_eq!(result.severity(), expected, "join2({a:?}, {b:?}) severity");
}
}
#[test]
fn pipeline_associativity_all_ok() {
let ltr = pipeline_n_outcomes(vec![ok(1), ok(2), ok(3)], 3);
assert!(
matches!(ltr, PipelineResult::Completed { .. }),
"3-stage all-ok should complete"
);
let ab = pipeline_n_outcomes(vec![ok(1), ok(2)], 2);
assert!(
matches!(ab, PipelineResult::Completed { .. }),
"2-stage all-ok should complete"
);
}
#[test]
fn pipeline_associativity_short_circuit() {
let flat = pipeline_n_outcomes(vec![err(1)], 3);
match flat {
PipelineResult::Failed { failed_at, .. } => assert_eq!(failed_at.index, 0),
other => panic!("expected Failed, got {other:?}"),
}
let nested = pipeline_n_outcomes(vec![err(1)], 2);
match nested {
PipelineResult::Failed { failed_at, .. } => assert_eq!(failed_at.index, 0),
other => panic!("expected Failed, got {other:?}"),
}
}
#[test]
fn law_debug_clone_copy_eq_hash() {
use std::collections::HashSet;
let l = Law::SeverityOrder;
let l2 = l; let l3 = l;
assert_eq!(l, l2);
assert_eq!(l, l3);
assert_ne!(l, Law::JoinCommutativity);
let dbg = format!("{l:?}");
assert!(dbg.contains("SeverityOrder"));
let mut set = HashSet::new();
set.insert(l);
assert!(set.contains(&l2));
}
#[test]
fn law_classification_debug_clone_copy_eq_hash() {
use std::collections::HashSet;
let c = LawClassification::Unconditional;
let c2 = c; let c3 = c;
assert_eq!(c, c2);
assert_eq!(c, c3);
assert_ne!(c, LawClassification::SeverityLevelOnly);
let dbg = format!("{c:?}");
assert!(dbg.contains("Unconditional"));
let mut set = HashSet::new();
set.insert(c);
assert!(set.contains(&c2));
}
#[test]
fn law_entry_debug_clone() {
let e = LawEntry {
law: Law::SeverityBounded,
classification: LawClassification::Unconditional,
statement: "test statement",
};
let e2 = e.clone();
assert_eq!(e.law, e2.law);
assert_eq!(e.classification, e2.classification);
assert_eq!(e.statement, e2.statement);
let dbg = format!("{e:?}");
assert!(dbg.contains("LawEntry"));
}
}