[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