Fail loudly when FUZZ_TEST is registered but InitFuzzTest is not called in main() when running with GTest.

PiperOrigin-RevId: 978765150
diff --git a/e2e_tests/BUILD b/e2e_tests/BUILD
index e58abc9..b392ee6 100644
--- a/e2e_tests/BUILD
+++ b/e2e_tests/BUILD
@@ -48,6 +48,7 @@
         "@com_google_fuzztest//centipede:centipede_uninstrumented",
         "@com_google_fuzztest//e2e_tests/testdata:data",
         "@com_google_fuzztest//e2e_tests/testdata:dynamically_registered_fuzz_tests.stripped",
+        "@com_google_fuzztest//e2e_tests/testdata:fuzz_test_without_init_fuzztest.stripped",
         "@com_google_fuzztest//e2e_tests/testdata:fuzz_tests_for_functional_testing.stripped",
         "@com_google_fuzztest//e2e_tests/testdata:fuzz_tests_with_invalid_seeds.stripped",
         "@com_google_fuzztest//e2e_tests/testdata:llvm_fuzzer_with_custom_mutator.stripped",
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc
index 943eb72..e2b447f 100644
--- a/e2e_tests/functional_test.cc
+++ b/e2e_tests/functional_test.cc
@@ -151,6 +151,19 @@
   EXPECT_THAT(status, Eq(ExitCode(0)));
 }
 
+TEST_F(UnitTestModeTest, FailsLoudlyWhenInitFuzzTestIsNotCalled) {
+  auto [status, std_out, std_err] =
+      Run(/*test_filter=*/"*",
+          /*target_binary=*/"testdata/fuzz_test_without_init_fuzztest");
+  EXPECT_THAT(status, Ne(ExitCode(0)));
+  const std::string output = absl::StrCat(std_out, std_err);
+  EXPECT_THAT_LOG(output, HasSubstr("FuzzTest was not initialized!"));
+  EXPECT_THAT_LOG(output,
+                  HasSubstr("FUZZ_TEST was registered, but InitFuzzTest was "
+                            "never called in main()."));
+  EXPECT_THAT_LOG(output, HasSubstr("InitFuzzTest"));
+}
+
 TEST_F(UnitTestModeTest, InvalidSeedsAreSkippedAndReported) {
   auto [status, std_out, std_err] =
       Run(/*test_filter=*/"*",
diff --git a/e2e_tests/testdata/BUILD b/e2e_tests/testdata/BUILD
index 0db3a80..eee026c 100644
--- a/e2e_tests/testdata/BUILD
+++ b/e2e_tests/testdata/BUILD
@@ -156,3 +156,13 @@
         "@com_google_fuzztest//fuzztest:llvm_fuzzer_wrapper",
     ],
 )
+
+cc_binary(
+    name = "fuzz_test_without_init_fuzztest",
+    testonly = 1,
+    srcs = ["fuzz_test_without_init_fuzztest.cc"],
+    deps = [
+        "@com_google_fuzztest//fuzztest",
+        "@googletest//:gtest",
+    ],
+)
diff --git a/e2e_tests/testdata/CMakeLists.txt b/e2e_tests/testdata/CMakeLists.txt
index ec93a80..bc8f01b 100644
--- a/e2e_tests/testdata/CMakeLists.txt
+++ b/e2e_tests/testdata/CMakeLists.txt
@@ -116,3 +116,20 @@
   PROPERTIES RUNTIME_OUTPUT_DIRECTORY
   "${CMAKE_BINARY_DIR}/_main/e2e_tests/testdata"
 )
