use std::fmt::Display;
use std::future::Future;
use std::panic::Location;
use std::time::Duration;
use test_better_async::{Elapsed, RuntimeAvailable, run_within};
use test_better_core::{ErrorKind, Payload, TestError, TestResult};
use test_better_snapshot::{
InlineLocation, InlineSnapshotFailure, Redactions, SnapshotFailure, SnapshotMode,
};
use crate::description::Description;
use crate::matcher::{Matcher, Mismatch};
pub struct Subject<T> {
actual: T,
expr: &'static str,
module_path: &'static str,
}
impl<T> Subject<T> {
#[must_use]
pub fn new(actual: T, expr: &'static str, module_path: &'static str) -> Self {
Self {
actual,
expr,
module_path,
}
}
#[track_caller]
pub fn satisfies<M>(self, matcher: M) -> TestResult
where
M: Matcher<T>,
{
match matcher.check(&self.actual).failure {
None => Ok(()),
Some(mismatch) => Err(mismatch_error(self.expr, mismatch)),
}
}
#[track_caller]
pub fn violates<M>(self, matcher: M) -> TestResult
where
M: Matcher<T>,
{
if matcher.check(&self.actual).matched {
Err(unexpected_match_error(self.expr, matcher.description()))
} else {
Ok(())
}
}
#[track_caller]
pub fn resolves_to<M>(self, matcher: M) -> impl Future<Output = TestResult>
where
T: Future,
M: Matcher<T::Output>,
{
let location = Location::caller();
async move {
let output = self.actual.await;
match matcher.check(&output).failure {
None => Ok(()),
Some(mismatch) => Err(mismatch_error(self.expr, mismatch).with_location(location)),
}
}
}
#[track_caller]
pub fn completes_within(self, limit: Duration) -> impl Future<Output = TestResult>
where
T: Future + RuntimeAvailable,
{
let location = Location::caller();
async move {
match run_within(limit, self.actual).await {
Ok(_) => Ok(()),
Err(elapsed) => Err(timeout_error(self.expr, elapsed).with_location(location)),
}
}
}
#[track_caller]
pub fn matches_snapshot(self, name: &str) -> TestResult
where
T: Display,
{
self.matches_snapshot_with(name, &Redactions::new())
}
#[track_caller]
pub fn matches_snapshot_with(self, name: &str, redactions: &Redactions) -> TestResult
where
T: Display,
{
let actual = redactions.apply(&self.actual.to_string());
match test_better_snapshot::assert_snapshot(self.module_path, name, &actual) {
Ok(()) => Ok(()),
Err(failure) => Err(snapshot_error(self.expr, name, failure)),
}
}
#[track_caller]
pub fn matches_inline_snapshot(self, expected: &str) -> TestResult
where
T: Display,
{
self.matches_inline_snapshot_with(expected, &Redactions::new())
}
#[track_caller]
pub fn matches_inline_snapshot_with(self, expected: &str, redactions: &Redactions) -> TestResult
where
T: Display,
{
let actual = redactions.apply(&self.actual.to_string());
let caller = Location::caller();
let location = InlineLocation {
file: caller.file().to_string(),
line: caller.line(),
column: caller.column(),
};
match test_better_snapshot::assert_inline_snapshot(
&actual,
expected,
&location,
SnapshotMode::from_env(),
) {
Ok(()) => Ok(()),
Err(failure) => Err(inline_snapshot_error(self.expr, failure)),
}
}
}
#[track_caller]
fn mismatch_error(expr: &str, mismatch: Mismatch) -> TestError {
TestError::new(ErrorKind::Assertion)
.with_message(format!("check!({expr})"))
.with_payload(Payload::ExpectedActual {
expected: mismatch.expected.to_string(),
actual: mismatch.actual,
diff: mismatch.diff,
})
}
#[track_caller]
fn unexpected_match_error(expr: &str, description: Description) -> TestError {
TestError::new(ErrorKind::Assertion).with_message(format!(
"check!({expr}): expected it not to be {description}, but it was"
))
}
#[track_caller]
fn timeout_error(expr: &str, elapsed: Elapsed) -> TestError {
TestError::new(ErrorKind::Assertion).with_message(format!(
"check!({expr}): did not complete within {:?}",
elapsed.limit
))
}
#[track_caller]
fn snapshot_error(expr: &str, name: &str, failure: SnapshotFailure) -> TestError {
match failure {
SnapshotFailure::Mismatch {
path,
expected,
actual,
} => {
let diff = snapshot_diff(&expected, &actual);
TestError::new(ErrorKind::Snapshot)
.with_message(format!(
"check!({expr}): snapshot {name:?} at {} does not match",
path.display()
))
.with_payload(Payload::ExpectedActual {
expected,
actual,
diff,
})
}
SnapshotFailure::Missing { path } => {
TestError::new(ErrorKind::Snapshot).with_message(format!(
"check!({expr}): snapshot {name:?} does not exist at {}; \
rerun with UPDATE_SNAPSHOTS=1 to create it",
path.display()
))
}
SnapshotFailure::Io {
path,
action,
source,
} => TestError::new(ErrorKind::Snapshot).with_message(format!(
"check!({expr}): snapshot {name:?} I/O error {action} ({}): {source}",
path.display()
)),
}
}
#[track_caller]
fn inline_snapshot_error(expr: &str, failure: InlineSnapshotFailure) -> TestError {
let InlineSnapshotFailure { expected, actual } = failure;
let diff = snapshot_diff(&expected, &actual);
TestError::new(ErrorKind::Snapshot)
.with_message(format!(
"check!({expr}): inline snapshot does not match; \
rerun with UPDATE_SNAPSHOTS=1 to update it"
))
.with_payload(Payload::ExpectedActual {
expected,
actual,
diff,
})
}
#[cfg(feature = "diff")]
fn snapshot_diff(expected: &str, actual: &str) -> Option<String> {
Some(crate::diff::diff_lines(expected, actual))
}
#[cfg(not(feature = "diff"))]
fn snapshot_diff(_expected: &str, _actual: &str) -> Option<String> {
None
}
#[macro_export]
macro_rules! check {
($actual:expr) => {
$crate::Subject::new($actual, ::core::stringify!($actual), ::core::module_path!())
};
}
#[cfg(test)]
mod tests {
use test_better_core::TestResult;
use crate::{eq, is_true};
#[test]
fn satisfies_returns_ok_on_a_match() -> TestResult {
let result = check!(2 + 2).satisfies(eq(4));
check!(result.is_ok()).satisfies(is_true())?;
Ok(())
}
#[test]
fn satisfies_failure_mentions_the_expression_and_the_expected_value() -> TestResult {
let error = check!(2 + 2).satisfies(eq(5)).expect_err("2 + 2 is not 5");
let rendered = error.to_string();
check!(rendered.contains("2 + 2")).satisfies(is_true())?;
check!(rendered.contains("equal to 5")).satisfies(is_true())?;
check!(rendered.contains("actual: 4")).satisfies(is_true())?;
Ok(())
}
#[test]
fn satisfies_failure_captures_the_caller_location() -> TestResult {
let line = line!() + 1;
let error = check!(2 + 2).satisfies(eq(5)).expect_err("2 + 2 is not 5");
check!(error.location.line()).satisfies(eq(line))?;
check!(error.location.file().ends_with("subject.rs")).satisfies(is_true())?;
Ok(())
}
#[test]
fn violates_returns_ok_when_the_matcher_does_not_match() -> TestResult {
let result = check!(2 + 2).violates(eq(5));
check!(result.is_ok()).satisfies(is_true())?;
Ok(())
}
#[test]
fn violates_failure_mentions_the_expression_and_the_matcher() -> TestResult {
let error = check!(true).violates(is_true()).expect_err("true is true");
let rendered = error.to_string();
check!(rendered.contains("check!(true)")).satisfies(is_true())?;
check!(rendered.contains("not to be true")).satisfies(is_true())?;
Ok(())
}
#[test]
fn violates_captures_the_caller_location() -> TestResult {
let line = line!() + 1;
let error = check!(true).violates(is_true()).expect_err("true is true");
check!(error.location.line()).satisfies(eq(line))?;
Ok(())
}
#[test]
fn resolves_to_returns_ok_when_the_output_matches() -> TestResult {
pollster::block_on(async {
let result = check!(async { 2 + 2 }).resolves_to(eq(4)).await;
check!(result.is_ok()).satisfies(is_true())
})
}
#[test]
fn resolves_to_failure_mentions_the_expression_and_the_output() -> TestResult {
pollster::block_on(async {
let error = check!(async { 2 + 2 })
.resolves_to(eq(5))
.await
.expect_err("2 + 2 does not resolve to 5");
let rendered = error.to_string();
check!(rendered.contains("async { 2 + 2 }")).satisfies(is_true())?;
check!(rendered.contains("equal to 5")).satisfies(is_true())?;
check!(rendered.contains("actual: 4")).satisfies(is_true())
})
}
#[test]
fn resolves_to_failure_captures_the_call_site_not_the_await() -> TestResult {
pollster::block_on(async {
let line = line!() + 1;
let pending = check!(async { 2 + 2 }).resolves_to(eq(5));
let error = pending.await.expect_err("2 + 2 does not resolve to 5");
check!(error.location.line()).satisfies(eq(line))?;
check!(error.location.file().ends_with("subject.rs")).satisfies(is_true())
})
}
#[test]
fn snapshot_mismatch_renders_as_a_snapshot_error_with_a_diff() -> TestResult {
use std::path::PathBuf;
use test_better_core::ErrorKind;
use test_better_snapshot::SnapshotFailure;
let failure = SnapshotFailure::Mismatch {
path: PathBuf::from("tests/snapshots/m__page.snap"),
expected: "line one\nline two".to_string(),
actual: "line one\nline TWO".to_string(),
};
let error = super::snapshot_error("page", "page", failure);
check!(error.kind == ErrorKind::Snapshot).satisfies(is_true())?;
let rendered = error.to_string();
check!(rendered.contains("snapshot \"page\"")).satisfies(is_true())?;
check!(rendered.contains("line one")).satisfies(is_true())?;
#[cfg(feature = "diff")]
check!(rendered.contains("-line two")).satisfies(is_true())?;
Ok(())
}
#[test]
fn snapshot_missing_renders_as_a_snapshot_error_pointing_at_update() -> TestResult {
use std::path::PathBuf;
use test_better_core::ErrorKind;
use test_better_snapshot::SnapshotFailure;
let failure = SnapshotFailure::Missing {
path: PathBuf::from("tests/snapshots/m__page.snap"),
};
let error = super::snapshot_error("page", "page", failure);
check!(error.kind == ErrorKind::Snapshot).satisfies(is_true())?;
check!(error.to_string().contains("UPDATE_SNAPSHOTS=1")).satisfies(is_true())?;
Ok(())
}
#[test]
fn inline_snapshot_mismatch_renders_as_a_snapshot_error_with_a_diff() -> TestResult {
use test_better_core::ErrorKind;
use test_better_snapshot::InlineSnapshotFailure;
let failure = InlineSnapshotFailure {
expected: "one\ntwo".to_string(),
actual: "one\nTWO".to_string(),
};
let error = super::inline_snapshot_error("value", failure);
check!(error.kind == ErrorKind::Snapshot).satisfies(is_true())?;
let rendered = error.to_string();
check!(rendered.contains("inline snapshot does not match")).satisfies(is_true())?;
check!(rendered.contains("UPDATE_SNAPSHOTS=1")).satisfies(is_true())?;
#[cfg(feature = "diff")]
check!(rendered.contains("-two")).satisfies(is_true())?;
Ok(())
}
}