Skip to content

Commit 70a6da1

Browse files
bjacotgcopybara-github
authored andcommitted
Generalize into_test_result() extension method to all std::result::Result and Option.
PiperOrigin-RevId: 658739260
1 parent 63421a6 commit 70a6da1

File tree

7 files changed

+198
-56
lines changed

7 files changed

+198
-56
lines changed

googletest/crate_docs.md

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -437,61 +437,96 @@ fn always_fails() -> Result<()> {
437437
# always_fails().unwrap_err();
438438
```
439439

440-
## Integrations with other crates
440+
## Conversion from `Result::Err` and `Option::None`
441441

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

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

451450
```
452451
# use googletest::prelude::*;
453-
# #[cfg(feature = "anyhow")]
454-
# use anyhow::anyhow;
455-
# #[cfg(feature = "anyhow")]
456-
# /* The attribute macro would prevent the function from being compiled in a doctest.
457-
#[test]
458-
# */
459-
fn has_anyhow_failure() -> Result<()> {
460-
Ok(just_return_error().into_test_result()?)
452+
struct PngImage { h: i32, w: i32 /* ... */ }
453+
impl PngImage {
454+
fn new_from_file(file_name: &str) -> std::result::Result<Self, std::io::Error> {
455+
Err(std::io::Error::new(std::io::ErrorKind::Other, "oh no!"))
456+
457+
}
458+
fn rotate(&mut self) { std::mem::swap(&mut self.h, &mut self.w);}
459+
fn dimensions(&self) -> (i32, i32) { (self.h, self.w)}
461460
}
462461
463-
# #[cfg(feature = "anyhow")]
464-
fn just_return_error() -> anyhow::Result<()> {
465-
anyhow::Result::Err(anyhow!("This is an error"))
462+
fn test_png_image_dimensions() -> googletest::Result<()> {
463+
// Arrange
464+
let mut png = PngImage::new_from_file("example.png")?;
465+
verify_eq!(png.dimensions(), (128, 64))?;
466+
467+
// Act
468+
png.rotate();
469+
470+
// Assert
471+
expect_eq!(png.dimensions(), (64, 128));
472+
Ok(())
466473
}
467-
# #[cfg(feature = "anyhow")]
468-
# has_anyhow_failure().unwrap_err();
474+
475+
# test_png_image_dimensions().unwrap_err();
469476
```
470477

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

475482
```
476483
# use googletest::prelude::*;
477-
# #[cfg(feature = "proptest")]
478-
# use proptest::test_runner::{Config, TestRunner};
479-
# #[cfg(feature = "proptest")]
484+
# struct PngImage;
485+
# static PNG_BINARY: [u8;0] = [];
486+
487+
impl PngImage {
488+
fn new_from_binary(bin: &[u8]) -> std::result::Result<Self, String> {
489+
Err("Parsing failed".into())
490+
}
491+
}
492+
480493
# /* The attribute macro would prevent the function from being compiled in a doctest.
481-
#[test]
494+
#[googletest::test]
482495
# */
483-
fn numbers_are_greater_than_zero() -> Result<()> {
484-
let mut runner = TestRunner::new(Config::default());
485-
runner.run(&(1..100i32), |v| Ok(verify_that!(v, gt(0))?)).into_test_result()
496+
fn test_png_image_binary() -> googletest::Result<()> {
497+
// Arrange
498+
let png_image = PngImage::new_from_binary(&PNG_BINARY).into_test_result()?;
499+
/* ... */
500+
# Ok(())
501+
}
502+
# test_png_image_binary().unwrap_err();
503+
504+
impl PngImage {
505+
fn new_from_cache(key: u64) -> Option<Self> {
506+
None
507+
}
486508
}
487-
# #[cfg(feature = "proptest")]
488-
# numbers_are_greater_than_zero().unwrap();
509+
510+
# /* The attribute macro would prevent the function from being compiled in a doctest.
511+
#[googletest::test]
512+
# */
513+
fn test_png_from_cache() -> googletest::Result<()> {
514+
// Arrange
515+
let png_image = PngImage::new_from_cache(123).into_test_result()?;
516+
/* ... */
517+
# Ok(())
518+
}
519+
# test_png_from_cache().unwrap_err();
489520
```
490521

491-
Similarly, when the `proptest` feature is enabled, GoogleTest assertion failures
492-
can automatically be converted into Proptest
522+
523+
## Integrations with other crates
524+
525+
GoogleTest Rust includes integrations with the
526+
[Proptest](https://crates.io/crates/proptest) crates to simplify turning
527+
GoogleTest assertion failures into Proptest
493528
[`TestCaseError`](https://docs.rs/proptest/latest/proptest/test_runner/enum.TestCaseError.html)
494-
through the `?` operator as the example above shows.
529+
through the `?` operator.
495530

496531
[`and_log_failure()`]: GoogleTestSupport::and_log_failure
497532
[`into_test_result()`]: IntoTestResult::into_test_result

googletest/src/lib.rs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -236,45 +236,52 @@ impl<T> GoogleTestSupport for std::result::Result<T, TestAssertionFailure> {
236236
///
237237
/// A type can implement this trait to provide an easy way to return immediately
238238
/// from a test in conjunction with the `?` operator. This is useful for
239-
/// [`Result`][std::result::Result] types whose `Result::Err` variant does not
240-
/// implement [`std::error::Error`].
239+
/// [`Option`] and [`Result`][std::result::Result] types whose `Result::Err`
240+
/// variant does not implement [`std::error::Error`].
241241
///
242-
/// There is an implementation of this trait for [`anyhow::Error`] (which does
243-
/// not implement `std::error::Error`) when the `anyhow` feature is enabled.
244-
/// Importing this trait allows one to easily map [`anyhow::Error`] to a test
245-
/// failure:
242+
/// If `Result::Err` implements [`std::error::Error`] you can just use the `?`
243+
/// operator directly.
246244
///
247245
/// ```ignore
248246
/// #[test]
249-
/// fn should_work() -> Result<()> {
247+
/// fn should_work() -> googletest::Result<()> {
250248
/// let value = something_which_can_fail().into_test_result()?;
249+
/// let value = something_which_can_fail_with_option().into_test_result()?;
251250
/// ...
252251
/// }
253252
///
254-
/// fn something_which_can_fail() -> anyhow::Result<...> { ... }
253+
/// fn something_which_can_fail() -> std::result::Result<T, String> { ... }
254+
/// fn something_which_can_fail_with_option() -> Option<T> { ... }
255255
/// ```
256256
pub trait IntoTestResult<T> {
257257
/// Converts this instance into a [`Result`].
258258
///
259-
/// Typically, the `Self` type is itself a [`std::result::Result`]. This
260-
/// method should then map the `Err` variant to a [`TestAssertionFailure`]
261-
/// and leave the `Ok` variant unchanged.
259+
/// Typically, the `Self` type is itself an implementation of the
260+
/// [`std::ops::Try`] trait. This method should then map the `Residual`
261+
/// variant to a [`TestAssertionFailure`] and leave the `Output` variant
262+
/// unchanged.
262263
fn into_test_result(self) -> Result<T>;
263264
}
264265

265-
#[cfg(feature = "anyhow")]
266-
impl<T> IntoTestResult<T> for std::result::Result<T, anyhow::Error> {
266+
impl<T, E: std::fmt::Debug> IntoTestResult<T> for std::result::Result<T, E> {
267267
#[track_caller]
268268
fn into_test_result(self) -> std::result::Result<T, TestAssertionFailure> {
269-
self.map_err(|e| TestAssertionFailure::create(format!("{e}")))
269+
match self {
270+
Ok(t) => Ok(t),
271+
Err(e) => Err(TestAssertionFailure::create(format!("{e:?}"))),
272+
}
270273
}
271274
}
272275

273-
#[cfg(feature = "proptest")]
274-
impl<OkT, CaseT: std::fmt::Debug> IntoTestResult<OkT>
275-
for std::result::Result<OkT, proptest::test_runner::TestError<CaseT>>
276-
{
277-
fn into_test_result(self) -> std::result::Result<OkT, TestAssertionFailure> {
278-
self.map_err(|e| TestAssertionFailure::create(format!("{e}")))
276+
impl<T> IntoTestResult<T> for Option<T> {
277+
#[track_caller]
278+
fn into_test_result(self) -> std::result::Result<T, TestAssertionFailure> {
279+
match self {
280+
Some(t) => Ok(t),
281+
None => Err(TestAssertionFailure::create(format!(
282+
"called `Option::into_test_result()` on a `Option::<{}>::None` value",
283+
std::any::type_name::<T>()
284+
))),
285+
}
279286
}
280287
}

integration_tests/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,16 @@ name = "test_returning_anyhow_error"
365365
path = "src/test_returning_anyhow_error.rs"
366366
test = false
367367

368+
[[bin]]
369+
name = "test_returning_string_error"
370+
path = "src/test_returning_string_error.rs"
371+
test = false
372+
373+
[[bin]]
374+
name = "test_returning_option"
375+
path = "src/test_returning_option.rs"
376+
test = false
377+
368378
[[bin]]
369379
name = "two_expect_pred_failures"
370380
path = "src/two_expect_pred_failures.rs"

integration_tests/src/integration_tests.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,6 +1746,34 @@ mod tests {
17461746
verify_that!(output, contains_substring("Error from Anyhow"))
17471747
}
17481748

1749+
#[test]
1750+
fn test_can_return_option_generated_error() -> Result<()> {
1751+
let output = run_external_process_in_tests_directory("test_returning_option")?;
1752+
1753+
verify_that!(
1754+
output,
1755+
all![
1756+
contains_substring(
1757+
"called `Option::into_test_result()` on a `Option::<()>::None` value"
1758+
),
1759+
contains_substring("test_returning_option.rs:23")
1760+
]
1761+
)
1762+
}
1763+
1764+
#[test]
1765+
fn test_can_return_string_error_generated_error() -> Result<()> {
1766+
let output = run_external_process_in_tests_directory("test_returning_string_error")?;
1767+
1768+
verify_that!(
1769+
output,
1770+
all![
1771+
contains_substring("Error as a String"),
1772+
contains_substring("test_returning_string_error.rs:23")
1773+
]
1774+
)
1775+
}
1776+
17491777
#[::core::prelude::v1::test]
17501778
#[should_panic]
17511779
fn should_panic_when_expect_that_runs_without_attribute_macro() {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
fn main() {}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use googletest::prelude::*;
20+
21+
#[test]
22+
fn should_fail_due_to_none_in_subroutine() -> Result<()> {
23+
returns_option().into_test_result()?;
24+
Ok(())
25+
}
26+
27+
fn returns_option() -> Option<()> {
28+
None
29+
}
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
fn main() {}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use googletest::prelude::*;
20+
21+
#[test]
22+
fn should_fail_due_to_error_in_subroutine() -> Result<()> {
23+
returns_string_error().into_test_result()?;
24+
Ok(())
25+
}
26+
27+
fn returns_string_error() -> std::result::Result<(), String> {
28+
Err("Error as a String".into())
29+
}
30+
}

run_integration_tests.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ INTEGRATION_TEST_BINARIES=(
8989
"simple_assertion_failure"
9090
"simple_assertion_failure_with_assert_that"
9191
"test_returning_anyhow_error"
92+
"test_returning_string_error"
93+
"test_returning_option"
9294
"two_expect_pred_failures"
9395
"two_expect_that_failures"
9496
"two_non_fatal_failures"

0 commit comments

Comments
 (0)