Add support for a custom error type in ErrorOr (#5834)

This doesn't split apart the current error type into one that tracks
location and one that doesn't, although that might be easier to do once
we have this.

Instead, this is primarily intended to support custom error types that
lazily materialize the error message in case that can be avoided by
completely handling the error. For example, many file system operations
are *expected* to produce errors even in the hot path and we don't want
to render `ENOENT` (for example) to a pretty string and instead will
directly query the error to understand and handle it in code.

The type parameter ordering isn't the most obvious, but helpfully allows
us to default the error type in a useful way.

---------

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
This commit is contained in:
Chandler Carruth
2025-07-23 23:15:41 +00:00
committed by GitHub
co-authored by Jon Ross-Perkins
parent b8ca7bf18f
commit e99448eecf
3 changed files with 263 additions and 97 deletions
+181 -78
View File
@@ -6,6 +6,8 @@
#include <gtest/gtest.h>
#include <concepts>
#include "common/error_test_helpers.h"
#include "common/raw_string_ostream.h"
@@ -29,84 +31,6 @@ auto IndirectError() -> Error { return Error("test"); }
TEST(ErrorTest, IndirectError) { EXPECT_EQ(IndirectError().message(), "test"); }
TEST(ErrorTest, ErrorOr) {
ErrorOr<int> err(Error("test"));
EXPECT_THAT(err, IsError("test"));
}
TEST(ErrorTest, ErrorOrValue) { EXPECT_TRUE(ErrorOr<int>(0).ok()); }
auto IndirectErrorOrTest() -> ErrorOr<int> { return Error("test"); }
TEST(ErrorTest, IndirectErrorOr) { EXPECT_FALSE(IndirectErrorOrTest().ok()); }
struct Val {
int val;
};
TEST(ErrorTest, ErrorOrArrowOp) {
ErrorOr<Val> err({1});
EXPECT_EQ(err->val, 1);
}
TEST(ErrorTest, ErrorOrReference) {
Val val = {1};
ErrorOr<Val&> maybe_val(val);
EXPECT_EQ(maybe_val->val, 1);
}
auto IndirectErrorOrSuccessTest() -> ErrorOr<Success> { return Success(); }
TEST(ErrorTest, IndirectErrorOrSuccess) {
EXPECT_TRUE(IndirectErrorOrSuccessTest().ok());
}
TEST(ErrorTest, ReturnIfErrorNoError) {
auto result = []() -> ErrorOr<Success> {
CARBON_RETURN_IF_ERROR(ErrorOr<Success>(Success()));
CARBON_RETURN_IF_ERROR(ErrorOr<Success>(Success()));
return Success();
}();
EXPECT_TRUE(result.ok());
}
TEST(ErrorTest, ReturnIfErrorHasError) {
auto result = []() -> ErrorOr<Success> {
CARBON_RETURN_IF_ERROR(ErrorOr<Success>(Success()));
CARBON_RETURN_IF_ERROR(ErrorOr<Success>(Error("error")));
return Success();
}();
EXPECT_THAT(result, IsError("error"));
}
TEST(ErrorTest, AssignOrReturnNoError) {
auto result = []() -> ErrorOr<int> {
CARBON_ASSIGN_OR_RETURN(int a, ErrorOr<int>(1));
CARBON_ASSIGN_OR_RETURN(const int b, ErrorOr<int>(2));
int c = 0;
CARBON_ASSIGN_OR_RETURN(c, ErrorOr<int>(3));
return a + b + c;
}();
EXPECT_THAT(result, IsSuccess(Eq(6)));
}
TEST(ErrorTest, AssignOrReturnHasDirectError) {
auto result = []() -> ErrorOr<int> {
CARBON_RETURN_IF_ERROR(ErrorOr<int>(Error("error")));
return 0;
}();
EXPECT_THAT(result, IsError("error"));
}
TEST(ErrorTest, AssignOrReturnHasErrorInExpected) {
auto result = []() -> ErrorOr<int> {
CARBON_ASSIGN_OR_RETURN(int a, ErrorOr<int>(Error("error")));
return a;
}();
EXPECT_THAT(result, IsError("error"));
}
TEST(ErrorTest, ErrorBuilderOperatorImplicitCast) {
ErrorOr<int> result = ErrorBuilder() << "msg";
EXPECT_THAT(result, IsError("msg"));
@@ -119,5 +43,184 @@ TEST(ErrorTest, StreamError) {
EXPECT_EQ(result_stream.TakeStr(), "TestFunc: msg");
}
class CustomError : public ErrorBase<CustomError> {
public:
auto Print(llvm::raw_ostream& os) const -> void {
os << "Custom test error!";
}
};
template <typename ErrorT>
class ErrorOrTest : public ::testing::Test {
public:
auto ErrorStr() -> std::string {
if constexpr (std::same_as<ErrorT, Error>) {
return "test error";
} else if constexpr (std::same_as<ErrorT, CustomError>) {
return CustomError().ToString();
} else {
static_assert(false, "Unsupported custom error type!");
}
}
auto MakeError() -> ErrorT {
if constexpr (std::same_as<ErrorT, Error>) {
return Error("test error");
} else if constexpr (std::same_as<ErrorT, CustomError>) {
return CustomError();
} else {
static_assert(false, "Unsupported custom error type!");
}
}
};
using ErrorOrTestParams = ::testing::Types<Error, CustomError>;
TYPED_TEST_SUITE(ErrorOrTest, ErrorOrTestParams);
TYPED_TEST(ErrorOrTest, ErrorOr) {
using TestErrorOr = ErrorOr<int, TypeParam>;
TestErrorOr err(this->MakeError());
EXPECT_THAT(err, IsError(this->ErrorStr()));
}
TYPED_TEST(ErrorOrTest, ErrorOrValue) {
using TestErrorOr = ErrorOr<int, TypeParam>;
EXPECT_TRUE(TestErrorOr(0).ok());
}
template <typename ErrorT, typename Fixture>
auto IndirectErrorOrTest(Fixture& fixture) -> ErrorOr<int, ErrorT> {
return fixture.MakeError();
}
TYPED_TEST(ErrorOrTest, IndirectErrorOr) {
EXPECT_FALSE(IndirectErrorOrTest<TypeParam>(*this).ok());
}
struct Val {
int val;
};
TYPED_TEST(ErrorOrTest, ErrorOrArrowOp) {
using TestErrorOr = ErrorOr<Val, TypeParam>;
TestErrorOr err({1});
EXPECT_EQ(err->val, 1);
}
TYPED_TEST(ErrorOrTest, ErrorOrReference) {
using TestErrorOr = ErrorOr<Val&, TypeParam>;
Val val = {1};
TestErrorOr maybe_val(val);
EXPECT_EQ(maybe_val->val, 1);
}
template <typename ErrorT>
auto IndirectErrorOrSuccessTest() -> ErrorOr<Success, ErrorT> {
return Success();
}
TYPED_TEST(ErrorOrTest, IndirectErrorOrSuccess) {
EXPECT_TRUE(IndirectErrorOrSuccessTest<TypeParam>().ok());
}
TYPED_TEST(ErrorOrTest, ReturnIfErrorNoError) {
using TestErrorOr = ErrorOr<Success, TypeParam>;
auto result = []() -> TestErrorOr {
CARBON_RETURN_IF_ERROR(TestErrorOr(Success()));
CARBON_RETURN_IF_ERROR(TestErrorOr(Success()));
return Success();
}();
EXPECT_TRUE(result.ok());
}
TYPED_TEST(ErrorOrTest, ReturnIfErrorHasError) {
using TestErrorOr = ErrorOr<Success, TypeParam>;
auto result = [this]() -> TestErrorOr {
CARBON_RETURN_IF_ERROR(TestErrorOr(Success()));
CARBON_RETURN_IF_ERROR(TestErrorOr(this->MakeError()));
return Success();
}();
EXPECT_THAT(result, IsError(this->ErrorStr()));
}
TYPED_TEST(ErrorOrTest, AssignOrReturnNoError) {
using TestErrorOr = ErrorOr<int, TypeParam>;
auto result = []() -> TestErrorOr {
CARBON_ASSIGN_OR_RETURN(int a, TestErrorOr(1));
CARBON_ASSIGN_OR_RETURN(const int b, TestErrorOr(2));
int c = 0;
CARBON_ASSIGN_OR_RETURN(c, TestErrorOr(3));
return a + b + c;
}();
EXPECT_THAT(result, IsSuccess(Eq(6)));
}
TYPED_TEST(ErrorOrTest, AssignOrReturnHasDirectError) {
using TestErrorOr = ErrorOr<int, TypeParam>;
auto result = [this]() -> TestErrorOr {
CARBON_RETURN_IF_ERROR(TestErrorOr(this->MakeError()));
return 0;
}();
EXPECT_THAT(result, IsError(this->ErrorStr()));
}
TYPED_TEST(ErrorOrTest, AssignOrReturnHasErrorInExpected) {
using TestErrorOr = ErrorOr<int, TypeParam>;
auto result = [this]() -> TestErrorOr {
CARBON_ASSIGN_OR_RETURN(int a, TestErrorOr(this->MakeError()));
return a;
}();
EXPECT_THAT(result, IsError(this->ErrorStr()));
}
class AnotherCustomError : public ErrorBase<AnotherCustomError> {
public:
auto Print(llvm::raw_ostream& os) const -> void {
os << "Another custom test error!";
}
explicit operator CustomError() { return CustomError(); }
};
TYPED_TEST(ErrorOrTest, AssignOrReturnNoErrorAcrossErrorTypes) {
using TestErrorOr = ErrorOr<int, TypeParam>;
auto result = []() -> ErrorOr<int> {
CARBON_ASSIGN_OR_RETURN(int a, TestErrorOr(1));
CARBON_ASSIGN_OR_RETURN(const int b, []() -> TestErrorOr {
CARBON_ASSIGN_OR_RETURN(int inner, (ErrorOr<int, AnotherCustomError>(2)));
return inner;
}());
int c = 0;
CARBON_ASSIGN_OR_RETURN(c, TestErrorOr(3));
return a + b + c;
}();
EXPECT_THAT(result, IsSuccess(Eq(6)));
}
TYPED_TEST(ErrorOrTest, AssignOrReturnErrorAcrossErrorTypes) {
using TestErrorOr = ErrorOr<int, TypeParam>;
auto result = []() -> ErrorOr<int> {
CARBON_ASSIGN_OR_RETURN(int a, TestErrorOr(1));
CARBON_ASSIGN_OR_RETURN(const int b, []() -> TestErrorOr {
CARBON_ASSIGN_OR_RETURN(
int inner, (ErrorOr<int, AnotherCustomError>(AnotherCustomError())));
return inner;
}());
int c = 0;
CARBON_ASSIGN_OR_RETURN(c, TestErrorOr(3));
return a + b + c;
}();
// When directly using the `Error` type, the explicit custom type above has
// its message preserved. When testing against `CustomError`, that one
// overrides the message.
if constexpr (std::same_as<TypeParam, Error>) {
EXPECT_THAT(result, IsError("Another custom test error!"));
} else {
EXPECT_THAT(result, IsError("Custom test error!"));
}
}
} // namespace
} // namespace Carbon