use crate::colored;
use crate::expectations::satisfies;
#[cfg(feature = "recursive")]
use crate::recursive_comparison::RecursiveComparison;
use crate::std::any;
use crate::std::borrow::Borrow;
use crate::std::borrow::Cow;
use crate::std::error::Error as StdError;
use crate::std::fmt::{self, Debug, Display};
use crate::std::format;
use crate::std::ops::Deref;
use crate::std::string::{String, ToString};
use crate::std::vec;
use crate::std::vec::Vec;
#[cfg(feature = "panic")]
use crate::std::{cell::RefCell, rc::Rc};
#[macro_export]
macro_rules! assert_that {
($subject:expr) => {
$crate::prelude::assert_that($subject)
.named(&stringify!($subject).replace("\n", " "))
.located_at($crate::prelude::Location {
file: file!(),
line: line!(),
column: column!(),
})
};
}
#[macro_export]
macro_rules! verify_that {
($subject:expr) => {
$crate::prelude::verify_that($subject)
.named(&stringify!($subject).replace("\n", " "))
.located_at($crate::prelude::Location {
file: file!(),
line: line!(),
column: column!(),
})
};
}
#[cfg(feature = "panic")]
#[cfg_attr(feature = "panic", macro_export)]
#[cfg_attr(docsrs, doc(cfg(feature = "panic")))]
macro_rules! assert_that_code {
($subject:expr) => {
$crate::prelude::assert_that_code($subject)
.named(&stringify!($subject).replace("\n", " "))
.located_at($crate::prelude::Location {
file: file!(),
line: line!(),
column: column!(),
})
};
}
#[cfg(feature = "panic")]
#[cfg_attr(feature = "panic", macro_export)]
#[cfg_attr(docsrs, doc(cfg(feature = "panic")))]
macro_rules! verify_that_code {
($subject:expr) => {
$crate::prelude::verify_that_code($subject)
.named(&stringify!($subject).replace("\n", " "))
.located_at($crate::prelude::Location {
file: file!(),
line: line!(),
column: column!(),
})
};
}
#[track_caller]
pub fn assert_that<'a, S>(subject: S) -> Spec<'a, S, PanicOnFail> {
#[cfg(not(feature = "colored"))]
{
Spec::new(subject, PanicOnFail)
}
#[cfg(feature = "colored")]
{
Spec::new(subject, PanicOnFail).with_configured_diff_format()
}
}
#[track_caller]
pub fn verify_that<'a, S>(subject: S) -> Spec<'a, S, CollectFailures> {
Spec::new(subject, CollectFailures)
}
#[cfg(feature = "panic")]
#[cfg_attr(docsrs, doc(cfg(feature = "panic")))]
pub fn assert_that_code<'a, S>(code: S) -> Spec<'a, Code<S>, PanicOnFail>
where
S: FnOnce(),
{
#[cfg(not(feature = "colored"))]
{
Spec::new(Code::from(code), PanicOnFail).named("the closure")
}
#[cfg(feature = "colored")]
{
Spec::new(Code::from(code), PanicOnFail)
.named("the closure")
.with_configured_diff_format()
}
}
#[cfg(feature = "panic")]
#[cfg_attr(docsrs, doc(cfg(feature = "panic")))]
pub fn verify_that_code<'a, S>(code: S) -> Spec<'a, Code<S>, CollectFailures>
where
S: FnOnce(),
{
Spec::new(Code::from(code), CollectFailures).named("the closure")
}
pub trait Expectation<S: ?Sized> {
fn test(&mut self, subject: &S) -> bool;
fn message(
&self,
expression: &Expression<'_>,
actual: &S,
inverted: bool,
format: &DiffFormat,
) -> String;
}
pub trait Invertible {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Expression<'a>(pub Cow<'a, str>);
impl Default for Expression<'_> {
fn default() -> Self {
Self("subject".into())
}
}
impl Display for Expression<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Borrow<str> for Expression<'_> {
fn borrow(&self) -> &str {
&self.0
}
}
impl Deref for Expression<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> From<&'a str> for Expression<'a> {
fn from(s: &'a str) -> Self {
Self(s.into())
}
}
impl From<String> for Expression<'_> {
fn from(s: String) -> Self {
Self(s.into())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Location<'a> {
pub file: &'a str,
pub line: u32,
pub column: u32,
}
impl Display for Location<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(not(test))]
let file = self.file;
#[cfg(test)]
let file = self.file.replace('\\', "/");
write!(f, "{file}:{}:{}", self.line, self.column)
}
}
impl<'a> Location<'a> {
#[must_use]
pub const fn new(file: &'a str, line: u32, column: u32) -> Self {
Self { file, line, column }
}
}
impl Location<'_> {
pub fn file(&self) -> &str {
self.file
}
pub fn line(&self) -> u32 {
self.line
}
pub fn column(&self) -> u32 {
self.column
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct OwnedLocation {
pub file: String,
pub line: u32,
pub column: u32,
}
impl Display for OwnedLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(not(test))]
let file = self.file.clone();
#[cfg(test)]
let file = self.file.replace('\\', "/");
write!(f, "{file}:{}:{}", self.line, self.column)
}
}
impl OwnedLocation {
#[must_use]
pub fn new(file: impl Into<String>, line: u32, column: u32) -> Self {
Self {
file: file.into(),
line,
column,
}
}
pub fn as_location(&self) -> Location<'_> {
Location {
file: &self.file,
line: self.line,
column: self.column,
}
}
}
impl From<Location<'_>> for OwnedLocation {
fn from(value: Location<'_>) -> Self {
Self {
file: value.file.into(),
line: value.line,
column: value.column,
}
}
}
impl OwnedLocation {
pub fn file(&self) -> &str {
&self.file
}
pub fn line(&self) -> u32 {
self.line
}
pub fn column(&self) -> u32 {
self.column
}
}
pub struct Spec<'a, S, R> {
subject: S,
expression: Expression<'a>,
description: Option<Cow<'a, str>>,
location: Option<Location<'a>>,
failures: Vec<AssertFailure>,
diff_format: DiffFormat,
failing_strategy: R,
}
impl<S, R> Spec<'_, S, R> {
pub fn subject(&self) -> &S {
&self.subject
}
pub fn expression(&self) -> &Expression<'_> {
&self.expression
}
pub fn location(&self) -> Option<Location<'_>> {
self.location
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub const fn diff_format(&self) -> &DiffFormat {
&self.diff_format
}
pub fn failing_strategy(&self) -> &R {
&self.failing_strategy
}
}
impl<'a, S, R> Spec<'a, S, R> {
#[must_use = "a spec does nothing unless an assertion method is called"]
pub fn new(subject: S, failing_strategy: R) -> Self {
Self {
subject,
expression: Expression::default(),
description: None,
location: None,
failures: vec![],
diff_format: colored::DIFF_FORMAT_NO_HIGHLIGHT,
failing_strategy,
}
}
#[must_use = "a spec does nothing unless an assertion method is called"]
pub fn named(mut self, subject_name: impl Into<Cow<'a, str>>) -> Self {
self.expression = Expression(subject_name.into());
self
}
#[must_use = "a spec does nothing unless an assertion method is called"]
pub fn described_as(mut self, description: impl Into<Cow<'a, str>>) -> Self {
self.description = Some(description.into());
self
}
#[must_use = "a spec does nothing unless an assertion method is called"]
pub const fn located_at(mut self, location: Location<'a>) -> Self {
self.location = Some(location);
self
}
#[must_use = "a spec does nothing unless an assertion method is called"]
pub const fn with_diff_format(mut self, diff_format: DiffFormat) -> Self {
self.diff_format = diff_format;
self
}
#[cfg(feature = "colored")]
#[cfg_attr(docsrs, doc(cfg(feature = "colored")))]
#[must_use = "a spec does nothing unless an assertion method is called"]
pub fn with_configured_diff_format(self) -> Self {
use crate::colored::configured_diff_format;
#[cfg(not(feature = "std"))]
{
self.with_diff_format(configured_diff_format())
}
#[cfg(feature = "std")]
{
use crate::std::sync::OnceLock;
static DIFF_FORMAT: OnceLock<DiffFormat> = OnceLock::new();
let diff_format = DIFF_FORMAT.get_or_init(configured_diff_format);
self.with_diff_format(diff_format.clone())
}
}
#[cfg(feature = "recursive")]
#[cfg_attr(docsrs, doc(cfg(feature = "recursive")))]
#[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"]
pub fn using_recursive_comparison(self) -> RecursiveComparison<'a, S, R> {
RecursiveComparison::new(self)
}
#[must_use = "a spec does nothing unless an assertion method is called"]
pub fn extracting<F, U>(self, extractor: F) -> Spec<'a, U, R>
where
F: FnOnce(S) -> U,
{
self.mapping(extractor)
}
#[must_use = "a spec does nothing unless an assertion method is called"]
pub fn mapping<F, U>(self, mapper: F) -> Spec<'a, U, R>
where
F: FnOnce(S) -> U,
{
Spec {
subject: mapper(self.subject),
expression: self.expression,
description: self.description,
location: self.location,
failures: self.failures,
diff_format: self.diff_format,
failing_strategy: self.failing_strategy,
}
}
}
impl<S, R> Spec<'_, S, R>
where
R: FailingStrategy,
{
#[allow(clippy::needless_pass_by_value, clippy::return_self_not_must_use)]
#[track_caller]
pub fn expecting(mut self, mut expectation: impl Expectation<S>) -> Self {
if !expectation.test(&self.subject) {
let message =
expectation.message(&self.expression, &self.subject, false, &self.diff_format);
self.do_fail_with_message(message);
}
self
}
#[allow(clippy::return_self_not_must_use)]
#[track_caller]
pub fn satisfies<P>(self, predicate: P) -> Self
where
P: Fn(&S) -> bool,
{
self.expecting(satisfies(predicate))
}
#[allow(clippy::return_self_not_must_use)]
#[track_caller]
pub fn satisfies_with_message<P>(self, message: impl Into<String>, predicate: P) -> Self
where
P: Fn(&S) -> bool,
{
self.expecting(satisfies(predicate).with_message(message))
}
}
impl<'a, I, R> Spec<'a, I, R> {
#[allow(clippy::return_self_not_must_use)]
#[track_caller]
pub fn each_element<T, A, B>(mut self, assert: A) -> Spec<'a, (), R>
where
I: IntoIterator<Item = T>,
A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>,
{
let root_expression = &self.expression;
let mut position = -1;
for item in self.subject {
position += 1;
let element_spec = Spec {
subject: item,
expression: format!("{root_expression} [{position}]").into(),
description: None,
location: self.location,
failures: vec![],
diff_format: self.diff_format.clone(),
failing_strategy: CollectFailures,
};
let failures = assert(element_spec).failures;
self.failures.extend(failures);
}
if !self.failures.is_empty()
&& any::type_name_of_val(&self.failing_strategy) == any::type_name::<PanicOnFail>()
{
PanicOnFail.do_fail_with(&self.failures);
}
Spec {
subject: (),
expression: self.expression,
description: self.description,
location: self.location,
failures: self.failures,
diff_format: self.diff_format,
failing_strategy: self.failing_strategy,
}
}
#[track_caller]
pub fn any_element<T, A, B>(mut self, assert: A) -> Spec<'a, (), R>
where
I: IntoIterator<Item = T>,
A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>,
{
let root_expression = &self.expression;
let mut any_success = false;
let mut position = -1;
for item in self.subject {
position += 1;
let element_spec = Spec {
subject: item,
expression: format!("{root_expression} [{position}]").into(),
description: None,
location: self.location,
failures: vec![],
diff_format: self.diff_format.clone(),
failing_strategy: CollectFailures,
};
let failures = assert(element_spec).failures;
if failures.is_empty() {
any_success = true;
break;
}
self.failures.extend(failures);
}
if !any_success
&& any::type_name_of_val(&self.failing_strategy) == any::type_name::<PanicOnFail>()
{
PanicOnFail.do_fail_with(&self.failures);
}
Spec {
subject: (),
expression: self.expression,
description: self.description,
location: self.location,
failures: self.failures,
diff_format: self.diff_format,
failing_strategy: self.failing_strategy,
}
}
}
pub trait DoFail {
fn do_fail_with(&mut self, failures: impl IntoIterator<Item = AssertFailure>);
fn do_fail_with_message(&mut self, message: impl Into<String>);
}
impl<S, R> DoFail for Spec<'_, S, R>
where
R: FailingStrategy,
{
fn do_fail_with(&mut self, failures: impl IntoIterator<Item = AssertFailure>) {
self.failures.extend(failures);
self.failing_strategy.do_fail_with(&self.failures);
}
fn do_fail_with_message(&mut self, message: impl Into<String>) {
let message = message.into();
let failure = AssertFailure {
description: self.description.clone().map(String::from),
message,
location: self.location.map(OwnedLocation::from),
};
self.failures.push(failure);
self.failing_strategy.do_fail_with(&self.failures);
}
}
pub trait SoftPanic {
fn soft_panic(&self);
}
impl<S> SoftPanic for Spec<'_, S, CollectFailures> {
fn soft_panic(&self) {
if !self.failures.is_empty() {
PanicOnFail.do_fail_with(&self.failures);
}
}
}
pub trait GetFailures {
fn has_failures(&self) -> bool;
fn failures(&self) -> Vec<AssertFailure>;
fn display_failures(&self) -> Vec<String>;
}
impl<S, R> GetFailures for Spec<'_, S, R> {
fn has_failures(&self) -> bool {
!self.failures.is_empty()
}
fn failures(&self) -> Vec<AssertFailure> {
self.failures.clone()
}
fn display_failures(&self) -> Vec<String> {
self.failures.iter().map(ToString::to_string).collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssertFailure {
description: Option<String>,
message: String,
location: Option<OwnedLocation>,
}
impl Display for AssertFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.description {
None => {
writeln!(f, "{}", &self.message)?;
},
Some(description) => {
writeln!(f, "{description}\n{}", &self.message)?;
},
}
Ok(())
}
}
impl StdError for AssertFailure {}
#[allow(clippy::must_use_candidate)]
impl AssertFailure {
pub fn description(&self) -> Option<&String> {
self.description.as_ref()
}
#[allow(clippy::missing_const_for_fn)]
pub fn message(&self) -> &str {
&self.message
}
pub fn location(&self) -> Option<&OwnedLocation> {
self.location.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Highlight {
pub(crate) start: &'static str,
pub(crate) end: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffFormat {
pub(crate) unexpected: Highlight,
pub(crate) missing: Highlight,
}
pub trait FailingStrategy {
fn do_fail_with(&self, failures: &[AssertFailure]);
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct PanicOnFail;
impl FailingStrategy for PanicOnFail {
#[track_caller]
fn do_fail_with(&self, failures: &[AssertFailure]) {
let message = failures
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
panic!("{}", message);
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct CollectFailures;
impl FailingStrategy for CollectFailures {
fn do_fail_with(&self, _failures: &[AssertFailure]) {
}
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct Unknown;
impl Debug for Unknown {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
impl Display for Unknown {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "_")
}
}
#[cfg(feature = "panic")]
#[cfg_attr(docsrs, doc(cfg(feature = "panic")))]
pub struct Code<F>(Rc<RefCell<Option<F>>>);
#[cfg(feature = "panic")]
mod code {
use super::Code;
use crate::std::cell::RefCell;
use crate::std::rc::Rc;
impl<F> From<F> for Code<F>
where
F: FnOnce(),
{
fn from(value: F) -> Self {
Self(Rc::new(RefCell::new(Some(value))))
}
}
impl<F> Code<F> {
#[must_use]
pub fn take(&self) -> Option<F> {
self.0.borrow_mut().take()
}
}
}
#[cfg(test)]
mod tests;