+
+add_executable(
+  fuzz_test_without_init_fuzztest.stripped
+  fuzz_test_without_init_fuzztest.cc
+)
+target_link_libraries(
+  fuzz_test_without_init_fuzztest.stripped
+  PUBLIC
+  fuzztest::fuzztest
+  GTest::gtest
+)
+set_target_properties(
+  fuzz_test_without_init_fuzztest.stripped
+  PROPERTIES RUNTIME_OUTPUT_DIRECTORY
+  "${CMAKE_BINARY_DIR}/_main/e2e_tests/testdata"
+)
+
diff --git a/e2e_tests/testdata/fuzz_test_without_init_fuzztest.cc b/e2e_tests/testdata/fuzz_test_without_init_fuzztest.cc
new file mode 100644
index 0000000..81f1b81
--- /dev/null
+++ b/e2e_tests/testdata/fuzz_test_without_init_fuzztest.cc
@@ -0,0 +1,28 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest.h"
+#include "./fuzztest/fuzztest.h"
+
+namespace {
+
+void MyFuzzTest(int x) {}
+FUZZ_TEST(MySuite, MyFuzzTest);
+
+}  // namespace
+
+int main(int argc, char** argv) {
+  testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/fuzztest/BUILD b/fuzztest/BUILD
index ab25066..c0848b4 100644
--- a/fuzztest/BUILD
+++ b/fuzztest/BUILD
@@ -55,10 +55,14 @@
 
 cc_library(
     name = "fuzztest",
-    hdrs = ["fuzztest.h"],
+    testonly = True,
+    hdrs = [
+        "fuzztest.h",
+    ],
     deps = [
         ":domain",
         ":fuzztest_macros",
+        ":init_fuzztest_checker",
     ],
 )
 
@@ -142,6 +146,18 @@
     alwayslink = True,
 )
 
