diff --git a/e2e_tests/BUILD b/e2e_tests/BUILD index e58abc91a..b392ee62c 100644 --- a/e2e_tests/BUILD +++ b/e2e_tests/BUILD @@ -48,6 +48,7 @@ cc_test( "@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 943eb7256..e2b447f6c 100644 --- a/e2e_tests/functional_test.cc +++ b/e2e_tests/functional_test.cc @@ -151,6 +151,19 @@ TEST_F(UnitTestModeTest, PassingTestPassesInUnitTestingMode) { 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 0db3a8084..eee026c17 100644 --- a/e2e_tests/testdata/BUILD +++ b/e2e_tests/testdata/BUILD @@ -156,3 +156,13 @@ cc_binary( "@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 ec93a80aa..bc8f01bf6 100644 --- a/e2e_tests/testdata/CMakeLists.txt +++ b/e2e_tests/testdata/CMakeLists.txt @@ -116,3 +116,20 @@ set_target_properties( 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 000000000..81f1b81e5 --- /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 ab25066a7..c0848b409 100644 --- a/fuzztest/BUILD +++ b/fuzztest/BUILD @@ -55,10 +55,14 @@ config_setting( cc_library( name = "fuzztest", - hdrs = ["fuzztest.h"], + testonly = True, + hdrs = [ + "fuzztest.h", + ], deps = [ ":domain", ":fuzztest_macros", + ":init_fuzztest_checker", ], ) @@ -142,6 +146,18 @@ cc_library( 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 c42d5296f..7949dbc82 100644 --- a/fuzztest/CMakeLists.txt +++ b/fuzztest/CMakeLists.txt @@ -84,6 +84,18 @@ fuzztest_cc_test( GTest::gmock_main ) +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 @@ -92,6 +104,7 @@ fuzztest_cc_library( DEPS fuzztest::domain fuzztest::fuzztest_macros + fuzztest::init_fuzztest_checker ) fuzztest_cc_library( diff --git a/fuzztest/fuzztest.h b/fuzztest/fuzztest.h index a153e36e1..867ac1dd9 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 bb94b8477..07f85bc8d 100644 --- a/fuzztest/init_fuzztest.cc +++ b/fuzztest/init_fuzztest.cc @@ -435,6 +435,7 @@ void RunSpecifiedFuzzTest(std::string_view name, std::string_view binary_id) { 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 000000000..2b2d7bfdd --- /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 ac695f00c..aba8023da 100644 --- a/fuzztest/internal/registry.cc +++ b/fuzztest/internal/registry.cc @@ -57,6 +57,8 @@ void ForEachTest(absl::FunctionRef func) { 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 0e8a44646..a8e5dd44d 100644 --- a/fuzztest/internal/registry.h +++ b/fuzztest/internal/registry.h @@ -36,6 +36,8 @@ namespace internal { void RegisterImpl(BasicTestInfo test_info, FuzzTestFuzzerFactory factory); +bool HasRegisteredFuzzTests(); + void ForEachTest(absl::FunctionRef func); using SetUpTearDownTestSuiteFunction = void (*)(); diff --git a/fuzztest/internal/runtime.h b/fuzztest/internal/runtime.h index 1e0cbdd2b..21bfa3d3d 100644 --- a/fuzztest/internal/runtime.h +++ b/fuzztest/internal/runtime.h @@ -214,6 +214,9 @@ class Runtime { // 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 @@ class Runtime { std::vector crash_metadata_listeners_; // In case of a crash, contains the crash type. std::optional crash_type_; + + bool init_fuzztest_called_ = false; }; struct ReproducerOutputLocation {