diff options
author | Billy Donahue <billy.donahue@mongodb.com> | 2020-10-24 17:48:22 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-03-08 21:27:04 +0000 |
commit | 26d68fee401a76d27d18270becefe1f7cd8bae5c (patch) | |
tree | 64c0460692922bcdfa1d3d379f72744023446e61 | |
parent | 95e40bc43e1492fb329ae9b845314ccf24dcee18 (diff) | |
download | mongo-26d68fee401a76d27d18270becefe1f7cd8bae5c.tar.gz |
SERVER-58069 ASSERT_THAT: a matcher-based assert for unit tests
(cherry picked from commit da9ccccbda9538db40394f37d6f7446f33b39c0a)
-rw-r--r-- | src/mongo/platform/atomic_word_test.cpp | 12 | ||||
-rw-r--r-- | src/mongo/unittest/SConscript | 3 | ||||
-rw-r--r-- | src/mongo/unittest/assert_that.h | 62 | ||||
-rw-r--r-- | src/mongo/unittest/assert_that_test.cpp | 261 | ||||
-rw-r--r-- | src/mongo/unittest/matcher.cpp | 64 | ||||
-rw-r--r-- | src/mongo/unittest/matcher.h | 651 | ||||
-rw-r--r-- | src/mongo/unittest/matcher_core.cpp | 47 | ||||
-rw-r--r-- | src/mongo/unittest/matcher_core.h | 269 | ||||
-rw-r--r-- | src/mongo/unittest/unittest.cpp | 13 | ||||
-rw-r--r-- | src/mongo/unittest/unittest.h | 33 | ||||
-rw-r--r-- | src/mongo/unittest/unittest_test.cpp | 9 | ||||
-rw-r--r-- | src/mongo/util/concepts_test.cpp | 6 |
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 b7b8b5201b0..d7d16c4c0b6 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), |