summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBilly Donahue <billy.donahue@mongodb.com>2020-10-24 17:48:22 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-08-12 16:17:07 +0000
commitda9ccccbda9538db40394f37d6f7446f33b39c0a (patch)
treef7c7f09c1204347802520a1f9a6b835aa71c94c4
parent98a21127ae4957594276a35228a95d8d79d6ad25 (diff)
downloadmongo-da9ccccbda9538db40394f37d6f7446f33b39c0a.tar.gz
SERVER-58069 ASSERT_THAT: a matcher-based assert for unit tests
-rw-r--r--src/mongo/platform/atomic_word_test.cpp12
-rw-r--r--src/mongo/unittest/SConscript3
-rw-r--r--src/mongo/unittest/assert_that.h62
-rw-r--r--src/mongo/unittest/assert_that_test.cpp261
-rw-r--r--src/mongo/unittest/matcher.cpp64
-rw-r--r--src/mongo/unittest/matcher.h651
-rw-r--r--src/mongo/unittest/matcher_core.cpp47
-rw-r--r--src/mongo/unittest/matcher_core.h269
-rw-r--r--src/mongo/unittest/unittest.cpp13
-rw-r--r--src/mongo/unittest/unittest.h33
-rw-r--r--src/mongo/unittest/unittest_test.cpp9
-rw-r--r--src/mongo/util/concepts_test.cpp6
12 files changed, 1393 insertions, 37 deletions
diff --git a/src/mongo/platform/atomic_word_test.cpp b/src/mongo/platform/atomic_word_test.cpp
index 7f55057a7c6..6b30c70eca0 100644
--- a/src/mongo/platform/atomic_word_test.cpp
+++ b/src/mongo/platform/atomic_word_test.cpp
@@ -98,13 +98,13 @@ void testAtomicWordBitOperations() {
ASSERT_EQUALS(WordType(highBit | 0xFF00ull), w.load());
}
-ASSERT_DOES_NOT_COMPILE(typename T = int, AtomicWord<T>().fetchAndBitAnd(0));
-ASSERT_DOES_NOT_COMPILE(typename T = int, AtomicWord<T>().fetchAndBitOr(0));
-ASSERT_DOES_NOT_COMPILE(typename T = int, AtomicWord<T>().fetchAndBitXor(0));
+ASSERT_DOES_NOT_COMPILE(CharFetchAndBitAnd, typename T = int, AtomicWord<T>().fetchAndBitAnd(0));
+ASSERT_DOES_NOT_COMPILE(CharFetchAndBitOr, typename T = int, AtomicWord<T>().fetchAndBitOr(0));
+ASSERT_DOES_NOT_COMPILE(CharFetchAndBitXor, typename T = int, AtomicWord<T>().fetchAndBitXor(0));
-ASSERT_DOES_NOT_COMPILE(typename T = char, AtomicWord<T>().fetchAndBitAnd(0));
-ASSERT_DOES_NOT_COMPILE(typename T = char, AtomicWord<T>().fetchAndBitOr(0));
-ASSERT_DOES_NOT_COMPILE(typename T = char, AtomicWord<T>().fetchAndBitXor(0));
+ASSERT_DOES_NOT_COMPILE(IntFetchAndBitAnd, typename T = char, AtomicWord<T>().fetchAndBitAnd(0));
+ASSERT_DOES_NOT_COMPILE(IntFetchAndBitOr, typename T = char, AtomicWord<T>().fetchAndBitOr(0));
+ASSERT_DOES_NOT_COMPILE(IntFetchAndBitXor, typename T = char, AtomicWord<T>().fetchAndBitXor(0));
enum TestEnum { E0, E1, E2, E3 };
diff --git a/src/mongo/unittest/SConscript b/src/mongo/unittest/SConscript
index 680eb3ae258..defb9b2afa7 100644
--- a/src/mongo/unittest/SConscript
+++ b/src/mongo/unittest/SConscript
@@ -10,6 +10,8 @@ env.Library(
'barrier.cpp',
'bson_test_util.cpp',
'death_test.cpp',
+ 'matcher.cpp',
+ 'matcher_core.cpp',
'temp_dir.cpp',
'unittest_helpers.cpp',
'unittest.cpp',
@@ -100,6 +102,7 @@ env.CppUnitTest(
'fixture_test.cpp',
'temp_dir_test.cpp',
'thread_assertion_monitor_test.cpp',
+ 'assert_that_test.cpp',
],
)
diff --git a/src/mongo/unittest/assert_that.h b/src/mongo/unittest/assert_that.h
new file mode 100644
index 00000000000..ab1d3cbf75c
--- /dev/null
+++ b/src/mongo/unittest/assert_that.h
@@ -0,0 +1,62 @@
+/**
+ * Copyright (C) 2021-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#pragma once
+
+#include <tuple>
+
+#include "mongo/unittest/matcher.h"
+#include "mongo/unittest/matcher_core.h"
+#include "mongo/unittest/unittest.h"
+
+/**
+ * unittest-style ASSERT that an `expr` successfully matches a `matcher`.
+ *
+ * Like most other ASSERT macros, can accept further information via a trailing
+ * stream `<<` operation.
+ *
+ * Example (assumed to be enclosed in namespace mongo):
+ *
+ * ASSERT_THAT(std::sqrt(4.0), unittest::match::Eq(2.0));
+ *
+ * namespace m = unittest::match;
+ * ASSERT_THAT(std::sqrt(4.0), m::Eq(2.0)) << "std::sqrt must be reasonable";
+ *
+ * // Combine several matchers on the same value into nice one-liners.
+ * using namespace unittest::match;
+ * ASSERT_THAT(getGreeting(),
+ * AllOf(ContainsRegex("^Hello, "),
+ * Not(ContainsRegex("bye"))));
+ *
+ * See https://google.github.io/googletest/reference/matchers.html for inspiration.
+ */
+#define ASSERT_THAT(expr, matcher) \
+ if (auto args_ = ::mongo::unittest::match::detail::MatchAssertion{expr, matcher, #expr}) { \
+ } else \
+ FAIL(args_.failMsg())
diff --git a/src/mongo/unittest/assert_that_test.cpp b/src/mongo/unittest/assert_that_test.cpp
new file mode 100644
index 00000000000..01733f54b2f
--- /dev/null
+++ b/src/mongo/unittest/assert_that_test.cpp
@@ -0,0 +1,261 @@
+/**
+ * Copyright (C) 2018-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
+#include "mongo/platform/basic.h"
+
+#include <functional>
+#include <list>
+#include <string>
+#include <vector>
+
+#include "mongo/base/status.h"
+#include "mongo/logv2/log.h"
+#include "mongo/unittest/assert_that.h"
+#include "mongo/unittest/unittest.h"
+#include "mongo/util/assert_util.h"
+
+
+namespace mongo::unittest::match {
+namespace {
+
+#define GET_FAILURE_STRING(v, m) \
+ [&] { \
+ try { \
+ ASSERT_THAT(v, m); \
+ return std::string{}; \
+ } catch (const TestAssertionFailureException& ex) { \
+ return ex.what(); \
+ } \
+ }()
+
+TEST(AssertThat, AssertThat) {
+ ASSERT_THAT(123, Eq(123));
+ ASSERT_THAT(0, Not(Eq(123)));
+ ASSERT_THAT(std::string("hi"), Eq(std::string("hi")));
+ ASSERT_THAT("hi", Not(Eq(std::string("Hi"))));
+ ASSERT_THAT(123., Eq(123));
+ int x = 456;
+ auto failStr = GET_FAILURE_STRING(x + 1, Eq(123));
+ ASSERT_EQ(failStr, "value: x + 1, actual: 457, expected: Eq(123)");
+}
+
+TEST(AssertThat, MatcherDescribe) {
+ ASSERT_EQ(Eq(123).describe(), "Eq(123)");
+ ASSERT_EQ(Not(Eq(123)).describe(), "Not(Eq(123))");
+}
+
+TEST(AssertThat, AllOf) {
+ {
+ auto m = AllOf(Eq(123), Not(Eq(0)));
+ ASSERT_TRUE(m.match(123));
+ ASSERT_EQ(m.describe(), "AllOf(Eq(123), Not(Eq(0)))");
+ ASSERT_THAT(123, m);
+ }
+ {
+ auto m = AllOf(Eq(1), Eq(2), Eq(3));
+ ASSERT_FALSE(m.match(2));
+ ASSERT_EQ(m.describe(), "AllOf(Eq(1), Eq(2), Eq(3))");
+ ASSERT_EQ(m.match(2).message(), "failed: [0:(Eq(1)), 2:(Eq(3))]");
+ }
+}
+
+TEST(AssertThat, AnyOf) {
+ auto m = AnyOf(Eq(123), Not(Eq(4)));
+ ASSERT_TRUE(m.match(123));
+ ASSERT_EQ(m.describe(), "AnyOf(Eq(123), Not(Eq(4)))");
+ ASSERT_THAT(123, m);
+ ASSERT_FALSE(m.match(4));
+ ASSERT_EQ(m.match(4).message(), "failed: [0:(Eq(123)), 1:(Not(Eq(4)))]");
+}
+
+// Googlemock has `IsNull`, a relic of the pre-`nullptr` era. We do not.
+TEST(AssertThat, IsNull) {
+ int v1 = 123;
+ int* np = nullptr;
+ auto m = Eq(nullptr); // Equivalent to IsNull()
+ ASSERT_EQ(m.describe(), "Eq(nullptr)"); // Make sure `nullptr` stringifies.
+ ASSERT_TRUE(m.match(np));
+ ASSERT_FALSE(m.match(&v1));
+ ASSERT_THAT(np, m);
+ ASSERT_EQ(m.match(&v1).message(), "");
+ ASSERT_EQ(m.match(np).message(), "");
+}
+
+TEST(AssertThat, Pointee) {
+ int v1 = 123;
+ int v2 = 4;
+ auto m = Pointee(Eq(123));
+ ASSERT_EQ(m.describe(), "Pointee(Eq(123))");
+ ASSERT_TRUE(m.match(&v1));
+ ASSERT_FALSE(m.match(&v2));
+ ASSERT_THAT(&v1, m);
+ ASSERT_EQ(m.match(&v2).message(), "");
+ ASSERT_EQ(m.match((int*)nullptr).message(), "empty pointer");
+}
+
+TEST(AssertThat, ContainsRegex) {
+ auto m = ContainsRegex("aa*\\d*");
+ ASSERT_EQ(m.describe(), "ContainsRegex(\"aa*\\d*\")");
+ ASSERT_TRUE(m.match("aaa123"));
+ ASSERT_FALSE(m.match("zzz"));
+ ASSERT_THAT("aaa123", m);
+ ASSERT_EQ(m.match("zzz").message(), "");
+ ASSERT_THAT("a", Not(ContainsRegex("ab*c")));
+ ASSERT_THAT("ac", ContainsRegex("ab*c"));
+ ASSERT_THAT("abc", ContainsRegex("ab*c"));
+ ASSERT_THAT("abbc", ContainsRegex("ab*c"));
+}
+
+TEST(AssertThat, ContainsRegexIsPartialMatch) {
+ ASSERT_THAT("a", ContainsRegex("a"));
+ ASSERT_THAT("za", ContainsRegex("a"));
+ ASSERT_THAT("az", ContainsRegex("a"));
+ ASSERT_THAT("zaz", ContainsRegex("a"));
+ // Check ^ and $ anchors
+ ASSERT_THAT("az", ContainsRegex("^a"));
+ ASSERT_THAT("za", Not(ContainsRegex("^a")));
+ ASSERT_THAT("za", ContainsRegex("a$"));
+ ASSERT_THAT("az", Not(ContainsRegex("a$")));
+}
+
+TEST(AssertThat, ElementsAre) {
+ auto m = ElementsAre(Eq(111), Eq(222), Eq(333));
+ ASSERT_EQ(m.describe(), "ElementsAre(Eq(111), Eq(222), Eq(333))");
+ ASSERT_TRUE(m.match(std::vector<int>{111, 222, 333}));
+ ASSERT_FALSE(m.match(std::vector<int>{111, 222, 333, 444}));
+ ASSERT_FALSE(m.match(std::vector<int>{111, 222, 444}));
+ ASSERT_FALSE(m.match(std::vector<int>{111, 222, 444}));
+ {
+ auto failStr = GET_FAILURE_STRING(std::vector<int>({111, 222, 444}), m);
+ ASSERT_EQ(failStr,
+ "value: std::vector<int>({111, 222, 444})"
+ ", actual: [111, 222, 444]"
+ ", failed: [2]"
+ ", expected: ElementsAre(Eq(111), Eq(222), Eq(333))");
+ }
+ {
+ auto failStr = GET_FAILURE_STRING(std::vector<int>({111, 222}), m);
+ ASSERT_EQ(failStr,
+ "value: std::vector<int>({111, 222})"
+ ", actual: [111, 222]"
+ ", failed: size 2 != expected size 3"
+ ", expected: ElementsAre(Eq(111), Eq(222), Eq(333))");
+ }
+}
+
+TEST(AssertThat, TupleElementsAre) {
+ ASSERT_THAT((std::tuple{123, std::string{"hi"}}), TupleElementsAre(Eq(123), Eq("hi")));
+}
+
+TEST(AssertThat, StructuredBindingsAre) {
+ struct X {
+ int i;
+ std::string str;
+ };
+ ASSERT_THAT((X{123, "hi"}), StructuredBindingsAre(Eq(123), Eq("hi")));
+#if 0 // Must not compile. Check manually I guess.
+ struct MoreFields {int i1; int i2; };
+ ASSERT_THAT((MoreFields{123, 456}), StructuredBindingsAre(Eq(123)));
+#endif
+}
+
+
+TEST(AssertThat, StatusIs) {
+ ASSERT_THAT(Status::OK(), StatusIs(Eq(ErrorCodes::OK), Eq("")));
+ Status oops{ErrorCodes::InternalError, "oops I did it again"};
+ ASSERT_THAT(oops, StatusIs(Eq(ErrorCodes::InternalError), Eq("oops I did it again")));
+ ASSERT_THAT(oops, StatusIs(Ne(ErrorCodes::OK), Any()));
+ ASSERT_THAT(oops, StatusIs(Ne(ErrorCodes::OK), ContainsRegex("o*ps")));
+}
+
+TEST(AssertThat, BSONObj) {
+ auto obj = BSONObjBuilder{}.append("i", 123).append("s", "hi").obj();
+ ASSERT_THAT(obj, BSONObjHas(BSONElementIs(Eq("i"), Eq(NumberInt), Any())));
+ ASSERT_THAT(obj,
+ AllOf(BSONObjHas(BSONElementIs(Eq("i"), Eq(NumberInt), Eq(123))),
+ BSONObjHas(BSONElementIs(Eq("s"), Eq(String), Eq("hi")))));
+ ASSERT_THAT(obj, Not(BSONObjHas(BSONElementIs(Eq("x"), Any(), Any()))));
+}
+
+
+TEST(AssertThat, Demo) {
+ ASSERT_THAT(123, Eq(123));
+ ASSERT_THAT(123, Not(Eq(0)));
+ ASSERT_THAT("hi", Eq("hi"));
+ ASSERT_THAT("Four score and seven",
+ AllOf(Ne("hi"), ContainsRegex("score"), ContainsRegex(R"( \w{5} )")));
+
+ // Composing matchers
+ ASSERT_THAT(123, Not(Eq(0)));
+ ASSERT_THAT(123, AllOf(Gt(0), Lt(1000)));
+
+ // Sequences
+ std::vector<int> myVec{111, 222, 333};
+ std::list<int> myList{111, 222, 333};
+ ASSERT_THAT(myVec, Eq(std::vector<int>{111, 222, 333}));
+ ASSERT_THAT(myVec, ElementsAre(Eq(111), AllOf(Lt(1000), Gt(0)), Any()));
+ ASSERT_THAT(myList, ElementsAre(Eq(111), AllOf(Lt(1000), Gt(0)), Any()));
+
+ // Structs/Tuples
+ struct {
+ int i;
+ std::string s;
+ } x{123, "hello"};
+ ASSERT_THAT(x, StructuredBindingsAre(Eq(123), ContainsRegex("hel*o")));
+
+ // Status
+ Status oops{ErrorCodes::InternalError, "oops I did it again"};
+ ASSERT_THAT(oops, StatusIs(Eq(ErrorCodes::InternalError), Eq("oops I did it again")));
+ ASSERT_THAT(oops, StatusIs(Ne(ErrorCodes::OK), Any()));
+ ASSERT_THAT(oops, StatusIs(Ne(ErrorCodes::OK), ContainsRegex("o*ps")));
+
+ // BSONElement and BSONObj
+ auto obj = BSONObjBuilder{}.append("i", 123).append("s", "hi").obj();
+ ASSERT_THAT(obj,
+ AllOf(BSONObjHas(BSONElementIs(Eq("i"), Eq(NumberInt), Eq(123))),
+ BSONObjHas(BSONElementIs(Eq("s"), Eq(String), Eq("hi")))));
+ ASSERT_THAT(obj, Not(BSONObjHas(BSONElementIs(Eq("x"), Any(), Any()))));
+}
+
+TEST(AssertThat, UnprintableValues) {
+ struct Unprintable {
+ int i;
+ } v{123};
+ std::string lastResort = detail::lastResortFormat(typeid(v), &v, sizeof(v));
+ // Test that the lastResortFormat function is used for unprintable values.
+ using detail::stringifyForAssert; // Augment ADL with the "detail" NS.
+ ASSERT_EQ(stringifyForAssert(v), lastResort);
+ // Test that a typical matcher like Eq uses it.
+ ASSERT_STRING_CONTAINS(Eq(v).describe(), lastResort);
+}
+
+
+} // namespace
+} // namespace mongo::unittest::match
diff --git a/src/mongo/unittest/matcher.cpp b/src/mongo/unittest/matcher.cpp
new file mode 100644
index 00000000000..da0bbcb5a6c
--- /dev/null
+++ b/src/mongo/unittest/matcher.cpp
@@ -0,0 +1,64 @@
+/**
+ * Copyright (C) 2021-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#include "mongo/unittest/matcher.h"
+
+#include <memory>
+#include <utility>
+
+#include <fmt/format.h>
+#include <pcrecpp.h>
+
+namespace mongo::unittest::match {
+
+using namespace fmt::literals;
+
+struct ContainsRegex::Impl {
+ explicit Impl(pcrecpp::RE pat) : re(std::move(pat)) {}
+ pcrecpp::RE re;
+};
+
+ContainsRegex::ContainsRegex(std::string pattern)
+ : _impl{std::make_shared<Impl>(std::move(pattern))} {}
+
+ContainsRegex::~ContainsRegex() = default;
+
+MatchResult ContainsRegex::match(StringData x) const {
+ bool res =
+ _impl->re.PartialMatch(pcrecpp::StringPiece{x.rawData(), static_cast<int>(x.size())});
+ if (res)
+ return {};
+ return MatchResult(false, "");
+}
+
+std::string ContainsRegex::describe() const {
+ return R"(ContainsRegex("{}"))"_format(_impl->re.pattern());
+}
+
+} // namespace mongo::unittest::match
diff --git a/src/mongo/unittest/matcher.h b/src/mongo/unittest/matcher.h
new file mode 100644
index 00000000000..be7e7014e12
--- /dev/null
+++ b/src/mongo/unittest/matcher.h
@@ -0,0 +1,651 @@
+/**
+ * Copyright (C) 2021-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <fmt/format.h>
+#include <memory>
+#include <string>
+#include <tuple>
+#include <utility>
+
+#include "mongo/base/string_data.h"
+#include "mongo/bson/bsonelement.h"
+#include "mongo/bson/bsonobj.h"
+#include "mongo/stdx/type_traits.h"
+#include "mongo/unittest/matcher_core.h"
+
+/**
+ * Defines a basic set of matchers to be used with the ASSERT_THAT macro (see
+ * `assert_that.h`). It's intended that matchers to support higher-level
+ * components will be defined alongside that component's other unit testing
+ * support classes, rather than in this file.
+ */
+namespace mongo::unittest::match {
+
+/*
+ * A uniform wrapper around any matcher that accepts a `T`, so they can
+ * be treated homogeneously.
+ *
+ * Example:
+ * std::vector<TypeErasedMatcher<int>> vec{Eq(123), AllOf(Gt(100), Lt(200))};
+ **/
+template <typename T>
+class TypeErasedMatcher {
+public:
+ using value_type = T;
+
+ template <typename M>
+ explicit TypeErasedMatcher(const M& m) : _m{std::make_shared<TypedMatch<M>>(m)} {}
+
+ virtual ~TypeErasedMatcher() = default;
+
+ std::string describe() const {
+ return _m->describe();
+ }
+
+ MatchResult match(const T& v) const {
+ return _m->match(v);
+ }
+
+private:
+ struct BasicMatch {
+ virtual std::string describe() const = 0;
+ virtual MatchResult match(const T& v) const = 0;
+ };
+
+ template <typename M>
+ class TypedMatch : public BasicMatch {
+ template <typename X>
+ using CanMatchOp = decltype(std::declval<M>().match(std::declval<X>()));
+
+ public:
+ explicit TypedMatch(const M& m) : _m{&m} {}
+ virtual ~TypedMatch() = default;
+
+ std::string describe() const override {
+ return _m->describe();
+ }
+
+ MatchResult match(const T& v) const override {
+ if constexpr (!stdx::is_detected_v<CanMatchOp, T>) {
+ return MatchResult{
+ false,
+ format(FMT_STRING("Matcher does not accept {}"), demangleName(typeid(T)))};
+ } else {
+ return _m->match(v);
+ }
+ }
+
+ private:
+ const M* _m;
+ };
+
+ std::shared_ptr<BasicMatch> _m;
+};
+
+/** Always true: matches any value of any type. */
+class Any : public Matcher {
+public:
+ std::string describe() const {
+ return "Any";
+ }
+
+ template <typename X>
+ MatchResult match(const X&) const {
+ return MatchResult{true};
+ }
+};
+
+namespace detail {
+
+/**
+ * MatchResult will be false when `m.match(v)` fails template substitution.
+ * Can be used e.g. to produce a runtime-dispatched matcher for variant types.
+ *
+ * Example:
+ * typeTolerantMatch(Eq("hello"), 1234); // Fails to match but compiles
+ */
+template <typename M, typename T>
+MatchResult typeTolerantMatch(const M& m, const T& v) {
+ return TypeErasedMatcher<T>(m).match(v);
+}
+
+template <template <typename> class D, typename T, typename Cmp>
+class RelOpBase : public Matcher {
+ const D<T>& self() const {
+ return static_cast<const D<T>&>(*this);
+ }
+
+ template <typename X>
+ using CanMatchOp = decltype(Cmp()(std::declval<X>(), std::declval<T>()));
+
+public:
+ explicit RelOpBase(T v) : _v{std::move(v)} {}
+
+ std::string describe() const {
+ return format(FMT_STRING("{}({})"), self().name, stringifyForAssert(_v));
+ }
+
+ template <typename X, std::enable_if_t<stdx::is_detected_v<CanMatchOp, X>, int> = 0>
+ MatchResult match(const X& x) const {
+ return Cmp{}(x, _v);
+ }
+
+private:
+ T _v;
+};
+
+} // namespace detail
+
+/** Equal to. */
+template <typename T>
+struct Eq : detail::RelOpBase<Eq, T, std::equal_to<>> {
+ using detail::RelOpBase<Eq, T, std::equal_to<>>::RelOpBase;
+ static constexpr auto name = "Eq"_sd;
+};
+template <typename T>
+Eq(T v)->Eq<T>;
+
+/** Not equal. */
+template <typename T>
+struct Ne : detail::RelOpBase<Ne, T, std::not_equal_to<>> {
+ using detail::RelOpBase<Ne, T, std::not_equal_to<>>::RelOpBase;
+ static constexpr auto name = "Ne"_sd;
+};
+template <typename T>
+Ne(T v)->Ne<T>;
+
+/** Less than. */
+template <typename T>
+struct Lt : detail::RelOpBase<Lt, T, std::less<>> {
+ using detail::RelOpBase<Lt, T, std::less<>>::RelOpBase;
+ static constexpr auto name = "Lt"_sd;
+};
+template <typename T>
+Lt(T v)->Lt<T>;
+
+/** Greater than. */
+template <typename T>
+struct Gt : detail::RelOpBase<Gt, T, std::greater<>> {
+ using detail::RelOpBase<Gt, T, std::greater<>>::RelOpBase;
+ static constexpr auto name = "Gt"_sd;
+};
+template <typename T>
+Gt(T v)->Gt<T>;
+
+/** Less than or equal to. */
+template <typename T>
+struct Le : detail::RelOpBase<Le, T, std::less_equal<>> {
+ using detail::RelOpBase<Le, T, std::less_equal<>>::RelOpBase;
+ static constexpr auto name = "Le"_sd;
+};
+template <typename T>
+Le(T v)->Le<T>;
+
+/** Greater than or equal to. */
+template <typename T>
+struct Ge : detail::RelOpBase<Ge, T, std::greater_equal<>> {
+ using detail::RelOpBase<Ge, T, std::greater_equal<>>::RelOpBase;
+ static constexpr auto name = "Ge"_sd;
+};
+template <typename T>
+Ge(T v)->Ge<T>;
+
+/**
+ * Wrapper that inverts the sense of a matcher.
+ * Example:
+ * ASSERT_THAT("hi there", Not(ContainsRegex("hello")));
+ */
+template <typename M>
+class Not : public Matcher {
+public:
+ explicit Not(M m) : _m(std::move(m)) {}
+
+ std::string describe() const {
+ return format(FMT_STRING("Not({})"), _m.describe());
+ }
+
+ template <typename X>
+ MatchResult match(X&& x) const {
+ auto r = _m.match(x);
+ return MatchResult{!r};
+ }
+
+private:
+ M _m;
+};
+
+/**
+ * Given a pack of matchers, composes a matcher that passes when all matchers
+ * in the pack pass.
+ *
+ * Example:
+ * ASSERT_THAT(123, AllOf(Gt(100), Lt(200), Eq(123)));
+ */
+template <typename... Ms>
+class AllOf : public Matcher {
+public:
+ explicit AllOf(Ms... ms) : _ms(std::move(ms)...) {}
+
+ std::string describe() const {
+ return format(FMT_STRING("AllOf({})"), detail::describeTupleOfMatchers(_ms));
+ }
+
+ template <typename X>
+ MatchResult match(const X& x) const {
+ return _match(x, std::index_sequence_for<Ms...>{});
+ }
+
+private:
+ template <typename X, size_t... Is>
+ MatchResult _match(const X& x, std::index_sequence<Is...>) const {
+ std::array arr{std::get<Is>(_ms).match(x)...};
+ if (!std::all_of(arr.begin(), arr.end(), [](auto&& re) { return !!re; }))
+ return MatchResult{false, detail::matchTupleMessage(_ms, arr)};
+ return MatchResult{true};
+ }
+
+ std::tuple<Ms...> _ms;
+};
+
+/**
+ * Given a pack of matchers, composees a matcher that passes when any matcher
+ * in the pack passes.
+ *
+ * Example:
+ * ASSERT_THAT(123, AnyOf(Lt(100), Gt(200), Eq(123)));
+ */
+template <typename... Ms>
+class AnyOf : public Matcher {
+public:
+ explicit AnyOf(Ms... ms) : _ms(std::move(ms)...) {}
+
+ std::string describe() const {
+ return format(FMT_STRING("AnyOf({})"), detail::describeTupleOfMatchers(_ms));
+ }
+
+ template <typename X>
+ MatchResult match(const X& x) const {
+ return _match(x, std::index_sequence_for<Ms...>{});
+ }
+
+private:
+ template <typename X, size_t... Is>
+ MatchResult _match(const X& x, std::index_sequence<Is...>) const {
+ std::array arr{std::get<Is>(_ms).match(x)...};
+ if (!std::any_of(arr.begin(), arr.end(), [](auto&& re) { return !!re; }))
+ return MatchResult{false, detail::matchTupleMessage(_ms, arr)};
+ return MatchResult{true};
+ }
+
+ std::tuple<Ms...> _ms;
+};
+
+/**
+ * Match the result dereferencing pointer-like expression with unary `*`.
+ * Also fails if `!x`.
+ *
+ * Example:
+ * int x = 123;
+ * ASSERT_THAT(&x, Pointee(Eq(123)));
+ */
+template <typename M>
+class Pointee : public Matcher {
+public:
+ explicit Pointee(M m) : _m(std::move(m)) {}
+
+ std::string describe() const {
+ return format(FMT_STRING("Pointee({})"), _m.describe());
+ }
+
+ template <typename X>
+ MatchResult match(const X& x) const {
+ if (!x)
+ return MatchResult{false, "empty pointer"};
+ MatchResult res = _m.match(*x);
+ if (res)
+ return MatchResult{true};
+ return MatchResult{false, format(FMT_STRING("{}"), res.message())};
+ }
+
+private:
+ M _m;
+};
+
+/**
+ * Match a string-like expression using a PCRE partial match.
+ *
+ * Example:
+ * ASSERT_THAT("Hello, world!", ContainsRegex("world"));
+ */
+class ContainsRegex : public Matcher {
+public:
+ explicit ContainsRegex(std::string pattern);
+ ~ContainsRegex();
+
+ std::string describe() const;
+
+ // Should accept anything string-like
+ MatchResult match(StringData x) const;
+
+private:
+ struct Impl;
+ std::shared_ptr<Impl> _impl;
+};
+
+
+/**
+ * Match a sequence container's elements against a sequence of matchers.
+ * The matchers need not be of the same type.
+ *
+ * Example:
+ * std::vector<int> vec{5,6,7};
+ * ASSERT_THAT(vec, ElementsAre(Eq(5), Eq(6), Ge(5)));
+ */
+template <typename... Ms>
+class ElementsAre : public Matcher {
+public:
+ explicit ElementsAre(const Ms&... ms) : _ms(std::move(ms)...) {}
+
+ std::string describe() const {
+ return format(FMT_STRING("ElementsAre({})"), detail::describeTupleOfMatchers(_ms));
+ }
+
+ template <typename X>
+ MatchResult match(X&& x) const {
+ if (x.size() != sizeof...(Ms)) {
+ return MatchResult{
+ false,
+ format(FMT_STRING("failed: size {} != expected size {}"), x.size(), sizeof...(Ms))};
+ }
+ return _match(x, std::make_index_sequence<sizeof...(Ms)>{});
+ }
+
+private:
+ template <typename X, size_t... Is>
+ MatchResult _match(const X& x, std::index_sequence<Is...>) const {
+ using std::begin;
+ auto it = begin(x);
+ std::array arr{std::get<Is>(_ms).match(*it++)...};
+ bool allOk = true;
+ detail::Joiner joiner;
+ for (size_t i = 0; i != sizeof...(Ms); ++i) {
+ if (!arr[i]) {
+ allOk = false;
+ std::string m;
+ if (!arr[i].message().empty())
+ m = format(FMT_STRING(":{}"), arr[i].message());
+ joiner(format(FMT_STRING("{}{}"), i, m));
+ }
+ }
+ if (!allOk)
+ return MatchResult{false, format(FMT_STRING("failed: [{}]"), std::string{joiner})};
+ return MatchResult{true};
+ }
+
+ std::tuple<Ms...> _ms;
+};
+
+/**
+ * Match the tuple elements of an expression.
+ *
+ * Example:
+ * ASSERT_THAT(std::tuple(123, "Hello, world!"),
+ * TupleElementsAre(Gt(100), ContainsRegex("Hello")));
+ */
+template <typename... Ms>
+class TupleElementsAre : public Matcher {
+public:
+ explicit TupleElementsAre(const Ms&... ms) : _ms(std::move(ms)...) {}
+
+ std::string describe() const {
+ return format(FMT_STRING("TupleElementsAre({})"), detail::describeTupleOfMatchers(_ms));
+ }
+
+ template <typename X>
+ MatchResult match(X&& x) const {
+ size_t xSize = std::tuple_size_v<std::decay_t<X>>;
+ if (xSize != sizeof...(Ms))
+ return MatchResult{
+ false,
+ format(FMT_STRING("failed: size {} != expected size {}"), xSize, sizeof...(Ms))};
+ return _match(x, std::make_index_sequence<sizeof...(Ms)>{});
+ }
+
+private:
+ template <typename X, size_t... Is>
+ MatchResult _match(const X& x, std::index_sequence<Is...>) const {
+ std::array arr{std::get<Is>(_ms).match(std::get<Is>(x))...};
+ if (!std::all_of(arr.begin(), arr.end(), [](auto&& r) { return !!r; }))
+ return MatchResult{false, detail::matchTupleMessage(_ms, arr)};
+ return MatchResult{true};
+ }
+
+ std::tuple<Ms...> _ms;
+};
+
+/**
+ * Match that each of the structured bindings for an expression match a field matcher.
+ *
+ * Example:
+ * struct Obj { int x; std::string s; } obj{123, "Hello, world!"};
+ * ASSERT_THAT(obj, StructuredBindingsAre(Gt(100), ContainsRegex("Hello")));
+ */
+template <typename... Ms>
+class StructuredBindingsAre : public Matcher {
+public:
+ explicit StructuredBindingsAre(const Ms&... ms) : _ms(std::move(ms)...) {}
+
+ std::string describe() const {
+ return format(FMT_STRING("StructuredBindingsAre({})"),
+ detail::describeTupleOfMatchers(_ms));
+ }
+
+ template <typename X>
+ MatchResult match(const X& x) const {
+ return _match(x, std::make_index_sequence<sizeof...(Ms)>{});
+ }
+
+private:
+ /**
+ * There are no variadic structured bindings, but it can be simulated
+ * for a fixed member count up to a hardcoded limit.
+ */
+ template <size_t N, typename X>
+ static auto _tieStruct(const X& x) {
+ /*
+ Can be regenerated by Python:
+ N = 10
+ print(" if constexpr (N == 0) {")
+ print(" return std::tie();")
+ for n in range(1, N):
+ fs = ["f{}".format(j) for j in range(n)]
+ print(" }} else if constexpr (N == {}) {{".format(n))
+ print(" const auto& [{}] = x;".format(",".join(fs)))
+ print(" return std::tie({});".format(",".join(fs)))
+ print(" }")
+ */
+ if constexpr (N == 0) {
+ return std::tie();
+ } else if constexpr (N == 1) {
+ const auto& [f0] = x;
+ return std::tie(f0);
+ } else if constexpr (N == 2) {
+ const auto& [f0, f1] = x;
+ return std::tie(f0, f1);
+ } else if constexpr (N == 3) {
+ const auto& [f0, f1, f2] = x;
+ return std::tie(f0, f1, f2);
+ } else if constexpr (N == 4) {
+ const auto& [f0, f1, f2, f3] = x;
+ return std::tie(f0, f1, f2, f3);
+ } else if constexpr (N == 5) {
+ const auto& [f0, f1, f2, f3, f4] = x;
+ return std::tie(f0, f1, f2, f3, f4);
+ } else if constexpr (N == 6) {
+ const auto& [f0, f1, f2, f3, f4, f5] = x;
+ return std::tie(f0, f1, f2, f3, f4, f5);
+ } else if constexpr (N == 7) {
+ const auto& [f0, f1, f2, f3, f4, f5, f6] = x;
+ return std::tie(f0, f1, f2, f3, f4, f5, f6);
+ } else if constexpr (N == 8) {
+ const auto& [f0, f1, f2, f3, f4, f5, f6, f7] = x;
+ return std::tie(f0, f1, f2, f3, f4, f5, f6, f7);
+ } else if constexpr (N == 9) {
+ const auto& [f0, f1, f2, f3, f4, f5, f6, f7, f8] = x;
+ return std::tie(f0, f1, f2, f3, f4, f5, f6, f7, f8);
+ }
+ MONGO_UNREACHABLE;
+ }
+
+ template <typename X, size_t... Is>
+ MatchResult _match(const X& x, std::index_sequence<Is...>) const {
+ auto tied = _tieStruct<sizeof...(Ms)>(x);
+ std::array arr{std::get<Is>(_ms).match(std::get<Is>(tied))...};
+ if (!std::all_of(arr.begin(), arr.end(), [](auto&& r) { return !!r; }))
+ return MatchResult{false, detail::matchTupleMessage(_ms, arr)};
+ return MatchResult{true};
+ }
+
+private:
+ std::tuple<Ms...> _ms;
+};
+
+/**
+ * `StatusIs(code, reason)` matches a `Status` against matchers
+ * for its code and its reason string.
+ *
+ * Example:
+ * ASSERT_THAT(status, StatusIs(Eq(ErrorCodes::InternalError), ContainsRegex("ouch")));
+ */
+template <typename CodeM, typename ReasonM>
+class StatusIs : public Matcher {
+public:
+ StatusIs(CodeM code, ReasonM reason) : _code{std::move(code)}, _reason{std::move(reason)} {}
+ std::string describe() const {
+ return format(FMT_STRING("StatusIs({}, {})"), _code.describe(), _reason.describe());
+ }
+ MatchResult match(const Status& st) const {
+ MatchResult cr = _code.match(st.code());
+ MatchResult rr = _reason.match(st.reason());
+ detail::Joiner joiner;
+ if (!cr.message().empty())
+ joiner(format(FMT_STRING("code:{}"), cr.message()));
+ if (!rr.message().empty()) {
+ joiner(format(FMT_STRING("reason:{}"), rr.message()));
+ }
+ return MatchResult{cr && rr, std::string{joiner}};
+ }
+
+private:
+ CodeM _code;
+ ReasonM _reason;
+};
+
+/**
+ * `BSONElementIs(name,type,value)` matches a `BSONElement` against matchers
+ * for its name, type, and value. Experimental: only covers some simple scalar
+ * types.
+ *
+ * Example:
+ * ASSERT_THAT(obj, BSONObjHas(BSONElementIs(Eq("i"), Eq(NumberInt), Any())));
+ */
+template <typename NameM, typename TypeM, typename ValueM>
+class BSONElementIs : public Matcher {
+public:
+ BSONElementIs(NameM nameM, TypeM typeM, ValueM valueM)
+ : _name{std::move(nameM)}, _type{std::move(typeM)}, _value{std::move(valueM)} {}
+
+ std::string describe() const {
+ return format(FMT_STRING("BSONElementIs(name:{}, type:{}, value:{})"),
+ _name.describe(),
+ _type.describe(),
+ _value.describe());
+ }
+
+ MatchResult match(const BSONElement& x) const {
+ auto nr = _name.match(std::string{x.fieldNameStringData()});
+ if (!nr)
+ return MatchResult{
+ false,
+ format(FMT_STRING("name failed: {} {}"), x.fieldNameStringData(), nr.message())};
+ auto t = x.type();
+ auto tr = _type.match(t);
+ if (!tr)
+ return MatchResult{
+ false, format(FMT_STRING("type failed: {} {}"), typeName(x.type()), tr.message())};
+ if (t == NumberInt)
+ return detail::typeTolerantMatch(_value, x.Int());
+ if (t == NumberLong)
+ return detail::typeTolerantMatch(_value, x.Long());
+ if (t == NumberDouble)
+ return detail::typeTolerantMatch(_value, x.Double());
+ if (t == String)
+ return detail::typeTolerantMatch(_value, x.String());
+ // need to support more BSON element types.
+ return MatchResult{
+ false, format(FMT_STRING("Cannot match BSON Elements holding type {}"), typeName(t))};
+ }
+
+private:
+ NameM _name;
+ TypeM _type;
+ ValueM _value;
+};
+
+/**
+ * `BSONObjHas(m)` matches a `BSONObj` having an element matching `m`.
+ */
+template <typename M>
+class BSONObjHas : public Matcher {
+public:
+ explicit BSONObjHas(M m) : _m{std::move(m)} {}
+
+ std::string describe() const {
+ return format(FMT_STRING("BSONObjHas({})"), _m.describe());
+ }
+
+ MatchResult match(const BSONObj& x) const {
+ std::vector<MatchResult> res;
+ for (const auto& e : x) {
+ if (auto mr = _m.match(e))
+ return mr;
+ else
+ res.push_back(mr);
+ }
+ return MatchResult{false, "None of the elements matched"};
+ }
+
+private:
+ M _m;
+};
+
+} // namespace mongo::unittest::match
diff --git a/src/mongo/unittest/matcher_core.cpp b/src/mongo/unittest/matcher_core.cpp
new file mode 100644
index 00000000000..370e79faf93
--- /dev/null
+++ b/src/mongo/unittest/matcher_core.cpp
@@ -0,0 +1,47 @@
+/**
+ * Copyright (C) 2021-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#include "mongo/unittest/matcher_core.h"
+
+#include <cstdlib>
+#include <string>
+#include <typeinfo>
+
+#include <fmt/format.h>
+
+#include "mongo/util/assert_util.h"
+#include "mongo/util/hex.h"
+
+namespace mongo::unittest::match::detail {
+
+std::string lastResortFormat(const std::type_info& ti, const void* p, size_t sz) {
+ return format(FMT_STRING("[{}={}]"), demangleName(ti), hexdump(p, sz));
+}
+
+} // namespace mongo::unittest::match::detail
diff --git a/src/mongo/unittest/matcher_core.h b/src/mongo/unittest/matcher_core.h
new file mode 100644
index 00000000000..5e7963fe078
--- /dev/null
+++ b/src/mongo/unittest/matcher_core.h
@@ -0,0 +1,269 @@
+/**
+ * Copyright (C) 2021-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <fmt/format.h>
+#include <memory>
+#include <string>
+#include <tuple>
+#include <typeinfo>
+#include <utility>
+
+#include "mongo/base/string_data.h"
+#include "mongo/stdx/type_traits.h"
+#include "mongo/unittest/unittest.h"
+
+/**
+ * This file defines infrastructure used in the ASSERT_THAT system.
+ * (See `assert_that.h`).
+ *
+ * It also contains utilities that can be reused in the implementation of
+ * matcher types. The basic set of matchers are defined in `matcher.h`.
+ */
+namespace mongo::unittest::match {
+
+/**
+ * A result returned by a Matcher's `match` function. The `message` should only
+ * be given if it contains information than the matcher's description or the
+ * match's stringified input value.
+ */
+class MatchResult {
+public:
+ MatchResult() = default;
+ /* implicit */ MatchResult(bool ok) : _ok{ok} {}
+ MatchResult(bool ok, std::string msg) : _ok{ok}, _msg{std::move(msg)} {}
+ explicit operator bool() const {
+ return _ok;
+ }
+ const std::string& message() const {
+ return _msg;
+ }
+
+private:
+ bool _ok = true;
+ std::string _msg;
+};
+
+/**
+ * Base class that identifies matchers.
+ * Technically doesn't do anything but indicate intent.
+ *
+ * Conceptually, a Matcher `m` must have:
+ *
+ * m.describe() -> std::string
+ *
+ * Returns a very compact description of the matcher.
+ *
+ * And for some value `v`:
+ *
+ * m.match(v) -> MatchResult
+ *
+ * Returns a true MatchResult if match succeeds.
+ * Otherwise false and a more detailed message only if necessary.
+ *
+ * `match` should be SFINAE-friendly and only participate in overload
+ * resolution if the type of `v` can be matched.
+ *
+ * Matchers must be copyable.
+ */
+class Matcher {};
+
+namespace detail {
+
+/**
+ * `stringifyForAssert` can be overloaded to extend stringification
+ * capabilities of the matchers via ADL.
+ *
+ * The overload in the match::detail namespace is used for types for
+ * which the unittest library has built-in support.
+ */
+template <typename T>
+std::string stringifyForAssert(const T& x);
+
+
+template <typename T>
+std::string doFormat(const T& x) {
+ return format(FMT_STRING("{}"), x);
+}
+
+template <typename T>
+std::string doOstream(const T& x) {
+ std::ostringstream os;
+ os << x;
+ return os.str();
+}
+
+using std::begin;
+using std::end;
+
+template <typename T>
+using HasToStringOp = decltype(std::declval<T>().toString());
+template <typename T>
+constexpr bool HasToString = stdx::is_detected_v<HasToStringOp, T>;
+
+template <typename T>
+using HasOstreamOp = decltype(std::declval<std::ostream&>() << std::declval<T>());
+template <typename T>
+constexpr bool HasOstream = stdx::is_detected_v<HasOstreamOp, T>;
+
+template <typename T>
+using HasBeginEndOp =
+ std::tuple<decltype(begin(std::declval<T>())), decltype(end(std::declval<T>()))>;
+template <typename T>
+constexpr bool IsSequence = stdx::is_detected_v<HasBeginEndOp, T>;
+
+class Joiner {
+public:
+ template <typename T>
+ Joiner& operator()(const T& v) {
+ _out += format(FMT_STRING("{}{}"), _sep, stringifyForAssert(v));
+ _sep = ", ";
+ return *this;
+ }
+ explicit operator const std::string&() const {
+ return _out;
+ }
+
+private:
+ std::string _out;
+ const char* _sep = "";
+};
+
+/**
+ * Describes a tuple of matchers. This is just a comma-separated list of descriptions.
+ * Used in the `describe()` function of variadic matchers.
+ */
+template <typename MTuple, size_t I = 0>
+std::string describeTupleOfMatchers(const MTuple& ms, Joiner&& joiner = {}) {
+ if constexpr (I == std::tuple_size_v<MTuple>) {
+ return std::string{joiner};
+ } else {
+ joiner(std::get<I>(ms).describe());
+ return describeTupleOfMatchers<MTuple, I + 1>(ms, std::move(joiner));
+ }
+}
+
+/**
+ * Describe an array of MatchResult that was generated by a tuple of
+ * matchers. Returns a string describing only the failed match results, each
+ * preceded by an indication of its array position.
+ *
+ * Used in the production of MatchResult strings for variadic matchers.
+ */
+template <typename MTuple, size_t N, size_t I = 0>
+std::string matchTupleMessage(const MTuple& ms,
+ const std::array<MatchResult, N>& arr,
+ Joiner&& joiner = {}) {
+ if constexpr (I == std::tuple_size_v<MTuple>) {
+ return format(FMT_STRING("failed: [{}]"), std::string{joiner});
+ } else {
+ auto&& ri = arr[I];
+ if (!ri) {
+ joiner(format(FMT_STRING("{}:({}{}{})"),
+ I,
+ std::get<I>(ms).describe(),
+ ri.message().empty() ? "" : ":",
+ ri.message()));
+ }
+ return matchTupleMessage<MTuple, N, I + 1>(ms, arr, std::move(joiner));
+ }
+}
+
+template <typename T>
+std::string doSequence(const T& seq) {
+ std::string r;
+ Joiner joiner;
+ for (const auto& e : seq)
+ joiner(e);
+ return format(FMT_STRING("[{}]"), std::string{joiner});
+}
+
+std::string lastResortFormat(const std::type_info& ti, const void* p, size_t sz);
+
+/**
+ * The default stringifyForAssert implementation.
+ * Encodes the steps by which we determine how to print an object.
+ * There's a wildcard branch so everything is printable in some way.
+ */
+template <typename T>
+std::string stringifyForAssert(const T& x) {
+ if constexpr (HasOstream<T>) {
+ return doOstream(x);
+ } else if constexpr (HasToString<T>) {
+ return x.toString();
+ } else if constexpr (std::is_convertible_v<T, StringData>) {
+ return doFormat(StringData(x));
+ } else if constexpr (std::is_pointer_v<T>) {
+ return doFormat(static_cast<const void*>(x));
+ } else if constexpr (IsSequence<T>) {
+ return doSequence(x);
+ } else {
+ return lastResortFormat(typeid(x), &x, sizeof(x));
+ }
+}
+
+/** Portably support stringifying `nullptr`. */
+inline std::string stringifyForAssert(std::nullptr_t) {
+ return "nullptr";
+}
+
+/** Built-in support to stringify `ErrorCode::Error`. */
+inline std::string stringifyForAssert(ErrorCodes::Error ec) {
+ return ErrorCodes::errorString(ec);
+}
+
+template <typename E, typename M>
+struct MatchAssertion {
+ MatchAssertion(const E& e, const M& m, const char* eStr) : mr{m.match(e)} {
+ if (!mr) {
+ msg = format(FMT_STRING("value: {}, actual: {}{}, expected: {}"),
+ eStr,
+ stringifyForAssert(e),
+ mr.message().empty() ? "" : format(FMT_STRING(", {}"), mr.message()),
+ m.describe());
+ }
+ }
+
+ explicit operator bool() const {
+ return !!mr;
+ }
+
+ const std::string& failMsg() const {
+ return msg;
+ }
+
+ MatchResult mr;
+ std::string msg;
+};
+
+} // namespace detail
+
+} // namespace mongo::unittest::match
diff --git a/src/mongo/unittest/unittest.cpp b/src/mongo/unittest/unittest.cpp
index 54e55efbe6f..35626c38b20 100644
--- a/src/mongo/unittest/unittest.cpp
+++ b/src/mongo/unittest/unittest.cpp
@@ -60,8 +60,7 @@
#include "mongo/util/stacktrace.h"
#include "mongo/util/timer.h"
-namespace mongo {
-namespace unittest {
+namespace mongo::unittest {
namespace {
bool stringContains(const std::string& haystack, const std::string& needle) {
@@ -556,9 +555,10 @@ Suite& Suite::getSuite(StringData name) {
return *sp;
}
-TestAssertionFailureException::TestAssertionFailureException(
- const std::string& theFile, unsigned theLine, const std::string& theFailingExpression)
- : _file(theFile), _line(theLine), _message(theFailingExpression) {
+TestAssertionFailureException::TestAssertionFailureException(std::string file,
+ unsigned line,
+ std::string message)
+ : _file(std::move(file)), _line(line), _message(std::move(message)) {
std::ostringstream ostream;
printStackTrace(ostream);
_stacktrace = ostream.str();
@@ -645,5 +645,4 @@ namespace {
}();
} // namespace
-} // namespace unittest
-} // namespace mongo
+} // namespace mongo::unittest
diff --git a/src/mongo/unittest/unittest.h b/src/mongo/unittest/unittest.h
index 37f5abab417..2d397fb8ea8 100644
--- a/src/mongo/unittest/unittest.h
+++ b/src/mongo/unittest/unittest.h
@@ -35,7 +35,6 @@
#pragma once
-#include <boost/preprocessor/cat.hpp>
#include <cmath>
#include <fmt/format.h>
#include <functional>
@@ -195,23 +194,25 @@
* This should be used at namespace scope, not inside a TEST function.
*
* Examples that pass:
- * ASSERT_DOES_NOT_COMPILE(typename Char = char, *std::declval<Char>());
- * ASSERT_DOES_NOT_COMPILE(bool B = false, std::enable_if_t<B, int>{});
+ * ASSERT_DOES_NOT_COMPILE(MyTest1, typename Char = char, *std::declval<Char>());
+ * ASSERT_DOES_NOT_COMPILE(MyTest2, bool B = false, std::enable_if_t<B, int>{});
*
* Examples that fail:
- * ASSERT_DOES_NOT_COMPILE(typename Char = char, *std::declval<Char*>());
- * ASSERT_DOES_NOT_COMPILE(bool B = true, std::enable_if_t<B, int>{});
+ * ASSERT_DOES_NOT_COMPILE(MyTest3, typename Char = char, *std::declval<Char*>());
+ * ASSERT_DOES_NOT_COMPILE(MyTest4, bool B = true, std::enable_if_t<B, int>{});
*
*/
-#define ASSERT_DOES_NOT_COMPILE(Alias, /*expr*/...) \
- ASSERT_DOES_NOT_COMPILE_1_( \
- BOOST_PP_CAT(compileCheck_, __LINE__), Alias, #Alias, (__VA_ARGS__), #__VA_ARGS__)
-
-#define ASSERT_DOES_NOT_COMPILE_1_(Id, Alias, AliasString, Expr, ExprString) \
- static auto Id(...)->std::true_type; \
- template <Alias> \
- static auto Id(int)->std::conditional_t<true, std::false_type, decltype(Expr)>; \
- static_assert(decltype(Id(0))::value, \
+#define ASSERT_DOES_NOT_COMPILE(Id, Alias, ...) \
+ ASSERT_DOES_NOT_COMPILE_1_(Id, Alias, #Alias, (__VA_ARGS__), #__VA_ARGS__)
+
+#define ASSERT_DOES_NOT_COMPILE_1_(Id, Alias, AliasString, Expr, ExprString) \
+ \
+ static std::true_type Id(...); \
+ \
+ template <Alias> \
+ static std::conditional_t<true, std::false_type, decltype(Expr)> Id(int); \
+ \
+ static_assert(decltype(Id(0))::value, \
"Expression '" ExprString "' [with " AliasString "] shouldn't compile.");
/**
@@ -621,9 +622,7 @@ private:
*/
class TestAssertionFailureException {
public:
- TestAssertionFailureException(const std::string& theFile,
- unsigned theLine,
- const std::string& theMessage);
+ TestAssertionFailureException(std::string file, unsigned line, std::string message);
const std::string& getFile() const {
return _file;
diff --git a/src/mongo/unittest/unittest_test.cpp b/src/mongo/unittest/unittest_test.cpp
index c660e0328ae..59ebe3a5f50 100644
--- a/src/mongo/unittest/unittest_test.cpp
+++ b/src/mongo/unittest/unittest_test.cpp
@@ -318,13 +318,14 @@ TEST(UnitTestSelfTest, ComparisonAssertionOverloadResolution) {
ASSERT_NE(x, "x");
}
-ASSERT_DOES_NOT_COMPILE(typename Char = char, *std::declval<Char>());
-ASSERT_DOES_NOT_COMPILE(bool B = false, std::enable_if_t<B, int>{});
+ASSERT_DOES_NOT_COMPILE(DoesNotCompileCheckDeclval, typename Char = char, *std::declval<Char>());
+ASSERT_DOES_NOT_COMPILE(DoesNotCompileCheckEnableIf, bool B = false, std::enable_if_t<B, int>{});
// Uncomment to check that it fails when it is supposed to. Unfortunately we can't check in a test
// that this fails when it is supposed to, only that it passes when it should.
//
-// ASSERT_DOES_NOT_COMPILE(typename Char = char, *std::declval<Char*>());
-// ASSERT_DOES_NOT_COMPILE(bool B = true, std::enable_if_t<B, int>{});
+// ASSERT_DOES_NOT_COMPILE(DoesNotCompileCheckDeclvalFail, typename Char = char,
+// *std::declval<Char*>()); ASSERT_DOES_NOT_COMPILE(DoesNotCompileCheckEnableIfFail, bool B = true,
+// std::enable_if_t<B, int>{});
} // namespace
diff --git a/src/mongo/util/concepts_test.cpp b/src/mongo/util/concepts_test.cpp
index 5991d6c2878..bf9efa260f8 100644
--- a/src/mongo/util/concepts_test.cpp
+++ b/src/mongo/util/concepts_test.cpp
@@ -74,7 +74,7 @@ constexpr inline auto sizeof_ = sizeof(T);
static_assert(std::is_void_v<decltype(NonTemplateTest<int32_t>::test())>);
static_assert(std::is_void_v<decltype(NonTemplateTest<int64_t>::test())>);
-ASSERT_DOES_NOT_COMPILE(typename Char = char, NonTemplateTest<Char>::test());
+ASSERT_DOES_NOT_COMPILE(CharNonTemplateTest, typename Char = char, NonTemplateTest<Char>::test());
// Uncomment to see error message.
// auto x = NonTemplateTest<char>::test();
@@ -122,12 +122,12 @@ Overload<13> requiresTest() {
ASSERT_SELECTS_OVERLOAD(2, requiresTest<char>());
ASSERT_SELECTS_OVERLOAD(3, requiresTest<int32_t>());
-ASSERT_DOES_NOT_COMPILE(typename Int64_t = int64_t, requiresTest<Int64_t>());
+ASSERT_DOES_NOT_COMPILE(Int64RequiresTest, typename Int64_t = int64_t, requiresTest<Int64_t>());
ASSERT_SELECTS_OVERLOAD(11, requiresTest<0>());
ASSERT_SELECTS_OVERLOAD(12, requiresTest<1>());
ASSERT_SELECTS_OVERLOAD(13, requiresTest<-1>());
-ASSERT_DOES_NOT_COMPILE(int i = -10, requiresTest<i>());
+ASSERT_DOES_NOT_COMPILE(IntRequiresTest, int i = -10, requiresTest<i>());
MONGO_MAKE_BOOL_TRAIT(isAddable,
(typename LHS, typename RHS),