use std::collections::BTreeSet;
use super::{Assert, AssertDetail, AssertResult, DetailKind, NoteValue};
#[must_use = "Verdict accumulates claims; call into_result() to consume"]
#[derive(Debug, Clone)]
pub struct Verdict {
assert: Option<Assert>,
result: AssertResult,
}
impl Default for Verdict {
fn default() -> Self {
Self::new()
}
}
impl Verdict {
pub fn new() -> Self {
Self {
assert: None,
result: AssertResult::pass(),
}
}
pub fn with_assert(assert: Assert) -> Self {
Self {
assert: Some(assert),
result: AssertResult::pass(),
}
}
#[doc(hidden)]
pub fn claim<T>(&mut self, name: &'static str, value: T) -> ClaimBuilder<'_, T> {
ClaimBuilder {
verdict: self,
name,
value,
kind: DetailKind::Other,
reason: None,
}
}
#[doc(hidden)]
pub fn claim_set<'a, T: Ord + std::fmt::Debug>(
&'a mut self,
name: &'static str,
set: &'a BTreeSet<T>,
) -> SetClaim<'a, T> {
SetClaim {
verdict: self,
name,
value: set,
kind: DetailKind::Other,
reason: None,
}
}
#[doc(hidden)]
pub fn claim_seq<'a, T: std::fmt::Debug>(
&'a mut self,
name: &'static str,
seq: &'a [T],
) -> SeqClaim<'a, T> {
SeqClaim {
verdict: self,
name,
value: seq,
kind: DetailKind::Other,
reason: None,
}
}
pub fn note(&mut self, msg: impl Into<String>) -> &mut Self {
self.result.note(msg);
self
}
pub fn note_value(&mut self, key: impl Into<String>, value: impl Into<NoteValue>) -> &mut Self {
self.result.note_value(key, value);
self
}
pub fn skip(&mut self, reason: &'static str) -> &mut Self {
self.result.skipped = true;
self.result
.details
.push(AssertDetail::new(DetailKind::Skip, reason));
self
}
pub fn skip_if(&mut self, cond: bool, reason: &'static str) -> &mut Self {
if cond {
self.skip(reason);
}
self
}
pub fn merge(&mut self, other: AssertResult) -> &mut Self {
self.result.merge(other);
self
}
pub fn passed(&self) -> bool {
self.result.passed
}
pub fn detail_count(&self) -> usize {
self.result.details.len()
}
pub fn assert(&self) -> Option<&Assert> {
self.assert.as_ref()
}
pub fn into_result(self) -> AssertResult {
self.result
}
fn record(&mut self, outcome: ClaimOutcome) {
if let ClaimOutcome::Fail { kind, message } = outcome {
self.result.passed = false;
self.result.details.push(AssertDetail::new(kind, message));
}
}
}
enum ClaimOutcome {
Pass,
Fail { kind: DetailKind, message: String },
}
#[must_use = "ClaimBuilder records nothing until a comparator is invoked"]
pub struct ClaimBuilder<'a, T> {
verdict: &'a mut Verdict,
name: &'static str,
value: T,
kind: DetailKind,
reason: Option<&'a str>,
}
impl<'a, T> ClaimBuilder<'a, T> {
pub fn kind(mut self, kind: DetailKind) -> Self {
self.kind = kind;
self
}
pub fn because(mut self, reason: &'a str) -> Self {
self.reason = Some(reason);
self
}
}
fn append_reason(msg: String, reason: Option<&str>) -> String {
match reason {
Some(r) => format!("{msg} ({r})"),
None => msg,
}
}
impl<'a, T> ClaimBuilder<'a, T>
where
T: PartialEq + std::fmt::Display,
{
pub fn eq(self, expected: T) -> &'a mut Verdict {
let ClaimBuilder {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value == expected {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected {expected}, was {value}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn ne(self, forbidden: T) -> &'a mut Verdict {
let ClaimBuilder {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value != forbidden {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected != {forbidden}, was {value}"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
}
impl<'a, T> ClaimBuilder<'a, T>
where
T: PartialOrd + std::fmt::Display,
{
pub fn at_least(self, floor: T) -> &'a mut Verdict {
let ClaimBuilder {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value >= floor {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected at least {floor}, was {value}"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn at_most(self, ceiling: T) -> &'a mut Verdict {
let ClaimBuilder {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value <= ceiling {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected at most {ceiling}, was {value}"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn lt(self, ceiling: T) -> &'a mut Verdict {
let ClaimBuilder {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value < ceiling {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected less than {ceiling}, was {value}"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn gt(self, floor: T) -> &'a mut Verdict {
let ClaimBuilder {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value > floor {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected greater than {floor}, was {value}"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn between(self, lo: T, hi: T) -> &'a mut Verdict {
let ClaimBuilder {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if lo > hi {
let msg = append_reason(
format!("{name}: caller error: interval inverted (lo={lo} > hi={hi})"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
} else if value >= lo && value <= hi {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected in [{lo}, {hi}], was {value}"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
}
impl<'a> ClaimBuilder<'a, f64> {
pub fn is_finite(self) -> &'a mut Verdict {
let ClaimBuilder {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value.is_finite() {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected finite, was {value}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn near(self, target: f64, tolerance: f64) -> &'a mut Verdict {
let ClaimBuilder {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if tolerance < 0.0 {
let msg = append_reason(
format!("{name}: caller error: tolerance negative (={tolerance})"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
} else if value == target || (value - target).abs() <= tolerance {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected near {target} (±{tolerance}), was {value}"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
}
#[must_use = "SetClaim records nothing until a comparator is invoked"]
pub struct SetClaim<'a, T: Ord + std::fmt::Debug> {
verdict: &'a mut Verdict,
name: &'static str,
value: &'a BTreeSet<T>,
kind: DetailKind,
reason: Option<&'a str>,
}
impl<'a, T: Ord + std::fmt::Debug> SetClaim<'a, T> {
pub fn kind(mut self, kind: DetailKind) -> Self {
self.kind = kind;
self
}
pub fn because(mut self, reason: &'a str) -> Self {
self.reason = Some(reason);
self
}
pub fn empty(self) -> &'a mut Verdict {
let SetClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value.is_empty() {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected empty, was {value:?}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn nonempty(self) -> &'a mut Verdict {
let SetClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if !value.is_empty() {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected non-empty, was empty"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn contains(self, needle: &T) -> &'a mut Verdict {
let SetClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value.contains(needle) {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected to contain {needle:?}, set was {value:?}"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn len_eq(self, n: usize) -> &'a mut Verdict {
let SetClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let actual = value.len();
let outcome = if actual == n {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected len == {n}, was {actual}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn len_at_most(self, n: usize) -> &'a mut Verdict {
let SetClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let actual = value.len();
let outcome = if actual <= n {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected len <= {n}, was {actual}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn len_at_least(self, n: usize) -> &'a mut Verdict {
let SetClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let actual = value.len();
let outcome = if actual >= n {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected len >= {n}, was {actual}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn subset_of(self, whitelist: &BTreeSet<T>) -> &'a mut Verdict {
let SetClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let bad: Vec<&T> = value.iter().filter(|x| !whitelist.contains(x)).collect();
let outcome = if bad.is_empty() {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected subset of {whitelist:?}, but {bad:?} are not in the set"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn disjoint_from(self, forbidden: &BTreeSet<T>) -> &'a mut Verdict {
let SetClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let bad: Vec<&T> = value.iter().filter(|x| forbidden.contains(x)).collect();
let outcome = if bad.is_empty() {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!(
"{name}: expected disjoint from {forbidden:?}, but {bad:?} are present in both"
),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
}
#[must_use = "SeqClaim records nothing until a comparator is invoked"]
pub struct SeqClaim<'a, T: std::fmt::Debug> {
verdict: &'a mut Verdict,
name: &'static str,
value: &'a [T],
kind: DetailKind,
reason: Option<&'a str>,
}
impl<'a, T: std::fmt::Debug> SeqClaim<'a, T> {
pub fn kind(mut self, kind: DetailKind) -> Self {
self.kind = kind;
self
}
pub fn because(mut self, reason: &'a str) -> Self {
self.reason = Some(reason);
self
}
pub fn empty(self) -> &'a mut Verdict {
let SeqClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value.is_empty() {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected empty, was {value:?}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn nonempty(self) -> &'a mut Verdict {
let SeqClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if !value.is_empty() {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected non-empty, was empty"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn contains(self, needle: &T) -> &'a mut Verdict
where
T: PartialEq,
{
let SeqClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let outcome = if value.iter().any(|x| x == needle) {
ClaimOutcome::Pass
} else {
let msg = append_reason(
format!("{name}: expected to contain {needle:?}, sequence was {value:?}"),
reason,
);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn len_eq(self, n: usize) -> &'a mut Verdict {
let SeqClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let actual = value.len();
let outcome = if actual == n {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected len == {n}, was {actual}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn len_at_most(self, n: usize) -> &'a mut Verdict {
let SeqClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let actual = value.len();
let outcome = if actual <= n {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected len <= {n}, was {actual}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
pub fn len_at_least(self, n: usize) -> &'a mut Verdict {
let SeqClaim {
verdict,
name,
value,
kind,
reason,
} = self;
let actual = value.len();
let outcome = if actual >= n {
ClaimOutcome::Pass
} else {
let msg = append_reason(format!("{name}: expected len >= {n}, was {actual}"), reason);
ClaimOutcome::Fail { kind, message: msg }
};
verdict.record(outcome);
verdict
}
}
#[macro_export]
macro_rules! claim {
($verdict:expr, $value:expr) => {
$verdict.claim(stringify!($value), $value)
};
}