use serde_json::Value;
use super::Outcome;
use crate::message::{Message, ToolCall};
#[derive(Debug, Clone, PartialEq)]
pub struct AssertionFailure {
message: String,
}
impl AssertionFailure {
pub fn new(message: impl Into<String>) -> AssertionFailure {
AssertionFailure {
message: message.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for AssertionFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TrialError {
pub class: String,
pub message: String,
}
impl TrialError {
pub fn new(class: impl Into<String>, message: impl Into<String>) -> TrialError {
TrialError {
class: class.into(),
message: message.into(),
}
}
pub fn runtime(message: impl Into<String>) -> TrialError {
TrialError::new("RuntimeError", message)
}
pub(crate) fn from_error(error: &crate::Error) -> TrialError {
TrialError::new(error.class_name(), error.to_string())
}
pub fn to_h(&self) -> Value {
serde_json::json!({ "class": self.class, "message": self.message })
}
}
impl std::fmt::Display for TrialError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Failure {
Assertion(AssertionFailure),
Error(TrialError),
}
impl Failure {
pub fn error(message: impl Into<String>) -> Failure {
Failure::Error(TrialError::runtime(message))
}
}
impl From<AssertionFailure> for Failure {
fn from(failure: AssertionFailure) -> Failure {
Failure::Assertion(failure)
}
}
impl<E: std::error::Error + 'static> From<E> for Failure {
fn from(error: E) -> Failure {
let any: &dyn std::any::Any = &error;
match any.downcast_ref::<crate::Error>() {
Some(e) => Failure::Error(TrialError::from_error(e)),
None => Failure::Error(TrialError::new(
std::any::type_name::<E>(),
error.to_string(),
)),
}
}
}
pub struct Assertions<'a> {
pub(crate) input: &'a Value,
pub(crate) expected_output: Option<&'a Value>,
pub(crate) metadata: &'a serde_json::Map<String, Value>,
pub(crate) result: &'a Outcome,
pub(crate) output: &'a Value,
pub(crate) messages: &'a [Message],
pub(crate) tool_calls: &'a [ToolCall],
pub(crate) count: usize,
}
type Check = std::result::Result<(), AssertionFailure>;
fn inspect(value: &Value) -> String {
match value {
Value::Null => "nil".into(),
other => other.to_string(),
}
}
impl<'a> Assertions<'a> {
pub fn input(&self) -> &'a Value {
self.input
}
pub fn expected_output(&self) -> &'a Value {
self.expected_output.unwrap_or(&Value::Null)
}
pub fn metadata(&self) -> &'a serde_json::Map<String, Value> {
self.metadata
}
pub fn result(&self) -> &'a Outcome {
self.result
}
pub fn output(&self) -> &'a Value {
self.output
}
pub fn messages(&self) -> &'a [Message] {
self.messages
}
pub fn tool_calls(&self) -> &'a [ToolCall] {
self.tool_calls
}
pub fn count(&self) -> usize {
self.count
}
fn check(&mut self, ok: bool, message: impl FnOnce() -> String) -> Check {
self.count += 1;
if ok {
Ok(())
} else {
Err(AssertionFailure::new(message()))
}
}
pub fn assert(&mut self, condition: bool, message: Option<&str>) -> Check {
let message = message
.unwrap_or("Expected false to be truthy.")
.to_string();
self.check(condition, || message)
}
pub fn refute(&mut self, condition: bool, message: Option<&str>) -> Check {
let message = message
.unwrap_or("Expected true to not be truthy.")
.to_string();
self.check(!condition, || message)
}
pub fn assert_equal(&mut self, expected: impl Into<Value>, actual: impl Into<Value>) -> Check {
let (expected, actual) = (expected.into(), actual.into());
self.check(expected == actual, || {
format!(
"Expected: {}\n Actual: {}",
inspect(&expected),
inspect(&actual)
)
})
}
pub fn assert_nil(&mut self, value: &Value) -> Check {
self.check(value.is_null(), || {
format!("Expected {} to be nil.", inspect(value))
})
}
pub fn refute_empty(&mut self, value: &Value) -> Check {
let (responds, empty) = match value {
Value::String(s) => (true, s.is_empty()),
Value::Array(a) => (true, a.is_empty()),
Value::Object(o) => (true, o.is_empty()),
_ => (false, false),
};
self.check(responds, || {
format!("Expected {} to respond to #empty?.", inspect(value))
})?;
self.check(!empty, || {
format!("Expected {} to not be empty.", inspect(value))
})
}
pub fn assert_includes<T: PartialEq + std::fmt::Debug>(
&mut self,
collection: &[T],
item: &T,
) -> Check {
self.count += 1;
self.check(collection.contains(item), || {
format!("Expected {collection:?} to include {item:?}.")
})
}
pub fn assert_kind_of(&mut self, kind: &str, value: &Value) -> Check {
let actual = match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
};
self.check(actual == kind, || {
format!(
"Expected {} to be a kind of {kind}, not {actual}.",
inspect(value)
)
})
}
}