| // 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/status/status.h" |
| #include "absl/strings/numbers.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/time/clock.h" |
| #include "absl/time/time.h" |
| #include "./common/logging.h" |
| |
| namespace fuzztest::internal { |
| |
| namespace { |
| |
| absl::Duration GetBazelTestTimeout() { |
| const char *test_timeout_env = std::getenv("TEST_TIMEOUT"); |
| if (test_timeout_env == nullptr) return absl::InfiniteDuration(); |
| // When using `bazel run`, `TEST_TIMEOUT` is set but not used, and we should |
| // ignore the timeout. We detect this by the presence of |
| // `BUILD_WORKSPACE_DIRECTORY` |
| // (https://bazel.build/docs/user-manual#running-executables). |
| if (std::getenv("BUILD_WORKSPACE_DIRECTORY") != nullptr) { |
| return absl::InfiniteDuration(); |
| } |
| int timeout_s = 0; |
| FUZZTEST_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) { |
| FUZZTEST_CHECK( |
| absl::SimpleAtoi(test_total_shards_env, &test_shard.total_shards)) |
| << "Failed to parse TEST_TOTAL_SHARDS as an integer: \"" |
| << test_total_shards_env << "\""; |
| FUZZTEST_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) { |
| FUZZTEST_CHECK(absl::SimpleAtoi(test_shard_index_env, &test_shard.index)) |
| << "Failed to parse TEST_SHARD_INDEX as an integer: \"" |
| << test_shard_index_env << "\""; |
| FUZZTEST_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. |
| FUZZTEST_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. |
| FUZZTEST_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 fuzztest::internal |