diff --git a/Cargo.toml b/Cargo.toml index 598739b..db9b924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,12 +15,13 @@ categories = ["development-tools::testing", "development-tools::debugging"] [dependencies] num-traits = { version = "0.2.15", optional = true } +anyhow = { version = "1.0.86", optional = true } [dev-dependencies] test-case = "3.1.0" [features] default = ["float"] -float = ["num-traits"] +float = ["dep:num-traits"] testing = [] - +anyhow = ["dep:anyhow"] diff --git a/README.md b/README.md index 6e621f2..2aa3f48 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,31 @@ fn test_it() { } ``` +## anyhow + +Supports asserting error value of `anyhow` under `anyhow` feature flag. + +```toml +[dependencies] +assertor = { version = "*", features = ["anyhow"] } +``` + +```rust +use assertor::*; +use anyhow::anyhow; + +fn anyhow_func() -> anyhow::Result<()> { + Err(anyhow!("failed to parse something in foobar")) +} + +fn test_it() { + assert_that!(anyhow_func()).err().has_message("failed to parse something in foobar"); + assert_that!(anyhow_func()).err().as_string().contains("parse something"); + assert_that!(anyhow_func()).err().as_string().starts_with("failed"); + assert_that!(anyhow_func()).err().as_string().ends_with("foobar"); +} +``` + ## Feature ideas - [ ] Color / Bold diff --git a/src/assertions/anyhow.rs b/src/assertions/anyhow.rs new file mode 100644 index 0000000..5edcd24 --- /dev/null +++ b/src/assertions/anyhow.rs @@ -0,0 +1,118 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; +use crate::StringAssertion; + +/// Trait for anyhow error assertion. +/// +/// # Example +/// +/// ```rust +/// use assertor::*; +/// use anyhow::anyhow; +/// +/// fn anyhow_func() -> anyhow::Result<()> { +/// Err(anyhow!("failed to parse something in foobar")) +/// } +/// +/// fn test_it() { +/// assert_that!(anyhow_func()).err().has_message("failed to parse something in foobar"); +/// assert_that!(anyhow_func()).err().as_string().contains("parse something"); +/// assert_that!(anyhow_func()).err().as_string().starts_with("failed"); +/// assert_that!(anyhow_func()).err().as_string().ends_with("foobar"); +/// } +/// ``` +pub trait AnyhowErrorAssertion { + /// Returns a new `String` subject which is the message of the error. + /// + /// Related: [`StringAssertion`](crate::StringAssertion) + /// + /// ``` + /// use assertor::*; + /// use anyhow::anyhow; + /// + /// assert_that!(anyhow!("error message")).as_string().is_same_string_to("error message"); + /// assert_that!(anyhow!("error message")).as_string().contains("error"); + /// + /// + /// fn some_func() -> anyhow::Result<()> { + /// Err(anyhow!("error message")) + /// } + /// assert_that!(some_func()).err().as_string().starts_with("error"); + /// assert_that!(some_func()).err().as_string().ends_with("message"); + /// ``` + fn as_string(&self) -> Subject; + + /// Checks that the error message contains `expected`. + /// ``` + /// use assertor::*; + /// use anyhow::anyhow; + /// + /// assert_that!(anyhow!("error message")).has_message("error message"); + /// + /// fn some_func() -> anyhow::Result<()> { + /// Err(anyhow!("error message")) + /// } + /// assert_that!(some_func()).err().has_message("error message") + /// ``` + fn has_message>(&self, expected: E) -> R; +} + +impl AnyhowErrorAssertion for Subject<'_, anyhow::Error, (), R> + where + AssertionResult: AssertionStrategy, +{ + fn as_string(&self) -> Subject { + let message = self.actual().to_string(); + self.new_owned_subject(message, Some(format!("{}.to_string()", self.description_or_expr())), ()) + } + + fn has_message>(&self, expected: E) -> R { + self.as_string().is_same_string_to(expected) + } +} + +#[cfg(test)] +mod tests { + use crate::testing::*; + + use super::*; + + #[test] + fn as_string() { + assert_that!(anyhow::Error::msg("error message")).as_string().is_same_string_to("error message"); + assert_that!(anyhow::Error::msg("error message")).as_string().starts_with("error"); + assert_that!(anyhow::Error::msg("error message")).as_string().contains("or"); + + assert_that!(check_that!(anyhow::Error::msg("error message")).as_string().is_same_string_to("wrong")).facts_are( + vec![ + Fact::new("expected", "\"wrong\""), + Fact::new("actual", "\"error message\""), + ] + ); + } + + #[test] + fn has_message() { + assert_that!(anyhow::Error::msg("error message")).has_message("error message"); + assert_that!(check_that!(anyhow::Error::msg("error message")).has_message("wrong")).facts_are( + vec![ + Fact::new("expected", "\"wrong\""), + Fact::new("actual", "\"error message\""), + ] + ); + } +} \ No newline at end of file diff --git a/src/assertions/mod.rs b/src/assertions/mod.rs index f5ad27c..4f7fd04 100644 --- a/src/assertions/mod.rs +++ b/src/assertions/mod.rs @@ -25,5 +25,8 @@ pub mod vec; #[cfg(feature = "float")] pub mod float; +#[cfg(feature = "anyhow")] +pub mod anyhow; + #[cfg(any(test, doc, feature = "testing"))] pub(crate) mod testing; diff --git a/src/assertions/result.rs b/src/assertions/result.rs index c2a2530..9e6b1f6 100644 --- a/src/assertions/result.rs +++ b/src/assertions/result.rs @@ -15,6 +15,7 @@ use std::borrow::Borrow; use std::fmt::Debug; +use crate::assert_that; use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; /// Trait for result assertion. @@ -40,18 +41,24 @@ pub trait ResultAssertion { /// Checks that the subject is [`Result::Ok(expected)`](`std::result::Result::Err`). fn has_ok>(&self, expected: B) -> R - where - OK: PartialEq; + where + OK: PartialEq; /// Checks that the subject is [`Result::Err(expected)`](`std::result::Result::Err`). fn has_err>(&self, expected: B) -> R - where - ERR: PartialEq; + where + ERR: PartialEq; + + /// Returns a new subject which is the ok value of the subject if the subject has ok value. Otherwise, it fails. + fn ok(&self) -> Subject; + + /// Returns a new subject which is the error value of the subject if the subject has error value. Otherwise, it fails. + fn err(&self) -> Subject; } impl ResultAssertion for Subject<'_, Result, (), R> -where - AssertionResult: AssertionStrategy, + where + AssertionResult: AssertionStrategy, { fn is_ok(&self) -> R { if self.actual().is_ok() { @@ -80,8 +87,8 @@ where } fn has_ok>(&self, expected: B) -> R - where - OK: PartialEq, + where + OK: PartialEq, { match self.actual() { Ok(actual) if actual.eq(expected.borrow()) => self.new_result().do_ok(), @@ -99,8 +106,8 @@ where } fn has_err>(&self, expected: B) -> R - where - ERR: PartialEq, + where + ERR: PartialEq, { match self.actual() { Err(actual) if actual.eq(expected.borrow()) => self.new_result().do_ok(), @@ -116,10 +123,21 @@ where .do_fail(), } } + + fn ok(&self) -> Subject { + assert_that!(*self.actual()).is_ok(); + self.new_subject(self.actual().as_ref().ok().unwrap(), Some(format!("{}.ok", self.description_or_expr())), ()) + } + + fn err(&self) -> Subject { + assert_that!(*self.actual()).is_err(); + self.new_subject(self.actual().as_ref().err().unwrap(), Some(format!("{}.err", self.description_or_expr())), ()) + } } #[cfg(test)] mod tests { + use crate::ComparableAssertion; use crate::testing::*; use super::*; @@ -189,4 +207,26 @@ mod tests { ], ); } + + #[test] + fn ok() { + assert_that!(Result::::Ok(0.)).ok().is_at_most(1.); + } + + #[test] + #[should_panic] + fn ok_panic() { + assert_that!(Result::::Err(())).ok().is_at_most(1.); + } + + #[test] + fn err() { + assert_that!(Result::<(), f64>::Err(0.)).err().is_at_most(1.); + } + + #[test] + #[should_panic] + fn err_panic() { + assert_that!(Result::<(), f64>::Ok(())).err().is_at_most(1.); + } } diff --git a/src/lib.rs b/src/lib.rs index b2adf41..78cbdab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,8 @@ #[cfg(feature = "float")] extern crate num_traits; +#[cfg(feature = "anyhow")] +pub use assertions::anyhow::AnyhowErrorAssertion; pub use assertions::basic::{ComparableAssertion, EqualityAssertion}; pub use assertions::boolean::BooleanAssertion; #[cfg(feature = "float")]