Eliminate exit-time destructor in TypedTestSuitePState.

`TYPED_TEST_SUITE_P` defines a static `TypedTestSuitePState` variable at namespace scope. Storing registered tests in a `std::map<std::string, CodeLocation>` required dynamic initialization, heap allocations, and an exit-time destructor, triggering `-Wexit-time-destructors` warnings in builds that enforce trivial exit.

Store registered tests in an intrusive sorted singly-linked list of static POD `RegisteredTest` nodes defined by `TYPED_TEST_P`. `TypedTestSuitePState` is now `constexpr`-constructible and trivially destructible without heap allocations.

PiperOrigin-RevId: 998634744
Change-Id: I0c833ffeebdf21b155038b7e95e497637e68564a
This commit is contained in:
Derek Mauro 2026-10-13 07:53:27 -07:00 committed by Copybara-Service
parent 988ea2c179
commit b66cffcd5f
4 changed files with 89 additions and 45 deletions

View File

@ -256,8 +256,11 @@ INSTANTIATE_TYPED_TEST_SUITE_P(My, FooTest, MyTypes);
// The variables defined in the type-parameterized test macros are // The variables defined in the type-parameterized test macros are
// static as typically these macros are used in a .h file that can be // static as typically these macros are used in a .h file that can be
// #included in multiple translation units linked together. // #included in multiple translation units linked together.
#define TYPED_TEST_SUITE_P(SuiteName) \ // Marked [[maybe_unused]] because TypedTestSuitePState is
static ::testing::internal::TypedTestSuitePState \ // constexpr-constructible and trivially destructible, so an uninstantiated
// suite would otherwise trigger -Wunused-variable.
#define TYPED_TEST_SUITE_P(SuiteName) \
[[maybe_unused]] static ::testing::internal::TypedTestSuitePState \
GTEST_TYPED_TEST_SUITE_P_STATE_(SuiteName) GTEST_TYPED_TEST_SUITE_P_STATE_(SuiteName)
// Legacy API is deprecated but still available // Legacy API is deprecated but still available
@ -267,22 +270,24 @@ INSTANTIATE_TYPED_TEST_SUITE_P(My, FooTest, MyTypes);
TYPED_TEST_SUITE_P TYPED_TEST_SUITE_P
#endif // GTEST_REMOVE_LEGACY_TEST_CASEAPI_ #endif // GTEST_REMOVE_LEGACY_TEST_CASEAPI_
#define TYPED_TEST_P(SuiteName, TestName) \ #define TYPED_TEST_P(SuiteName, TestName) \
namespace GTEST_SUITE_NAMESPACE_(SuiteName) { \ namespace GTEST_SUITE_NAMESPACE_(SuiteName) { \
template <typename gtest_TypeParam_> \ template <typename gtest_TypeParam_> \
class TestName : public SuiteName<gtest_TypeParam_> { \ class TestName : public SuiteName<gtest_TypeParam_> { \
private: \ private: \
typedef SuiteName<gtest_TypeParam_> TestFixture; \ typedef SuiteName<gtest_TypeParam_> TestFixture; \
typedef gtest_TypeParam_ TypeParam; \ typedef gtest_TypeParam_ TypeParam; \
void TestBody() override; \ void TestBody() override; \
}; \ }; \
[[maybe_unused]] static bool gtest_##TestName##_defined_ = \ static ::testing::internal::TypedTestSuitePState::RegisteredTest \
GTEST_TYPED_TEST_SUITE_P_STATE_(SuiteName).AddTestName( \ gtest_##TestName##_registered_test_ = {GTEST_STRINGIFY_(TestName), \
__FILE__, __LINE__, GTEST_STRINGIFY_(SuiteName), \ __FILE__, __LINE__, nullptr}; \
GTEST_STRINGIFY_(TestName)); \ [[maybe_unused]] static bool gtest_##TestName##_defined_ = \
} \ GTEST_TYPED_TEST_SUITE_P_STATE_(SuiteName).AddTest( \
template <typename gtest_TypeParam_> \ &gtest_##TestName##_registered_test_, GTEST_STRINGIFY_(SuiteName)); \
void GTEST_SUITE_NAMESPACE_( \ } \
template <typename gtest_TypeParam_> \
void GTEST_SUITE_NAMESPACE_( \
SuiteName)::TestName<gtest_TypeParam_>::TestBody() SuiteName)::TestName<gtest_TypeParam_>::TestBody()
// Note: this won't work correctly if the trailing arguments are macros. // Note: this won't work correctly if the trailing arguments are macros.

