[llvm] [orc-rt] Add gmock matchers for Error and Expected<T> (PR #225596)
Lang Hames via llvm-commits
llvm-commits at lists.llvm.org
Tue Sep 22 22:12:32 PDT 2026
https://github.com/lhames created https://github.com/llvm/llvm-project/pull/225596
Ports llvm/Testing/Support/Error.h to orc_rt: EXPECT_THAT_ERROR and ASSERT_THAT_ERROR, EXPECT_THAT_EXPECTED and ASSERT_THAT_EXPECTED, and the Succeeded, Failed, Failed<InfoT>, FailedWithMessage and HasValue matchers.
Two departures from the original. An orc_rt Error carries at most one ErrorInfoBase -- there is no joinErrors -- so the holder keeps a single error, FailedWithMessage takes one matcher rather than a variadic pack, and FailedWithMessageArray is dropped. ErrorInfoBase has dynamicRTTIName, so a type mismatch names the type that did turn up rather than reporting only that the expected one was absent.
Header-only, under test/unit with the other test helpers rather than in a shipped testing library. ErrorMatchersTest exercises each matcher against a matching and a non-matching value, the latter through gtest's failure interception.
>From 3c443de49866c33cf005f0ddb16209d42d15e0b5 Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Wed, 23 Sep 2026 14:21:02 +1000
Subject: [PATCH] [orc-rt] Add gmock matchers for Error and Expected<T>
Ports llvm/Testing/Support/Error.h to orc_rt: EXPECT_THAT_ERROR and
ASSERT_THAT_ERROR, EXPECT_THAT_EXPECTED and ASSERT_THAT_EXPECTED, and
the Succeeded, Failed, Failed<InfoT>, FailedWithMessage and HasValue
matchers.
Two departures from the original. An orc_rt Error carries at most one
ErrorInfoBase -- there is no joinErrors -- so the holder keeps a
single error, FailedWithMessage takes one matcher rather than a
variadic pack, and FailedWithMessageArray is dropped. ErrorInfoBase
has dynamicRTTIName, so a type mismatch names the type that did turn
up rather than reporting only that the expected one was absent.
Header-only, under test/unit with the other test helpers rather than
in a shipped testing library. ErrorMatchersTest exercises each matcher
against a matching and a non-matching value, the latter through
gtest's failure interception.
---
orc-rt/test/unit/CMakeLists.txt | 2 +
orc-rt/test/unit/ErrorMatchers.h | 304 +++++++++++++++++++++++++
orc-rt/test/unit/ErrorMatchersTest.cpp | 185 +++++++++++++++
3 files changed, 491 insertions(+)
create mode 100644 orc-rt/test/unit/ErrorMatchers.h
create mode 100644 orc-rt/test/unit/ErrorMatchersTest.cpp
diff --git a/orc-rt/test/unit/CMakeLists.txt b/orc-rt/test/unit/CMakeLists.txt
index 80434ea6bdc220..856fefd370bf60 100644
--- a/orc-rt/test/unit/CMakeLists.txt
+++ b/orc-rt/test/unit/CMakeLists.txt
@@ -38,6 +38,8 @@ endif()
# support-to-bedrock dependency shows up here as a link error rather than going
# unnoticed. BedrockTests links both, Bedrock being a Support client.
add_orc_rt_unittest(SupportTests
+ ErrorMatchersTest.cpp
+
support/AllocActionTest.cpp
support/BitmaskEnumTest.cpp
support/CAPICompileTest.c
diff --git a/orc-rt/test/unit/ErrorMatchers.h b/orc-rt/test/unit/ErrorMatchers.h
new file mode 100644
index 00000000000000..07b7c4e4630b41
--- /dev/null
+++ b/orc-rt/test/unit/ErrorMatchers.h
@@ -0,0 +1,304 @@
+//===- ErrorMatchers.h - gmock matchers for Error/Expected<T> ---*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// gmock matchers for orc_rt::Error and orc_rt::Expected<T>, after
+// llvm/Testing/Support/Error.h.
+//
+// EXPECT_THAT_ERROR(mightFail(), Succeeded());
+// EXPECT_THAT_ERROR(mustFail(), Failed<StringError>());
+// EXPECT_THAT_ERROR(mustFail(), FailedWithMessage(HasSubstr("range")));
+// ASSERT_THAT_EXPECTED(compute(), HasValue(42));
+// ASSERT_THAT_EXPECTED(compute(), HasValue(Gt(40)));
+//
+// An Error argument is consumed, so an lvalue must be moved in:
+//
+// ASSERT_THAT_ERROR(std::move(Err), Succeeded());
+//
+// An Expected<T> argument is not, and keeps its value through a successful
+// match:
+//
+// ASSERT_THAT_EXPECTED(V, Succeeded());
+// use(*V);
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef ORC_RT_UNITTEST_ERRORMATCHERS_H
+#define ORC_RT_UNITTEST_ERRORMATCHERS_H
+
+#include "orc-rt/support/Error.h"
+
+#include "gmock/gmock.h"
+
+#include <cassert>
+#include <memory>
+#include <optional>
+#include <ostream>
+#include <string>
+#include <utility>
+
+namespace orc_rt::test {
+
+namespace detail {
+
+/// An Error consumed up front, which matchers then inspect in its place.
+///
+/// Holds a single ErrorInfoBase: orc_rt has no joinErrors, so an Error carries
+/// at most one.
+class ErrorHolder {
+public:
+ ErrorHolder() = default;
+ explicit ErrorHolder(std::shared_ptr<ErrorInfoBase> Info)
+ : Info(std::move(Info)) {}
+
+ bool success() const noexcept { return Info == nullptr; }
+
+ /// The contained error, which must be present. Non-const through a const
+ /// holder, as for the pointer this stands in for.
+ ErrorInfoBase &info() const noexcept {
+ assert(Info && "No error to inspect");
+ return *Info;
+ }
+
+private:
+ std::shared_ptr<ErrorInfoBase> Info;
+};
+
+/// An Expected<T> consumed the same way, keeping a reference to the original so
+/// a matcher can reach the value.
+template <typename T> class ExpectedHolder : public ErrorHolder {
+public:
+ ExpectedHolder(ErrorHolder Err, Expected<T> &Exp)
+ : ErrorHolder(std::move(Err)), Exp(Exp) {}
+
+ Expected<T> &Exp;
+};
+
+/// Renders a holder for the "Actual:" line of a failure. Found by gmock through
+/// ADL.
+inline void PrintTo(const ErrorHolder &Err, std::ostream *Out) {
+ if (Err.success()) {
+ *Out << "succeeded";
+ return;
+ }
+ *Out << "failed with " << Err.info().dynamicRTTIName() << ": "
+ << Err.info().toString();
+}
+
+template <typename T>
+void PrintTo(const ExpectedHolder<T> &Item, std::ostream *Out) {
+ if (!Item.success()) {
+ PrintTo(static_cast<const ErrorHolder &>(Item), Out);
+ return;
+ }
+ *Out << "succeeded with value " << ::testing::PrintToString(*Item.Exp);
+}
+
+inline ErrorHolder takeError(Error Err) {
+ std::shared_ptr<ErrorInfoBase> Info;
+ handleAllErrors(std::move(Err), [&Info](std::unique_ptr<ErrorInfoBase> I) {
+ Info = std::move(I);
+ });
+ return ErrorHolder(std::move(Info));
+}
+
+template <typename T> ExpectedHolder<T> takeExpected(Expected<T> &Exp) {
+ return ExpectedHolder<T>(takeError(Exp.takeError()), Exp);
+}
+
+template <typename T> ExpectedHolder<T> takeExpected(Expected<T> &&Exp) {
+ return takeExpected(Exp);
+}
+
+/// Matches a failure Error whose contained error is of type InfoT, and which
+/// additionally satisfies Matcher if one is given.
+template <typename InfoT>
+class ErrorMatchesMono
+ : public ::testing::MatcherInterface<const ErrorHolder &> {
+public:
+ explicit ErrorMatchesMono(std::optional<::testing::Matcher<InfoT &>> Matcher)
+ : Matcher(std::move(Matcher)) {}
+
+ bool
+ MatchAndExplain(const ErrorHolder &Holder,
+ ::testing::MatchResultListener *Listener) const override {
+ if (Holder.success()) {
+ *Listener << "succeeded";
+ return false;
+ }
+
+ ErrorInfoBase &Info = Holder.info();
+ if (!Info.isA<InfoT>()) {
+ *Listener << "failed with " << Info.dynamicRTTIName() << ": "
+ << Info.toString();
+ return false;
+ }
+
+ if (!Matcher)
+ return true;
+
+ return Matcher->MatchAndExplain(static_cast<InfoT &>(Info), Listener);
+ }
+
+ void DescribeTo(std::ostream *OS) const override {
+ *OS << "failed with " << InfoT::RTTIName;
+ if (Matcher) {
+ *OS << " and the error ";
+ Matcher->DescribeTo(OS);
+ }
+ }
+
+ void DescribeNegationTo(std::ostream *OS) const override {
+ *OS << "succeeded, or failed with something other than " << InfoT::RTTIName;
+ if (Matcher) {
+ *OS << ", or the error ";
+ Matcher->DescribeNegationTo(OS);
+ }
+ }
+
+private:
+ std::optional<::testing::Matcher<InfoT &>> Matcher;
+};
+
+/// Matches a failure Error whose message satisfies Matcher.
+class ErrorMessageMatches
+ : public ::testing::MatcherInterface<const ErrorHolder &> {
+public:
+ explicit ErrorMessageMatches(::testing::Matcher<std::string> Matcher)
+ : Matcher(std::move(Matcher)) {}
+
+ bool
+ MatchAndExplain(const ErrorHolder &Holder,
+ ::testing::MatchResultListener *Listener) const override {
+ if (Holder.success()) {
+ *Listener << "succeeded";
+ return false;
+ }
+ return Matcher.MatchAndExplain(Holder.info().toString(), Listener);
+ }
+
+ void DescribeTo(std::ostream *OS) const override {
+ *OS << "failed with an error whose message ";
+ Matcher.DescribeTo(OS);
+ }
+
+ void DescribeNegationTo(std::ostream *OS) const override {
+ *OS << "succeeded, or failed with an error whose message ";
+ Matcher.DescribeNegationTo(OS);
+ }
+
+private:
+ ::testing::Matcher<std::string> Matcher;
+};
+
+/// Matches a success Expected<T> whose value satisfies Matcher.
+template <typename T>
+class ValueMatchesMono
+ : public ::testing::MatcherInterface<const ExpectedHolder<T> &> {
+public:
+ explicit ValueMatchesMono(const ::testing::Matcher<T> &Matcher)
+ : Matcher(Matcher) {}
+
+ bool
+ MatchAndExplain(const ExpectedHolder<T> &Holder,
+ ::testing::MatchResultListener *Listener) const override {
+ if (!Holder.success()) {
+ *Listener << "failed with " << Holder.info().dynamicRTTIName() << ": "
+ << Holder.info().toString();
+ return false;
+ }
+
+ if (Matcher.MatchAndExplain(*Holder.Exp, Listener))
+ return true;
+
+ if (Listener->IsInterested()) {
+ *Listener << "(";
+ Matcher.DescribeNegationTo(Listener->stream());
+ *Listener << ")";
+ }
+ return false;
+ }
+
+ void DescribeTo(std::ostream *OS) const override {
+ *OS << "succeeded with value (";
+ Matcher.DescribeTo(OS);
+ *OS << ")";
+ }
+
+ void DescribeNegationTo(std::ostream *OS) const override {
+ *OS << "did not succeed, or value (";
+ Matcher.DescribeNegationTo(OS);
+ *OS << ")";
+ }
+
+private:
+ ::testing::Matcher<T> Matcher;
+};
+
+/// Defers naming T until the ExpectedHolder<T> being matched is known, so
+/// HasValue(42) needs no explicit value type.
+template <typename M> class ValueMatchesPoly {
+public:
+ explicit ValueMatchesPoly(const M &Matcher) : Matcher(Matcher) {}
+
+ template <typename T>
+ operator ::testing::Matcher<const ExpectedHolder<T> &>() const {
+ return ::testing::MakeMatcher(
+ new ValueMatchesMono<T>(::testing::SafeMatcherCast<T>(Matcher)));
+ }
+
+private:
+ M Matcher;
+};
+
+} // namespace detail
+
+#define EXPECT_THAT_ERROR(Err, Matcher) \
+ EXPECT_THAT(::orc_rt::test::detail::takeError(Err), Matcher)
+#define ASSERT_THAT_ERROR(Err, Matcher) \
+ ASSERT_THAT(::orc_rt::test::detail::takeError(Err), Matcher)
+
+#define EXPECT_THAT_EXPECTED(Val, Matcher) \
+ EXPECT_THAT(::orc_rt::test::detail::takeExpected(Val), Matcher)
+#define ASSERT_THAT_EXPECTED(Val, Matcher) \
+ ASSERT_THAT(::orc_rt::test::detail::takeExpected(Val), Matcher)
+
+/// Matches an Error or Expected<T> holding no error.
+MATCHER(Succeeded, "") { return arg.success(); }
+
+/// Matches an Error or Expected<T> holding an error, of any type.
+MATCHER(Failed, "") { return !arg.success(); }
+
+/// Matches an Error holding an error of type InfoT.
+template <typename InfoT>
+::testing::Matcher<const detail::ErrorHolder &> Failed() {
+ return ::testing::MakeMatcher(
+ new detail::ErrorMatchesMono<InfoT>(std::nullopt));
+}
+
+/// Matches an Error holding an error of type InfoT that satisfies Matcher.
+template <typename InfoT, typename M>
+::testing::Matcher<const detail::ErrorHolder &> Failed(M Matcher) {
+ return ::testing::MakeMatcher(new detail::ErrorMatchesMono<InfoT>(
+ ::testing::SafeMatcherCast<InfoT &>(Matcher)));
+}
+
+/// Matches an Error holding an error whose message satisfies Matcher. A bare
+/// string means exact equality.
+template <typename M>
+::testing::Matcher<const detail::ErrorHolder &> FailedWithMessage(M Matcher) {
+ return ::testing::MakeMatcher(new detail::ErrorMessageMatches(Matcher));
+}
+
+/// Matches an Expected<T> holding a value that satisfies Matcher.
+template <typename M> detail::ValueMatchesPoly<M> HasValue(M Matcher) {
+ return detail::ValueMatchesPoly<M>(Matcher);
+}
+
+} // namespace orc_rt::test
+
+#endif // ORC_RT_UNITTEST_ERRORMATCHERS_H
diff --git a/orc-rt/test/unit/ErrorMatchersTest.cpp b/orc-rt/test/unit/ErrorMatchersTest.cpp
new file mode 100644
index 00000000000000..fc09340a1591dc
--- /dev/null
+++ b/orc-rt/test/unit/ErrorMatchersTest.cpp
@@ -0,0 +1,185 @@
+//===- ErrorMatchersTest.cpp ----------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Tests for the Error / Expected<T> matchers in ErrorMatchers.h.
+//
+// Each matcher is exercised on a value it should match and on one it should
+// not. The latter run inside gtest's own failure interception (gtest-spi.h) and
+// assert on the reported text, since a matcher's diagnostic is most of what it
+// is for.
+//
+//===----------------------------------------------------------------------===//
+
+#include "ErrorMatchers.h"
+
+#include "gtest/gtest-spi.h"
+
+using namespace orc_rt;
+using namespace orc_rt::test;
+
+using ::testing::Gt;
+using ::testing::HasSubstr;
+using ::testing::Property;
+
+namespace {
+
+class CustomError : public ErrorExtends<CustomError, ErrorInfoBase> {
+public:
+ static constexpr const char *RTTIName = "::CustomError";
+
+ CustomError(int Info) : Info(Info) {}
+
+ std::string toString() const noexcept override {
+ return "CustomError (" + std::to_string(Info) + ")";
+ }
+
+ int getInfo() const { return Info; }
+
+protected:
+ int Info;
+};
+
+class OtherError : public ErrorExtends<OtherError, ErrorInfoBase> {
+public:
+ static constexpr const char *RTTIName = "::OtherError";
+
+ std::string toString() const noexcept override { return "OtherError"; }
+};
+
+TEST(ErrorMatchersTest, SucceededMatchesSuccess) {
+ EXPECT_THAT_ERROR(Error::success(), Succeeded());
+}
+
+TEST(ErrorMatchersTest, SucceededRejectsFailure) {
+ EXPECT_NONFATAL_FAILURE(
+ EXPECT_THAT_ERROR(make_error<CustomError>(7), Succeeded()), "7");
+}
+
+TEST(ErrorMatchersTest, FailedMatchesFailure) {
+ EXPECT_THAT_ERROR(make_error<CustomError>(7), Failed());
+}
+
+TEST(ErrorMatchersTest, FailedRejectsSuccess) {
+ EXPECT_NONFATAL_FAILURE(EXPECT_THAT_ERROR(Error::success(), Failed()),
+ "Actual");
+}
+
+TEST(ErrorMatchersTest, LValueErrorIsHandedOverExplicitly) {
+ Error E = make_error<CustomError>(7);
+ EXPECT_THAT_ERROR(std::move(E), Failed());
+}
+
+TEST(ErrorMatchersTest, FailedOfTypeMatchesThatType) {
+ EXPECT_THAT_ERROR(make_error<CustomError>(7), Failed<CustomError>());
+}
+
+TEST(ErrorMatchersTest, FailedOfTypeNamesTheTypeItFound) {
+ // The point of the type matcher: on a mismatch it says what did turn up,
+ // rather than only that the expectation was not met.
+ EXPECT_NONFATAL_FAILURE(
+ EXPECT_THAT_ERROR(make_error<OtherError>(), Failed<CustomError>()),
+ "failed with ::OtherError: OtherError");
+}
+
+TEST(ErrorMatchersTest, FailedOfTypeRejectsSuccess) {
+ EXPECT_NONFATAL_FAILURE(
+ EXPECT_THAT_ERROR(Error::success(), Failed<CustomError>()), "succeeded");
+}
+
+TEST(ErrorMatchersTest, FailedOfTypeWithInnerMatcher) {
+ EXPECT_THAT_ERROR(
+ make_error<CustomError>(7),
+ Failed<CustomError>(Property("getInfo", &CustomError::getInfo, 7)));
+}
+
+TEST(ErrorMatchersTest, FailedOfTypeRejectsFailingInnerMatcher) {
+ EXPECT_NONFATAL_FAILURE(
+ EXPECT_THAT_ERROR(
+ make_error<CustomError>(7),
+ Failed<CustomError>(Property("getInfo", &CustomError::getInfo, 8))),
+ "getInfo");
+}
+
+TEST(ErrorMatchersTest, FailedWithMessageMatchesExactMessage) {
+ EXPECT_THAT_ERROR(make_error<CustomError>(7),
+ FailedWithMessage("CustomError (7)"));
+}
+
+TEST(ErrorMatchersTest, FailedWithMessageTakesAMatcher) {
+ // The reason to prefer a matcher: a message that embeds errno text, a path,
+ // or an address cannot be compared for equality across platforms.
+ EXPECT_THAT_ERROR(make_error<CustomError>(7),
+ FailedWithMessage(HasSubstr("(7)")));
+}
+
+TEST(ErrorMatchersTest, FailedWithMessageRejectsOtherMessage) {
+ EXPECT_NONFATAL_FAILURE(
+ EXPECT_THAT_ERROR(make_error<CustomError>(7),
+ FailedWithMessage("CustomError (8)")),
+ "CustomError (7)");
+}
+
+TEST(ErrorMatchersTest, FailedWithMessageRejectsSuccess) {
+ EXPECT_NONFATAL_FAILURE(
+ EXPECT_THAT_ERROR(Error::success(), FailedWithMessage("anything")),
+ "succeeded");
+}
+
+TEST(ErrorMatchersTest, AssertThatErrorIsFatal) {
+ EXPECT_FATAL_FAILURE(ASSERT_THAT_ERROR(Error::success(), Failed()), "Actual");
+}
+
+TEST(ExpectedMatchersTest, SucceededMatchesValue) {
+ EXPECT_THAT_EXPECTED(Expected<int>(42), Succeeded());
+}
+
+TEST(ExpectedMatchersTest, FailedMatchesFailure) {
+ Expected<int> V = make_error<CustomError>(7);
+ EXPECT_THAT_EXPECTED(V, Failed());
+}
+
+TEST(ExpectedMatchersTest, HasValueMatchesValue) {
+ EXPECT_THAT_EXPECTED(Expected<int>(42), HasValue(42));
+}
+
+TEST(ExpectedMatchersTest, HasValueTakesAMatcher) {
+ EXPECT_THAT_EXPECTED(Expected<int>(42), HasValue(Gt(40)));
+}
+
+TEST(ExpectedMatchersTest, HasValueRejectsOtherValue) {
+ EXPECT_NONFATAL_FAILURE(EXPECT_THAT_EXPECTED(Expected<int>(42), HasValue(43)),
+ "42");
+}
+
+TEST(ExpectedMatchersTest, HasValueRejectsFailureAndNamesTheError) {
+ EXPECT_NONFATAL_FAILURE(
+ EXPECT_THAT_EXPECTED(Expected<int>(make_error<CustomError>(7)),
+ HasValue(42)),
+ "failed with ::CustomError: CustomError (7)");
+}
+
+TEST(ExpectedMatchersTest, FailedRejectsValue) {
+ EXPECT_NONFATAL_FAILURE(EXPECT_THAT_EXPECTED(Expected<int>(42), Failed()),
+ "Actual");
+}
+
+TEST(ExpectedMatchersTest, ValueSurvivesASucceededMatch) {
+ Expected<int> V(42);
+ ASSERT_THAT_EXPECTED(V, Succeeded());
+ EXPECT_EQ(*V, 42);
+}
+
+TEST(ExpectedMatchersTest, FailureLeavesExpectedDestructible) {
+ // The macro takes the Expected's error, which leaves it in a state where
+ // re-checking it would arm the abort in its destructor. Nothing in the
+ // matchers does that, so this scope exits cleanly rather than aborting.
+ Expected<int> V = make_error<CustomError>(7);
+ EXPECT_THAT_EXPECTED(V, Failed<CustomError>());
+}
+
+} // namespace
More information about the llvm-commits
mailing list