Skip to content

Commit

Permalink
Generalize into_test_result() extension method to all std::result::Re…
Browse files Browse the repository at this point in the history
…sult and Option.

PiperOrigin-RevId: 658739260
  • Loading branch information
bjacotg authored and copybara-github committed Aug 2, 2024
1 parent 63421a6 commit 70a6da1
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 56 deletions.
107 changes: 71 additions & 36 deletions googletest/crate_docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,61 +437,96 @@ fn always_fails() -> Result<()> {
# always_fails().unwrap_err();
```

## Integrations with other crates
## Conversion from `Result::Err` and `Option::None`

GoogleTest Rust includes integrations with the
[Anyhow](https://crates.io/crates/anyhow) and
[Proptest](https://crates.io/crates/proptest) crates to simplify turning
errors from those crates into test failures.
To simplify error management during a test arrangement, [`Result<T>`]
provides a few conversion utilities.

To use this, activate the `anyhow`, respectively `proptest` feature in
GoogleTest Rust and invoke the extension method [`into_test_result()`] on a
`Result` value in your test. For example:
If your setup function returns `std::result::Result<T, E>` where `E: std::error::Error`,
the `std::result::Result<T, E>` can simply be handled with the `?` operator. If an `Err(e)`
is returned, the test will report a failure at the line where the `?` operator has been
applied (or the lowest caller without `#[track_caller]`).

```
# use googletest::prelude::*;
# #[cfg(feature = "anyhow")]
# use anyhow::anyhow;
# #[cfg(feature = "anyhow")]
# /* The attribute macro would prevent the function from being compiled in a doctest.
#[test]
# */
fn has_anyhow_failure() -> Result<()> {
Ok(just_return_error().into_test_result()?)
struct PngImage { h: i32, w: i32 /* ... */ }
impl PngImage {
fn new_from_file(file_name: &str) -> std::result::Result<Self, std::io::Error> {
Err(std::io::Error::new(std::io::ErrorKind::Other, "oh no!"))
}
fn rotate(&mut self) { std::mem::swap(&mut self.h, &mut self.w);}
fn dimensions(&self) -> (i32, i32) { (self.h, self.w)}
}
# #[cfg(feature = "anyhow")]
fn just_return_error() -> anyhow::Result<()> {
anyhow::Result::Err(anyhow!("This is an error"))
fn test_png_image_dimensions() -> googletest::Result<()> {
// Arrange
let mut png = PngImage::new_from_file("example.png")?;
verify_eq!(png.dimensions(), (128, 64))?;
// Act
png.rotate();
// Assert
expect_eq!(png.dimensions(), (64, 128));
Ok(())
}
# #[cfg(feature = "anyhow")]
# has_anyhow_failure().unwrap_err();
# test_png_image_dimensions().unwrap_err();
```

One can convert Proptest test failures into GoogleTest test failures when the
test is invoked with
[`TestRunner::run`](https://docs.rs/proptest/latest/proptest/test_runner/struct.TestRunner.html#method.run):
If your setup function returns `Option<T>` or `std::result::Result<T, E>` where
`E: !std::error::Error`, then you can convert these types with `into_test_result()`
from the `IntoTestResult` extension trait.

```
# use googletest::prelude::*;
# #[cfg(feature = "proptest")]
# use proptest::test_runner::{Config, TestRunner};
# #[cfg(feature = "proptest")]
# struct PngImage;
# static PNG_BINARY: [u8;0] = [];
impl PngImage {
fn new_from_binary(bin: &[u8]) -> std::result::Result<Self, String> {
Err("Parsing failed".into())
}
}
# /* The attribute macro would prevent the function from being compiled in a doctest.
#[test]
#[googletest::test]
# */
fn numbers_are_greater_than_zero() -> Result<()> {
let mut runner = TestRunner::new(Config::default());
runner.run(&(1..100i32), |v| Ok(verify_that!(v, gt(0))?)).into_test_result()
fn test_png_image_binary() -> googletest::Result<()> {
// Arrange
let png_image = PngImage::new_from_binary(&PNG_BINARY).into_test_result()?;
/* ... */
# Ok(())
}
# test_png_image_binary().unwrap_err();
impl PngImage {
fn new_from_cache(key: u64) -> Option<Self> {
None
}
}
# #[cfg(feature = "proptest")]
# numbers_are_greater_than_zero().unwrap();
# /* The attribute macro would prevent the function from being compiled in a doctest.
#[googletest::test]
# */
fn test_png_from_cache() -> googletest::Result<()> {
// Arrange
let png_image = PngImage::new_from_cache(123).into_test_result()?;
/* ... */
# Ok(())
}
# test_png_from_cache().unwrap_err();
```

Similarly, when the `proptest` feature is enabled, GoogleTest assertion failures
can automatically be converted into Proptest

## Integrations with other crates

GoogleTest Rust includes integrations with the
[Proptest](https://crates.io/crates/proptest) crates to simplify turning
GoogleTest assertion failures into Proptest
[`TestCaseError`](https://docs.rs/proptest/latest/proptest/test_runner/enum.TestCaseError.html)
through the `?` operator as the example above shows.
through the `?` operator.

[`and_log_failure()`]: GoogleTestSupport::and_log_failure
[`into_test_result()`]: IntoTestResult::into_test_result
Expand Down
47 changes: 27 additions & 20 deletions googletest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,45 +236,52 @@ impl<T> GoogleTestSupport for std::result::Result<T, TestAssertionFailure> {
///
/// A type can implement this trait to provide an easy way to return immediately
/// from a test in conjunction with the `?` operator. This is useful for
/// [`Result`][std::result::Result] types whose `Result::Err` variant does not
/// implement [`std::error::Error`].
/// [`Option`] and [`Result`][std::result::Result] types whose `Result::Err`
/// variant does not implement [`std::error::Error`].
///
/// There is an implementation of this trait for [`anyhow::Error`] (which does
/// not implement `std::error::Error`) when the `anyhow` feature is enabled.
/// Importing this trait allows one to easily map [`anyhow::Error`] to a test
/// failure:
/// If `Result::Err` implements [`std::error::Error`] you can just use the `?`
/// operator directly.
///
/// ```ignore
/// #[test]
/// fn should_work() -> Result<()> {
/// fn should_work() -> googletest::Result<()> {
/// let value = something_which_can_fail().into_test_result()?;
/// let value = something_which_can_fail_with_option().into_test_result()?;
/// ...
/// }
///
/// fn something_which_can_fail() -> anyhow::Result<...> { ... }
/// fn something_which_can_fail() -> std::result::Result<T, String> { ... }
/// fn something_which_can_fail_with_option() -> Option<T> { ... }
/// ```
pub trait IntoTestResult<T> {
/// Converts this instance into a [`Result`].
///
/// Typically, the `Self` type is itself a [`std::result::Result`]. This
/// method should then map the `Err` variant to a [`TestAssertionFailure`]
/// and leave the `Ok` variant unchanged.
/// Typically, the `Self` type is itself an implementation of the
/// [`std::ops::Try`] trait. This method should then map the `Residual`
/// variant to a [`TestAssertionFailure`] and leave the `Output` variant
/// unchanged.
fn into_test_result(self) -> Result<T>;
}

#[cfg(feature = "anyhow")]
impl<T> IntoTestResult<T> for std::result::Result<T, anyhow::Error> {
impl<T, E: std::fmt::Debug> IntoTestResult<T> for std::result::Result<T, E> {
#[track_caller]
fn into_test_result(self) -> std::result::Result<T, TestAssertionFailure> {
self.map_err(|e| TestAssertionFailure::create(format!("{e}")))
match self {
Ok(t) => Ok(t),
Err(e) => Err(TestAssertionFailure::create(format!("{e:?}"))),
}
}
}

#[cfg(feature = "proptest")]
impl<OkT, CaseT: std::fmt::Debug> IntoTestResult<OkT>
for std::result::Result<OkT, proptest::test_runner::TestError<CaseT>>
{
fn into_test_result(self) -> std::result::Result<OkT, TestAssertionFailure> {
self.map_err(|e| TestAssertionFailure::create(format!("{e}")))
impl<T> IntoTestResult<T> for Option<T> {
#[track_caller]
fn into_test_result(self) -> std::result::Result<T, TestAssertionFailure> {
match self {
Some(t) => Ok(t),
None => Err(TestAssertionFailure::create(format!(
"called `Option::into_test_result()` on a `Option::<{}>::None` value",
std::any::type_name::<T>()
))),
}
}
}
10 changes: 10 additions & 0 deletions integration_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,16 @@ name = "test_returning_anyhow_error"
path = "src/test_returning_anyhow_error.rs"
test = false

[[bin]]
name = "test_returning_string_error"
path = "src/test_returning_string_error.rs"
test = false

[[bin]]
name = "test_returning_option"
path = "src/test_returning_option.rs"
test = false

[[bin]]
name = "two_expect_pred_failures"
path = "src/two_expect_pred_failures.rs"
Expand Down
28 changes: 28 additions & 0 deletions integration_tests/src/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,34 @@ mod tests {
verify_that!(output, contains_substring("Error from Anyhow"))
}

#[test]
fn test_can_return_option_generated_error() -> Result<()> {
let output = run_external_process_in_tests_directory("test_returning_option")?;

verify_that!(
output,
all![
contains_substring(
"called `Option::into_test_result()` on a `Option::<()>::None` value"
),
contains_substring("test_returning_option.rs:23")
]
)
}

#[test]
fn test_can_return_string_error_generated_error() -> Result<()> {
let output = run_external_process_in_tests_directory("test_returning_string_error")?;

verify_that!(
output,
all![
contains_substring("Error as a String"),
contains_substring("test_returning_string_error.rs:23")
]
)
}

#[::core::prelude::v1::test]
#[should_panic]
fn should_panic_when_expect_that_runs_without_attribute_macro() {
Expand Down
30 changes: 30 additions & 0 deletions integration_tests/src/test_returning_option.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.

fn main() {}

#[cfg(test)]
mod tests {
use googletest::prelude::*;

#[test]
fn should_fail_due_to_none_in_subroutine() -> Result<()> {
returns_option().into_test_result()?;
Ok(())
}

fn returns_option() -> Option<()> {
None
}
}
30 changes: 30 additions & 0 deletions integration_tests/src/test_returning_string_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.

fn main() {}

#[cfg(test)]
mod tests {
use googletest::prelude::*;

#[test]
fn should_fail_due_to_error_in_subroutine() -> Result<()> {
returns_string_error().into_test_result()?;
Ok(())
}

fn returns_string_error() -> std::result::Result<(), String> {
Err("Error as a String".into())
}
}
2 changes: 2 additions & 0 deletions run_integration_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ INTEGRATION_TEST_BINARIES=(
"simple_assertion_failure"
"simple_assertion_failure_with_assert_that"
"test_returning_anyhow_error"
"test_returning_string_error"
"test_returning_option"
"two_expect_pred_failures"
"two_expect_that_failures"
"two_non_fatal_failures"
Expand Down

0 comments on commit 70a6da1

Please sign in to comment.