Files
carbon-lang/common/error_test_helpers.h
T
Chandler Carruth eeea9dc9e5 Make minor improvements to ErrorOr based on usage (#5857)
When using this with filesystem errors, a few issues came up that I'm
fixing here. They're small enough and near enough in code that it didn't
seem worth splitting part.

- It's nice to forward declare custom error types and an API using them
and then define both later. That doesn't work with `requires` but works
fine with `static_assert`, so go back to that pattern here. A test is
added that checks this pattern compiles.

- The `operator*` didn't support moving out of `ErrorOr`, which is
especially important when writing code that is happy with just
`CARBON_CHECK`-failing on any errors. For example, we have a lot of
filesystem code in tests that is made *much* more concise by just using
`*` on a function return and letting the built-in checking ensure no
errors were present. But when the value is move-only, this requires
special overloading. Add that and add a test with a move-only value.

- There wasn't an idiomatic way to do something like `operator*` for
`ErrorOr<Success, ...>`. This PR factors out the checking for `ok()`
into a `Check()` method that can be used to make code more readable that
is intentionally just verifying no error. Also makes the result of
`operator*` `[[nodiscard]]` to improve error messages and help void
accidental bugs.

- The `IsError` and `IsSuccess` test helpers required printable values
which isn't always realistic. Teach the printing logic to be conditional
on some indication of a printable value and gracefully fall back to a
generic string otherwise for testing output.

- The use of the `listener` in `IsError` and `IsSuccess` assumed a
non-null stream. Instead, streaming should go directly to the `listener`
as it is configured to only actually do the output when a stream is
installed. When a stream isn't installed, the previous code would crash
if the `MatchAndExplain` method ended up called without an 'interesting'
stream attached to the listener.

- When doing a `CARBON_CHECK` that there isn't an error, print the error
out as the check failure message. Without this, all the nice error
message work doesn't end up helping the debugging of test code that hits
these errors, etc.
2025-07-28 17:42:36 +00:00

120 lines
3.5 KiB
C++

// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#ifndef CARBON_COMMON_ERROR_TEST_HELPERS_H_
#define CARBON_COMMON_ERROR_TEST_HELPERS_H_
#include <gmock/gmock.h>
#include "common/error.h"
namespace Carbon::Testing {
// Matches the message for an error state of `ErrorOr<T>`. For example:
// EXPECT_THAT(my_result, IsError(StrEq("error message")));
class IsError {
public:
// NOLINTNEXTLINE(readability-identifier-naming)
using is_gtest_matcher = void;
explicit IsError(::testing::Matcher<std::string> matcher)
: matcher_(std::move(matcher)) {}
template <typename T, typename ErrorT>
auto MatchAndExplain(const ErrorOr<T, ErrorT>& result,
::testing::MatchResultListener* listener) const -> bool {
if (result.ok()) {
*listener << "is a success";
return false;
} else {
RawStringOstream os;
os << result.error();
return matcher_.MatchAndExplain(os.TakeStr(), listener);
}
}
auto DescribeTo(std::ostream* os) const -> void {
*os << "is an error and matches ";
matcher_.DescribeTo(os);
}
auto DescribeNegationTo(std::ostream* os) const -> void {
*os << "is a success or does not match ";
matcher_.DescribeTo(os);
}
private:
::testing::Matcher<std::string> matcher_;
};
// Matches the value for a non-error state of `ErrorOr<T>`. For example:
// EXPECT_THAT(my_result, IsSuccess(Eq(3)));
template <typename InnerMatcher>
class IsSuccessMatcher {
public:
// NOLINTNEXTLINE(readability-identifier-naming)
using is_gtest_matcher = void;
explicit IsSuccessMatcher(InnerMatcher matcher)
: matcher_(std::move(matcher)) {}
template <typename T, typename ErrorT>
auto MatchAndExplain(const ErrorOr<T, ErrorT>& result,
::testing::MatchResultListener* listener) const -> bool {
if (result.ok()) {
return ::testing::Matcher<T>(matcher_).MatchAndExplain(*result, listener);
} else {
*listener << "is an error with `" << result.error() << "`";
return false;
}
}
auto DescribeTo(std::ostream* os) const -> void {
*os << "is a success and matches ";
matcher_.DescribeTo(os);
}
auto DescribeNegationTo(std::ostream* os) const -> void {
*os << "is an error or does not match ";
matcher_.DescribeTo(os);
}
private:
InnerMatcher matcher_;
};
// Wraps `IsSuccessMatcher` for the inner matcher deduction.
template <typename InnerMatcher>
auto IsSuccess(InnerMatcher matcher) -> IsSuccessMatcher<InnerMatcher> {
return IsSuccessMatcher<InnerMatcher>(matcher);
}
} // namespace Carbon::Testing
namespace Carbon {
// Supports printing `ErrorOr<T>` to `std::ostream` in tests.
template <typename T, typename ErrorT>
auto operator<<(std::ostream& out, const ErrorOr<T, ErrorT>& error_or)
-> std::ostream& {
if (error_or.ok()) {
// Try and print the value, but only if we can find a viable `<<` overload
// for the value type. This should ensure that the `formatv` below can
// compile cleanly, and avoid erroring when using matchers on `ErrorOr` with
// unprintable value types.
if constexpr (requires(const T& value) { out << value; }) {
out << llvm::formatv("ErrorOr{{.value = `{0}`}}", *error_or);
} else {
out << "ErrorOr{{.value = `<unknown>`}}";
}
} else {
out << llvm::formatv("ErrorOr{{.error = \"{0}\"}}", error_or.error());
}
return out;
}
} // namespace Carbon
#endif // CARBON_COMMON_ERROR_TEST_HELPERS_H_