diff options
-rw-r--r-- | src/mongo/unittest/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/unittest/assert.h | 547 | ||||
-rw-r--r-- | src/mongo/unittest/assert_that.h | 2 | ||||
-rw-r--r-- | src/mongo/unittest/assert_that_test.cpp | 4 | ||||
-rw-r--r-- | src/mongo/unittest/bson_test_util.h | 2 | ||||
-rw-r--r-- | src/mongo/unittest/death_test.cpp | 4 | ||||
-rw-r--r-- | src/mongo/unittest/death_test.h | 2 | ||||
-rw-r--r-- | src/mongo/unittest/framework.h | 444 | ||||
-rw-r--r-- | src/mongo/unittest/matcher.h | 7 | ||||
-rw-r--r-- | src/mongo/unittest/matcher_core.h | 105 | ||||
-rw-r--r-- | src/mongo/unittest/stringify.cpp (renamed from src/mongo/unittest/matcher_core.cpp) | 10 | ||||
-rw-r--r-- | src/mongo/unittest/stringify.h | 154 | ||||
-rw-r--r-- | src/mongo/unittest/unittest.h | 902 | ||||
-rw-r--r-- | src/mongo/unittest/unittest_test.cpp | 161 |
14 files changed, 1226 insertions, 1120 deletions
diff --git a/src/mongo/unittest/SConscript b/src/mongo/unittest/SConscript index 3c5bb2b21ea..9d3dedeca8c 100644 --- a/src/mongo/unittest/SConscript +++ b/src/mongo/unittest/SConscript @@ -27,7 +27,7 @@ utEnv.Library( 'death_test.cpp', 'golden_test.cpp', 'matcher.cpp', - 'matcher_core.cpp', + 'stringify.cpp', 'temp_dir.cpp', 'unittest.cpp', ], diff --git a/src/mongo/unittest/assert.h b/src/mongo/unittest/assert.h new file mode 100644 index 00000000000..c70ccc644ad --- /dev/null +++ b/src/mongo/unittest/assert.h @@ -0,0 +1,547 @@ +/** + * Copyright (C) 2022-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. + */ + +/* + * ASSERTion macros for the C++ unit testing framework. + */ + +#pragma once + +#include <cmath> +#include <fmt/format.h> +#include <functional> +#include <sstream> +#include <string> +#include <tuple> +#include <type_traits> +#include <utility> +#include <vector> + +#include "mongo/base/status_with.h" +#include "mongo/base/string_data.h" +#include "mongo/logv2/log_debug.h" +#include "mongo/logv2/log_detail.h" +#include "mongo/unittest/bson_test_util.h" +#include "mongo/unittest/framework.h" +#include "mongo/unittest/stringify.h" +#include "mongo/unittest/test_info.h" +#include "mongo/util/assert_util.h" +#include "mongo/util/str.h" + +/** + * Fail unconditionally, reporting the given message. + */ +#define FAIL(MESSAGE) ::mongo::unittest::TestAssertionFailure(__FILE__, __LINE__, MESSAGE).stream() + +/** + * Fails unless "EXPRESSION" is true. + */ +#define ASSERT_TRUE(EXPRESSION) \ + if (EXPRESSION) { \ + } else \ + FAIL("Expected: " #EXPRESSION) + +#define ASSERT(EXPRESSION) ASSERT_TRUE(EXPRESSION) + +/** + * Fails if "EXPRESSION" is true. + */ +#define ASSERT_FALSE(EXPRESSION) ASSERT(!(EXPRESSION)) + +/** + * Asserts that a Status code is OK. + */ +#define ASSERT_OK(EXPRESSION) ASSERT_EQUALS(::mongo::Status::OK(), (EXPRESSION)) + +/** + * Asserts that a status code is anything but OK. + */ +#define ASSERT_NOT_OK(EXPRESSION) ASSERT_NOT_EQUALS(::mongo::Status::OK(), (EXPRESSION)) + +/* + * Binary comparison assertions. + */ +#define ASSERT_EQUALS(a, b) ASSERT_EQ(a, b) +#define ASSERT_NOT_EQUALS(a, b) ASSERT_NE(a, b) +#define ASSERT_LESS_THAN(a, b) ASSERT_LT(a, b) +#define ASSERT_NOT_LESS_THAN(a, b) ASSERT_GTE(a, b) +#define ASSERT_GREATER_THAN(a, b) ASSERT_GT(a, b) +#define ASSERT_NOT_GREATER_THAN(a, b) ASSERT_LTE(a, b) +#define ASSERT_LESS_THAN_OR_EQUALS(a, b) ASSERT_LTE(a, b) +#define ASSERT_GREATER_THAN_OR_EQUALS(a, b) ASSERT_GTE(a, b) + +#define ASSERT_EQ(a, b) ASSERT_COMPARISON_(kEq, a, b) +#define ASSERT_NE(a, b) ASSERT_COMPARISON_(kNe, a, b) +#define ASSERT_LT(a, b) ASSERT_COMPARISON_(kLt, a, b) +#define ASSERT_LTE(a, b) ASSERT_COMPARISON_(kLe, a, b) +#define ASSERT_GT(a, b) ASSERT_COMPARISON_(kGt, a, b) +#define ASSERT_GTE(a, b) ASSERT_COMPARISON_(kGe, a, b) + +/** + * Binary comparison utility macro. Do not use directly. + */ +#define ASSERT_COMPARISON_(OP, a, b) ASSERT_COMPARISON_STR_(OP, a, b, #a, #b) + +#define ASSERT_COMPARISON_STR_(OP, a, b, aExpr, bExpr) \ + if (auto ca = \ + ::mongo::unittest::ComparisonAssertion<::mongo::unittest::ComparisonOp::OP>::make( \ + __FILE__, __LINE__, aExpr, bExpr, a, b); \ + !ca) { \ + } else \ + ca.failure().stream() + +/** + * Approximate equality assertion. Useful for comparisons on limited precision floating point + * values. + */ +#define ASSERT_APPROX_EQUAL(a, b, ABSOLUTE_ERR) ASSERT_LTE(std::abs((a) - (b)), ABSOLUTE_ERR) + +/** + * Assert a function call returns its input unchanged. + */ +#define ASSERT_IDENTITY(INPUT, FUNCTION) \ + if (auto ca = \ + [&](const auto& v, const auto& fn) { \ + return ::mongo::unittest::ComparisonAssertion< \ + ::mongo::unittest::ComparisonOp::kEq>::make(__FILE__, \ + __LINE__, \ + #INPUT, \ + #FUNCTION "(" #INPUT ")", \ + v, \ + fn(v)); \ + }(INPUT, [&](auto&& x) { return FUNCTION(x); }); \ + !ca) { \ + } else \ + ca.failure().stream() + +/** + * Verify that the evaluation of "EXPRESSION" throws an exception of type EXCEPTION_TYPE. + * + * If "EXPRESSION" throws no exception, or one that is neither of type "EXCEPTION_TYPE" nor + * of a subtype of "EXCEPTION_TYPE", the test is considered a failure and further evaluation + * halts. + */ +#define ASSERT_THROWS(EXPRESSION, EXCEPTION_TYPE) \ + ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, ([](const EXCEPTION_TYPE&) {})) + +/** + * Verify that the evaluation of "EXPRESSION" does not throw any exceptions. + * + * If "EXPRESSION" throws an exception the test is considered a failure and further evaluation + * halts. + */ +#define ASSERT_DOES_NOT_THROW(EXPRESSION) \ + try { \ + EXPRESSION; \ + } catch (const AssertionException& e) { \ + str::stream err; \ + err << "Threw an exception incorrectly: " << e.toString(); \ + FAIL(err); \ + } + +/** + * Behaves like ASSERT_THROWS, above, but also fails if calling what() on the thrown exception + * does not return a string equal to EXPECTED_WHAT. + */ +#define ASSERT_THROWS_WHAT(EXPRESSION, EXCEPTION_TYPE, EXPECTED_WHAT) \ + ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, ([&](const EXCEPTION_TYPE& ex) { \ + ASSERT_EQ(::mongo::StringData(ex.what()), \ + ::mongo::StringData(EXPECTED_WHAT)); \ + })) + +/** + * Behaves like ASSERT_THROWS, above, but also fails if calling getCode() on the thrown exception + * does not return an error code equal to EXPECTED_CODE. + */ +#define ASSERT_THROWS_CODE(EXPRESSION, EXCEPTION_TYPE, EXPECTED_CODE) \ + ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, ([&](const EXCEPTION_TYPE& ex) { \ + ASSERT_EQ(ex.toStatus().code(), EXPECTED_CODE); \ + })) + +/** + * Behaves like ASSERT_THROWS, above, but also fails if calling getCode() on the thrown exception + * does not return an error code equal to EXPECTED_CODE or if calling what() on the thrown exception + * does not return a string equal to EXPECTED_WHAT. + */ +#define ASSERT_THROWS_CODE_AND_WHAT(EXPRESSION, EXCEPTION_TYPE, EXPECTED_CODE, EXPECTED_WHAT) \ + ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, ([&](const EXCEPTION_TYPE& ex) { \ + ASSERT_EQ(ex.toStatus().code(), EXPECTED_CODE); \ + ASSERT_EQ(::mongo::StringData(ex.what()), \ + ::mongo::StringData(EXPECTED_WHAT)); \ + })) + + +/** + * Compiles if expr doesn't compile. + * + * This only works for compile errors in the "immediate context" of the expression, which matches + * the rules for SFINAE. The first argument is a defaulted template parameter that is used in the + * expression to make it dependent. This only works with expressions, not statements, although you + * can separate multiple expressions with a comma. + * + * This should be used at namespace scope, not inside a TEST function. + * + * Examples that pass: + * 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(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(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."); + +/** + * This internal helper is used to ignore warnings about unused results. Some unit tests which test + * `ASSERT_THROWS` and its variations are used on functions which both throw and return `Status` or + * `StatusWith` objects. Although such function designs are undesirable, they do exist, presently. + * Therefore this internal helper macro is used by `ASSERT_THROWS` and its variations to silence + * such warnings without forcing the caller to invoke `.ignore()` on the called function. + * + * NOTE: This macro should NOT be used inside regular unit test code to ignore unchecked `Status` or + * `StatusWith` instances -- if a `Status` or `StatusWith` result is to be ignored, please use the + * normal `.ignore()` code. This macro exists only to make using `ASSERT_THROWS` less inconvenient + * on functions which both throw and return `Status` or `StatusWith`. + */ +#define UNIT_TEST_INTERNALS_IGNORE_UNUSED_RESULT_WARNINGS(EXPRESSION) \ + do { \ + (void)(EXPRESSION); \ + } while (false) + +/** + * Behaves like ASSERT_THROWS, above, but also calls CHECK(caughtException) which may contain + * additional assertions. + */ +#define ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, CHECK) \ + if ([&] { \ + try { \ + UNIT_TEST_INTERNALS_IGNORE_UNUSED_RESULT_WARNINGS(EXPRESSION); \ + return false; \ + } catch (const EXCEPTION_TYPE& ex) { \ + CHECK(ex); \ + return true; \ + } \ + }()) { \ + } else \ + /* Fail outside of the try/catch, this way the code in the `FAIL` macro */ \ + /* doesn't have the potential to throw an exception which we might also */ \ + /* be checking for. */ \ + FAIL("Expected expression " #EXPRESSION " to throw " #EXCEPTION_TYPE \ + " but it threw nothing.") + +#define ASSERT_STRING_CONTAINS(BIG_STRING, CONTAINS) \ + if (auto tup_ = std::tuple(std::string(BIG_STRING), std::string(CONTAINS)); \ + std::get<0>(tup_).find(std::get<1>(tup_)) != std::string::npos) { \ + } else \ + FAIL(([&] { \ + const auto& [haystack, sub] = tup_; \ + return format(FMT_STRING("Expected to find {} ({}) in {} ({})"), \ + #CONTAINS, \ + sub, \ + #BIG_STRING, \ + haystack); \ + }())) + +#define ASSERT_STRING_OMITS(BIG_STRING, OMITS) \ + if (auto tup_ = std::tuple(std::string(BIG_STRING), std::string(OMITS)); \ + std::get<0>(tup_).find(std::get<1>(tup_)) == std::string::npos) { \ + } else \ + FAIL(([&] { \ + const auto& [haystack, omits] = tup_; \ + return format(FMT_STRING("Did not expect to find {} ({}) in {} ({})"), \ + #OMITS, \ + omits, \ + #BIG_STRING, \ + haystack); \ + }())) + +#define ASSERT_STRING_SEARCH_REGEX(BIG_STRING, REGEX) \ + if (auto tup_ = std::tuple(std::string(BIG_STRING), std::string(REGEX)); \ + ::mongo::unittest::searchRegex(std::get<1>(tup_), std::get<0>(tup_))) { \ + } else \ + FAIL(([&] { \ + const auto& [haystack, sub] = tup_; \ + return format(FMT_STRING("Expected to find regular expression {} /{}/ in {} ({})"), \ + #REGEX, \ + sub, \ + #BIG_STRING, \ + haystack); \ + }())) + +namespace mongo::unittest { + +bool searchRegex(const std::string& pattern, const std::string& string); + +class Result; + +/** + * Exception thrown when a test assertion fails. + * + * Typically thrown by helpers in the TestAssertion class and its ilk, below. + * + * NOTE(schwerin): This intentionally does _not_ extend std::exception, so that code under + * test that (foolishly?) catches std::exception won't swallow test failures. Doesn't + * protect you from code that foolishly catches ..., but you do what you can. + */ +class TestAssertionFailureException { +public: + TestAssertionFailureException(std::string file, unsigned line, std::string message); + + const std::string& getFile() const { + return _file; + } + unsigned getLine() const { + return _line; + } + const std::string& getMessage() const { + return _message; + } + void setMessage(const std::string& message) { + _message = message; + } + + const std::string& what() const { + return getMessage(); + } + + std::string toString() const; + + const std::string& getStacktrace() const { + return _stacktrace; + } + +private: + std::string _file; + unsigned _line; + std::string _message; + std::string _stacktrace; +}; + +class TestAssertionFailure { +public: + TestAssertionFailure(const std::string& file, unsigned line, const std::string& message); + TestAssertionFailure(const TestAssertionFailure& other); + ~TestAssertionFailure() noexcept(false); + + TestAssertionFailure& operator=(const TestAssertionFailure& other); + + std::ostream& stream(); + +private: + TestAssertionFailureException _exception; + std::ostringstream _stream; + bool _enabled; +}; + +enum class ComparisonOp { kEq, kNe, kLt, kLe, kGt, kGe }; + +template <ComparisonOp op> +class ComparisonAssertion { +private: + static constexpr auto comparator() { + if constexpr (op == ComparisonOp::kEq) { + return std::equal_to<>{}; + } else if constexpr (op == ComparisonOp::kNe) { + return std::not_equal_to<>{}; + } else if constexpr (op == ComparisonOp::kLt) { + return std::less<>{}; + } else if constexpr (op == ComparisonOp::kLe) { + return std::less_equal<>{}; + } else if constexpr (op == ComparisonOp::kGt) { + return std::greater<>{}; + } else if constexpr (op == ComparisonOp::kGe) { + return std::greater_equal<>{}; + } + } + + static constexpr StringData name() { + if constexpr (op == ComparisonOp::kEq) { + return "=="_sd; + } else if constexpr (op == ComparisonOp::kNe) { + return "!="_sd; + } else if constexpr (op == ComparisonOp::kLt) { + return "<"_sd; + } else if constexpr (op == ComparisonOp::kLe) { + return "<="_sd; + } else if constexpr (op == ComparisonOp::kGt) { + return ">"_sd; + } else if constexpr (op == ComparisonOp::kGe) { + return ">="_sd; + } + } + + template <typename A, typename B> + MONGO_COMPILER_NOINLINE ComparisonAssertion(const char* theFile, + unsigned theLine, + StringData aExpression, + StringData bExpression, + const A& a, + const B& b) { + if (comparator()(a, b)) { + return; + } + _assertion = std::make_unique<TestAssertionFailure>( + theFile, + theLine, + format(FMT_STRING("Expected {1} {0} {2} ({3} {0} {4})"), + name(), + aExpression, + bExpression, + stringify::stringifyForAssert(a), + stringify::stringifyForAssert(b))); + } + +public: + // Use a single implementation (identical to the templated one) for all string-like types. + // This is particularly important to avoid making unique instantiations for each length of + // string literal. + static ComparisonAssertion make(const char* theFile, + unsigned theLine, + StringData aExpression, + StringData bExpression, + StringData a, + StringData b); + + + // Use a single implementation (identical to the templated one) for all pointer and array types. + // Note: this is selected instead of the StringData overload for char* and string literals + // because they are supposed to compare pointers, not contents. + static ComparisonAssertion make(const char* theFile, + unsigned theLine, + StringData aExpression, + StringData bExpression, + const void* a, + const void* b); + TEMPLATE(typename A, typename B) + REQUIRES(!(std::is_convertible_v<A, StringData> && std::is_convertible_v<B, StringData>)&& // + !(std::is_pointer_v<A> && std::is_pointer_v<B>)&& // + !(std::is_array_v<A> && std::is_array_v<B>)) + static ComparisonAssertion make(const char* theFile, + unsigned theLine, + StringData aExpression, + StringData bExpression, + const A& a, + const B& b) { + return ComparisonAssertion(theFile, theLine, aExpression, bExpression, a, b); + } + + explicit operator bool() const { + return static_cast<bool>(_assertion); + } + TestAssertionFailure failure() { + return *_assertion; + } + +private: + std::unique_ptr<TestAssertionFailure> _assertion; +}; + +// Explicit instantiation of ComparisonAssertion ctor and factory, for "A OP B". +#define TEMPLATE_COMPARISON_ASSERTION_CTOR_A_OP_B(EXTERN, OP, A, B) \ + EXTERN template ComparisonAssertion<ComparisonOp::OP>::ComparisonAssertion( \ + const char*, unsigned, StringData, StringData, const A&, const B&); \ + EXTERN template ComparisonAssertion<ComparisonOp::OP> \ + ComparisonAssertion<ComparisonOp::OP>::make( \ + const char*, unsigned, StringData, StringData, const A&, const B&); + +// Explicit instantiation of ComparisonAssertion ctor and factory for a pair of types. +#define TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(EXTERN, OP, A, B) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_A_OP_B(EXTERN, OP, A, B) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_A_OP_B(EXTERN, OP, B, A) + +// Explicit instantiation of ComparisonAssertion ctor and factory for a single type. +#define TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(EXTERN, OP, T) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_A_OP_B(EXTERN, OP, T, T) + +// Call with `extern` to declace extern instantiations, and with no args to explicitly instantiate. +#define INSTANTIATE_COMPARISON_ASSERTION_CTORS(...) \ + __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kEq>; \ + __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kNe>; \ + __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kGt>; \ + __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kGe>; \ + __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kLt>; \ + __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kLe>; \ + \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, int) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, long) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, long long) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, unsigned int) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, unsigned long) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, unsigned long long) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, bool) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, double) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, OID) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, BSONType) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, Timestamp) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, Date_t) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, Status) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, ErrorCodes::Error) \ + \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, int, long) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, int, long long) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, long, long long) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, unsigned int, unsigned long) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, Status, ErrorCodes::Error) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, ErrorCodes::Error, int) \ + \ + /* These are the only types that are often used with ASSERT_NE*/ \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kNe, Status) \ + TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kNe, unsigned long) + +// Declare that these definitions will be provided in unittest.cpp. +INSTANTIATE_COMPARISON_ASSERTION_CTORS(extern); + +/** + * Get the value out of a StatusWith<T>, or throw an exception if it is not OK. + */ +template <typename T> +const T& assertGet(const StatusWith<T>& swt) { + ASSERT_OK(swt.getStatus()); + return swt.getValue(); +} + +template <typename T> +T assertGet(StatusWith<T>&& swt) { + ASSERT_OK(swt.getStatus()); + return std::move(swt.getValue()); +} + +} // namespace mongo::unittest diff --git a/src/mongo/unittest/assert_that.h b/src/mongo/unittest/assert_that.h index ab1d3cbf75c..9a79871cc27 100644 --- a/src/mongo/unittest/assert_that.h +++ b/src/mongo/unittest/assert_that.h @@ -31,9 +31,9 @@ #include <tuple> +#include "mongo/unittest/assert.h" #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`. diff --git a/src/mongo/unittest/assert_that_test.cpp b/src/mongo/unittest/assert_that_test.cpp index 27febc03fbe..a20ec42197a 100644 --- a/src/mongo/unittest/assert_that_test.cpp +++ b/src/mongo/unittest/assert_that_test.cpp @@ -249,9 +249,9 @@ TEST(AssertThat, UnprintableValues) { struct Unprintable { int i; } v{123}; - std::string lastResort = detail::lastResortFormat(typeid(v), &v, sizeof(v)); + std::string lastResort = stringify::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. + using stringify::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); diff --git a/src/mongo/unittest/bson_test_util.h b/src/mongo/unittest/bson_test_util.h index 2e7c928c339..fb8bfe4ce3b 100644 --- a/src/mongo/unittest/bson_test_util.h +++ b/src/mongo/unittest/bson_test_util.h @@ -31,7 +31,7 @@ #include "mongo/bson/simple_bsonelement_comparator.h" #include "mongo/bson/simple_bsonobj_comparator.h" -#include "mongo/unittest/unittest.h" +#include "mongo/unittest/assert.h" /** * BSON comparison utility macro. Do not use directly. diff --git a/src/mongo/unittest/death_test.cpp b/src/mongo/unittest/death_test.cpp index d7f48112953..6df47dece01 100644 --- a/src/mongo/unittest/death_test.cpp +++ b/src/mongo/unittest/death_test.cpp @@ -29,11 +29,13 @@ #include "mongo/platform/basic.h" +#include "mongo/unittest/death_test.h" + #include <fmt/format.h> #include <stdio.h> #include "mongo/bson/json.h" -#include "mongo/unittest/death_test.h" +#include "mongo/unittest/assert.h" #include "mongo/unittest/temp_dir.h" #include "mongo/util/exit_code.h" diff --git a/src/mongo/unittest/death_test.h b/src/mongo/unittest/death_test.h index 4e1643625e5..cbdf8778d75 100644 --- a/src/mongo/unittest/death_test.h +++ b/src/mongo/unittest/death_test.h @@ -33,7 +33,7 @@ #include <memory> #include <string> -#include "mongo/unittest/unittest.h" +#include "mongo/unittest/framework.h" /** * Constructs a single death test named `TEST_NAME` within the test suite `SUITE_NAME`. diff --git a/src/mongo/unittest/framework.h b/src/mongo/unittest/framework.h new file mode 100644 index 00000000000..6f0a4f85c89 --- /dev/null +++ b/src/mongo/unittest/framework.h @@ -0,0 +1,444 @@ +/** + * 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. + */ + +/* + * A C++ unit testing framework. + * + * Most users will include the umbrella header "mongo/unittest/unittest.h". + * + * For examples of basic usage, see mongo/unittest/unittest_test.cpp. + * + * ASSERT macros and supporting definitions are in mongo/unittest/assert.h. + * + */ + +#pragma once + +#include <boost/optional.hpp> +#include <cmath> +#include <fmt/format.h> +#include <functional> +#include <optional> +#include <sstream> +#include <string> +#include <tuple> +#include <type_traits> +#include <utility> +#include <vector> + +#include "mongo/base/status_with.h" +#include "mongo/base/string_data.h" +#include "mongo/logv2/log_debug.h" +#include "mongo/logv2/log_detail.h" +#include "mongo/unittest/test_info.h" +#include "mongo/util/assert_util.h" +#include "mongo/util/optional_util.h" +#include "mongo/util/str.h" + +/** + * Construct a single test, named `TEST_NAME` within the test Suite `SUITE_NAME`. + * + * Usage: + * + * TEST(MyModuleTests, TestThatFooFailsOnErrors) { + * ASSERT_EQUALS(error_success, foo(invalidValue)); + * } + */ +#define TEST(SUITE_NAME, TEST_NAME) \ + UNIT_TEST_DETAIL_DEFINE_TEST_(SUITE_NAME, TEST_NAME, ::mongo::unittest::Test) + +/** + * Construct a single test named TEST_NAME that has access to a common class (a "fixture") + * named "FIXTURE_NAME". FIXTURE_NAME will be the name of the Suite in which the test appears. + * + * Usage: + * + * class FixtureClass : public mongo::unittest::Test { + * protected: + * int myVar; + * void setUp() { myVar = 10; } + * }; + * + * TEST(FixtureClass, TestThatUsesFixture) { + * ASSERT_EQUALS(10, myVar); + * } + */ +#define TEST_F(FIXTURE_NAME, TEST_NAME) \ + UNIT_TEST_DETAIL_DEFINE_TEST_(FIXTURE_NAME, TEST_NAME, FIXTURE_NAME) + +#define UNIT_TEST_DETAIL_DEFINE_TEST_(SUITE_NAME, TEST_NAME, TEST_BASE) \ + UNIT_TEST_DETAIL_DEFINE_TEST_PRIMITIVE_( \ + SUITE_NAME, TEST_NAME, UNIT_TEST_DETAIL_TEST_TYPE_NAME(SUITE_NAME, TEST_NAME), TEST_BASE) + +#define UNIT_TEST_DETAIL_DEFINE_TEST_PRIMITIVE_(FIXTURE_NAME, TEST_NAME, TEST_TYPE, TEST_BASE) \ + class TEST_TYPE : public TEST_BASE { \ + private: \ + void _doTest() override; \ + static inline const ::mongo::unittest::TestInfo _testInfo{ \ + #FIXTURE_NAME, #TEST_NAME, __FILE__, __LINE__}; \ + static inline const RegistrationAgent<TEST_TYPE> _agent{&_testInfo}; \ + }; \ + void TEST_TYPE::_doTest() + +/** + * Macro to construct a type name for a test, from its `SUITE_NAME` and `TEST_NAME`. + * Do not use directly in test code. + */ +#define UNIT_TEST_DETAIL_TEST_TYPE_NAME(SUITE_NAME, TEST_NAME) \ + UnitTest_SuiteName##SUITE_NAME##TestName##TEST_NAME + +namespace mongo::unittest { + +class Result; + +/** + * Representation of a collection of tests. + * + * One Suite is constructed for each SUITE_NAME when using the TEST macro. + * + * See `OldStyleSuiteSpecification` which adapts dbtests into this framework. + */ +class Suite : public std::enable_shared_from_this<Suite> { +private: + struct SuiteTest { + std::string name; + std::string fileName; + std::function<void()> fn; + }; + + struct ConstructorEnable { + explicit ConstructorEnable() = default; + }; + +public: + explicit Suite(ConstructorEnable, std::string name); + Suite(const Suite&) = delete; + Suite& operator=(const Suite&) = delete; + + void add(std::string name, std::string fileName, std::function<void()> testFn); + + std::unique_ptr<Result> run(const std::string& filter, + const std::string& fileNameFilter, + int runsPerTest); + + static int run(const std::vector<std::string>& suites, + const std::string& filter, + const std::string& fileNameFilter, + int runsPerTest); + + /** + * Get a suite with the given name, creating and registering it if necessary. + * This is the only way to make a Suite object. + * + * Safe to call during static initialization. + */ + static Suite& getSuite(StringData name); + +private: + /** Points to the string data of the _name field. */ + StringData key() const { + return _name; + } + + std::string _name; + std::vector<SuiteTest> _tests; +}; + +/** + * Adaptor to set up a Suite from a dbtest-style suite. + * Support for deprecated dbtest-style test suites. Tests are are added by overriding setupTests() + * in a subclass of OldStyleSuiteSpecification, and defining an OldStyleSuiteInstance<T> object. + * This approach is + * deprecated. + * + * Example: + * class All : public OldStyleSuiteSpecification { + * public: + * All() : OldStyleSuiteSpecification("BunchaTests") {} + * void setupTests() { + * add<TestThis>(); + * add<TestThat>(); + * add<TestTheOtherThing>(); + * } + * }; + * OldStyleSuiteInitializer<All> all; + */ +class OldStyleSuiteSpecification { +public: + struct SuiteTest { + std::string name; + std::function<void()> fn; + }; + + OldStyleSuiteSpecification(std::string name) : _name(std::move(name)) {} + virtual ~OldStyleSuiteSpecification() = default; + + // Note: setupTests() is run by a OldStyleSuiteInitializer at static initialization time. + // It should in most cases be just a simple sequence of add<T>() calls. + virtual void setupTests() = 0; + + const std::string& name() const { + return _name; + } + + const std::vector<SuiteTest>& tests() const { + return _tests; + } + + /** + * Add an old-style test of type `T` to this Suite, saving any test constructor args + * that would be needed at test run time. + * The added test's name will be synthesized as the demangled typename of T. + * At test run time, the test will be created and run with `T(args...).run()`. + */ + template <typename T, typename... Args> + void add(Args&&... args) { + addNameCallback(nameForTestClass<T>(), [=] { T(args...).run(); }); + } + + void addNameCallback(std::string name, std::function<void()> cb) { + _tests.push_back({std::move(name), std::move(cb)}); + } + + template <typename T> + static std::string nameForTestClass() { + return demangleName(typeid(T)); + } + +private: + std::string _name; + std::vector<SuiteTest> _tests; +}; + +/** + * Define a namespace-scope instance of `OldStyleSuiteInitializer<T>` to properly create and + * initialize an instance of `T` (an `OldStyleSuiteSpecification`). See + * `OldStyleSuiteSpecification`. + */ +template <typename T> +struct OldStyleSuiteInitializer { + template <typename... Args> + explicit OldStyleSuiteInitializer(Args&&... args) { + T t(std::forward<Args>(args)...); + init(t); + } + + void init(OldStyleSuiteSpecification& suiteSpec) const { + suiteSpec.setupTests(); + auto& suite = Suite::getSuite(suiteSpec.name()); + for (auto&& t : suiteSpec.tests()) { + suite.add(t.name, "", t.fn); + } + } +}; + +/** + * UnitTest singleton class. Provides access to information about current execution state. + */ +class UnitTest { + UnitTest() = default; + +public: + static UnitTest* getInstance(); + + UnitTest(const UnitTest& other) = delete; + UnitTest& operator=(const UnitTest&) = delete; + +public: + /** + * Returns the currently running test, or `nullptr` if a test is not running. + */ + const TestInfo* currentTestInfo() const; + +public: + /** + * Used to set/unset currently running test information. + */ + class TestRunScope { + public: + explicit TestRunScope(const TestInfo* testInfo) { + UnitTest::getInstance()->setCurrentTestInfo(testInfo); + } + + ~TestRunScope() { + UnitTest::getInstance()->setCurrentTestInfo(nullptr); + } + }; + +private: + /** + * Sets the currently running tests. Internal: should only be used by unit test framework. + * testInfo - test info of the currently running test, or `nullptr` is a test is not running. + */ + void setCurrentTestInfo(const TestInfo* testInfo); + +private: + const TestInfo* _currentTestInfo = nullptr; +}; + +/** + * Base type for unit test fixtures. Also, the default fixture type used + * by the TEST() macro. + */ +class Test { +public: + Test(); + virtual ~Test(); + Test(const Test&) = delete; + Test& operator=(const Test&) = delete; + + void run(); + + /** + * Called on the test object before running the test. + */ + virtual void setUp() {} + + /** + * Called on the test object after running the test. + */ + virtual void tearDown() {} + +protected: + /** + * Adds a Test to a Suite, used by TEST/TEST_F macros. + */ + template <typename T> + class RegistrationAgent { + public: + /** + * These TestInfo must point to data that outlives this RegistrationAgent. + * In the case of TEST/TEST_F, these are static variables. + */ + explicit RegistrationAgent(const TestInfo* testInfo) : _testInfo{testInfo} { + Suite::getSuite(_testInfo->suiteName()) + .add( + std::string{_testInfo->testName()}, std::string{_testInfo->file()}, [testInfo] { + UnitTest::TestRunScope trs(testInfo); + T{}.run(); + }); + } + + StringData getSuiteName() const { + return _testInfo->suiteName(); + } + + StringData getTestName() const { + return _testInfo->testName(); + } + + StringData getFileName() const { + return _testInfo->file(); + } + + private: + const TestInfo* _testInfo; + }; + + /** + * This exception class is used to exercise the testing framework itself. If a test + * case throws it, the framework would not consider it an error. + */ + class FixtureExceptionForTesting : public std::exception {}; + + /** + * Starts capturing messages logged by code under test. + * + * Log messages will still also go to their default destination; this + * code simply adds an additional sink for log messages. + * + * Clears any previously captured log lines. + */ + void startCapturingLogMessages(); + + /** + * Stops capturing log messages logged by code under test. + */ + void stopCapturingLogMessages(); + + /** + * Gets a vector of strings, one log line per string, captured since + * the last call to startCapturingLogMessages() in this test. + */ + const std::vector<std::string>& getCapturedTextFormatLogMessages() const; + std::vector<BSONObj> getCapturedBSONFormatLogMessages() const; + + /** + * Returns the number of collected log lines containing "needle". + */ + int64_t countTextFormatLogLinesContaining(const std::string& needle); + + /** + * Returns the number of collected log lines where "needle" is a subset of a line. + * + * Does a Depth-First-Search of a BSON document. Validates each element in "needle" exists in + * "haystack". It ignores extra elements in "haystack". + * + * In example haystack: { i : 1, a : { b : 1 } }, + * a valid needles include: { i : 1} or {a : { b : 1}} + * It will not find { b: 1 } since it does not search recursively for sub-tree matches. + * + * If a BSON Element is undefined, it simply checks for its existence, not its type or value. + * This allows callers to test for the existence of elements in variable length log lines. + */ + int64_t countBSONFormatLogLinesIsSubset(const BSONObj& needle); + + /** + * Prints the captured log lines. + */ + void printCapturedTextFormatLogLines() const; + +private: + /** + * The test itself. + */ + virtual void _doTest() = 0; +}; + +/** + * Return a list of suite names. + */ +std::vector<std::string> getAllSuiteNames(); + +/** Invocation info (used e.g. by death test to exec). */ +struct SpawnInfo { + /** Copy of the original `argv` from main. */ + std::vector<std::string> argVec; + /** If nonempty, this process is a death test respawn. */ + std::string internalRunDeathTest; + /** + * A unit test main has to turn this on to indicate that it can be brought to + * the death test from a fresh exec with `--suite` and `--filter` options. + * Otherwise death tests simply fork. See death_test.cpp. + */ + bool deathTestExecAllowed = false; +}; +SpawnInfo& getSpawnInfo(); + +} // namespace mongo::unittest diff --git a/src/mongo/unittest/matcher.h b/src/mongo/unittest/matcher.h index be7e7014e12..f1895039918 100644 --- a/src/mongo/unittest/matcher.h +++ b/src/mongo/unittest/matcher.h @@ -40,6 +40,7 @@ #include "mongo/bson/bsonelement.h" #include "mongo/bson/bsonobj.h" #include "mongo/stdx/type_traits.h" +#include "mongo/unittest/assert.h" #include "mongo/unittest/matcher_core.h" /** @@ -151,7 +152,7 @@ public: explicit RelOpBase(T v) : _v{std::move(v)} {} std::string describe() const { - return format(FMT_STRING("{}({})"), self().name, stringifyForAssert(_v)); + return format(FMT_STRING("{}({})"), self().name, stringify::stringifyForAssert(_v)); } template <typename X, std::enable_if_t<stdx::is_detected_v<CanMatchOp, X>, int> = 0> @@ -396,7 +397,7 @@ private: auto it = begin(x); std::array arr{std::get<Is>(_ms).match(*it++)...}; bool allOk = true; - detail::Joiner joiner; + stringify::Joiner joiner; for (size_t i = 0; i != sizeof...(Ms); ++i) { if (!arr[i]) { allOk = false; @@ -556,7 +557,7 @@ public: MatchResult match(const Status& st) const { MatchResult cr = _code.match(st.code()); MatchResult rr = _reason.match(st.reason()); - detail::Joiner joiner; + stringify::Joiner joiner; if (!cr.message().empty()) joiner(format(FMT_STRING("code:{}"), cr.message())); if (!rr.message().empty()) { diff --git a/src/mongo/unittest/matcher_core.h b/src/mongo/unittest/matcher_core.h index c6ff0fb2509..9ae44c7801a 100644 --- a/src/mongo/unittest/matcher_core.h +++ b/src/mongo/unittest/matcher_core.h @@ -39,7 +39,8 @@ #include "mongo/base/string_data.h" #include "mongo/stdx/type_traits.h" -#include "mongo/unittest/unittest.h" +#include "mongo/unittest/assert.h" +#include "mongo/unittest/stringify.h" #include "mongo/util/optional_util.h" /** @@ -100,64 +101,11 @@ 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 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 = {}) { +std::string describeTupleOfMatchers(const MTuple& ms, stringify::Joiner&& joiner = {}) { if constexpr (I == std::tuple_size_v<MTuple>) { return std::string{joiner}; } else { @@ -176,7 +124,7 @@ std::string describeTupleOfMatchers(const MTuple& ms, Joiner&& joiner = {}) { template <typename MTuple, size_t N, size_t I = 0> std::string matchTupleMessage(const MTuple& ms, const std::array<MatchResult, N>& arr, - Joiner&& joiner = {}) { + stringify::Joiner&& joiner = {}) { if constexpr (I == std::tuple_size_v<MTuple>) { return format(FMT_STRING("failed: [{}]"), std::string{joiner}); } else { @@ -192,56 +140,13 @@ std::string matchTupleMessage(const MTuple& ms, } } -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 (optional_io::canStreamWithExtension<T>) { - return doOstream(optional_io::Extension{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), + stringify::stringifyForAssert(e), mr.message().empty() ? "" : format(FMT_STRING(", {}"), mr.message()), m.describe()); } diff --git a/src/mongo/unittest/matcher_core.cpp b/src/mongo/unittest/stringify.cpp index 370e79faf93..5ca50bf3d33 100644 --- a/src/mongo/unittest/matcher_core.cpp +++ b/src/mongo/unittest/stringify.cpp @@ -38,10 +38,14 @@ #include "mongo/util/assert_util.h" #include "mongo/util/hex.h" -namespace mongo::unittest::match::detail { +namespace mongo::unittest::stringify { + +std::string formatTypedObj(const std::type_info& ti, StringData s) { + return format(FMT_STRING("[{}={}]"), demangleName(ti), s); +} std::string lastResortFormat(const std::type_info& ti, const void* p, size_t sz) { - return format(FMT_STRING("[{}={}]"), demangleName(ti), hexdump(p, sz)); + return formatTypedObj(ti, hexdump(p, sz)); } -} // namespace mongo::unittest::match::detail +} // namespace mongo::unittest::stringify diff --git a/src/mongo/unittest/stringify.h b/src/mongo/unittest/stringify.h new file mode 100644 index 00000000000..ab62444c8d6 --- /dev/null +++ b/src/mongo/unittest/stringify.h @@ -0,0 +1,154 @@ +/** + * Copyright (C) 2022-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 <boost/optional.hpp> +#include <fmt/format.h> +#include <memory> +#include <optional> +#include <sstream> +#include <string> +#include <tuple> +#include <typeinfo> +#include <utility> + +#include "mongo/base/error_codes.h" +#include "mongo/base/string_data.h" +#include "mongo/stdx/type_traits.h" +#include "mongo/util/optional_util.h" + +/** + * Mechanisms and extensibility hooks used by this library to format arbitrary + * user-provided objects. + */ +namespace mongo::unittest::stringify { + +std::string formatTypedObj(const std::type_info& ti, StringData obj); + +std::string lastResortFormat(const std::type_info& ti, const void* p, size_t sz); + +/** + * `stringifyForAssert` can be overloaded to extend stringification + * capabilities of the matchers via ADL. + * + * The overload in this 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 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 = ""; +}; + +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}); +} + +/** + * 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 (optional_io::canStreamWithExtension<T>) { + return doOstream(optional_io::Extension{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 if constexpr (std::is_enum_v<T>) { + return formatTypedObj(typeid(T), doFormat(static_cast<std::underlying_type_t<T>>(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); +} + +} // namespace mongo::unittest::stringify diff --git a/src/mongo/unittest/unittest.h b/src/mongo/unittest/unittest.h index 4dd5515e22a..596b1501e25 100644 --- a/src/mongo/unittest/unittest.h +++ b/src/mongo/unittest/unittest.h @@ -31,904 +31,14 @@ * A C++ unit testing framework. * * For examples of basic usage, see mongo/unittest/unittest_test.cpp. - */ - -#pragma once - -#include <boost/optional.hpp> -#include <cmath> -#include <fmt/format.h> -#include <functional> -#include <optional> -#include <sstream> -#include <string> -#include <tuple> -#include <type_traits> -#include <utility> -#include <vector> - -#include "mongo/base/status_with.h" -#include "mongo/base/string_data.h" -#include "mongo/logv2/log_debug.h" -#include "mongo/logv2/log_detail.h" -#include "mongo/unittest/bson_test_util.h" -#include "mongo/unittest/test_info.h" -#include "mongo/util/assert_util.h" -#include "mongo/util/optional_util.h" -#include "mongo/util/str.h" - -/** - * Fail unconditionally, reporting the given message. - */ -#define FAIL(MESSAGE) ::mongo::unittest::TestAssertionFailure(__FILE__, __LINE__, MESSAGE).stream() - -/** - * Fails unless "EXPRESSION" is true. - */ -#define ASSERT_TRUE(EXPRESSION) \ - if (EXPRESSION) { \ - } else \ - FAIL("Expected: " #EXPRESSION) - -#define ASSERT(EXPRESSION) ASSERT_TRUE(EXPRESSION) - -/** - * Fails if "EXPRESSION" is true. - */ -#define ASSERT_FALSE(EXPRESSION) ASSERT(!(EXPRESSION)) - -/** - * Asserts that a Status code is OK. - */ -#define ASSERT_OK(EXPRESSION) ASSERT_EQUALS(::mongo::Status::OK(), (EXPRESSION)) - -/** - * Asserts that a status code is anything but OK. - */ -#define ASSERT_NOT_OK(EXPRESSION) ASSERT_NOT_EQUALS(::mongo::Status::OK(), (EXPRESSION)) - -/* - * Binary comparison assertions. - */ -#define ASSERT_EQUALS(a, b) ASSERT_EQ(a, b) -#define ASSERT_NOT_EQUALS(a, b) ASSERT_NE(a, b) -#define ASSERT_LESS_THAN(a, b) ASSERT_LT(a, b) -#define ASSERT_NOT_LESS_THAN(a, b) ASSERT_GTE(a, b) -#define ASSERT_GREATER_THAN(a, b) ASSERT_GT(a, b) -#define ASSERT_NOT_GREATER_THAN(a, b) ASSERT_LTE(a, b) -#define ASSERT_LESS_THAN_OR_EQUALS(a, b) ASSERT_LTE(a, b) -#define ASSERT_GREATER_THAN_OR_EQUALS(a, b) ASSERT_GTE(a, b) - -#define ASSERT_EQ(a, b) ASSERT_COMPARISON_(kEq, a, b) -#define ASSERT_NE(a, b) ASSERT_COMPARISON_(kNe, a, b) -#define ASSERT_LT(a, b) ASSERT_COMPARISON_(kLt, a, b) -#define ASSERT_LTE(a, b) ASSERT_COMPARISON_(kLe, a, b) -#define ASSERT_GT(a, b) ASSERT_COMPARISON_(kGt, a, b) -#define ASSERT_GTE(a, b) ASSERT_COMPARISON_(kGe, a, b) - -/** - * Binary comparison utility macro. Do not use directly. - */ -#define ASSERT_COMPARISON_(OP, a, b) ASSERT_COMPARISON_STR_(OP, a, b, #a, #b) - -#define ASSERT_COMPARISON_STR_(OP, a, b, aExpr, bExpr) \ - if (auto ca = \ - ::mongo::unittest::ComparisonAssertion<::mongo::unittest::ComparisonOp::OP>::make( \ - __FILE__, __LINE__, aExpr, bExpr, a, b); \ - !ca) { \ - } else \ - ca.failure().stream() - -/** - * Approximate equality assertion. Useful for comparisons on limited precision floating point - * values. - */ -#define ASSERT_APPROX_EQUAL(a, b, ABSOLUTE_ERR) ASSERT_LTE(std::abs((a) - (b)), ABSOLUTE_ERR) - -/** - * Assert a function call returns its input unchanged. - */ -#define ASSERT_IDENTITY(INPUT, FUNCTION) \ - if (auto ca = \ - [&](const auto& v, const auto& fn) { \ - return ::mongo::unittest::ComparisonAssertion< \ - ::mongo::unittest::ComparisonOp::kEq>::make(__FILE__, \ - __LINE__, \ - #INPUT, \ - #FUNCTION "(" #INPUT ")", \ - v, \ - fn(v)); \ - }(INPUT, [&](auto&& x) { return FUNCTION(x); }); \ - !ca) { \ - } else \ - ca.failure().stream() - -/** - * Verify that the evaluation of "EXPRESSION" throws an exception of type EXCEPTION_TYPE. - * - * If "EXPRESSION" throws no exception, or one that is neither of type "EXCEPTION_TYPE" nor - * of a subtype of "EXCEPTION_TYPE", the test is considered a failure and further evaluation - * halts. - */ -#define ASSERT_THROWS(EXPRESSION, EXCEPTION_TYPE) \ - ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, ([](const EXCEPTION_TYPE&) {})) - -/** - * Verify that the evaluation of "EXPRESSION" does not throw any exceptions. - * - * If "EXPRESSION" throws an exception the test is considered a failure and further evaluation - * halts. - */ -#define ASSERT_DOES_NOT_THROW(EXPRESSION) \ - try { \ - EXPRESSION; \ - } catch (const AssertionException& e) { \ - str::stream err; \ - err << "Threw an exception incorrectly: " << e.toString(); \ - FAIL(err); \ - } - -/** - * Behaves like ASSERT_THROWS, above, but also fails if calling what() on the thrown exception - * does not return a string equal to EXPECTED_WHAT. - */ -#define ASSERT_THROWS_WHAT(EXPRESSION, EXCEPTION_TYPE, EXPECTED_WHAT) \ - ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, ([&](const EXCEPTION_TYPE& ex) { \ - ASSERT_EQ(::mongo::StringData(ex.what()), \ - ::mongo::StringData(EXPECTED_WHAT)); \ - })) - -/** - * Behaves like ASSERT_THROWS, above, but also fails if calling getCode() on the thrown exception - * does not return an error code equal to EXPECTED_CODE. - */ -#define ASSERT_THROWS_CODE(EXPRESSION, EXCEPTION_TYPE, EXPECTED_CODE) \ - ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, ([&](const EXCEPTION_TYPE& ex) { \ - ASSERT_EQ(ex.toStatus().code(), EXPECTED_CODE); \ - })) - -/** - * Behaves like ASSERT_THROWS, above, but also fails if calling getCode() on the thrown exception - * does not return an error code equal to EXPECTED_CODE or if calling what() on the thrown exception - * does not return a string equal to EXPECTED_WHAT. - */ -#define ASSERT_THROWS_CODE_AND_WHAT(EXPRESSION, EXCEPTION_TYPE, EXPECTED_CODE, EXPECTED_WHAT) \ - ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, ([&](const EXCEPTION_TYPE& ex) { \ - ASSERT_EQ(ex.toStatus().code(), EXPECTED_CODE); \ - ASSERT_EQ(::mongo::StringData(ex.what()), \ - ::mongo::StringData(EXPECTED_WHAT)); \ - })) - - -/** - * Compiles if expr doesn't compile. - * - * This only works for compile errors in the "immediate context" of the expression, which matches - * the rules for SFINAE. The first argument is a defaulted template parameter that is used in the - * expression to make it dependent. This only works with expressions, not statements, although you - * can separate multiple expressions with a comma. - * - * This should be used at namespace scope, not inside a TEST function. - * - * Examples that pass: - * 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(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(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."); - -/** - * This internal helper is used to ignore warnings about unused results. Some unit tests which test - * `ASSERT_THROWS` and its variations are used on functions which both throw and return `Status` or - * `StatusWith` objects. Although such function designs are undesirable, they do exist, presently. - * Therefore this internal helper macro is used by `ASSERT_THROWS` and its variations to silence - * such warnings without forcing the caller to invoke `.ignore()` on the called function. - * - * NOTE: This macro should NOT be used inside regular unit test code to ignore unchecked `Status` or - * `StatusWith` instances -- if a `Status` or `StatusWith` result is to be ignored, please use the - * normal `.ignore()` code. This macro exists only to make using `ASSERT_THROWS` less inconvenient - * on functions which both throw and return `Status` or `StatusWith`. - */ -#define UNIT_TEST_INTERNALS_IGNORE_UNUSED_RESULT_WARNINGS(EXPRESSION) \ - do { \ - (void)(EXPRESSION); \ - } while (false) - -/** - * Behaves like ASSERT_THROWS, above, but also calls CHECK(caughtException) which may contain - * additional assertions. - */ -#define ASSERT_THROWS_WITH_CHECK(EXPRESSION, EXCEPTION_TYPE, CHECK) \ - if ([&] { \ - try { \ - UNIT_TEST_INTERNALS_IGNORE_UNUSED_RESULT_WARNINGS(EXPRESSION); \ - return false; \ - } catch (const EXCEPTION_TYPE& ex) { \ - CHECK(ex); \ - return true; \ - } \ - }()) { \ - } else \ - /* Fail outside of the try/catch, this way the code in the `FAIL` macro */ \ - /* doesn't have the potential to throw an exception which we might also */ \ - /* be checking for. */ \ - FAIL("Expected expression " #EXPRESSION " to throw " #EXCEPTION_TYPE \ - " but it threw nothing.") - -#define ASSERT_STRING_CONTAINS(BIG_STRING, CONTAINS) \ - if (auto tup_ = std::tuple(std::string(BIG_STRING), std::string(CONTAINS)); \ - std::get<0>(tup_).find(std::get<1>(tup_)) != std::string::npos) { \ - } else \ - FAIL(([&] { \ - const auto& [haystack, sub] = tup_; \ - return format(FMT_STRING("Expected to find {} ({}) in {} ({})"), \ - #CONTAINS, \ - sub, \ - #BIG_STRING, \ - haystack); \ - }())) - -#define ASSERT_STRING_OMITS(BIG_STRING, OMITS) \ - if (auto tup_ = std::tuple(std::string(BIG_STRING), std::string(OMITS)); \ - std::get<0>(tup_).find(std::get<1>(tup_)) == std::string::npos) { \ - } else \ - FAIL(([&] { \ - const auto& [haystack, omits] = tup_; \ - return format(FMT_STRING("Did not expect to find {} ({}) in {} ({})"), \ - #OMITS, \ - omits, \ - #BIG_STRING, \ - haystack); \ - }())) - -#define ASSERT_STRING_SEARCH_REGEX(BIG_STRING, REGEX) \ - if (auto tup_ = std::tuple(std::string(BIG_STRING), std::string(REGEX)); \ - ::mongo::unittest::searchRegex(std::get<1>(tup_), std::get<0>(tup_))) { \ - } else \ - FAIL(([&] { \ - const auto& [haystack, sub] = tup_; \ - return format(FMT_STRING("Expected to find regular expression {} /{}/ in {} ({})"), \ - #REGEX, \ - sub, \ - #BIG_STRING, \ - haystack); \ - }())) - - -/** - * Construct a single test, named `TEST_NAME` within the test Suite `SUITE_NAME`. - * - * Usage: - * - * TEST(MyModuleTests, TestThatFooFailsOnErrors) { - * ASSERT_EQUALS(error_success, foo(invalidValue)); - * } - */ -#define TEST(SUITE_NAME, TEST_NAME) \ - UNIT_TEST_DETAIL_DEFINE_TEST_(SUITE_NAME, TEST_NAME, ::mongo::unittest::Test) - -/** - * Construct a single test named TEST_NAME that has access to a common class (a "fixture") - * named "FIXTURE_NAME". FIXTURE_NAME will be the name of the Suite in which the test appears. - * - * Usage: - * - * class FixtureClass : public mongo::unittest::Test { - * protected: - * int myVar; - * void setUp() { myVar = 10; } - * }; - * - * TEST(FixtureClass, TestThatUsesFixture) { - * ASSERT_EQUALS(10, myVar); - * } - */ -#define TEST_F(FIXTURE_NAME, TEST_NAME) \ - UNIT_TEST_DETAIL_DEFINE_TEST_(FIXTURE_NAME, TEST_NAME, FIXTURE_NAME) - -#define UNIT_TEST_DETAIL_DEFINE_TEST_(SUITE_NAME, TEST_NAME, TEST_BASE) \ - UNIT_TEST_DETAIL_DEFINE_TEST_PRIMITIVE_( \ - SUITE_NAME, TEST_NAME, UNIT_TEST_DETAIL_TEST_TYPE_NAME(SUITE_NAME, TEST_NAME), TEST_BASE) - -#define UNIT_TEST_DETAIL_DEFINE_TEST_PRIMITIVE_(FIXTURE_NAME, TEST_NAME, TEST_TYPE, TEST_BASE) \ - class TEST_TYPE : public TEST_BASE { \ - private: \ - void _doTest() override; \ - static inline const ::mongo::unittest::TestInfo _testInfo{ \ - #FIXTURE_NAME, #TEST_NAME, __FILE__, __LINE__}; \ - static inline const RegistrationAgent<TEST_TYPE> _agent{&_testInfo}; \ - }; \ - void TEST_TYPE::_doTest() - -/** - * Macro to construct a type name for a test, from its `SUITE_NAME` and `TEST_NAME`. - * Do not use directly in test code. - */ -#define UNIT_TEST_DETAIL_TEST_TYPE_NAME(SUITE_NAME, TEST_NAME) \ - UnitTest_SuiteName##SUITE_NAME##TestName##TEST_NAME - -namespace mongo::unittest { - -template <typename T> -std::string extendedFormat(T&& t) { - std::ostringstream os; - os << optional_io::Extension{t}; - return os.str(); -} - -bool searchRegex(const std::string& pattern, const std::string& string); - -class Result; - -/** - * Representation of a collection of tests. * - * One Suite is constructed for each SUITE_NAME when using the TEST macro. - * - * See `OldStyleSuiteSpecification` which adapts dbtests into this framework. - */ -class Suite : public std::enable_shared_from_this<Suite> { -private: - struct SuiteTest { - std::string name; - std::string fileName; - std::function<void()> fn; - }; - - struct ConstructorEnable { - explicit ConstructorEnable() = default; - }; - -public: - explicit Suite(ConstructorEnable, std::string name); - Suite(const Suite&) = delete; - Suite& operator=(const Suite&) = delete; - - void add(std::string name, std::string fileName, std::function<void()> testFn); - - std::unique_ptr<Result> run(const std::string& filter, - const std::string& fileNameFilter, - int runsPerTest); - - static int run(const std::vector<std::string>& suites, - const std::string& filter, - const std::string& fileNameFilter, - int runsPerTest); - - /** - * Get a suite with the given name, creating and registering it if necessary. - * This is the only way to make a Suite object. - * - * Safe to call during static initialization. - */ - static Suite& getSuite(StringData name); - -private: - /** Points to the string data of the _name field. */ - StringData key() const { - return _name; - } - - std::string _name; - std::vector<SuiteTest> _tests; -}; - -/** - * Adaptor to set up a Suite from a dbtest-style suite. - * Support for deprecated dbtest-style test suites. Tests are are added by overriding setupTests() - * in a subclass of OldStyleSuiteSpecification, and defining an OldStyleSuiteInstance<T> object. - * This approach is - * deprecated. + * ASSERT macros and supporting definitions are in mongo/unittest/assert.h. * - * Example: - * class All : public OldStyleSuiteSpecification { - * public: - * All() : OldStyleSuiteSpecification("BunchaTests") {} - * void setupTests() { - * add<TestThis>(); - * add<TestThat>(); - * add<TestTheOtherThing>(); - * } - * }; - * OldStyleSuiteInitializer<All> all; */ -class OldStyleSuiteSpecification { -public: - struct SuiteTest { - std::string name; - std::function<void()> fn; - }; - - OldStyleSuiteSpecification(std::string name) : _name(std::move(name)) {} - virtual ~OldStyleSuiteSpecification() = default; - - // Note: setupTests() is run by a OldStyleSuiteInitializer at static initialization time. - // It should in most cases be just a simple sequence of add<T>() calls. - virtual void setupTests() = 0; - - const std::string& name() const { - return _name; - } - - const std::vector<SuiteTest>& tests() const { - return _tests; - } - - /** - * Add an old-style test of type `T` to this Suite, saving any test constructor args - * that would be needed at test run time. - * The added test's name will be synthesized as the demangled typename of T. - * At test run time, the test will be created and run with `T(args...).run()`. - */ - template <typename T, typename... Args> - void add(Args&&... args) { - addNameCallback(nameForTestClass<T>(), [=] { T(args...).run(); }); - } - - void addNameCallback(std::string name, std::function<void()> cb) { - _tests.push_back({std::move(name), std::move(cb)}); - } - - template <typename T> - static std::string nameForTestClass() { - return demangleName(typeid(T)); - } - -private: - std::string _name; - std::vector<SuiteTest> _tests; -}; - -/** - * Define a namespace-scope instance of `OldStyleSuiteInitializer<T>` to properly create and - * initialize an instance of `T` (an `OldStyleSuiteSpecification`). See - * `OldStyleSuiteSpecification`. - */ -template <typename T> -struct OldStyleSuiteInitializer { - template <typename... Args> - explicit OldStyleSuiteInitializer(Args&&... args) { - T t(std::forward<Args>(args)...); - init(t); - } - - void init(OldStyleSuiteSpecification& suiteSpec) const { - suiteSpec.setupTests(); - auto& suite = Suite::getSuite(suiteSpec.name()); - for (auto&& t : suiteSpec.tests()) { - suite.add(t.name, "", t.fn); - } - } -}; - -/** - * UnitTest singleton class. Provides access to information about current execution state. - */ -class UnitTest { - UnitTest() = default; - -public: - static UnitTest* getInstance(); - - UnitTest(const UnitTest& other) = delete; - UnitTest& operator=(const UnitTest&) = delete; - -public: - /** - * Returns the currently running test, or `nullptr` if a test is not running. - */ - const TestInfo* currentTestInfo() const; - -public: - /** - * Used to set/unset currently running test information. - */ - class TestRunScope { - public: - explicit TestRunScope(const TestInfo* testInfo) { - UnitTest::getInstance()->setCurrentTestInfo(testInfo); - } - - ~TestRunScope() { - UnitTest::getInstance()->setCurrentTestInfo(nullptr); - } - }; - -private: - /** - * Sets the currently running tests. Internal: should only be used by unit test framework. - * testInfo - test info of the currently running test, or `nullptr` is a test is not running. - */ - void setCurrentTestInfo(const TestInfo* testInfo); - -private: - const TestInfo* _currentTestInfo = nullptr; -}; - -/** - * Base type for unit test fixtures. Also, the default fixture type used - * by the TEST() macro. - */ -class Test { -public: - Test(); - virtual ~Test(); - Test(const Test&) = delete; - Test& operator=(const Test&) = delete; - - void run(); - - /** - * Called on the test object before running the test. - */ - virtual void setUp() {} - - /** - * Called on the test object after running the test. - */ - virtual void tearDown() {} - -protected: - /** - * Adds a Test to a Suite, used by TEST/TEST_F macros. - */ - template <typename T> - class RegistrationAgent { - public: - /** - * These TestInfo must point to data that outlives this RegistrationAgent. - * In the case of TEST/TEST_F, these are static variables. - */ - explicit RegistrationAgent(const TestInfo* testInfo) : _testInfo{testInfo} { - Suite::getSuite(_testInfo->suiteName()) - .add( - std::string{_testInfo->testName()}, std::string{_testInfo->file()}, [testInfo] { - UnitTest::TestRunScope trs(testInfo); - T{}.run(); - }); - } - - StringData getSuiteName() const { - return _testInfo->suiteName(); - } - - StringData getTestName() const { - return _testInfo->testName(); - } - - StringData getFileName() const { - return _testInfo->file(); - } - private: - const TestInfo* _testInfo; - }; - - /** - * This exception class is used to exercise the testing framework itself. If a test - * case throws it, the framework would not consider it an error. - */ - class FixtureExceptionForTesting : public std::exception {}; - - /** - * Starts capturing messages logged by code under test. - * - * Log messages will still also go to their default destination; this - * code simply adds an additional sink for log messages. - * - * Clears any previously captured log lines. - */ - void startCapturingLogMessages(); - - /** - * Stops capturing log messages logged by code under test. - */ - void stopCapturingLogMessages(); - - /** - * Gets a vector of strings, one log line per string, captured since - * the last call to startCapturingLogMessages() in this test. - */ - const std::vector<std::string>& getCapturedTextFormatLogMessages() const; - std::vector<BSONObj> getCapturedBSONFormatLogMessages() const; - - /** - * Returns the number of collected log lines containing "needle". - */ - int64_t countTextFormatLogLinesContaining(const std::string& needle); - - /** - * Returns the number of collected log lines where "needle" is a subset of a line. - * - * Does a Depth-First-Search of a BSON document. Validates each element in "needle" exists in - * "haystack". It ignores extra elements in "haystack". - * - * In example haystack: { i : 1, a : { b : 1 } }, - * a valid needles include: { i : 1} or {a : { b : 1}} - * It will not find { b: 1 } since it does not search recursively for sub-tree matches. - * - * If a BSON Element is undefined, it simply checks for its existence, not its type or value. - * This allows callers to test for the existence of elements in variable length log lines. - */ - int64_t countBSONFormatLogLinesIsSubset(const BSONObj& needle); - - /** - * Prints the captured log lines. - */ - void printCapturedTextFormatLogLines() const; - -private: - /** - * The test itself. - */ - virtual void _doTest() = 0; -}; - -/** - * Exception thrown when a test assertion fails. - * - * Typically thrown by helpers in the TestAssertion class and its ilk, below. - * - * NOTE(schwerin): This intentionally does _not_ extend std::exception, so that code under - * test that (foolishly?) catches std::exception won't swallow test failures. Doesn't - * protect you from code that foolishly catches ..., but you do what you can. - */ -class TestAssertionFailureException { -public: - TestAssertionFailureException(std::string file, unsigned line, std::string message); - - const std::string& getFile() const { - return _file; - } - unsigned getLine() const { - return _line; - } - const std::string& getMessage() const { - return _message; - } - void setMessage(const std::string& message) { - _message = message; - } - - const std::string& what() const { - return getMessage(); - } - - std::string toString() const; - - const std::string& getStacktrace() const { - return _stacktrace; - } - -private: - std::string _file; - unsigned _line; - std::string _message; - std::string _stacktrace; -}; - -class TestAssertionFailure { -public: - TestAssertionFailure(const std::string& file, unsigned line, const std::string& message); - TestAssertionFailure(const TestAssertionFailure& other); - ~TestAssertionFailure() noexcept(false); - - TestAssertionFailure& operator=(const TestAssertionFailure& other); - - std::ostream& stream(); - -private: - TestAssertionFailureException _exception; - std::ostringstream _stream; - bool _enabled; -}; - -enum class ComparisonOp { kEq, kNe, kLt, kLe, kGt, kGe }; - -template <ComparisonOp op> -class ComparisonAssertion { -private: - static constexpr auto comparator() { - if constexpr (op == ComparisonOp::kEq) { - return std::equal_to<>{}; - } else if constexpr (op == ComparisonOp::kNe) { - return std::not_equal_to<>{}; - } else if constexpr (op == ComparisonOp::kLt) { - return std::less<>{}; - } else if constexpr (op == ComparisonOp::kLe) { - return std::less_equal<>{}; - } else if constexpr (op == ComparisonOp::kGt) { - return std::greater<>{}; - } else if constexpr (op == ComparisonOp::kGe) { - return std::greater_equal<>{}; - } - } - - static constexpr StringData name() { - if constexpr (op == ComparisonOp::kEq) { - return "=="_sd; - } else if constexpr (op == ComparisonOp::kNe) { - return "!="_sd; - } else if constexpr (op == ComparisonOp::kLt) { - return "<"_sd; - } else if constexpr (op == ComparisonOp::kLe) { - return "<="_sd; - } else if constexpr (op == ComparisonOp::kGt) { - return ">"_sd; - } else if constexpr (op == ComparisonOp::kGe) { - return ">="_sd; - } - } - - template <typename A, typename B> - MONGO_COMPILER_NOINLINE ComparisonAssertion(const char* theFile, - unsigned theLine, - StringData aExpression, - StringData bExpression, - const A& a, - const B& b) { - if (comparator()(a, b)) { - return; - } - _assertion = std::make_unique<TestAssertionFailure>( - theFile, - theLine, - format(FMT_STRING("Expected {1} {0} {2} ({3} {0} {4})"), - name(), - aExpression, - bExpression, - extendedFormat(a), - extendedFormat(b))); - } - -public: - // Use a single implementation (identical to the templated one) for all string-like types. - // This is particularly important to avoid making unique instantiations for each length of - // string literal. - static ComparisonAssertion make(const char* theFile, - unsigned theLine, - StringData aExpression, - StringData bExpression, - StringData a, - StringData b); - - - // Use a single implementation (identical to the templated one) for all pointer and array types. - // Note: this is selected instead of the StringData overload for char* and string literals - // because they are supposed to compare pointers, not contents. - static ComparisonAssertion make(const char* theFile, - unsigned theLine, - StringData aExpression, - StringData bExpression, - const void* a, - const void* b); - TEMPLATE(typename A, typename B) - REQUIRES(!(std::is_convertible_v<A, StringData> && std::is_convertible_v<B, StringData>)&& // - !(std::is_pointer_v<A> && std::is_pointer_v<B>)&& // - !(std::is_array_v<A> && std::is_array_v<B>)) - static ComparisonAssertion make(const char* theFile, - unsigned theLine, - StringData aExpression, - StringData bExpression, - const A& a, - const B& b) { - return ComparisonAssertion(theFile, theLine, aExpression, bExpression, a, b); - } - - explicit operator bool() const { - return static_cast<bool>(_assertion); - } - TestAssertionFailure failure() { - return *_assertion; - } - -private: - std::unique_ptr<TestAssertionFailure> _assertion; -}; - -// Explicit instantiation of ComparisonAssertion ctor and factory, for "A OP B". -#define TEMPLATE_COMPARISON_ASSERTION_CTOR_A_OP_B(EXTERN, OP, A, B) \ - EXTERN template ComparisonAssertion<ComparisonOp::OP>::ComparisonAssertion( \ - const char*, unsigned, StringData, StringData, const A&, const B&); \ - EXTERN template ComparisonAssertion<ComparisonOp::OP> \ - ComparisonAssertion<ComparisonOp::OP>::make( \ - const char*, unsigned, StringData, StringData, const A&, const B&); - -// Explicit instantiation of ComparisonAssertion ctor and factory for a pair of types. -#define TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(EXTERN, OP, A, B) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_A_OP_B(EXTERN, OP, A, B) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_A_OP_B(EXTERN, OP, B, A) - -// Explicit instantiation of ComparisonAssertion ctor and factory for a single type. -#define TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(EXTERN, OP, T) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_A_OP_B(EXTERN, OP, T, T) - -// Call with `extern` to declace extern instantiations, and with no args to explicitly instantiate. -#define INSTANTIATE_COMPARISON_ASSERTION_CTORS(...) \ - __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kEq>; \ - __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kNe>; \ - __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kGt>; \ - __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kGe>; \ - __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kLt>; \ - __VA_ARGS__ template class ComparisonAssertion<ComparisonOp::kLe>; \ - \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, int) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, long) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, long long) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, unsigned int) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, unsigned long) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, unsigned long long) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, bool) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, double) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, OID) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, BSONType) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, Timestamp) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, Date_t) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, Status) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kEq, ErrorCodes::Error) \ - \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, int, long) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, int, long long) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, long, long long) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, unsigned int, unsigned long) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, Status, ErrorCodes::Error) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_SYMMETRIC(__VA_ARGS__, kEq, ErrorCodes::Error, int) \ - \ - /* These are the only types that are often used with ASSERT_NE*/ \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kNe, Status) \ - TEMPLATE_COMPARISON_ASSERTION_CTOR_REFLEXIVE(__VA_ARGS__, kNe, unsigned long) - -// Declare that these definitions will be provided in unittest.cpp. -INSTANTIATE_COMPARISON_ASSERTION_CTORS(extern); - -/** - * Get the value out of a StatusWith<T>, or throw an exception if it is not OK. - */ -template <typename T> -const T& assertGet(const StatusWith<T>& swt) { - ASSERT_OK(swt.getStatus()); - return swt.getValue(); -} - -template <typename T> -T assertGet(StatusWith<T>&& swt) { - ASSERT_OK(swt.getStatus()); - return std::move(swt.getValue()); -} - -/** - * Return a list of suite names. - */ -std::vector<std::string> getAllSuiteNames(); - -/** Invocation info (used e.g. by death test to exec). */ -struct SpawnInfo { - /** Copy of the original `argv` from main. */ - std::vector<std::string> argVec; - /** If nonempty, this process is a death test respawn. */ - std::string internalRunDeathTest; - /** - * A unit test main has to turn this on to indicate that it can be brought to - * the death test from a fresh exec with `--suite` and `--filter` options. - * Otherwise death tests simply fork. See death_test.cpp. - */ - bool deathTestExecAllowed = false; -}; -SpawnInfo& getSpawnInfo(); +#pragma once -} // namespace mongo::unittest +#include "mongo/unittest/assert.h" +#include "mongo/unittest/assert_that.h" +#include "mongo/unittest/bson_test_util.h" +#include "mongo/unittest/framework.h" diff --git a/src/mongo/unittest/unittest_test.cpp b/src/mongo/unittest/unittest_test.cpp index bcc4cbccc56..b4402ea770c 100644 --- a/src/mongo/unittest/unittest_test.cpp +++ b/src/mongo/unittest/unittest_test.cpp @@ -163,108 +163,32 @@ TEST(UnitTestSelfTest, TestNoDoubleEvaluation) { ASSERT_TEST_FAILS_MATCH(ASSERT_EQ(0, ++i), "(0 == 1)"); } -TEST(UnitTestSelfTest, BSONObjEQ) { - ASSERT_BSONOBJ_EQ(BSON("foo" - << "bar"), - BSON("foo" - << "bar")); +TEST(UnitTestSelfTest, BSONObjComparisons) { + auto a = mongo::BSONObjBuilder{}.append("foo", "bar").obj(); + auto b = mongo::BSONObjBuilder{}.append("foo", "baz").obj(); + ASSERT_BSONOBJ_EQ(a, a); + ASSERT_BSONOBJ_NE(a, b); + ASSERT_BSONOBJ_LT(a, b); + ASSERT_BSONOBJ_LTE(a, b); + ASSERT_BSONOBJ_LTE(a, b); + ASSERT_BSONOBJ_GT(b, a); + ASSERT_BSONOBJ_GTE(b, a); + ASSERT_BSONOBJ_GTE(a, a); } -TEST(UnitTestSelfTest, BSONObjNE) { - ASSERT_BSONOBJ_NE(BSON("foo" - << "bar"), - BSON("foo" - << "baz")); -} - -TEST(UnitTestSelfTest, BSONObjLT) { - ASSERT_BSONOBJ_LT(BSON("foo" - << "bar"), - BSON("foo" - << "baz")); -} - -TEST(UnitTestSelfTest, BSONObjLTE) { - ASSERT_BSONOBJ_LTE(BSON("foo" - << "bar"), - BSON("foo" - << "baz")); - ASSERT_BSONOBJ_LTE(BSON("foo" - << "bar"), - BSON("foo" - << "bar")); -} - -TEST(UnitTestSelfTest, BSONObjGT) { - ASSERT_BSONOBJ_GT(BSON("foo" - << "baz"), - BSON("foo" - << "bar")); -} - -TEST(UnitTestSelfTest, BSONObjGTE) { - ASSERT_BSONOBJ_GTE(BSON("foo" - << "baz"), - BSON("foo" - << "bar")); - ASSERT_BSONOBJ_GTE(BSON("foo" - << "bar"), - BSON("foo" - << "bar")); -} - -TEST(UnitTestSelfTest, BSONElementEQ) { - mongo::BSONObj obj1 = BSON("foo" - << "bar"); - mongo::BSONObj obj2 = BSON("foo" - << "bar"); - ASSERT_BSONELT_EQ(obj1.firstElement(), obj2.firstElement()); -} - -TEST(UnitTestSelfTest, BSONElementNE) { - mongo::BSONObj obj1 = BSON("foo" - << "bar"); - mongo::BSONObj obj2 = BSON("foo" - << "baz"); - ASSERT_BSONELT_NE(obj1.firstElement(), obj2.firstElement()); -} - -TEST(UnitTestSelfTest, BSONElementLT) { - mongo::BSONObj obj1 = BSON("foo" - << "bar"); - mongo::BSONObj obj2 = BSON("foo" - << "baz"); - ASSERT_BSONELT_LT(obj1.firstElement(), obj2.firstElement()); -} - -TEST(UnitTestSelfTest, BSONElementLTE) { - mongo::BSONObj obj1 = BSON("foo" - << "bar"); - mongo::BSONObj obj2 = BSON("foo" - << "bar"); - mongo::BSONObj obj3 = BSON("foo" - << "baz"); - ASSERT_BSONELT_LTE(obj1.firstElement(), obj2.firstElement()); - ASSERT_BSONELT_LTE(obj1.firstElement(), obj3.firstElement()); -} - -TEST(UnitTestSelfTest, BSONElementGT) { - mongo::BSONObj obj1 = BSON("foo" - << "bar"); - mongo::BSONObj obj2 = BSON("foo" - << "baz"); - ASSERT_BSONELT_GT(obj2.firstElement(), obj1.firstElement()); -} - -TEST(UnitTestSelfTest, BSONElementGTE) { - mongo::BSONObj obj1 = BSON("foo" - << "bar"); - mongo::BSONObj obj2 = BSON("foo" - << "bar"); - mongo::BSONObj obj3 = BSON("foo" - << "baz"); - ASSERT_BSONELT_GTE(obj3.firstElement(), obj2.firstElement()); - ASSERT_BSONELT_GTE(obj2.firstElement(), obj1.firstElement()); +TEST(UnitTestSelfTest, BSONElementComparisons) { + auto ao = mongo::BSONObjBuilder{}.append("foo", "bar").obj(); + auto bo = mongo::BSONObjBuilder{}.append("foo", "baz").obj(); + auto a = ao.firstElement(); + auto b = bo.firstElement(); + ASSERT_BSONELT_EQ(a, a); + ASSERT_BSONELT_NE(a, b); + ASSERT_BSONELT_LT(a, b); + ASSERT_BSONELT_LTE(a, a); + ASSERT_BSONELT_LTE(a, b); + ASSERT_BSONELT_GT(b, a); + ASSERT_BSONELT_GTE(b, a); + ASSERT_BSONELT_GTE(a, a); } class UnitTestFormatTest : public mongo::unittest::Test { @@ -275,16 +199,16 @@ public: } template <template <typename...> class OptionalTemplate> - void runFormatTest() { - using mongo::unittest::extendedFormat; - ASSERT_EQ(extendedFormat(mkOptional<OptionalTemplate, int>()), "--"); - ASSERT_EQ(extendedFormat(mkOptional<OptionalTemplate, std::string>()), "--"); - ASSERT_EQ(extendedFormat(mkOptional<OptionalTemplate, int>(123)), " 123"); - ASSERT_EQ(extendedFormat(mkOptional<OptionalTemplate, std::string>("hey")), " hey"); + void runFormatOptionalTest() { + using mongo::unittest::stringify::stringifyForAssert; + ASSERT_EQ(stringifyForAssert(mkOptional<OptionalTemplate, int>()), "--"); + ASSERT_EQ(stringifyForAssert(mkOptional<OptionalTemplate, std::string>()), "--"); + ASSERT_EQ(stringifyForAssert(mkOptional<OptionalTemplate, int>(123)), " 123"); + ASSERT_EQ(stringifyForAssert(mkOptional<OptionalTemplate, std::string>("hey")), " hey"); } template <template <typename...> class OptionalTemplate, class None> - void runEqTest(None none) { + void runEqOptionalTest(None none) { ASSERT_EQ(OptionalTemplate<int>{1}, OptionalTemplate<int>{1}); ASSERT_NE(OptionalTemplate<int>{1}, OptionalTemplate<int>{2}); ASSERT_EQ(OptionalTemplate<int>{}, OptionalTemplate<int>{}); @@ -293,19 +217,34 @@ public: }; TEST_F(UnitTestFormatTest, FormatBoostOptional) { - runFormatTest<boost::optional>(); + runFormatOptionalTest<boost::optional>(); } TEST_F(UnitTestFormatTest, EqBoostOptional) { - runEqTest<boost::optional>(boost::none); + runEqOptionalTest<boost::optional>(boost::none); } TEST_F(UnitTestFormatTest, FormatStdOptional) { - runFormatTest<std::optional>(); // NOLINT + runFormatOptionalTest<std::optional>(); // NOLINT } TEST_F(UnitTestFormatTest, EqStdOptional) { - runEqTest<std::optional>(std::nullopt); // NOLINT + runEqOptionalTest<std::optional>(std::nullopt); // NOLINT +} + +enum class Color { r, g, b }; +enum class NamedColor { r, g, b }; + +inline std::ostream& operator<<(std::ostream& os, const NamedColor& e) { + return os << std::array{"r", "g", "b"}[static_cast<size_t>(e)]; +} + +TEST_F(UnitTestFormatTest, FormatEnumClass) { + using mongo::unittest::stringify::stringifyForAssert; + ASSERT_STRING_CONTAINS(stringifyForAssert(Color::r), "Color=0"); + ASSERT_EQ(stringifyForAssert(NamedColor::r), "r"); + ASSERT_EQ(Color::r, Color::r); + ASSERT_EQ(NamedColor::r, NamedColor::r); } DEATH_TEST_REGEX(DeathTestSelfTest, TestDeath, "Invariant failure.*false") { |