summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mongo/unittest/SConscript2
-rw-r--r--src/mongo/unittest/death_test.h5
-rw-r--r--src/mongo/unittest/expected_output/golden_self_test/sanity_test.txt5
-rw-r--r--src/mongo/unittest/expected_output/golden_self_test2/sanity_test2.txt2
-rw-r--r--src/mongo/unittest/golden_test.cpp200
-rw-r--r--src/mongo/unittest/golden_test.h257
-rw-r--r--src/mongo/unittest/golden_test_test.cpp113
-rw-r--r--src/mongo/unittest/unittest.cpp13
-rw-r--r--src/mongo/unittest/unittest.h104
9 files changed, 684 insertions, 17 deletions
diff --git a/src/mongo/unittest/SConscript b/src/mongo/unittest/SConscript
index e4c0dac29e9..91f82f463ad 100644
--- a/src/mongo/unittest/SConscript
+++ b/src/mongo/unittest/SConscript
@@ -10,6 +10,7 @@ env.Library(
'barrier.cpp',
'bson_test_util.cpp',
'death_test.cpp',
+ 'golden_test.cpp',
'matcher.cpp',
'matcher_core.cpp',
'temp_dir.cpp',
@@ -100,6 +101,7 @@ env.Library(
env.CppUnitTest(
target='unittest_test',
source=[
+ 'golden_test_test.cpp',
'unittest_test.cpp',
'fixture_test.cpp',
'temp_dir_test.cpp',
diff --git a/src/mongo/unittest/death_test.h b/src/mongo/unittest/death_test.h
index f93f66c485f..1472b2f712d 100644
--- a/src/mongo/unittest/death_test.h
+++ b/src/mongo/unittest/death_test.h
@@ -106,12 +106,13 @@
\
private: \
void _doTest() override; \
+ static inline const ::mongo::unittest::TestInfo _testInfo{ \
+ #SUITE_NAME, #TEST_NAME, __FILE__, __LINE__}; \
static inline const RegistrationAgent<::mongo::unittest::DeathTest<TEST_TYPE>> _agent{ \
- #SUITE_NAME, #TEST_NAME, __FILE__}; \
+ &_testInfo}; \
}; \
void TEST_TYPE::_doTest()
-
namespace mongo::unittest {
class DeathTestBase : public Test {
diff --git a/src/mongo/unittest/expected_output/golden_self_test/sanity_test.txt b/src/mongo/unittest/expected_output/golden_self_test/sanity_test.txt
new file mode 100644
index 00000000000..9f02583d3c9
--- /dev/null
+++ b/src/mongo/unittest/expected_output/golden_self_test/sanity_test.txt
@@ -0,0 +1,5 @@
+Output 1:
+test test test 1
+Output 2:
+test test
+test 2
diff --git a/src/mongo/unittest/expected_output/golden_self_test2/sanity_test2.txt b/src/mongo/unittest/expected_output/golden_self_test2/sanity_test2.txt
new file mode 100644
index 00000000000..36eb19b0172
--- /dev/null
+++ b/src/mongo/unittest/expected_output/golden_self_test2/sanity_test2.txt
@@ -0,0 +1,2 @@
+Output 1:
+test 1
diff --git a/src/mongo/unittest/golden_test.cpp b/src/mongo/unittest/golden_test.cpp
new file mode 100644
index 00000000000..f06286cc3c5
--- /dev/null
+++ b/src/mongo/unittest/golden_test.cpp
@@ -0,0 +1,200 @@
+/**
+ * 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.
+ */
+
+#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
+
+#include <boost/filesystem.hpp>
+#include <boost/iostreams/device/file_descriptor.hpp>
+#include <boost/iostreams/stream.hpp>
+#include <boost/iostreams/stream_buffer.hpp>
+#include <boost/lexical_cast.hpp>
+#include <fmt/format.h>
+#include <fmt/ostream.h>
+#include <pcrecpp.h>
+
+#include "mongo/base/init.h"
+#include "mongo/base/status.h"
+#include "mongo/logv2/log.h"
+#include "mongo/unittest/golden_test.h"
+#include "mongo/util/ctype.h"
+#include "mongo/util/options_parser/option_section.h"
+#include "mongo/util/options_parser/startup_option_init.h"
+#include "mongo/util/options_parser/startup_options.h"
+
+namespace mongo::unittest {
+
+namespace fs = ::boost::filesystem;
+using namespace fmt::literals;
+
+static const pcrecpp::RE validNameRegex(R"([[:alnum:]_\-]*)");
+
+GoldenTestEnvironment* GoldenTestEnvironment::getInstance() {
+ static GoldenTestEnvironment instance;
+ return &instance;
+}
+
+GoldenTestEnvironment::GoldenTestEnvironment()
+ : _goldenDataRoot("."), _outputPathPrefix("test_output") {
+ // Parse environment variables
+ auto opts = GoldenTestOptions::parseEnvironment();
+
+ fs::path outputRoot;
+ if (opts.output) {
+ outputRoot = fs::path(opts.output.get());
+ } else {
+ fs::path tempRoot = fs::temp_directory_path();
+ fs::path uniqueDir = fs::unique_path(_outputPathPrefix + "-%%%%-%%%%-%%%%-%%%%");
+ outputRoot = tempRoot / uniqueDir;
+ }
+
+ _actualOutputRoot = outputRoot / "actual";
+ _expectedOutputRoot = outputRoot / "expected";
+}
+
+std::string GoldenTestContext::toSnakeCase(const std::string& str) {
+ std::string result;
+ bool lastAlpha = false;
+ for (char c : str) {
+ if (ctype::isUpper(c)) {
+ if (lastAlpha) {
+ result += '_';
+ }
+
+ result += ctype::toLower(c);
+ } else {
+ result += c;
+ }
+
+ lastAlpha = ctype::isAlpha(c);
+ }
+
+ return result;
+}
+
+std::string GoldenTestContext::sanitizeName(const std::string& str) {
+ if (!validNameRegex.FullMatch(str)) {
+ FAIL("Unsupported characters in name '{}'"_format(str));
+ }
+
+ return toSnakeCase(str);
+}
+
+void GoldenTestContext::verifyOutput() {
+ std::string actualStr = _outStream.str();
+
+ fs::path goldenDataPath = getGoldedDataPath();
+ if (!fs::exists(goldenDataPath)) {
+ failResultMismatch(actualStr, boost::none, "Golden data file doesn't exist.");
+ }
+
+ std::string expectedStr = readFile(goldenDataPath);
+ if (actualStr != expectedStr) {
+ failResultMismatch(actualStr, expectedStr, "Actual result doesn't match golden data.");
+ }
+}
+
+void GoldenTestContext::failResultMismatch(const std::string& actualStr,
+ const boost::optional<std::string>& expectedStr,
+ const std::string& message) {
+ fs::path actualOutputFilePath = getActualOutputPath();
+ fs::path expectedOutputFilePath = getExpectedOutputPath();
+
+ writeFile(actualOutputFilePath, actualStr);
+ if (expectedStr != boost::none) {
+ writeFile(expectedOutputFilePath, *expectedStr);
+ }
+
+ LOGV2_ERROR(6273501,
+ "Test output verification failed",
+ "message"_attr = message,
+ "actualOutput"_attr = actualStr,
+ "expectedOutput"_attr = expectedStr,
+ "actualOutputPath"_attr = actualOutputFilePath.string(),
+ "expectedOutputPath"_attr = expectedOutputFilePath.string(),
+ "actualOutputRoot"_attr = _env->actualOutputRoot().string(),
+ "expectedOutputRoot"_attr = _env->expectedOutputRoot().string());
+
+ throwAssertionFailureException(
+ "Test output verification failed: {}, "
+ "actual output file: {}, "
+ "expected output file: {}"
+ ""_format(message, actualOutputFilePath, expectedOutputFilePath));
+}
+
+std::string GoldenTestContext::readFile(const fs::path& path) {
+ ASSERT_FALSE(is_directory(path));
+ ASSERT_TRUE(is_regular_file(path));
+
+ std::ostringstream os;
+ os << fs::ifstream(path).rdbuf();
+ return os.str();
+}
+
+void GoldenTestContext::writeFile(const fs::path& path, const std::string& contents) {
+ create_directories(path.parent_path());
+ fs::ofstream ofs(path);
+ ofs << contents;
+}
+
+void GoldenTestContext::throwAssertionFailureException(const std::string& message) {
+ throw TestAssertionFailureException(_testInfo->file().toString(), _testInfo->line(), message);
+}
+
+fs::path GoldenTestContext::getActualOutputPath() const {
+ return _env->actualOutputRoot() / _config->relativePath / getTestPath();
+}
+
+fs::path GoldenTestContext::getExpectedOutputPath() const {
+ return _env->expectedOutputRoot() / _config->relativePath / getTestPath();
+}
+
+fs::path GoldenTestContext::getGoldedDataPath() const {
+ return _env->goldenDataRoot() / _config->relativePath / getTestPath();
+}
+
+fs::path GoldenTestContext::getTestPath() const {
+ return fs::path(sanitizeName(_testInfo->suiteName().toString())) /
+ fs::path(sanitizeName(_testInfo->testName().toString()) + ".txt");
+}
+
+GoldenTestOptions GoldenTestOptions::parseEnvironment() {
+ GoldenTestOptions opts;
+ namespace po = ::boost::program_options;
+ po::options_description desc_env;
+ desc_env.add_options() //
+ ("output", po::value<std::string>()->notifier([&opts](auto v) { opts.output = v; }));
+
+ po::variables_map vm_env;
+ po::store(po::parse_environment(desc_env, "GOLDEN_TEST_"), vm_env);
+ po::notify(vm_env);
+
+ return opts;
+}
+
+} // namespace mongo::unittest
diff --git a/src/mongo/unittest/golden_test.h b/src/mongo/unittest/golden_test.h
new file mode 100644
index 00000000000..85823d58974
--- /dev/null
+++ b/src/mongo/unittest/golden_test.h
@@ -0,0 +1,257 @@
+/**
+ * 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 <boost/filesystem.hpp>
+#include <functional>
+#include <iostream>
+#include <sstream>
+#include <string>
+
+#include "mongo/base/string_data.h"
+#include "mongo/unittest/temp_dir.h"
+#include "mongo/unittest/unittest.h"
+#include "mongo/util/assert_util.h"
+
+namespace mongo::unittest {
+
+/**
+ * Allows executing golden data tests. That is, tests that produce a text output which is compared
+ * against checked-in expected results (a.k.a "golden data".)
+ *
+ * The test fails if its output doesn't match the golden file's contents, or if
+ * the golden data file doesn't exist.
+ * if expected output doesnt exist. When this happens, the actual and expected outputs will be
+ * stored in the configured output location. This allows:
+ * - bulk comparison to determine if further code changes are needed or if the new results are
+ * acceptable.
+ * - bulk update of expected outputs if the new outputs are acceptable.
+ *
+ * Usage:
+ * GoldenTestConfig myConfig("src/mongo/my_expected_output");
+ * TEST(MySuite, MyTest) {
+ * GoldenTestContext ctx(myConfig)
+ * ctx.outStream() << "print something here" << std::endl;
+ * ctx.outStream() << "print something else" << std::endl;
+ * }
+ *
+ * TODO: SERVER-63734, Replace with proper developer tooling to diff/update
+ *
+ * In order to diff the results, find the failed test(s) and run the diff command to show the
+ * differences for tests that failed.
+ *
+ * Example:
+ * To obtain the expected and actual output folders:
+ * $> ninja -j400 +unittest_test | grep "^{" |\
+ * jq -s -c -r '.[] | select(.id == 6273501 ) | .attr.expectedOutputRoot + " "
+ * +.attr.actualOutputRoot ' | sort | uniq
+ *
+ * you may need to adjust the command to work with your favorite diff tool.
+ *
+ * In order to accept the new test outputs as new "golden data", copy the actual outputs to golden
+ * data folder.
+ *
+ * Example:
+ * To obtain the copy command:
+ * $> ninja -j400 +unittest_test | grep "^{" |\
+ * jq -s -c -r '.[] | select(.id == 6273501 ) | "cp -R " + .attr.actualOutputRoot + "/" + "*
+ * ."' | sort | uniq
+ */
+
+/**
+ * A configuration specific to each golden test suite.
+ */
+struct GoldenTestConfig {
+ /**
+ * A relative path to the golden data files. The path is relative to root of the repo.
+ * This path can be shared by multiple suites.
+ *
+ * It is recommended to keep golden data is a separate subfolder from other source code files.
+ * Good:
+ * src/mongo/unittest/expected_output
+ * src/mongo/my_module/my_sub_module/expected_output
+ *
+ * Bad:
+ * src/mongo/my_module/
+ * src
+ * ../..
+ * /etc
+ * C:\Windows
+ */
+ std::string relativePath;
+};
+
+/**
+ * Global environment shared across all golden test suites.
+ * Specifically, output directory is shared across all suites to allow simple directory diffing,
+ * even if multiple suites were executed.
+ */
+class GoldenTestEnvironment {
+private:
+ GoldenTestEnvironment();
+
+public:
+ GoldenTestEnvironment(const GoldenTestEnvironment&) = delete;
+ GoldenTestEnvironment& operator=(const GoldenTestEnvironment&) = delete;
+
+ static GoldenTestEnvironment* getInstance();
+
+ boost::filesystem::path actualOutputRoot() const {
+ return _actualOutputRoot;
+ }
+
+ boost::filesystem::path expectedOutputRoot() const {
+ return _expectedOutputRoot;
+ }
+
+ boost::filesystem::path goldenDataRoot() const {
+ return _goldenDataRoot;
+ }
+
+private:
+ boost::filesystem::path _goldenDataRoot;
+ std::string _outputPathPrefix;
+ boost::filesystem::path _actualOutputRoot;
+ boost::filesystem::path _expectedOutputRoot;
+};
+
+/**
+ * Context for each golden test that can be used to accumulate, verify and optionally overwrite test
+ * output data. Format of the output data is left to the test implementation. However it is
+ * recommended that the output: 1) Is in text format. 2) Can be udated incrementally. Incremental
+ * changes to the production or test code should result in incremental changes to the test output.
+ * This reduces the size the side of diffs and reduces chances of conflicts. 3) Includes both input
+ * and output. This helps with inspecting the changes, without need to pattern match across files.
+ */
+class GoldenTestContext {
+public:
+ explicit GoldenTestContext(const GoldenTestConfig* config,
+ const TestInfo* testInfo = currentTestInfo(),
+ bool validateOnClose = true)
+ : _env(GoldenTestEnvironment::getInstance()),
+ _config(config),
+ _testInfo(testInfo),
+ _validateOnClose(validateOnClose) {}
+
+ ~GoldenTestContext() noexcept(false) {
+ if (_validateOnClose && !std::uncaught_exceptions()) {
+ verifyOutput();
+ }
+ }
+
+public:
+ /**
+ * Returns the output stream that a test should write its output to.
+ * The output that is written here will be compared against expected golden data. If the output
+ * that test produced differs from the expected output, the test will fail.
+ */
+ std::ostream& outStream() {
+ return _outStream;
+ }
+
+ /**
+ * Verifies that output accumulated in this context matches the expected output golden data.
+ * If output does not match, the test fails with TestAssertionFailureException.
+ *
+ * Additionally, in case of mismatch:
+ * - a file with the actual test output is created.
+ * - a file with the expected output is created:
+ * this preserves the snapshot of the golden data that was used for verification, as the
+ * files in the source repo can subsequently change. Output files are written to a temp files
+ * location unless configured otherwise.
+ */
+ void verifyOutput();
+
+ /**
+ * Returns the path where the actual test output will be written.
+ */
+ boost::filesystem::path getActualOutputPath() const;
+
+ /**
+ * Returns the path where the expected test output will be written.
+ */
+ boost::filesystem::path getExpectedOutputPath() const;
+
+ /**
+ * Returns the path to the golden data used for verification.
+ */
+ boost::filesystem::path getGoldedDataPath() const;
+
+ /**
+ * Returns relative test path. Typically composed of suite and test names.
+ */
+ boost::filesystem::path getTestPath() const;
+
+private:
+ static const TestInfo* currentTestInfo() {
+ return UnitTest::getInstance()->currentTestInfo();
+ }
+
+ void throwAssertionFailureException(const std::string& message);
+
+ static std::string readFile(const boost::filesystem::path& path);
+ static void writeFile(const boost::filesystem::path& path, const std::string& contents);
+
+ static std::string sanitizeName(const std::string& str);
+ static std::string toSnakeCase(const std::string& str);
+ static boost::filesystem::path toTestPath(const std::string& suiteName,
+ const std::string& testName);
+
+ void failResultMismatch(const std::string& actualStr,
+ const boost::optional<std::string>& expectedStr,
+ const std::string& messsage);
+
+private:
+ GoldenTestEnvironment* _env;
+ const GoldenTestConfig* _config;
+ const TestInfo* _testInfo;
+ bool _validateOnClose;
+ std::ostringstream _outStream;
+};
+
+/**
+ * Represents configuration variables used by golden tests.
+ */
+struct GoldenTestOptions {
+ /**
+ * Parses the options from environment variables that start with GOLDEN_TEST_ prefix.
+ * Supported options:
+ * - GOLDEN_TEST_OUTPUT: (optional) specifies the "output" data member.
+ */
+ static GoldenTestOptions parseEnvironment();
+
+ /**
+ * Path that will be used to write expected and actual test outputs.
+ * If not specified a temporary folder location will be used.
+ */
+ boost::optional<std::string> output;
+};
+
+} // namespace mongo::unittest
diff --git a/src/mongo/unittest/golden_test_test.cpp b/src/mongo/unittest/golden_test_test.cpp
new file mode 100644
index 00000000000..7e0212af1d3
--- /dev/null
+++ b/src/mongo/unittest/golden_test_test.cpp
@@ -0,0 +1,113 @@
+/**
+ * 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.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include <string>
+
+#include "mongo/unittest/golden_test.h"
+#include "mongo/unittest/unittest.h"
+#include "mongo/util/assert_util.h"
+
+namespace mongo::unittest {
+namespace {
+
+namespace fs = boost::filesystem;
+using namespace fmt::literals;
+
+GoldenTestConfig goldenTestConfig{"src/mongo/unittest/expected_output"};
+
+class GoldenSelfTestException : public std::exception {};
+
+// Verify the basic output comparison works.
+TEST(GoldenSelfTest, SanityTest) {
+ GoldenTestContext ctx(&goldenTestConfig);
+ auto& os = ctx.outStream();
+
+ os << "Output 1:\n";
+ os << "test test test 1\n";
+ os << "Output 2:\n";
+ os << "test test\n";
+ os << "test 2\n";
+}
+
+// Verify the basic output comparison works, when config is reused.
+TEST(GoldenSelfTest2, SanityTest2) {
+ GoldenTestContext ctx(&goldenTestConfig);
+ auto& os = ctx.outStream();
+ os << "Output 1:\n";
+ os << "test 1\n";
+}
+
+// Verify that test path is correctly generated from TestInfo.
+TEST(GoldenSelfTest, GoldenTestContextGetPath) {
+ // Verify that valid names result in expected path.
+ {
+ TestInfo testInfo("SuiteName"_sd, "TestName"_sd, __FILE__, __LINE__);
+ GoldenTestContext ctx(&goldenTestConfig, &testInfo, false);
+ ASSERT_EQ(ctx.getTestPath(), fs::path("suite_name") / fs::path("test_name.txt"));
+ }
+
+ {
+ TestInfo testInfo("SuiteName_-Abc"_sd, "_Test_Name_A-B"_sd, __FILE__, __LINE__);
+ GoldenTestContext ctx(&goldenTestConfig, &testInfo, false);
+ ASSERT_EQ(ctx.getTestPath(), fs::path("suite_name_-abc") / fs::path("_test_name_a-b.txt"));
+ }
+
+ // Verify that names with invalid characters fail with test asertion.
+ std::string badChars = "./\\*~`!@#$%^&*()";
+ for (char c : badChars) {
+ std::string badName = "Bad{}Name"_format(c);
+
+ {
+ TestInfo testInfo(badName, "TestName"_sd, __FILE__, __LINE__);
+ GoldenTestContext ctx(&goldenTestConfig, &testInfo, false);
+ ASSERT_THROWS([&] { ctx.getTestPath(); }(), TestAssertionFailureException);
+ }
+
+ {
+ TestInfo testInfo("SuiteName"_sd, badName, __FILE__, __LINE__);
+ GoldenTestContext ctx(&goldenTestConfig, &testInfo, false);
+ ASSERT_THROWS([&] { ctx.getTestPath(); }(), TestAssertionFailureException);
+ }
+ }
+}
+
+// Verify the basic output comparison works, when config is reused.
+TEST(GoldenSelfTest2, DoesNotCompareWhenExceptionThrown) {
+ ASSERT_THROWS(
+ [&] {
+ GoldenTestContext ctx(&goldenTestConfig);
+ ctx.outStream() << "No such output" << std::endl;
+ throw GoldenSelfTestException();
+ }(),
+ GoldenSelfTestException);
+}
+} // namespace
+} // namespace mongo::unittest
diff --git a/src/mongo/unittest/unittest.cpp b/src/mongo/unittest/unittest.cpp
index 233bb38f3ed..63ecd1746a6 100644
--- a/src/mongo/unittest/unittest.cpp
+++ b/src/mongo/unittest/unittest.cpp
@@ -613,6 +613,19 @@ std::vector<std::string> getAllSuiteNames() {
return result;
}
+UnitTest* UnitTest::getInstance() {
+ static auto p = new UnitTest;
+ return p;
+}
+
+const TestInfo* UnitTest::currentTestInfo() const {
+ return _currentTestInfo;
+}
+
+void UnitTest::setCurrentTestInfo(const TestInfo* testInfo) {
+ _currentTestInfo = testInfo;
+}
+
template <ComparisonOp op>
ComparisonAssertion<op> ComparisonAssertion<op>::make(const char* theFile,
unsigned theLine,
diff --git a/src/mongo/unittest/unittest.h b/src/mongo/unittest/unittest.h
index 6e8d0bb153a..2b28667c3dc 100644
--- a/src/mongo/unittest/unittest.h
+++ b/src/mongo/unittest/unittest.h
@@ -331,8 +331,9 @@
class TEST_TYPE : public TEST_BASE { \
private: \
void _doTest() override; \
- static inline const RegistrationAgent<TEST_TYPE> _agent{ \
- #FIXTURE_NAME, #TEST_NAME, __FILE__}; \
+ 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()
@@ -492,6 +493,79 @@ struct OldStyleSuiteInitializer {
/**
+ * Represents data about a single unit test.
+ */
+class TestInfo {
+public:
+ TestInfo(StringData suiteName, StringData testName, StringData file, unsigned int line)
+ : _suiteName(suiteName), _testName(testName), _file(file), _line(line) {}
+
+ StringData suiteName() const {
+ return _suiteName;
+ }
+ StringData testName() const {
+ return _testName;
+ }
+ StringData file() const {
+ return _file;
+ }
+ unsigned int line() const {
+ return _line;
+ }
+
+private:
+ StringData _suiteName;
+ StringData _testName;
+ StringData _file;
+ unsigned int _line;
+};
+
+
+/**
+ * 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.
*/
@@ -522,32 +596,32 @@ protected:
class RegistrationAgent {
public:
/**
- * These StringData must point to data that outlives this RegistrationAgent.
- * In the case of TEST/TEST_F, these are string literals.
+ * These TestInfo must point to data that outlives this RegistrationAgent.
+ * In the case of TEST/TEST_F, these are static variables.
*/
- RegistrationAgent(StringData suiteName, StringData testName, StringData fileName)
- : _suiteName{suiteName}, _testName{testName}, _fileName{fileName} {
- Suite::getSuite(_suiteName).add(std::string{_testName}, std::string{_fileName}, [] {
- T{}.run();
- });
+ 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 _suiteName;
+ return _testInfo->suiteName();
}
StringData getTestName() const {
- return _testName;
+ return _testInfo->testName();
}
StringData getFileName() const {
- return _fileName;
+ return _testInfo->file();
}
private:
- StringData _suiteName;
- StringData _testName;
- StringData _fileName;
+ const TestInfo* _testInfo;
};
/**