View File

@ -577,51 +577,87 @@ GTEST_DISABLE_MSC_WARNINGS_PUSH_(4251 \
/* class A needs to have dll-interface to be used by clients of class B */) /* class A needs to have dll-interface to be used by clients of class B */)
// State of the definition of a type-parameterized test suite. // State of the definition of a type-parameterized test suite.
// Uses an intrusive linked list of caller-allocated POD RegisteredTest nodes
// rather than an STL container.
// REQUIRES: TypedTestSuitePState must be constexpr-constructible and trivially
// destructible so that namespace-scope static instances defined by
// TYPED_TEST_SUITE_P avoid -Wglobal-constructors and -Wexit-time-destructors.
class GTEST_API_ [[nodiscard]] TypedTestSuitePState { class GTEST_API_ [[nodiscard]] TypedTestSuitePState {
public: public:
TypedTestSuitePState() : registered_(false) {} // Caller-owned POD node linked into registered_tests_; must outlive this
// TypedTestSuitePState instance.
struct RegisteredTest {
const char* name;
const char* file;
int line;
RegisteredTest* next;
};
// Adds the given test name to defined_test_names_ and return true constexpr TypedTestSuitePState() = default;
// if the test suite hasn't been registered; otherwise aborts the
// program. // Adds the given test to registered_tests_ and returns true if the test
bool AddTestName(const char* file, int line, const char* case_name, // suite hasn't been registered; otherwise aborts the program.
const char* test_name) { bool AddTest(RegisteredTest* test, const char* case_name) {
if (registered_) { if (registered_) {
fprintf(stderr, fprintf(stderr,
"%s Test %s must be defined before " "%s Test %s must be defined before "
"REGISTER_TYPED_TEST_SUITE_P(%s, ...).\n", "REGISTER_TYPED_TEST_SUITE_P(%s, ...).\n",
FormatFileLocation(file, line).c_str(), test_name, case_name); FormatFileLocation(test->file, test->line).c_str(), test->name,
case_name);
fflush(stderr); fflush(stderr);
posix::Abort(); posix::Abort();
} }
registered_tests_.emplace(test_name, CodeLocation(file, line)); // Insert into registered_tests_ in lexicographical order by test name
// (ignoring duplicates) so VerifyRegisteredTestNames reports missing tests
// in deterministic sorted order.
RegisteredTest** p = &registered_tests_;
while (*p != nullptr && strcmp((*p)->name, test->name) < 0) {
p = &(*p)->next;
}
if (*p == nullptr || strcmp((*p)->name, test->name) != 0) {
test->next = *p;
*p = test;
}
return true; return true;
} }
bool TestExists(const std::string& test_name) const { bool TestExists(const std::string& test_name) const {
return registered_tests_.count(test_name) > 0; return FindTest(test_name) != nullptr;
} }
const CodeLocation& GetCodeLocation(const std::string& test_name) const { CodeLocation GetCodeLocation(const std::string& test_name) const {
RegisteredTestsMap::const_iterator it = registered_tests_.find(test_name); const RegisteredTest* test = FindTest(test_name);
GTEST_CHECK_(it != registered_tests_.end()); GTEST_CHECK_(test != nullptr);
return it->second; return CodeLocation(test->file, test->line);
} }
// Verifies that registered_tests match the test names in // Verifies that registered_tests match the test names in
// defined_test_names_; returns registered_tests if successful, or // registered_tests_; returns registered_tests if successful, or
// aborts the program otherwise. // aborts the program otherwise.
const char* VerifyRegisteredTestNames(const char* test_suite_name, const char* VerifyRegisteredTestNames(const char* test_suite_name,
const char* file, int line, const char* file, int line,
const char* registered_tests); const char* registered_tests);
private: private:
typedef ::std::map<std::string, CodeLocation, std::less<>> RegisteredTestsMap; const RegisteredTest* FindTest(const std::string& test_name) const {
for (const RegisteredTest* it = registered_tests_; it != nullptr;
it = it->next) {
if (test_name == it->name) {
return it;
}
}
return nullptr;
}
bool registered_; bool registered_ = false;
RegisteredTestsMap registered_tests_; RegisteredTest* registered_tests_ = nullptr;
}; };
static_assert((static_cast<void>(TypedTestSuitePState{}), true),
"TypedTestSuitePState must be constexpr-constructible");
static_assert(std::is_trivially_destructible_v<TypedTestSuitePState>,
"TypedTestSuitePState must be trivially destructible");
// Legacy API is deprecated but still available // Legacy API is deprecated but still available
#ifndef GTEST_REMOVE_LEGACY_TEST_CASEAPI_ #ifndef GTEST_REMOVE_LEGACY_TEST_CASEAPI_
using TypedTestCasePState = TypedTestSuitePState; using TypedTestCasePState = TypedTestSuitePState;

