diff options
-rw-r--r-- | src/mongo/unittest/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/unittest/death_test.h | 5 | ||||
-rw-r--r-- | src/mongo/unittest/expected_output/golden_self_test/sanity_test.txt | 5 | ||||
-rw-r--r-- | src/mongo/unittest/expected_output/golden_self_test2/sanity_test2.txt | 2 | ||||
-rw-r--r-- | src/mongo/unittest/golden_test.cpp | 200 | ||||
-rw-r--r-- | src/mongo/unittest/golden_test.h | 257 | ||||
-rw-r--r-- | src/mongo/unittest/golden_test_test.cpp | 113 | ||||
-rw-r--r-- | src/mongo/unittest/unittest.cpp | 13 | ||||
-rw-r--r-- | src/mongo/unittest/unittest.h | 104 |
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; }; /** |