Skip to content

Commit cc72baf

Browse files
hovinenbcopybara-github
authored andcommitted
Add support for easily converting anyhow errors into test failures.
This allows more easily returning early from functions returning `anyhow::Error` in conjunction with the `?` operator. Unfortunately, `anyhow::Error` does not implement `std::error::Error`, so it will not work transparently with the `?` operator. Furthermore, one cannot add a `From<anyhow::Error>` implementation, since theoretically `anyhow::Error` could implement `std::error::Error` in the future, which would then break this crate. The solution here is to introduce a new trait `IntoTestResult` which is then implemented for `anyhow::Error` and which provides a method `into_test_result()` to do the conversion. The anyhow crate is now an optional dependency enabled by a feature. This also adds a CI job which tests the library with no default features enabled. This ensures that the library still compiles and works if the downstream dependency does not enable optional dependencies. PiperOrigin-RevId: 524766870
1 parent 0733d77 commit cc72baf

File tree

6 files changed

+103
-1
lines changed

6 files changed

+103
-1
lines changed

.github/workflows/ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@ jobs:
5757
- name: cargo test --locked
5858
run: cargo test --locked --all-features
5959

60+
test-no-default-features:
61+
runs-on: ubuntu-latest
62+
name: test (no default features) / ubuntu / ${{ matrix.toolchain }}
63+
strategy:
64+
matrix:
65+
toolchain: [stable]
66+
steps:
67+
- uses: actions/checkout@v3
68+
- name: Install ${{ matrix.toolchain }}
69+
uses: dtolnay/rust-toolchain@b44cb146d03e8d870c57ab64b80f04586349ca5d
70+
with:
71+
toolchain: ${{ matrix.toolchain }}
72+
- name: cargo generate-lockfile
73+
if: hashFiles('Cargo.lock') == ''
74+
run: cargo generate-lockfile
75+
- name: cargo test --locked
76+
run: cargo test --locked --no-default-features
77+
6078
integration-test:
6179
runs-on: ubuntu-latest
6280
name: integration-test / ubuntu / ${{ matrix.toolchain }}

googletest/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ doctest = false
3737
googletest_macro = { path = "../googletest_macro", version = "0.4.2" }
3838
num-traits = "0.2.15"
3939
regex = "1.6.0"
40+
anyhow = { version = "1.0.70", optional = true }
4041
indoc = { version = "2", optional = true }
4142
rstest = { version = "0.17.0", optional = true }
4243

@@ -120,6 +121,12 @@ name = "simple_assertion_failure_with_assert_that"
120121
path = "integration_tests/simple_assertion_failure_with_assert_that.rs"
121122
test = false
122123

124+
[[bin]]
125+
name = "test_returning_anyhow_error"
126+
path = "integration_tests/test_returning_anyhow_error.rs"
127+
test = false
128+
required-features = ["anyhow"]
129+
123130
[[bin]]
124131
name = "two_expect_pred_failures"
125132
path = "integration_tests/two_expect_pred_failures.rs"

googletest/integration_tests/integration_tests.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,14 @@ mod tests {
481481
)
482482
}
483483

484+
#[cfg(feature = "anyhow")]
485+
#[google_test]
486+
fn test_can_return_anyhow_generated_error() -> Result<()> {
487+
let output = run_external_process_in_tests_directory("test_returning_anyhow_error")?;
488+
489+
verify_that!(output, contains_substring("Error from Anyhow"))
490+
}
491+
484492
fn run_external_process_in_tests_directory(name: &'static str) -> Result<String> {
485493
let mut command = run_external_process(name);
486494
let std::process::Output { stdout, .. } = command.output()?;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2023 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::{IntoTestResult, Result};
20+
21+
#[test]
22+
fn should_fail_due_to_error_in_subroutine() -> Result<()> {
23+
returns_anyhow_error().into_test_result()?;
24+
Ok(())
25+
}
26+
27+
fn returns_anyhow_error() -> std::result::Result<(), anyhow::Error> {
28+
Err(anyhow::anyhow!("Error from Anyhow"))
29+
}
30+
}

googletest/src/lib.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,41 @@ impl<T> GoogleTestSupport for std::result::Result<T, TestAssertionFailure> {
416416
self
417417
}
418418
}
419+
420+
/// Provides an extension method for converting an arbitrary type into a
421+
/// [`Result`].
422+
///
423+
/// A type can implement this trait to provide an easy way to return immediately
424+
/// from a test in conjunction with the `?` operator. This is useful for
425+
/// [`Result`][std::result::Result] types whose `Result::Err` variant does not
426+
/// implement [`std::error::Error`].
427+
///
428+
/// There is an implementation of this trait for [`anyhow::Error`] (which does
429+
/// not implement `std::error::Error`) when the `anyhow` feature is enabled.
430+
/// Importing this trait allows one to easily map [`anyhow::Error`] to a test
431+
/// failure:
432+
///
433+
/// ```
434+
/// #[test]
435+
/// fn should_work() -> Result<()> {
436+
/// let value = something_which_can_fail().into_test_result()?;
437+
/// ...
438+
/// }
439+
///
440+
/// fn something_which_can_fail() -> anyhow::Result<...> { ... }
441+
/// ```
442+
pub trait IntoTestResult<T> {
443+
/// Converts this instance into a [`Result`].
444+
///
445+
/// Typically, the `Self` type is itself a [`std::result::Result`]. This
446+
/// method should then map the `Err` variant to a [`TestAssertionFailure`]
447+
/// and leave the `Ok` variant unchanged.
448+
fn into_test_result(self) -> Result<T>;
449+
}
450+
451+
#[cfg(feature = "anyhow")]
452+
impl<T> IntoTestResult<T> for std::result::Result<T, anyhow::Error> {
453+
fn into_test_result(self) -> std::result::Result<T, TestAssertionFailure> {
454+
self.map_err(|e| TestAssertionFailure::create(format!("{e}")))
455+
}
456+
}

run_integration_tests.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ INTEGRATION_TEST_BINARIES=(
3838
"non_fatal_failure_in_subroutine"
3939
"simple_assertion_failure"
4040
"simple_assertion_failure_with_assert_that"
41+
"test_returning_anyhow_error"
4142
"two_expect_pred_failures"
4243
"two_expect_that_failures"
4344
"two_non_fatal_failures"
@@ -47,6 +48,6 @@ INTEGRATION_TEST_BINARIES=(
4748

4849
cargo build
4950
for binary in ${INTEGRATION_TEST_BINARIES[@]}; do
50-
cargo rustc -p googletest --bin $binary --features indoc,rstest -- --test
51+
cargo rustc -p googletest --bin $binary --features anyhow,indoc,rstest -- --test
5152
done
5253
./target/debug/integration_tests

0 commit comments

Comments
 (0)