+cc_library(
+    name = "init_fuzztest_checker",
+    testonly = True,
+    hdrs = ["init_fuzztest_checker.h"],
+    deps = [
+        "@abseil-cpp//absl/strings:string_view",
+        "@com_google_fuzztest//fuzztest/internal:registry",
+        "@com_google_fuzztest//fuzztest/internal:runtime",
+        "@googletest//:gtest",
+    ],
+)
+
 # TODO(hadi88): Add an e2e test for llvm_fuzzer_wrapper.
 cc_library(
     name = "llvm_fuzzer_main",
diff --git a/fuzztest/CMakeLists.txt b/fuzztest/CMakeLists.txt
index c42d529..7949dbc 100644
--- a/fuzztest/CMakeLists.txt
+++ b/fuzztest/CMakeLists.txt
@@ -86,12 +86,25 @@
 
 fuzztest_cc_library(
   NAME
+    init_fuzztest_checker
+  HDRS
+    "init_fuzztest_checker.h"
+  DEPS
+    GTest::gtest
+    absl::string_view
+    fuzztest::registry
+    fuzztest::runtime
+)
+
+fuzztest_cc_library(
+  NAME
     fuzztest
   HDRS
     "fuzztest.h"
   DEPS
     fuzztest::domain
     fuzztest::fuzztest_macros
+    fuzztest::init_fuzztest_checker
 )
 
 fuzztest_cc_library(
diff --git a/fuzztest/fuzztest.h b/fuzztest/fuzztest.h
index a153e36..867ac1d 100644
--- a/fuzztest/fuzztest.h
+++ b/fuzztest/fuzztest.h
@@ -18,6 +18,7 @@
 // IWYU pragma: begin_exports
 #include "./fuzztest/domain.h"
 #include "./fuzztest/fuzztest_macros.h"
+#include "./fuzztest/init_fuzztest_checker.h"
 // IWYU pragma: end_exports
 
 #endif  // FUZZTEST_FUZZTEST_FUZZTEST_H_
diff --git a/fuzztest/init_fuzztest.cc b/fuzztest/init_fuzztest.cc
index bb94b84..07f85bc 100644
--- a/fuzztest/init_fuzztest.cc
+++ b/fuzztest/init_fuzztest.cc
@@ -435,6 +435,7 @@
 
 void InitFuzzTest(int* argc, char*** argv, std::string_view binary_id) {
   auto& runtime = internal::Runtime::instance();
+  runtime.SetInitFuzzTestCalled(true);
   const bool is_listing = absl::GetFlag(FUZZTEST_FLAG(list_fuzz_tests));
   if (is_listing) {
     for (const auto& name : ListRegisteredTests()) {
diff --git a/fuzztest/init_fuzztest_checker.h b/fuzztest/init_fuzztest_checker.h
new file mode 100644
index 0000000..2b2d7bf
--- /dev/null
+++ b/fuzztest/init_fuzztest_checker.h
@@ -0,0 +1,63 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef FUZZTEST_FUZZTEST_INIT_FUZZTEST_CHECKER_H_
+#define FUZZTEST_FUZZTEST_INIT_FUZZTEST_CHECKER_H_
+
+#include "gtest/gtest.h"
+#include "absl/strings/string_view.h"
+#include "./fuzztest/internal/registry.h"
+#include "./fuzztest/internal/runtime.h"
+
+namespace fuzztest {
+namespace internal {
+
+inline constexpr absl::string_view kInitFuzzTestFailureMessage =
+    "FuzzTest was not initialized! "
+    "FUZZ_TEST was registered, but InitFuzzTest was never "
+    "called in main(). "
+    "If you are using a custom main(), please call "
+    "fuzztest::InitFuzzTest(&argc, &argv)"
+    " before RUN_ALL_TESTS().";
+
+inline void CheckFuzzTestInitialization() {
+  if (HasRegisteredFuzzTests() && !Runtime::instance().init_fuzztest_called()) {
+    ADD_FAILURE() << kInitFuzzTestFailureMessage;
+  }
+}
+
+class FuzzTestInitVerificationListener
+    : public ::testing::EmptyTestEventListener {
+ public:
+  void OnTestIterationStart(const ::testing::UnitTest&, int) override {
+    CheckFuzzTestInitialization();
+  }
+};
+
+inline bool RegisterFuzzTestInitVerification() {
+  static bool registered = [] {
+    ::testing::UnitTest::GetInstance()->listeners().Append(
+        new FuzzTestInitVerificationListener);
+    return true;
+  }();
+  return registered;
+}
+
+[[maybe_unused]] inline const bool g_fuzztest_init_checker_registered =
+    RegisterFuzzTestInitVerification();
+
+}  // namespace internal
+}  // namespace fuzztest
+
+#endif  // FUZZTEST_FUZZTEST_INIT_FUZZTEST_CHECKER_H_
diff --git a/fuzztest/internal/registry.cc b/fuzztest/internal/registry.cc
index ac695f0..aba8023 100644
--- a/fuzztest/internal/registry.cc
+++ b/fuzztest/internal/registry.cc
@@ -57,6 +57,8 @@
   for (auto& t : Regs()) func(t);
 }
 
+bool HasRegisteredFuzzTests() { return !Regs().empty(); }
+
 void RegisterImpl(BasicTestInfo test_info, FuzzTestFuzzerFactory factory) {
   Regs().emplace_back(std::move(test_info), std::move(factory));
 }
diff --git a/fuzztest/internal/registry.h b/fuzztest/internal/registry.h
index 0e8a446..a8e5dd4 100644
--- a/fuzztest/internal/registry.h
+++ b/fuzztest/internal/registry.h
@@ -36,6 +36,8 @@
 
 void RegisterImpl(BasicTestInfo test_info, FuzzTestFuzzerFactory factory);
 
+bool HasRegisteredFuzzTests();
+
 void ForEachTest(absl::FunctionRef<void(FuzzTest&)> func);
 
 using SetUpTearDownTestSuiteFunction = void (*)();
diff --git a/fuzztest/internal/runtime.h b/fuzztest/internal/runtime.h
index 1e0cbdd..21bfa3d 100644
--- a/fuzztest/internal/runtime.h
+++ b/fuzztest/internal/runtime.h
@@ -214,6 +214,9 @@
   // and aborts the process. Otherwise, does nothing.
   void HandleUnexpectedExit();
 
+  bool init_fuzztest_called() const { return init_fuzztest_called_; }
+  void SetInitFuzzTestCalled(bool b) { init_fuzztest_called_ = b; }
+
   class Watchdog;
   // Returns a watchdog that periodically checks the time and memory limits in a
   // separate thread. The watchdog handles the logic of starting and joining the
@@ -289,6 +292,8 @@
   std::vector<CrashMetadataListener> crash_metadata_listeners_;
   // In case of a crash, contains the crash type.
   std::optional<std::string> crash_type_;
+
+  bool init_fuzztest_called_ = false;
 };
 
 struct ReproducerOutputLocation {