Extract `MockFactory` as `NonOwningCallbacksFactory` for better reusability. PiperOrigin-RevId: 819892786
diff --git a/centipede/BUILD b/centipede/BUILD index de600c6..bbbea85 100644 --- a/centipede/BUILD +++ b/centipede/BUILD
@@ -1273,6 +1273,18 @@ ) cc_test( + name = "centipede_callbacks_test", + srcs = ["centipede_callbacks_test.cc"], + deps = [ + ":centipede_callbacks", + ":environment", + ":runner_result", + "@com_google_fuzztest//common:defs", + "@googletest//:gtest_main", + ], +) + +cc_test( name = "environment_test", srcs = ["environment_test.cc"], deps = [ @@ -1884,7 +1896,6 @@ ":stop", ":util", ":workdir", - "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/time",
diff --git a/centipede/centipede_callbacks.h b/centipede/centipede_callbacks.h index 2976cd1..5bfcf46 100644 --- a/centipede/centipede_callbacks.h +++ b/centipede/centipede_callbacks.h
@@ -15,8 +15,10 @@ #ifndef THIRD_PARTY_CENTIPEDE_CENTIPEDE_CALLBACKS_H_ #define THIRD_PARTY_CENTIPEDE_CENTIPEDE_CALLBACKS_H_ +#include <atomic> #include <cstddef> #include <filesystem> // NOLINT +#include <memory> #include <string> #include <string_view> #include <vector> @@ -33,6 +35,7 @@ #include "./centipede/shared_memory_blob_sequence.h" #include "./centipede/util.h" #include "./common/defs.h" +#include "./common/logging.h" namespace fuzztest::internal { @@ -222,6 +225,35 @@ void destroy(CentipedeCallbacks *callbacks) override { delete callbacks; } }; +// An implementation of `CentipedeCallbacksFactory` that always returns the same +// predefined `CentipedeCallbacks` object and never destroys it. The factory +// ensures that each `create()` call must be followed by a `destroy()` call +// before `create()` can be called again. +class NonOwningCallbacksFactory : public CentipedeCallbacksFactory { + public: + explicit NonOwningCallbacksFactory(CentipedeCallbacks& callbacks) + : callbacks_(callbacks) {} + CentipedeCallbacks* absl_nonnull create(const Environment& env) override { + const bool was_already_created = + is_created_.exchange(true, std::memory_order_acq_rel); + FUZZTEST_CHECK(!was_already_created) + << "create() called before destroy() that matches the previous " + "create()"; + return &callbacks_; + } + void destroy(CentipedeCallbacks* callbacks) override { + FUZZTEST_CHECK_EQ(callbacks, &callbacks_); + const bool was_created = + is_created_.exchange(false, std::memory_order_acq_rel); + FUZZTEST_CHECK(was_created) + << "destroy() called before the matching create()"; + } + + private: + std::atomic<bool> is_created_ = false; + CentipedeCallbacks& callbacks_; +}; + // Creates a CentipedeCallbacks object in CTOR and destroys it in DTOR. class ScopedCentipedeCallbacks { public:
diff --git a/centipede/centipede_callbacks_test.cc b/centipede/centipede_callbacks_test.cc new file mode 100644 index 0000000..60fb8fc --- /dev/null +++ b/centipede/centipede_callbacks_test.cc
@@ -0,0 +1,71 @@ +// Copyright 2025 The Centipede Authors. +// +// 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 "./centipede/centipede_callbacks.h" + +#include <string_view> +#include <vector> + +#include "gtest/gtest.h" +#include "./centipede/environment.h" +#include "./centipede/runner_result.h" +#include "./common/defs.h" + +namespace fuzztest::internal { +namespace { + +class FakeCallbacks : public CentipedeCallbacks { + public: + explicit FakeCallbacks(const Environment& env) : CentipedeCallbacks(env) {} + bool Execute(std::string_view binary, const std::vector<ByteArray>& inputs, + BatchResult& batch_result) override { + return true; + } +}; + +TEST(NonOwningCallbacksFactoryTest, CreateReturnsUnderlyingCallbacks) { + Environment env; + FakeCallbacks callbacks(env); + NonOwningCallbacksFactory factory(callbacks); + EXPECT_EQ(factory.create(env), &callbacks); +} + +TEST(NonOwningCallbacksFactoryTest, CannotCreateTwice) { + Environment env; + FakeCallbacks callbacks(env); + NonOwningCallbacksFactory factory(callbacks); + factory.create(env); + EXPECT_DEATH(factory.create(env), "create\\(\\) called before destroy\\(\\)"); +} + +TEST(NonOwningCallbacksFactoryTest, CannotDestroyBeforeCreate) { + Environment env; + FakeCallbacks callbacks(env); + NonOwningCallbacksFactory factory(callbacks); + EXPECT_DEATH(factory.destroy(&callbacks), + "destroy\\(\\) called before the matching create\\(\\)"); +} + +TEST(NonOwningCallbacksFactoryTest, CannotDestroyTwice) { + Environment env; + FakeCallbacks callbacks(env); + NonOwningCallbacksFactory factory(callbacks); + factory.create(env); + factory.destroy(&callbacks); + EXPECT_DEATH(factory.destroy(&callbacks), + "destroy\\(\\) called before the matching create\\(\\)"); +} + +} // namespace +} // namespace fuzztest::internal
diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc index 371ccab..deb06af 100644 --- a/centipede/centipede_test.cc +++ b/centipede/centipede_test.cc
@@ -29,7 +29,6 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "absl/base/nullability.h" #include "absl/container/flat_hash_set.h" #include "absl/strings/str_cat.h" #include "absl/time/time.h" @@ -133,19 +132,6 @@ size_t min_batch_size_ = -1; }; -// Returns the same CentipedeCallbacks object every time, never destroys it. -class MockFactory : public CentipedeCallbacksFactory { - public: - explicit MockFactory(CentipedeCallbacks &cb) : cb_(cb) {} - CentipedeCallbacks *absl_nonnull create(const Environment &env) override { - return &cb_; - } - void destroy(CentipedeCallbacks *cb) override { EXPECT_EQ(cb, &cb_); } - - private: - CentipedeCallbacks &cb_; -}; - TEST(Centipede, MockTest) { TempCorpusDir tmp_dir{test_info_->name()}; Environment env; @@ -155,7 +141,7 @@ env.batch_size = 7; // Just some small number. env.require_pc_table = false; // No PC table here. CentipedeMock mock(env); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); CentipedeMain(env, factory); // Run fuzzing with num_runs inputs. EXPECT_EQ(mock.num_inputs_, env.num_runs + 1); // num_runs and one dummy. EXPECT_EQ(mock.num_mutations_, env.num_runs); @@ -189,7 +175,7 @@ { // First, generate corpus files in corpus_dir. CentipedeMock mock_1(env); - MockFactory factory_1(mock_1); + NonOwningCallbacksFactory factory_1(mock_1); CentipedeMain(env, factory_1); ASSERT_EQ(mock_1.observed_1byte_inputs_.size(), 256); // all 1-byte seqs. ASSERT_EQ(mock_1.observed_2byte_inputs_.size(), 65536); // all 2-byte seqs. @@ -202,7 +188,7 @@ env.workdir = workdir_2.path(); env.num_runs = 0; CentipedeMock mock_2(env); - MockFactory factory_2(mock_2); + NonOwningCallbacksFactory factory_2(mock_2); CentipedeMain(env, factory_2); // Should observe all inputs in corpus_dir, plus the dummy seed input {0}. EXPECT_EQ(mock_2.num_inputs_, 513); @@ -224,7 +210,7 @@ { // First, generate corpus files in corpus_dir. CentipedeMock mock_1(env); - MockFactory factory_1(mock_1); + NonOwningCallbacksFactory factory_1(mock_1); CentipedeMain(env, factory_1); ASSERT_EQ(mock_1.observed_1byte_inputs_.size(), 256); // all 1-byte seqs. ASSERT_EQ(mock_1.observed_2byte_inputs_.size(), 65536); // all 2-byte seqs. @@ -239,7 +225,7 @@ env.num_runs = 0; env.first_corpus_dir_output_only = true; CentipedeMock mock_2(env); - MockFactory factory_2(mock_2); + NonOwningCallbacksFactory factory_2(mock_2); CentipedeMain(env, factory_2); // Should observe no inputs other than the seed input {0}. EXPECT_EQ(mock_2.num_inputs_, 1); @@ -259,7 +245,7 @@ env.corpus_dir.push_back(tmp_dir.CreateSubdir("cd")); CentipedeMock mock(env); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); CentipedeMain(env, factory); // Run fuzzing with num_runs inputs. EXPECT_EQ(mock.observed_1byte_inputs_.size(), 256); // all 1-byte seqs. EXPECT_EQ(mock.observed_2byte_inputs_.size(), 65536); // all 2-byte seqs. @@ -287,7 +273,7 @@ size_t max_shard_size = 0; for (size_t shard_index = 0; shard_index < env.total_shards; shard_index++) { env.my_shard_index = shard_index; - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); CentipedeMain(env, factory); // Run fuzzing in shard `shard_index`. auto corpus_size = tmp_dir.CountElementsInCorpusFile(shard_index); // Every byte should be present at least once. @@ -309,7 +295,7 @@ // Empty the corpus_dir[0] std::filesystem::remove_all(env.corpus_dir[0]); std::filesystem::create_directory(env.corpus_dir[0]); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); CentipedeMain(env, factory); // Run distilling in shard `shard_index`. EXPECT_EQ(CountFilesInDir(env.corpus_dir[0]), 0); size_t distilled_size = 0; @@ -342,7 +328,7 @@ env.input_filter = "%f" + std::string{GetDataDependencyFilepath( "centipede/testing/test_input_filter")}; CentipedeMock mock(env); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); CentipedeMain(env, factory); // Run fuzzing. auto corpus = tmp_dir.GetCorpus(0); std::set<ByteArray> corpus_set(corpus.begin(), corpus.end()); @@ -537,7 +523,7 @@ env.num_runs = 3; // Just a few runs. env.require_pc_table = false; // No PC table here. MergeMock mock(env); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); for (env.my_shard_index = 0; env.my_shard_index < 2; ++env.my_shard_index) { CentipedeMain(env, factory); } @@ -644,7 +630,7 @@ env.log_level = 0; env.function_filter = function_filter; FunctionFilterMock mock(env); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); CentipedeMain(env, factory); FUZZTEST_LOG(INFO) << mock.observed_inputs_.size(); std::vector<ByteArray> res(mock.observed_inputs_.begin(), @@ -771,7 +757,7 @@ ExtraBinariesMock mock(env, {Crash{"b1", 10, "b1-crash", "b1-sig"}, Crash{"b2", 30, "b2-crash", "b2-sig"}, Crash{"b3", 50, "b3-crash", "b3-sig"}}); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); CentipedeMain(env, factory); // Verify that we see the expected crashes. @@ -895,7 +881,7 @@ { UndetectedCrashingInputMock mock(env, kCrashingInputIdx); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); CentipedeMain(env, factory); // Verify that we see the expected inputs from the batch. @@ -926,7 +912,7 @@ env.workdir = suspect_only_temp_dir.path(); env.batch_triage_suspect_only = true; UndetectedCrashingInputMock suspect_only_mock(env, kCrashingInputIdx); - MockFactory suspect_only_factory(suspect_only_mock); + NonOwningCallbacksFactory suspect_only_factory(suspect_only_mock); CentipedeMain(env, suspect_only_factory); EXPECT_EQ(suspect_only_mock.num_inputs_triaged(), 1); @@ -1030,7 +1016,7 @@ BatchResult batch_result; const std::vector<ByteArray> inputs = {{0}}; env.num_runs = 100; - MockFactory factory(callbacks); + NonOwningCallbacksFactory factory(callbacks); EXPECT_EQ(CentipedeMain(env, factory), EXIT_SUCCESS); EXPECT_TRUE(callbacks.thread_check_passed()); } @@ -1081,7 +1067,7 @@ env.batch_size = 7; // Just some small number. env.require_pc_table = false; // No PC table here. SetupFailureCallbacks mock(env); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); EXPECT_EQ(CentipedeMain(env, factory), EXIT_FAILURE); EXPECT_EQ(mock.execute_count(), 1); } @@ -1119,7 +1105,7 @@ env.batch_size = 7; // Just some small number. env.require_pc_table = false; // No PC table here. SkippedTestCallbacks mock(env); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); EXPECT_EQ(CentipedeMain(env, factory), EXIT_SUCCESS); EXPECT_EQ(mock.execute_count(), 1); } @@ -1159,7 +1145,7 @@ env.require_pc_table = false; // No PC table here. env.exit_on_crash = true; IgnoredFailureCallbacks mock(env); - MockFactory factory(mock); + NonOwningCallbacksFactory factory(mock); EXPECT_EQ(CentipedeMain(env, factory), EXIT_SUCCESS); EXPECT_GE(mock.execute_count(), 2); }