Skip to content

Commit

Permalink
No public description
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 718872112
  • Loading branch information
xinhaoyuan authored and copybara-github committed Jan 29, 2025
1 parent dc3ea33 commit d1dc32c
Show file tree
Hide file tree
Showing 13 changed files with 249 additions and 96 deletions.
1 change: 1 addition & 0 deletions centipede/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,7 @@ cc_library(
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/time",
"@com_google_absl//absl/types:span",
"@com_google_fuzztest//common:bazel",
"@com_google_fuzztest//common:blob_file",
"@com_google_fuzztest//common:defs",
"@com_google_fuzztest//common:hash",
Expand Down
99 changes: 10 additions & 89 deletions centipede/centipede_interface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
#include "./centipede/thread_pool.h"
#include "./centipede/util.h"
#include "./centipede/workdir.h"
#include "./common/bazel.h"
#include "./common/blob_file.h"
#include "./common/defs.h"
#include "./common/hash.h"
Expand Down Expand Up @@ -271,92 +272,9 @@ int Fuzz(const Environment &env, const BinaryInfo &binary_info,
return ExitCode();
}

struct TestShard {
int index = 0;
int total_shards = 1;
};

// https://bazel.build/reference/test-encyclopedia#initial-conditions
absl::Duration GetBazelTestTimeout() {
const char *test_timeout_env = std::getenv("TEST_TIMEOUT");
if (test_timeout_env == nullptr) return absl::InfiniteDuration();
int timeout_s = 0;
CHECK(absl::SimpleAtoi(test_timeout_env, &timeout_s))
<< "Failed to parse TEST_TIMEOUT: \"" << test_timeout_env << "\"";
return absl::Seconds(timeout_s);
}

void ReportErrorWhenNotEnoughTimeToRunEverything(absl::Time start_time,
absl::Duration test_time_limit,
int executed_tests_in_shard,
int fuzz_test_count,
int shard_count) {
static const absl::Duration bazel_test_timeout = GetBazelTestTimeout();
constexpr float kTimeoutSafetyFactor = 1.2;
const auto required_test_time = kTimeoutSafetyFactor * test_time_limit;
const auto remaining_duration =
bazel_test_timeout - (absl::Now() - start_time);
if (required_test_time <= remaining_duration) return;
std::string error =
"Cannot fuzz a fuzz test within the given timeout. Please ";
if (executed_tests_in_shard == 0) {
// Increasing number of shards won't help.
const absl::Duration suggested_timeout =
required_test_time * ((fuzz_test_count - 1) / shard_count + 1);
absl::StrAppend(&error, "set the `timeout` to ", suggested_timeout,
" or reduce the fuzzing time, ");
} else {
constexpr int kMaxShardCount = 50;
const int suggested_shard_count = std::min(
(fuzz_test_count - 1) / executed_tests_in_shard + 1, kMaxShardCount);
const int suggested_tests_per_shard =
(fuzz_test_count - 1) / suggested_shard_count + 1;
if (suggested_tests_per_shard > executed_tests_in_shard) {
// We wouldn't be able to execute the suggested number of tests without
// timeout. This case can only happen if we would in fact need more than
// `kMaxShardCount` shards, indicating that there are simply too many fuzz
// tests in a binary.
CHECK_EQ(suggested_shard_count, kMaxShardCount);
absl::StrAppend(&error,
"split the fuzz tests into several test binaries where "
"each binary has at most ",
executed_tests_in_shard * kMaxShardCount, "tests ",
"with `shard_count` = ", kMaxShardCount, ", ");
} else {
// In this case, `suggested_shard_count` must be greater than
// `shard_count`, otherwise we would have already executed all the tests
// without a timeout.
CHECK_GT(suggested_shard_count, shard_count);
absl::StrAppend(&error, "increase the `shard_count` to ",
suggested_shard_count, ", ");
}
}
absl::StrAppend(&error, "to avoid this issue. ");
absl::StrAppend(&error,
"(https://bazel.build/reference/be/"
"common-definitions#common-attributes-tests)");
CHECK(false) << error;
}

TestShard SetUpTestSharding() {
TestShard test_shard;
if (const char *test_total_shards_env = std::getenv("TEST_TOTAL_SHARDS");
test_total_shards_env != nullptr) {
CHECK(absl::SimpleAtoi(test_total_shards_env, &test_shard.total_shards))
<< "Failed to parse TEST_TOTAL_SHARDS as an integer: \""
<< test_total_shards_env << "\"";
CHECK_GT(test_shard.total_shards, 0)
<< "TEST_TOTAL_SHARDS must be greater than 0.";
}
if (const char *test_shard_index_env = std::getenv("TEST_SHARD_INDEX");
test_shard_index_env != nullptr) {
CHECK(absl::SimpleAtoi(test_shard_index_env, &test_shard.index))
<< "Failed to parse TEST_SHARD_INDEX as an integer: \""
<< test_shard_index_env << "\"";
CHECK(0 <= test_shard.index && test_shard.index < test_shard.total_shards)
<< "TEST_SHARD_INDEX must be in the range [0, "
<< test_shard.total_shards << ").";
}
TestShard test_shard = GetBazelTestShard();
// Update the shard status file to indicate that we support test sharding.
// It suffices to update the file's modification time, but we clear the
// contents for simplicity. This is also what the GoogleTest framework does.
Expand All @@ -365,7 +283,6 @@ TestShard SetUpTestSharding() {
test_shard_status_file != nullptr) {
ClearLocalFileContents(test_shard_status_file);
}

return test_shard;
}

Expand Down Expand Up @@ -586,10 +503,14 @@ int UpdateCorpusDatabaseForFuzzTests(
for (int i = resuming_fuzztest_idx; i < fuzz_tests_to_run.size(); ++i) {
if (!env.fuzztest_single_test_mode &&
fuzztest_config.GetTimeLimitPerTest() < absl::InfiniteDuration()) {
ReportErrorWhenNotEnoughTimeToRunEverything(
start_time, fuzztest_config.GetTimeLimitPerTest(),
/*executed_tests_in_shard=*/i, fuzztest_config.fuzz_tests.size(),
total_test_shards);
const absl::Duration test_time_limit =
fuzztest_config.GetTimeLimitPerTest();
const absl::Status has_enough_time = VerifyBazelHasEnoughTimeToRunTest(
start_time, test_time_limit,
/*executed_tests_in_shard=*/i, fuzztest_config.fuzz_tests.size());
CHECK_OK(has_enough_time)
<< "Not enough time for running the fuzz test "
<< fuzz_tests_to_run[i] << " for " << test_time_limit;
}
if (!is_workdir_specified) {
env.workdir = base_workdir_path / fuzz_tests_to_run[i];
Expand Down
12 changes: 12 additions & 0 deletions common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ exports_files(

### Libraries

cc_library(
name = "bazel",
srcs = ["bazel.cc"],
hdrs = ["bazel.h"],
deps = [
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/time",
],
)

cc_library(
name = "blob_file",
srcs = ["blob_file.cc"],
Expand Down
14 changes: 14 additions & 0 deletions common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ endif()

### Libraries

fuzztest_cc_library(
NAME
bazel
HDRS
"bazel.h"
SRCS
"bazel.cc"
DEPS
absl::check
absl::strings
absl::status
absl::time
)

fuzztest_cc_library(
NAME
blob_file
Expand Down
120 changes: 120 additions & 0 deletions common/bazel.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2024 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 "./common/bazel.h"

#include <algorithm>
#include <cstdlib>
#include <string>

#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"

namespace centipede {

namespace {

absl::Duration GetBazelTestTimeout() {
const char *test_timeout_env = std::getenv("TEST_TIMEOUT");
if (test_timeout_env == nullptr) return absl::InfiniteDuration();
int timeout_s = 0;
CHECK(absl::SimpleAtoi(test_timeout_env, &timeout_s))
<< "Failed to parse TEST_TIMEOUT: \"" << test_timeout_env << "\"";
return absl::Seconds(timeout_s);
}

} // namespace

TestShard GetBazelTestShard() {
static TestShard cached_test_shard = [] {
TestShard test_shard;
if (const char *test_total_shards_env = std::getenv("TEST_TOTAL_SHARDS");
test_total_shards_env != nullptr) {
CHECK(absl::SimpleAtoi(test_total_shards_env, &test_shard.total_shards))
<< "Failed to parse TEST_TOTAL_SHARDS as an integer: \""
<< test_total_shards_env << "\"";
CHECK_GT(test_shard.total_shards, 0)
<< "TEST_TOTAL_SHARDS must be greater than 0.";
}
if (const char *test_shard_index_env = std::getenv("TEST_SHARD_INDEX");
test_shard_index_env != nullptr) {
CHECK(absl::SimpleAtoi(test_shard_index_env, &test_shard.index))
<< "Failed to parse TEST_SHARD_INDEX as an integer: \""
<< test_shard_index_env << "\"";
CHECK(0 <= test_shard.index && test_shard.index < test_shard.total_shards)
<< "TEST_SHARD_INDEX must be in the range [0, "
<< test_shard.total_shards << ").";
}
return test_shard;
}();
return cached_test_shard;
}

absl::Status VerifyBazelHasEnoughTimeToRunTest(absl::Time target_start_time,
absl::Duration test_time_limit,
int executed_tests_in_shard,
int fuzz_test_count) {
static const absl::Duration bazel_test_timeout = GetBazelTestTimeout();
const int shard_count = GetBazelTestShard().total_shards;
constexpr float kTimeoutSafetyFactor = 1.2;
const auto required_test_time = kTimeoutSafetyFactor * test_time_limit;
const auto remaining_duration =
bazel_test_timeout - (absl::Now() - target_start_time);
if (required_test_time <= remaining_duration) return absl::OkStatus();
std::string error =
"Cannot fuzz a fuzz test within the given timeout. Please ";
if (executed_tests_in_shard == 0) {
// Increasing number of shards won't help.
const absl::Duration suggested_timeout =
required_test_time * ((fuzz_test_count - 1) / shard_count + 1);
absl::StrAppend(&error, "set the `timeout` to ", suggested_timeout,
" or reduce the fuzzing time, ");
} else {
constexpr int kMaxShardCount = 50;
const int suggested_shard_count = std::min(
(fuzz_test_count - 1) / executed_tests_in_shard + 1, kMaxShardCount);
const int suggested_tests_per_shard =
(fuzz_test_count - 1) / suggested_shard_count + 1;
if (suggested_tests_per_shard > executed_tests_in_shard) {
// We wouldn't be able to execute the suggested number of tests without
// timeout. This case can only happen if we would in fact need more than
// `kMaxShardCount` shards, indicating that there are simply too many fuzz
// tests in a binary.
CHECK_EQ(suggested_shard_count, kMaxShardCount);
absl::StrAppend(&error,
"split the fuzz tests into several test binaries where "
"each binary has at most ",
executed_tests_in_shard * kMaxShardCount, "tests ",
"with `shard_count` = ", kMaxShardCount, ", ");
} else {
// In this case, `suggested_shard_count` must be greater than
// `shard_count`, otherwise we would have already executed all the tests
// without a timeout.
CHECK_GT(suggested_shard_count, shard_count);
absl::StrAppend(&error, "increase the `shard_count` to ",
suggested_shard_count, ", ");
}
}
absl::StrAppend(&error, "to avoid this issue. ");
absl::StrAppend(&error,
"(https://bazel.build/reference/be/"
"common-definitions#common-attributes-tests)");
return absl::ResourceExhaustedError(error);
}

} // namespace centipede
44 changes: 44 additions & 0 deletions common/bazel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2024 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.

#ifndef FUZZTEST_COMMON_BAZEL_H_
#define FUZZTEST_COMMON_BAZEL_H_

#include "absl/status/status.h"
#include "absl/time/time.h"

namespace centipede {

struct TestShard {
int index = 0;
int total_shards = 1;
};

// Get Bazel sharding information based on
// https://bazel.build/reference/test-encyclopedia#initial-conditions
TestShard GetBazelTestShard();

// Returns Ok if there is enough time left to run a single test for
// `test_time_limit` given `target_start_time` and timeout and sharding
// information from Bazel, or returns an error status otherwise. It uses
// `executed_tests_in_shard` and `fuzz_test_count` to include suggestions into
// the error status message.
absl::Status VerifyBazelHasEnoughTimeToRunTest(absl::Time target_start_time,
absl::Duration test_time_limit,
int executed_tests_in_shard,
int fuzz_test_count);

} // namespace centipede

#endif // FUZZTEST_COMMON_BAZEL_H_
15 changes: 15 additions & 0 deletions e2e_tests/corpus_database_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,21 @@ TEST_P(UpdateCorpusDatabaseTest, ReplaysFuzzTestsInParallel) {
HasSubstr("[S0.0] begin-fuzz"), HasSubstr("[S1.0] begin-fuzz")));
}

TEST_P(UpdateCorpusDatabaseTest, PrintsErrorsWhenBazelTimeoutIsNotEnough) {
auto [status, std_out, std_err] = RunBinaryMaybeWithCentipede(
GetCorpusDatabaseTestingBinaryPath(),
{
.fuzztest_flags = {{"corpus_database", GetCorpusDatabasePath()},
{"fuzz_for", "20s"}},
.env = {{"TEST_TIMEOUT", "30"}},
.timeout = absl::Seconds(40),
});
EXPECT_THAT(std_err, AllOf(HasSubstr("Fuzzing FuzzTest.FailsInTwoWays"),
HasSubstr("Not enough time for running the fuzz "
"test FuzzTest.FailsWithStackOverflow")))
<< std_err;
}

INSTANTIATE_TEST_SUITE_P(
UpdateCorpusDatabaseTestWithExecutionModel, UpdateCorpusDatabaseTest,
testing::ValuesIn({ExecutionModelParam::kSingleBinary,
Expand Down
1 change: 1 addition & 0 deletions fuzztest/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ cc_library(
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/time",
"@com_google_absl//absl/types:span",
"@com_google_fuzztest//common:bazel",
],
)

Expand Down
1 change: 1 addition & 0 deletions fuzztest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ fuzztest_cc_library(
SRCS
"internal/runtime.cc"
DEPS
fuzztest::bazel
fuzztest::configuration
fuzztest::corpus_database
fuzztest::coverage
Expand Down
3 changes: 2 additions & 1 deletion fuzztest/init_fuzztest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,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();
const bool is_listing = absl::GetFlag(FUZZTEST_FLAG(list_fuzz_tests));
if (is_listing) {
for (const auto& name : ListRegisteredTests()) {
Expand Down Expand Up @@ -409,7 +410,7 @@ void InitFuzzTest(int* argc, char*** argv, std::string_view binary_id) {
const RunMode run_mode =
fuzzing_time_limit.has_value() ? RunMode::kFuzz : RunMode::kUnitTest;
// TODO(b/307513669): Use the Configuration class instead of Runtime.
internal::Runtime::instance().SetRunMode(run_mode);
runtime.SetRunMode(run_mode);
}

void ParseAbslFlags(int argc, char** argv) {
Expand Down
Loading

0 comments on commit d1dc32c

Please sign in to comment.