From da9ccccbda9538db40394f37d6f7446f33b39c0a Mon Sep 17 00:00:00 2001 From: Billy Donahue Date: Sat, 24 Oct 2020 17:48:22 +0000 Subject: SERVER-58069 ASSERT_THAT: a matcher-based assert for unit tests --- src/mongo/platform/atomic_word_test.cpp | 12 +- src/mongo/unittest/SConscript | 3 + src/mongo/unittest/assert_that.h | 62 +++ src/mongo/unittest/assert_that_test.cpp | 261 +++++++++++++ src/mongo/unittest/matcher.cpp | 64 ++++ src/mongo/unittest/matcher.h | 651 ++++++++++++++++++++++++++++++++ src/mongo/unittest/matcher_core.cpp | 47 +++ src/mongo/unittest/matcher_core.h | 269 +++++++++++++ src/mongo/unittest/unittest.cpp | 13 +- src/mongo/unittest/unittest.h | 33 +- src/mongo/unittest/unittest_test.cpp | 9 +- src/mongo/util/concepts_test.cpp | 6 +- 12 files changed, 1393 insertions(+), 37 deletions(-) create mode 100644 src/mongo/unittest/assert_that.h create mode 100644 src/mongo/unittest/assert_that_test.cpp create mode 100644 src/mongo/unittest/matcher.cpp create mode 100644 src/mongo/unittest/matcher.h create mode 100644 src/mongo/unittest/matcher_core.cpp create mode 100644 src/mongo/unittest/matcher_core.h 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().fetchAndBitAnd(0)); -ASSERT_DOES_NOT_COMPILE(typename T = int, AtomicWord().fetchAndBitOr(0)); -ASSERT_DOES_NOT_COMPILE(typename T = int, AtomicWord().fetchAndBitXor(0)); +ASSERT_DOES_NOT_COMPILE(CharFetchAndBitAnd, typename T = int, AtomicWord().fetchAndBitAnd(0)); +ASSERT_DOES_NOT_COMPILE(CharFetchAndBitOr, typename T = int, AtomicWord().fetchAndBitOr(0)); +ASSERT_DOES_NOT_COMPILE(CharFetchAndBitXor, typename T = int, AtomicWord().fetchAndBitXor(0)); -ASSERT_DOES_NOT_COMPILE(typename T = char, AtomicWord().fetchAndBitAnd(0)); -ASSERT_DOES_NOT_COMPILE(typename T = char, AtomicWord().fetchAndBitOr(0)); -ASSERT_DOES_NOT_COMPILE(typename T = char, AtomicWord().fetchAndBitXor(0)); +ASSERT_DOES_NOT_COMPILE(IntFetchAndBitAnd, typename T = char, AtomicWord().fetchAndBitAnd(0)); +ASSERT_DOES_NOT_COMPILE(IntFetchAndBitOr, typename T = char, AtomicWord().fetchAndBitOr(0)); +ASSERT_DOES_NOT_COMPILE(IntFetchAndBitXor, typename T = char, AtomicWord().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 + * . + * + * 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 + +#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 + * . + * + * 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 +#include +#include +#include + +#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{111, 222, 333})); + ASSERT_FALSE(m.match(std::vector{111, 222, 333, 444})); + ASSERT_FALSE(m.match(std::vector{111, 222, 444})); + ASSERT_FALSE(m.match(std::vector{111, 222, 444})); + { + auto failStr = GET_FAILURE_STRING(std::vector({111, 222, 444}), m); + ASSERT_EQ(failStr, + "value: std::vector({111, 222, 444})" + ", actual: [111, 222, 444]" + ", failed: [2]" + ", expected: ElementsAre(Eq(111), Eq(222), Eq(333))"); + } + { + auto failStr = GET_FAILURE_STRING(std::vector({111, 222}), m); + ASSERT_EQ(failStr, + "value: std::vector({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 myVec{111, 222, 333}; + std::list myList{111, 222, 333}; + ASSERT_THAT(myVec, Eq(std::vector{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 + * . + * + * 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 +#include + +#include +#include + +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(std::move(pattern))} {} + +ContainsRegex::~ContainsRegex() = default; + +MatchResult ContainsRegex::match(StringData x) const { + bool res = + _impl->re.PartialMatch(pcrecpp::StringPiece{x.rawData(), static_cast(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 + * . + * + * 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 +#include +#include +#include +#include +#include + +#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> vec{Eq(123), AllOf(Gt(100), Lt(200))}; + **/ +template +class TypeErasedMatcher { +public: + using value_type = T; + + template + explicit TypeErasedMatcher(const M& m) : _m{std::make_shared>(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 + class TypedMatch : public BasicMatch { + template + using CanMatchOp = decltype(std::declval().match(std::declval())); + + 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) { + 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 _m; +}; + +/** Always true: matches any value of any type. */ +class Any : public Matcher { +public: + std::string describe() const { + return "Any"; + } + + template + 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 +MatchResult typeTolerantMatch(const M& m, const T& v) { + return TypeErasedMatcher(m).match(v); +} + +template