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 {