use super::ctx::{CallState, Ctx};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Clone)]
pub enum Value {
Unit,
Bool(bool),
Int(i64),
Str(String),
State(CallState),
List(Vec<Value>),
Map(Vec<(String, Value)>),
}
impl Value {
pub fn display(&self) -> String {
match self {
Value::Unit => "()".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Str(s) => s.clone(),
Value::State(s) => s.to_string(),
Value::List(items) => {
let body = items
.iter()
.map(Value::display)
.collect::<Vec<_>>()
.join(", ");
format!("[{body}]")
}
Value::Map(pairs) => {
let body = pairs
.iter()
.map(|(k, v)| format!("{k}: {}", v.display()))
.collect::<Vec<_>>()
.join(", ");
format!("#{{{body}}}")
}
}
}
fn type_tag(&self) -> u8 {
match self {
Value::Unit => 0,
Value::Bool(_) => 1,
Value::Int(_) => 2,
Value::Str(_) => 3,
Value::State(_) => 4,
Value::List(_) => 5,
Value::Map(_) => 6,
}
}
fn as_int(&self) -> Result<i64, String> {
match self {
Value::Int(i) => Ok(*i),
other => Err(format!(
"expected a number to compare, but was {}",
other.display()
)),
}
}
fn is_empty_collection(&self) -> Result<bool, String> {
match self {
Value::Str(s) => Ok(s.is_empty()),
Value::List(items) => Ok(items.is_empty()),
Value::Map(pairs) => Ok(pairs.is_empty()),
other => Err(format!(
"expected a string, array or map to check emptiness, but was {}",
other.display()
)),
}
}
}
fn value_eq(a: &Value, b: &Value) -> bool {
a.type_tag() == b.type_tag() && a.display() == b.display()
}
#[derive(Clone)]
pub struct Assertion {
actual: Value,
desc: Option<String>,
ctx: Arc<Ctx>,
}
impl Assertion {
pub fn new(ctx: Arc<Ctx>, actual: Value) -> Self {
Self {
actual,
desc: super::ctx::take_pending_label(),
ctx,
}
}
pub fn value(&self) -> &Value {
&self.actual
}
pub fn describe(&mut self, label: &str) {
self.desc = Some(label.to_string());
}
fn finish(&self, expect: String, pass: bool) -> Result<(), String> {
let actual = self.actual.display();
self.ctx
.report_assertion(self.desc.clone(), expect.clone(), actual.clone(), pass);
if pass {
Ok(())
} else {
let label = self
.desc
.as_deref()
.map(|d| format!("{d}: "))
.unwrap_or_default();
Err(format!("{label}expected {expect}, but was {actual}"))
}
}
pub fn equals(&self, expected: &Value) -> Result<(), String> {
let pass = value_eq(&self.actual, expected);
self.finish(format!("equals {}", expected.display()), pass)
}
pub fn not_equals(&self, expected: &Value) -> Result<(), String> {
let pass = !value_eq(&self.actual, expected);
self.finish(format!("not equals {}", expected.display()), pass)
}
pub fn is_true(&self) -> Result<(), String> {
let pass = matches!(self.actual, Value::Bool(true));
self.finish("is true".into(), pass)
}
pub fn is_false(&self) -> Result<(), String> {
let pass = matches!(self.actual, Value::Bool(false));
self.finish("is false".into(), pass)
}
pub fn is_present(&self) -> Result<(), String> {
let pass = !matches!(self.actual, Value::Unit);
self.finish("is present".into(), pass)
}
pub fn is_absent(&self) -> Result<(), String> {
let pass = matches!(self.actual, Value::Unit);
self.finish("is absent".into(), pass)
}
pub fn is_empty(&self) -> Result<(), String> {
let pass = self.actual.is_empty_collection()?;
self.finish("is empty".into(), pass)
}
pub fn is_not_empty(&self) -> Result<(), String> {
let pass = !self.actual.is_empty_collection()?;
self.finish("is not empty".into(), pass)
}
pub fn contains(&self, needle: &str) -> Result<(), String> {
let pass = self.actual.display().contains(needle);
self.finish(format!("contains {needle:?}"), pass)
}
pub fn matches(&self, pattern: &str) -> Result<(), String> {
let re =
regex::Regex::new(pattern).map_err(|e| format!("invalid regex {pattern:?}: {e}"))?;
let pass = re.is_match(&self.actual.display());
self.finish(format!("matches {pattern:?}"), pass)
}
pub fn greater_than(&self, n: i64) -> Result<(), String> {
let pass = self.actual.as_int()? > n;
self.finish(format!("greater than {n}"), pass)
}
pub fn at_least(&self, n: i64) -> Result<(), String> {
let pass = self.actual.as_int()? >= n;
self.finish(format!("at least {n}"), pass)
}
pub fn less_than(&self, n: i64) -> Result<(), String> {
let pass = self.actual.as_int()? < n;
self.finish(format!("less than {n}"), pass)
}
pub fn at_most(&self, n: i64) -> Result<(), String> {
let pass = self.actual.as_int()? <= n;
self.finish(format!("at most {n}"), pass)
}
}
pub fn await_until<F>(ctx: &Arc<Ctx>, mut body: F, timeout: Duration) -> Result<(), String>
where
F: FnMut() -> Result<(), String>,
{
ctx.set_assert_silent(true);
let deadline = Instant::now() + timeout;
let outcome = loop {
match body() {
Ok(()) => break Ok(()),
Err(e) => {
if Instant::now() >= deadline {
break Err(e);
}
std::thread::sleep(Duration::from_millis(25));
}
}
};
ctx.set_assert_silent(false);
ctx.emit_last_assert();
outcome.map_err(|e| format!("not satisfied within {timeout:?}: {e}"))
}
#[cfg(test)]
mod tests {
use super::Value;
#[test]
fn emptiness_and_display_for_collections() {
assert!(Value::List(vec![]).is_empty_collection().unwrap());
assert!(
!Value::List(vec![Value::Int(1)])
.is_empty_collection()
.unwrap()
);
assert!(Value::Map(vec![]).is_empty_collection().unwrap());
assert!(Value::Str(String::new()).is_empty_collection().unwrap());
assert!(Value::Int(1).is_empty_collection().is_err());
assert_eq!(
Value::List(vec![Value::Int(1), Value::Str("a".into())]).display(),
"[1, a]"
);
assert_eq!(
Value::Map(vec![("k".into(), Value::Int(2))]).display(),
"#{k: 2}"
);
}
}