use std::fmt;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::ops::Deref;
#[macro_export]
macro_rules! assert_that {
($actual:expr) => {
$crate::Subject::new(
&$actual,
stringify!($actual)
.to_string()
.replace(" ", "")
.replace("\n", ""),
None,
(),
Some($crate::Location::new(
file!().to_string(),
line!(),
column!(),
)),
std::marker::PhantomData::<()>,
)
};
}
pub struct Subject<'a, Sub, Opt, Ret> {
actual: ActualValue<'a, Sub>,
expr: String,
description: Option<String>,
option: Opt,
location: Option<Location>,
return_type: PhantomData<Ret>,
}
impl<'a, Sub, Opt, Ret> Subject<'a, Sub, Opt, Ret> {
#[allow(dead_code)] pub fn new(
actual: &'a Sub,
expr: String,
description: Option<String>,
option: Opt,
location: Option<Location>,
return_type: PhantomData<Ret>,
) -> Self {
Subject {
actual: ActualValue::Borrowed(actual),
expr,
description,
option,
location,
return_type,
}
}
pub(super) fn new_from_owned_actual(
actual: Sub,
expr: String,
description: Option<String>,
option: Opt,
location: Option<Location>,
return_type: PhantomData<Ret>,
) -> Self {
Subject {
actual: ActualValue::Owned(actual),
expr,
description,
option,
location,
return_type,
}
}
}
pub enum ActualValue<'a, S> {
Owned(S),
Borrowed(&'a S),
}
impl<'a, S> Deref for ActualValue<'a, S> {
type Target = S;
fn deref(&self) -> &Self::Target {
match &self {
ActualValue::Owned(value) => value,
ActualValue::Borrowed(value) => value,
}
}
}
pub trait AssertionApi<'a, Sub, Opt, Ret> {
fn new_result(&self) -> AssertionResult;
fn actual(&self) -> ⋐
fn expr(&self) -> &String;
fn description(&self) -> &Option<String>;
fn description_or_expr(&self) -> &String;
fn option(&self) -> &Opt;
fn option_mut(&mut self) -> &mut Opt;
fn location(&self) -> &Option<Location>;
fn new_subject<NewSub, NewOpt>(
&self,
new_actual: &'a NewSub,
new_description: Option<String>,
new_opt: NewOpt,
) -> Subject<NewSub, NewOpt, Ret>;
fn new_owned_subject<'b, NewSub, NewOpt>(
&self,
new_actual: NewSub,
new_description: Option<String>,
new_option: NewOpt,
) -> Subject<'b, NewSub, NewOpt, Ret>;
}
impl<'a, Sub, Opt, Ret> AssertionApi<'a, Sub, Opt, Ret> for Subject<'a, Sub, Opt, Ret> {
fn new_result(&self) -> AssertionResult {
let mut result = AssertionResult::new(self.location());
match &self.description {
None => {}
Some(description) => {
result = result.add_fact("value of", description);
}
};
result
}
fn actual(&self) -> &Sub {
&self.actual
}
fn expr(&self) -> &String {
&self.expr
}
fn description(&self) -> &Option<String> {
&self.description
}
fn description_or_expr(&self) -> &String {
match &self.description {
None => self.expr(),
Some(value) => value,
}
}
fn option(&self) -> &Opt {
&self.option
}
fn option_mut(&mut self) -> &mut Opt {
&mut self.option
}
fn location(&self) -> &Option<Location> {
&self.location
}
fn new_subject<NewSub, NewOpt>(
&self,
new_actual: &'a NewSub,
new_description: Option<String>,
new_option: NewOpt,
) -> Subject<NewSub, NewOpt, Ret> {
Subject::new(
new_actual,
self.expr.clone(),
new_description,
new_option,
self.location.clone(),
self.return_type,
)
}
fn new_owned_subject<'b, NewSub, NewOpt>(
&self,
new_actual: NewSub,
new_description: Option<String>,
new_option: NewOpt,
) -> Subject<'b, NewSub, NewOpt, Ret> {
Subject::new_from_owned_actual(
new_actual,
self.expr.clone(),
new_description,
new_option,
self.location.clone(),
self.return_type,
)
}
}
pub trait AssertionStrategy<R> {
#[track_caller]
fn do_fail(self) -> R;
#[track_caller]
fn do_ok(self) -> R;
}
impl AssertionStrategy<()> for AssertionResult {
fn do_fail(self) {
std::panic::panic_any(self.generate_message());
}
fn do_ok(self) {}
}
#[allow(missing_docs)]
#[derive(Clone)]
pub struct AssertionResult {
location: Option<String>,
facts: Vec<Fact>,
}
#[allow(missing_docs)]
impl AssertionResult {
const DEBUG_LENGTH_WRAP_LIMIT: usize = 80;
pub(self) fn new(location: &Option<Location>) -> Self {
AssertionResult {
location: location.as_ref().map(|loc| format!("{}", loc)),
facts: vec![],
}
}
#[inline]
pub fn add_fact<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.facts.push(Fact::new(key, value));
self
}
pub fn add_formatted_fact<K: Into<String>, V: Debug>(mut self, key: K, value: V) -> Self {
self.facts.push(Fact::new(key, format!("{:?}", value)));
self
}
#[inline]
pub fn add_formatted_values_fact<K: Into<String>, V: Debug>(
mut self,
key: K,
values: Vec<V>,
) -> Self {
let str_values = values.iter().map(|v| format!("{:?}", v)).collect();
self.facts.push(Fact::new_multi_value_fact(key, str_values));
self
}
#[inline]
pub fn add_simple_formatted_fact<V: Debug>(mut self, value: V) -> Self {
self.facts
.push(Fact::new_simple_fact(format!("{:?}", value)));
self
}
#[inline]
pub fn add_simple_fact<V: Into<String>>(mut self, value: V) -> Self {
self.facts.push(Fact::new_simple_fact(value));
self
}
#[inline]
pub fn add_splitter(mut self) -> Self {
self.facts.push(Fact::new_splitter());
self
}
pub fn generate_message(&self) -> String {
let mut messages = vec![];
messages.push(format!(
"assertion failed{maybe_loc}",
maybe_loc = match &self.location {
None => String::new(),
Some(loc) => format!(": {}", loc),
}
));
let longest_key_length = self
.facts
.iter()
.flat_map(|fact| match fact {
Fact::KeyValue { key, .. } => Some(key),
Fact::KeyValues { key, .. } => Some(key),
_ => None,
})
.map(|key| key.len())
.max()
.unwrap_or(0);
for x in self.facts.iter() {
match x {
Fact::KeyValue { key, value } => messages.push(format!(
"{key:width$}: {value}",
key = key,
value = value,
width = longest_key_length
)),
Fact::KeyValues { key, values } => {
let values_size = values.len();
let use_multiline_output = values
.clone()
.iter()
.map(|x| format!("{:?}", x).len())
.max_by(|x, y| x.cmp(y))
.unwrap_or(0)
> Self::DEBUG_LENGTH_WRAP_LIMIT;
let formatted_values = format!(
"{}",
if use_multiline_output {
let elements = values
.iter()
.map(|el| format!(" - {}", el))
.collect::<Vec<_>>()
.join("\n");
if values_size > 0 {
format!("[\n{}\n]", elements)
} else {
"[]".to_string()
}
} else {
format!(
"[ {} ]",
values
.iter()
.map(|el| format!("{}", el))
.collect::<Vec<_>>()
.join(", ")
)
}
);
println!("{}", formatted_values);
messages.push(format!(
"{key:width$}: {value}",
key = key,
value = formatted_values,
width = longest_key_length
));
}
Fact::Value { value } => messages.push(value.to_string()),
Fact::Splitter => messages.push(String::from("---")),
}
}
messages.join("\n")
}
pub fn facts(&self) -> &Vec<Fact> {
&self.facts
}
}
impl Debug for AssertionResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.generate_message())
}
}
#[derive(Debug, Clone)]
pub struct Location {
file: String,
line: u32,
column: u32,
}
impl Location {
#[allow(dead_code)] pub fn new<I: Into<String>>(file: I, line: u32, column: u32) -> Self {
Location {
file: file.into(),
line,
column,
}
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("{}:{}:{}", self.file, self.line, self.column))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Fact {
KeyValue { key: String, value: String },
KeyValues { key: String, values: Vec<String> },
Value { value: String },
Splitter,
}
#[allow(missing_docs)]
impl Fact {
pub fn new<K: Into<String>, V: Into<String>>(key: K, value: V) -> Fact {
Fact::KeyValue {
key: key.into(),
value: value.into(),
}
}
pub fn new_simple_fact<V: Into<String>>(value: V) -> Fact {
Fact::Value {
value: value.into(),
}
}
pub fn new_multi_value_fact<K: Into<String>, V: Into<String>>(key: K, values: Vec<V>) -> Fact {
Fact::KeyValues {
key: key.into(),
values: values.into_iter().map(|v| v.into()).collect(),
}
}
pub fn new_splitter() -> Fact {
Fact::Splitter
}
}
#[cfg(test)]
mod tests {
use crate::*;
use crate::testing::CheckThatResult;
use super::*;
#[test]
fn assert_that() {
assert_that!(1);
assert_that!(vec![""]);
}
#[test]
fn assert_that_unit_return_type() {
assert_eq!(assert_that!(1).return_type, PhantomData::<()>::default());
assert_eq!(
assert_that!(vec![""]).return_type,
PhantomData::<()>::default()
);
}
#[test]
fn check_that() {
check_that!(1);
check_that!(vec![""]);
}
#[test]
fn check_that_result_return_type() {
assert_eq!(
check_that!(1).return_type,
PhantomData::<CheckThatResult>::default()
);
assert_eq!(
check_that!(vec![""]).return_type,
PhantomData::<CheckThatResult>::default()
);
}
#[test]
fn assert_result_message_generation() {
assert_eq!(
AssertionResult::new(&None).generate_message(),
"assertion failed"
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))).generate_message(),
"assertion failed: foo.rs:123:456"
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_fact("foo", "bar")
.generate_message(),
r#"assertion failed: foo.rs:123:456
foo: bar"#
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_fact("foo", "bar")
.add_fact("looooong key", "align indent")
.generate_message(),
r#"assertion failed: foo.rs:123:456
foo : bar
looooong key: align indent"#
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_fact("foo", "bar")
.add_fact("looooong key", "align indent")
.add_fact("s", "hort")
.generate_message(),
r#"assertion failed: foo.rs:123:456
foo : bar
looooong key: align indent
s : hort"#
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_fact("foo", "bar")
.add_splitter()
.add_fact("s", "hort")
.generate_message(),
r#"assertion failed: foo.rs:123:456
foo: bar
---
s : hort"#
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_fact("foo", "bar")
.add_simple_fact("I am ninja")
.add_fact("s", "hort")
.generate_message(),
r#"assertion failed: foo.rs:123:456
foo: bar
I am ninja
s : hort"#
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_fact("looooong key", "align indent")
.add_formatted_values_fact("kv_key", vec!["short_value"])
.generate_message(),
r#"assertion failed: foo.rs:123:456
looooong key: align indent
kv_key : [ "short_value" ]"#
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_fact("looooong key", "align indent")
.add_formatted_values_fact("kv_key", vec!["short_value", "Very long value is formatted using new lines, this is done to improve output readability."])
.generate_message(),
r#"assertion failed: foo.rs:123:456
looooong key: align indent
kv_key : [
- "short_value"
- "Very long value is formatted using new lines, this is done to improve output readability."
]"#
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_formatted_values_fact("kv_key", vec![1, 2, 3])
.generate_message(),
r#"assertion failed: foo.rs:123:456
kv_key: [ 1, 2, 3 ]"#
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_formatted_values_fact("kv_key", vec!["1", "2", "3"])
.generate_message(),
r#"assertion failed: foo.rs:123:456
kv_key: [ "1", "2", "3" ]"#
);
#[derive(Debug)]
struct LongOutputData<'a> {
#[allow(dead_code)]
val: Option<i32>,
#[allow(dead_code)]
nested: Vec<&'a str>,
}
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_formatted_values_fact(
"kv_key_sht",
vec![LongOutputData {
val: None,
nested: vec!["123", "321"]
}]
)
.add_formatted_values_fact(
"kv_key_lng",
vec![
LongOutputData {
val: Some(123456789),
nested: vec!["hello", "long", "debug", "output"]
},
LongOutputData {
val: None,
nested: vec![]
}
]
)
.generate_message(),
r#"assertion failed: foo.rs:123:456
kv_key_sht: [ LongOutputData { val: None, nested: ["123", "321"] } ]
kv_key_lng: [
- LongOutputData { val: Some(123456789), nested: ["hello", "long", "debug", "output"] }
- LongOutputData { val: None, nested: [] }
]"#
);
assert_eq!(
AssertionResult::new(&Some(Location::new("foo.rs", 123, 456)))
.add_formatted_fact(
"k",
LongOutputData {
val: Some(1),
nested: vec!["123", "321"]
}
)
.add_simple_formatted_fact(LongOutputData {
val: Some(2),
nested: vec!["1234"]
})
.generate_message(),
r#"assertion failed: foo.rs:123:456
k: LongOutputData { val: Some(1), nested: ["123", "321"] }
LongOutputData { val: Some(2), nested: ["1234"] }"#
);
}
}