View File

@ -62,7 +62,6 @@ const char* TypedTestSuitePState::VerifyRegisteredTestNames(
const char* registered_tests) { const char* registered_tests) {
RegisterTypeParameterizedTestSuite(test_suite_name, CodeLocation(file, line)); RegisterTypeParameterizedTestSuite(test_suite_name, CodeLocation(file, line));
typedef RegisteredTestsMap::const_iterator RegisteredTestIter;
registered_ = true; registered_ = true;
std::vector<std::string> name_vec = SplitIntoTestNames(registered_tests); std::vector<std::string> name_vec = SplitIntoTestNames(registered_tests);
@ -78,7 +77,7 @@ const char* TypedTestSuitePState::VerifyRegisteredTestNames(
continue; continue;
} }
if (registered_tests_.count(name) != 0) { if (TestExists(name)) {
tests.insert(name); tests.insert(name);
} else { } else {
errors << "No test named " << name errors << "No test named " << name
@ -86,10 +85,10 @@ const char* TypedTestSuitePState::VerifyRegisteredTestNames(
} }
} }
for (RegisteredTestIter it = registered_tests_.begin(); for (const RegisteredTest* it = registered_tests_; it != nullptr;
it != registered_tests_.end(); ++it) { it = it->next) {
if (tests.count(it->first) == 0) { if (tests.count(it->name) == 0) {
errors << "You forgot to list test " << it->first << ".\n"; errors << "You forgot to list test " << it->name << ".\n";
} }
} }

View File

@ -200,12 +200,15 @@ using testing::internal::TypedTestSuitePState;
class TypedTestSuitePStateTest : public Test { class TypedTestSuitePStateTest : public Test {
protected: protected:
void SetUp() override { void SetUp() override {
state_.AddTestName("foo.cc", 0, "FooTest", "A"); state_.AddTest(&test_a_, "FooTest");
state_.AddTestName("foo.cc", 0, "FooTest", "B"); state_.AddTest(&test_b_, "FooTest");
state_.AddTestName("foo.cc", 0, "FooTest", "C"); state_.AddTest(&test_c_, "FooTest");
} }
TypedTestSuitePState state_; TypedTestSuitePState state_;
TypedTestSuitePState::RegisteredTest test_a_ = {"A", "foo.cc", 0, nullptr};
TypedTestSuitePState::RegisteredTest test_b_ = {"B", "foo.cc", 0, nullptr};
TypedTestSuitePState::RegisteredTest test_c_ = {"C", "foo.cc", 0, nullptr};
}; };
TEST_F(TypedTestSuitePStateTest, SucceedsForMatchingList) { TEST_F(TypedTestSuitePStateTest, SucceedsForMatchingList) {
@ -246,8 +249,9 @@ TEST_F(TypedTestSuitePStateDeathTest, DetectsMissedTest) {
// a run-time error if the test case has been registered. // a run-time error if the test case has been registered.
TEST_F(TypedTestSuitePStateDeathTest, DetectsTestAfterRegistration) { TEST_F(TypedTestSuitePStateDeathTest, DetectsTestAfterRegistration) {
state_.VerifyRegisteredTestNames("Suite", "foo.cc", 1, "A, B, C"); state_.VerifyRegisteredTestNames("Suite", "foo.cc", 1, "A, B, C");
TypedTestSuitePState::RegisteredTest test_d = {"D", "foo.cc", 2, nullptr};
EXPECT_DEATH_IF_SUPPORTED( EXPECT_DEATH_IF_SUPPORTED(
state_.AddTestName("foo.cc", 2, "FooTest", "D"), state_.AddTest(&test_d, "FooTest"),
"foo\\.cc.2.?: Test D must be defined before REGISTER_TYPED_TEST_SUITE_P" "foo\\.cc.2.?: Test D must be defined before REGISTER_TYPED_TEST_SUITE_P"
"\\(FooTest, \\.\\.\\.\\)\\."); "\\(FooTest, \\.\\.\\.\\)\\.");
} }