diff --git a/CMakeLists.txt b/CMakeLists.txt index f9f71df0..04eecfce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,13 @@ cmake_minimum_required(VERSION 3.19) project(fuzztest) +# Limit parallel link jobs to avoid OOM in CI (GitHub Actions) +if (CMAKE_GENERATOR STREQUAL "Ninja" AND DEFINED ENV{GITHUB_ACTIONS}) + set_property(GLOBAL PROPERTY JOB_POOLS link_pool=1) + set(CMAKE_JOB_POOL_LINK link_pool) +endif () + + option(FUZZTEST_BUILD_TESTING "Building the tests." OFF) option(FUZZTEST_BUILD_FLATBUFFERS "Building the flatbuffers support." OFF) option(FUZZTEST_FUZZING_MODE "Building the fuzztest in fuzzing mode." OFF) diff --git a/domain_tests/BUILD b/domain_tests/BUILD index 0dfd652b..45e766ce 100644 --- a/domain_tests/BUILD +++ b/domain_tests/BUILD @@ -31,11 +31,10 @@ cc_test( "@abseil-cpp//absl/random:bit_gen_ref", "@abseil-cpp//absl/status", "@abseil-cpp//absl/types:optional", - "@abseil-cpp//absl/types:span", - "@abseil-cpp//absl/types:variant", "@com_google_fuzztest//fuzztest:domain_core", "@com_google_fuzztest//fuzztest/internal:serialization", "@com_google_fuzztest//fuzztest/internal:type_support", + "@com_google_fuzztest//fuzztest/internal/domains:core_domains_impl", "@googletest//:gtest_main", ], ) @@ -137,6 +136,7 @@ cc_test( "@abseil-cpp//absl/random", "@com_google_fuzztest//fuzztest:domain_core", "@com_google_fuzztest//fuzztest/internal:table_of_recent_compares", + "@com_google_fuzztest//fuzztest/internal/domains:core_domains_impl", "@googletest//:gtest_main", ], ) @@ -199,7 +199,9 @@ cc_test( ":domain_testing", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/random", + "@abseil-cpp//absl/status", "@com_google_fuzztest//fuzztest:domain_core", + "@com_google_fuzztest//fuzztest/internal/domains:core_domains_impl", "@googletest//:gtest_main", ], ) @@ -256,6 +258,17 @@ cc_test( ":domain_testing", "@abseil-cpp//absl/random", "@com_google_fuzztest//fuzztest:domain_core", + "@com_google_fuzztest//fuzztest/internal/domains:core_domains_impl", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "traversal_context_test", + srcs = ["traversal_context_test.cc"], + deps = [ + "@abseil-cpp//absl/status", + "@com_google_fuzztest//fuzztest/internal/domains:core_domains_impl", "@googletest//:gtest_main", ], ) diff --git a/domain_tests/CMakeLists.txt b/domain_tests/CMakeLists.txt index be59c476..821a14ce 100644 --- a/domain_tests/CMakeLists.txt +++ b/domain_tests/CMakeLists.txt @@ -281,3 +281,15 @@ fuzztest_cc_test( fuzztest::table_of_recent_compares GTest::gmock_main ) + +fuzztest_cc_test( + NAME + traversal_context_test + SRCS + "traversal_context_test.cc" + DEPS + fuzztest::domain_core + fuzztest::meta + absl::status + GTest::gmock_main +) diff --git a/domain_tests/aggregate_combinators_test.cc b/domain_tests/aggregate_combinators_test.cc index 28eeda4e..b903a568 100644 --- a/domain_tests/aggregate_combinators_test.cc +++ b/domain_tests/aggregate_combinators_test.cc @@ -31,6 +31,7 @@ #include "absl/types/optional.h" #include "./fuzztest/domain_core.h" #include "./domain_tests/domain_testing.h" +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/serialization.h" #include "./fuzztest/internal/type_support.h" @@ -491,5 +492,57 @@ TEST(TupleOf, DomainWithCustomPairCorpusType) { EXPECT_TRUE(optional_corpus_tuple.has_value()); } +TEST(StructOf, InitWithTrackerUpdatesCount) { + struct LocalStruct { + int a; + double b; + }; + auto domain = StructOf(Arbitrary(), Arbitrary()); + + absl::BitGen prng; + internal::TraversalState state; + state.count = 10; + + Value val(domain, prng, state); + + // 1 (root) + 2 (fields: int, double) = 3 decrements + EXPECT_EQ(*state.count, 7); +} + +TEST(StructOf, InitWithTrackerPropagatesFailureFromInnerDomain) { + struct LocalStruct { + int a; + std::vector v; + }; + // Inner container cannot be empty. + auto domain = StructOf( + Arbitrary(), VectorOf(Arbitrary()).WithMinSize(1)); + + absl::BitGen prng; + internal::TraversalState state; + // The parent init decrements to 0 and the inner vector init decrements to -1 + // and fails due to the min size constraint. + state.depth = 1; + + Value val(domain, prng, state); + + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.ToString(), + testing::HasSubstr("Traversal budget exceeded")); +} + +TEST(StructOf, InitWithTrackerHandlesPreExistingFailure) { + auto domain = StructOf(Arbitrary(), Arbitrary()); + + absl::BitGen prng; + internal::TraversalState state; + state.status = absl::CancelledError("Pre-existing failure"); + + Value val(domain, prng, state); + + EXPECT_FALSE(state.status.ok()); + EXPECT_EQ(state.status.message(), "Pre-existing failure"); +} + } // namespace } // namespace fuzztest diff --git a/domain_tests/arbitrary_domains_test.cc b/domain_tests/arbitrary_domains_test.cc index 828464ff..53bf83d4 100644 --- a/domain_tests/arbitrary_domains_test.cc +++ b/domain_tests/arbitrary_domains_test.cc @@ -673,7 +673,7 @@ TEST(ArbitraryStatusTest, GeneratesOkAndError) { bool found_ok = false; bool found_error = false; - for (int i = 0; i < 100 && (!found_ok || !found_error); ++i) { + for (int i = 0; i < 1000 && (!found_ok || !found_error); ++i) { absl::Status s = domain.GetRandomValue(prng); if (s.ok()) { found_ok = true; diff --git a/domain_tests/container_test.cc b/domain_tests/container_test.cc index c599fa48..fd6584ac 100644 --- a/domain_tests/container_test.cc +++ b/domain_tests/container_test.cc @@ -34,6 +34,7 @@ #include "absl/random/random.h" #include "./fuzztest/domain_core.h" #include "./domain_tests/domain_testing.h" +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/table_of_recent_compares.h" namespace fuzztest { @@ -369,5 +370,95 @@ TEST(Container, ValidatesMemoryDictionaryMutationForInnerDomain) { EXPECT_THAT(mutants, Not(Contains(std::vector{129, 129, 129, 129}))); } +TEST(ContainerTest, + SequenceInitWithTrackerDepthExhaustedWithMinSizeReturnsEmptyAndFails) { + auto domain = VectorOf(Arbitrary()).WithMinSize(3); + + absl::BitGen prng; + internal::TraversalState state; + state.depth = 0; + + Value val(domain, prng, state); + + EXPECT_TRUE(val.user_value.empty()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.ToString(), + testing::HasSubstr("Traversal budget exceeded")); +} + +TEST(ContainerTest, + SequenceInitWithTrackerDepthExhaustedNoMinSizeReturnsEmptyAndSucceeds) { + auto domain = VectorOf(Arbitrary()); + + absl::BitGen prng; + internal::TraversalState state; + state.depth = 0; + + Value val(domain, prng, state); + + EXPECT_TRUE(val.user_value.empty()); + EXPECT_TRUE(state.status.ok()); +} + +TEST(ContainerTest, SequenceInitWithTrackerUpdatesCount) { + auto domain = VectorOf(Arbitrary()).WithSize(3); + + absl::BitGen prng; + internal::TraversalState state; + state.count = 10; + + Value val(domain, prng, state); + + EXPECT_EQ(val.user_value.size(), 3); + // 1 (root) + 3 (elements) = 4 decrements + EXPECT_EQ(*state.count, 6); +} + +TEST(ContainerTest, SequenceInitWithTrackerCountExhaustedWithMinSizeFails) { + auto domain = VectorOf(Arbitrary()).WithMinSize(3); + + absl::BitGen prng; + internal::TraversalState state; + state.count = 0; // Enter() will decrement to -1 + + Value val(domain, prng, state); + + EXPECT_TRUE(val.user_value.empty()); + EXPECT_FALSE(state.status.ok()); +} + +TEST(ContainerTest, + SequenceInitWithTrackerCountExhaustedNoMinSizeReturnsEmptyAndSucceeds) { + auto domain = VectorOf(Arbitrary()); + + absl::BitGen prng; + internal::TraversalState state; + state.count = 0; + + Value val(domain, prng, state); + + EXPECT_TRUE(val.user_value.empty()); + EXPECT_TRUE(state.status.ok()); +} + +TEST(ContainerTest, + SequenceInitWithTrackerPropagatesFailureFromInnerExhaustion) { + // Both the parent and inner vectors must not be empty. + auto domain = + VectorOf(VectorOf(Arbitrary()).WithMinSize(1)).WithMinSize(1); + + absl::BitGen prng; + internal::TraversalState state; + // The parent init decrements to 0 and the inner init decrements to -1 + // and fails due to the min size constraint. + state.depth = 1; + + Value val(domain, prng, state); + + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.ToString(), + testing::HasSubstr("Traversal budget exceeded")); +} + } // namespace } // namespace fuzztest diff --git a/domain_tests/domain_testing.h b/domain_tests/domain_testing.h index ca2aa3b9..bb365fd6 100644 --- a/domain_tests/domain_testing.h +++ b/domain_tests/domain_testing.h @@ -37,6 +37,7 @@ #include "absl/strings/string_view.h" #include "./common/logging.h" #include "./fuzztest/internal/domains/mutation_metadata.h" +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/logging.h" #include "./fuzztest/internal/meta.h" #include "./fuzztest/internal/serialization.h" @@ -137,6 +138,11 @@ struct Value { : corpus_value(domain.Init(prng)), user_value(domain.GetValue(corpus_value)) {} + Value(Domain& domain, absl::BitGenRef prng, internal::TraversalState& state) + : corpus_value(domain.InitWithTracker( + prng, internal::TraversalContextWithTotalCount(state))), + user_value(domain.GetValue(corpus_value)) {} + // If the value_type is not copy constructible we have to copy the corpus and // regenerate the value. Value(const Value& other, Domain& domain) diff --git a/domain_tests/map_filter_combinator_test.cc b/domain_tests/map_filter_combinator_test.cc index ebf66ed1..4d9724c7 100644 --- a/domain_tests/map_filter_combinator_test.cc +++ b/domain_tests/map_filter_combinator_test.cc @@ -26,8 +26,10 @@ #include "gtest/gtest.h" #include "absl/algorithm/container.h" #include "absl/random/random.h" +#include "absl/status/status.h" #include "./fuzztest/domain_core.h" #include "./domain_tests/domain_testing.h" +#include "./fuzztest/internal/domains/traversal_context.h" namespace fuzztest { namespace { @@ -408,5 +410,65 @@ TEST(Filter, ValidationRejectsInvalidValue) { HasSubstr("Invalid corpus value for the inner domain in Filter()"))); } +TEST(Filter, InitWithTrackerRestoresBudgetOnPredicateFailure) { + int attempts = 0; + // Fails 50 times, then succeeds. + auto domain = + Filter([&attempts](int i) { return ++attempts > 50; }, Arbitrary()); + + absl::BitGen prng; + internal::TraversalState state; + state.depth = 100; + state.count = 10; + + Value val(domain, prng, state); + + EXPECT_TRUE(state.status.ok()); + // 10 - 1 (root) - 3 (successful inner with type erasure) = 6. + // The 50 failed attempts are restored. + EXPECT_EQ(*state.count, 6); + EXPECT_EQ(state.depth, 100); +} + +TEST(Filter, InitWithTrackerReturnsEarlyOnPreExistingFailure) { + auto domain = Filter( + [](int i) { + ADD_FAILURE() << "Predicate should not be called"; + return false; + }, + Arbitrary()); + + absl::BitGen prng; + internal::TraversalState state; + state.status = absl::CancelledError("Pre-existing failure"); + + Value val(domain, prng, state); + + EXPECT_FALSE(state.status.ok()); + EXPECT_EQ(state.status.message(), "Pre-existing failure"); +} + +TEST(Filter, InitWithTrackerReturnsInvalidValueOnFailure) { + auto domain = Filter( + [](const std::vector& v) { + ADD_FAILURE() << "Predicate should not be called"; + return false; + }, + VectorOf(Arbitrary()).WithMinSize(3)); + + absl::BitGen prng; + internal::TraversalState state; + // This causes the inner VectorOf initialization to fail due to budget + // exhaustion. + state.count = 2; + + Value val(domain, prng, state); + + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.ToString(), + testing::HasSubstr("Traversal budget exceeded")); + EXPECT_TRUE(val.user_value.empty()); +} + } // namespace } // namespace fuzztest diff --git a/domain_tests/recursive_domains_test.cc b/domain_tests/recursive_domains_test.cc index aa5a9566..6c442d0d 100644 --- a/domain_tests/recursive_domains_test.cc +++ b/domain_tests/recursive_domains_test.cc @@ -18,10 +18,12 @@ #include #include +#include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/random/random.h" #include "./fuzztest/domain_core.h" #include "./domain_tests/domain_testing.h" +#include "./fuzztest/internal/domains/traversal_context.h" namespace fuzztest { namespace { @@ -105,5 +107,68 @@ TEST(DomainBuilder, DiesOnInvalidFinalize) { "Finalize\\(\\) has been called with an unknown name: typo"); } +TEST(DomainBuilder, RecursiveDomainReachesDepthLimit) { + DomainBuilder builder; + builder.Set( + "tree", StructOf(InRange(0, 10), ContainerOf>( + builder.Get("tree")) + .WithSize(2))); + Domain domain = std::move(builder).Finalize("tree"); + + absl::BitGen bitgen; + internal::TraversalState state; + state.depth = 5; + + Value tree(domain, bitgen, state); + + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.ToString(), + testing::HasSubstr("Traversal budget exceeded")); + EXPECT_FALSE(state.error_trace.empty()); +} + +TEST(DomainBuilder, RecursiveDomainReachesNodeCountLimit) { + DomainBuilder builder; + builder.Set( + "tree", StructOf(InRange(0, 10), ContainerOf>( + builder.Get("tree")) + .WithSize(2))); + Domain domain = std::move(builder).Finalize("tree"); + + absl::BitGen bitgen; + internal::TraversalState state; + state.depth = 100; + state.count = 5; + + Value tree(domain, bitgen, state); + + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.ToString(), + testing::HasSubstr("Traversal budget exceeded")); + EXPECT_FALSE(state.error_trace.empty()); +} + +TEST(DomainBuilder, RecursiveDomainWithFilterReachesDepthLimit) { + DomainBuilder builder; + builder.Set( + "tree", + Filter([](const Tree& t) { return t.value % 2 == 0; }, + StructOf(InRange(0, 10), ContainerOf>( + builder.Get("tree")) + .WithSize(2)))); + Domain domain = std::move(builder).Finalize("tree"); + + absl::BitGen bitgen; + internal::TraversalState state; + state.depth = 5; + + Value tree(domain, bitgen, state); + + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.ToString(), + testing::HasSubstr("Traversal budget exceeded")); + EXPECT_FALSE(state.error_trace.empty()); +} + } // namespace } // namespace fuzztest diff --git a/domain_tests/traversal_context_test.cc b/domain_tests/traversal_context_test.cc new file mode 100644 index 00000000..dc4296b8 --- /dev/null +++ b/domain_tests/traversal_context_test.cc @@ -0,0 +1,280 @@ +// Copyright 2026 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 "./fuzztest/internal/domains/traversal_context.h" + +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/status/status.h" + +namespace fuzztest { +namespace { + +using ::fuzztest::internal::TraversalCheckpoint; +using ::fuzztest::internal::TraversalContext; +using ::fuzztest::internal::TraversalContextWithTotalCount; +using ::fuzztest::internal::TraversalState; + +struct TestDomain {}; +struct AnotherTestDomain {}; + +TEST(TraversalContextTest, DepthTrackingDecrementsAndRestores) { + TraversalState state; + state.depth = 2; + + EXPECT_TRUE(state.status.ok()); + EXPECT_EQ(state.depth, 2); + + { + TraversalContext ctx1(state); + EXPECT_EQ(state.depth, 1); + EXPECT_FALSE(ctx1.IsResourceExhausted()); + + { + TraversalContext ctx2(ctx1); + EXPECT_EQ(state.depth, 0); + EXPECT_FALSE(ctx2.IsResourceExhausted()); + + { + TraversalContext ctx3(ctx2); + EXPECT_EQ(state.depth, -1); + EXPECT_TRUE(ctx3.IsResourceExhausted()); + } + EXPECT_EQ(state.depth, 0); + } + EXPECT_EQ(state.depth, 1); + } + EXPECT_EQ(state.depth, 2); +} + +TEST(TraversalContextTest, CountTrackingDecrementsAndDoesNotRestore) { + TraversalState state; + state.count = 2; + + { + TraversalContextWithTotalCount ctx1(state); + EXPECT_EQ(*state.count, 1); + EXPECT_FALSE(ctx1.IsResourceExhausted()); + + { + TraversalContextWithTotalCount ctx2(ctx1); + EXPECT_EQ(*state.count, 0); + EXPECT_FALSE(ctx2.IsResourceExhausted()); + + { + TraversalContextWithTotalCount ctx3(ctx2); + EXPECT_EQ(*state.count, -1); + EXPECT_TRUE(ctx3.IsResourceExhausted()); + } + } + } + + EXPECT_TRUE(state.count.has_value()); + EXPECT_EQ(*state.count, -1); +} + +TEST(TraversalContextTest, CheckpointAndRestore) { + TraversalState state; + state.depth = 5; + state.count = 5; + + TraversalContextWithTotalCount ctx(state); + EXPECT_EQ(state.depth, 4); + EXPECT_EQ(*state.count, 4); + + TraversalCheckpoint cp = ctx.Checkpoint(); + + { + TraversalContextWithTotalCount ctx1(ctx); // depth 3, count 3 + TraversalContextWithTotalCount ctx2(ctx1); // depth 2, count 2 + state.status = absl::ResourceExhaustedError("Forced failure"); + EXPECT_FALSE(state.status.ok()); + } + + EXPECT_EQ(state.depth, 4); + EXPECT_EQ(*state.count, 2); + EXPECT_FALSE(state.status.ok()); + + ctx.Restore(cp); + + EXPECT_TRUE(state.status.ok()); + EXPECT_EQ(state.depth, 4); + EXPECT_EQ(*state.count, 4); +} + +TEST(TraversalContextTest, ExhaustedContextFailsOnlyOnExplicitFail) { + TraversalState state; + state.depth = 0; // Next enter will exhaust it + + TraversalContextWithTotalCount ctx(state); // depth -1 + EXPECT_TRUE(ctx.IsResourceExhausted()); + EXPECT_FALSE(ctx.IsFailed()); + + // We can choose to fail: + ctx.Fail(); + EXPECT_TRUE(ctx.IsFailed()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.ToString(), + testing::HasSubstr("Traversal budget exceeded")); +} + +TEST(TraversalContextTest, ExistingCountIsNotReset) { + TraversalState state; + // Initially count is nullopt. + EXPECT_FALSE(state.count.has_value()); + + { + TraversalContextWithTotalCount ctx1(state); + // Root context initializes count to kDefaultMaxCount because it was + // nullopt. + EXPECT_TRUE(state.count.has_value()); + // Decremented during Enter() + EXPECT_EQ(*state.count, 999); + + { + // Copy to another domain type. + TraversalContextWithTotalCount ctx2(ctx1); + EXPECT_EQ(*state.count, 998); // Decremented during ctx2's Enter() + } + // ctx2 destructed. Since it is a copy, it should NOT reset state.count to + // nullopt. + + EXPECT_TRUE(state.count.has_value()); + EXPECT_EQ(*state.count, 998); + } + // ctx1 destructed. Since it is the root, it SHOULD reset state.count to + // nullopt. + + EXPECT_FALSE(state.count.has_value()); +} +TEST(TraversalContextTest, NewCountIsInitializedAndReset) { + TraversalState state; + state.depth = 5; + + TraversalContext ctx_without_count(state); + EXPECT_EQ(state.depth, 4); + EXPECT_FALSE(state.count.has_value()); + + { + // Construct TraversalContextWithTotalCount from TraversalContext. + // It should initialize the count. + TraversalContextWithTotalCount ctx_with_count( + ctx_without_count); + EXPECT_EQ(state.depth, 3); + EXPECT_TRUE(state.count.has_value()); + // Decremented in InitCount() from the max value of 1000. + EXPECT_EQ(*state.count, 999); + } // ctx_with_count destructed. It should reset count to nullopt. + + EXPECT_EQ(state.depth, 4); + EXPECT_FALSE(state.count.has_value()); +} + +struct ExhaustionTestParam { + TraversalState state; + bool expected_exhausted; + bool expected_failed; +}; + +class TraversalContextExhaustionTest + : public testing::TestWithParam {}; + +TEST_P(TraversalContextExhaustionTest, ChecksIsResourceExhaustedAndIsFailed) { + const auto& param = GetParam(); + TraversalState state = param.state; + TraversalContextWithTotalCount ctx(state); + EXPECT_EQ(ctx.IsResourceExhausted(), param.expected_exhausted); + EXPECT_EQ(ctx.IsFailed(), param.expected_failed); +} + +INSTANTIATE_TEST_SUITE_P( + TraversalContextTests, TraversalContextExhaustionTest, + testing::Values(ExhaustionTestParam{TraversalState{1, 1}, false, false}, + ExhaustionTestParam{TraversalState{0, std::nullopt}, true, + false}, + ExhaustionTestParam{TraversalState{1, 0}, true, false}, + ExhaustionTestParam{ + TraversalState{1, 1, absl::CancelledError("cancelled")}, + false, true})); + +TEST(TraversalContextTest, ErrorTraceAccumulatesOnUnwinding) { + TraversalState state; + struct DomainC {}; + struct DomainB {}; + struct DomainA {}; + + { + TraversalContext ctxA(state); + { + TraversalContext ctxB(ctxA); + { + TraversalContext ctxC(ctxB); + ctxC.Fail(); + } + } + } + + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.error_trace, + testing::ElementsAre(testing::HasSubstr("DomainC"), + testing::HasSubstr("DomainB"), + testing::HasSubstr("DomainA"))); +} + +TEST(TraversalContextTest, DepthCappedAtMinusOne) { + TraversalState state; + state.depth = 1; + + { + // depth 0 + TraversalContext ctx1(state); + EXPECT_EQ(state.depth, 0); + { + // depth -1 + TraversalContext ctx2(ctx1); + EXPECT_EQ(state.depth, -1); + { + // depth capped at -1 + TraversalContext ctx3(ctx2); + EXPECT_EQ(state.depth, -1); + } // exit ctx3 -> depth stays -1 + EXPECT_EQ(state.depth, -1); + } // exit ctx2 -> depth becomes 0 + EXPECT_EQ(state.depth, 0); + } // exit ctx1 -> depth becomes 1 + EXPECT_EQ(state.depth, 1); +} + +TEST(TraversalContextTest, CountCappedAtMinusOne) { + TraversalState state; + state.count = 1; + + { + TraversalContextWithTotalCount ctx(state); + EXPECT_EQ(*state.count, 0); + } + { + TraversalContextWithTotalCount ctx(state); + EXPECT_EQ(*state.count, -1); + } + { + TraversalContextWithTotalCount ctx(state); + EXPECT_EQ(*state.count, -1); + } +} + +} // namespace +} // namespace fuzztest diff --git a/fuzztest/domain_core.h b/fuzztest/domain_core.h index f0a6a3d6..1f8f7ae0 100644 --- a/fuzztest/domain_core.h +++ b/fuzztest/domain_core.h @@ -62,6 +62,7 @@ #include "./fuzztest/internal/domains/optional_of_impl.h" #include "./fuzztest/internal/domains/overlap_of_impl.h" #include "./fuzztest/internal/domains/smart_pointer_of_impl.h" +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/domains/unique_elements_container_of_impl.h" #include "./fuzztest/internal/domains/utf.h" #include "./fuzztest/internal/domains/variant_of_impl.h" @@ -160,6 +161,12 @@ class DomainBuilder { return GetInnerDomain().Init(prng); } + corpus_type InitWithTracker( + absl::BitGenRef prng, + internal::TraversalContextWithTotalCount ctx) { + return GetInnerDomain().InitWithTracker(prng, ctx); + } + void Mutate(corpus_type& val, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, bool only_shrink) { @@ -218,6 +225,12 @@ class DomainBuilder { corpus_type Init(absl::BitGenRef prng) { return inner_.Init(prng); } + corpus_type InitWithTracker( + absl::BitGenRef prng, + internal::TraversalContextWithTotalCount ctx) { + return inner_.InitWithTracker(prng, ctx); + } + void Mutate(corpus_type& val, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, bool only_shrink) { diff --git a/fuzztest/internal/domains/BUILD b/fuzztest/internal/domains/BUILD index 545fef36..d675f042 100644 --- a/fuzztest/internal/domains/BUILD +++ b/fuzztest/internal/domains/BUILD @@ -52,6 +52,7 @@ cc_library( "serialization_helpers.h", "smart_pointer_of_impl.h", "special_values.h", + "traversal_context.h", "unique_elements_container_of_impl.h", "value_mutation_helpers.h", "variant_of_impl.h", diff --git a/fuzztest/internal/domains/CMakeLists.txt b/fuzztest/internal/domains/CMakeLists.txt index e9702ff9..f4221d59 100644 --- a/fuzztest/internal/domains/CMakeLists.txt +++ b/fuzztest/internal/domains/CMakeLists.txt @@ -52,6 +52,7 @@ fuzztest_cc_library( "serialization_helpers.h" "smart_pointer_of_impl.h" "special_values.h" + "traversal_context.h" "unique_elements_container_of_impl.h" "value_mutation_helpers.h" "variant_of_impl.h" diff --git a/fuzztest/internal/domains/aggregate_of_impl.h b/fuzztest/internal/domains/aggregate_of_impl.h index cd70ca1f..b383e164 100644 --- a/fuzztest/internal/domains/aggregate_of_impl.h +++ b/fuzztest/internal/domains/aggregate_of_impl.h @@ -26,6 +26,7 @@ #include "absl/random/distributions.h" #include "./fuzztest/internal/domains/domain_base.h" #include "./fuzztest/internal/domains/serialization_helpers.h" +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/meta.h" #include "./fuzztest/internal/serialization.h" #include "./fuzztest/internal/status.h" @@ -64,6 +65,17 @@ class AggregateOfImpl inner_); } + corpus_type InitWithTracker( + absl::BitGenRef prng, + TraversalContextWithTotalCount ctx) { + if (auto seed = this->MaybeGetRandomSeed(prng)) return *seed; + return std::apply( + [&](auto&... inner) { + return corpus_type{inner.InitWithTracker(prng, ctx)...}; + }, + inner_); + } + void Mutate(corpus_type& val, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, bool only_shrink) { diff --git a/fuzztest/internal/domains/container_of_impl.h b/fuzztest/internal/domains/container_of_impl.h index 7a8cd456..ec8fc920 100644 --- a/fuzztest/internal/domains/container_of_impl.h +++ b/fuzztest/internal/domains/container_of_impl.h @@ -34,6 +34,7 @@ #include "./common/logging.h" #include "./fuzztest/internal/domains/container_mutation_helpers.h" #include "./fuzztest/internal/domains/domain_base.h" +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/logging.h" #include "./fuzztest/internal/meta.h" #include "./fuzztest/internal/serialization.h" @@ -42,6 +43,21 @@ namespace fuzztest::internal { +inline void AbortIfIneffectiveSizeFilters(size_t min_size, size_t actual_size) { + if (actual_size < min_size) { + AbortInTest(absl::StrFormat(R"( + +[!] Ineffective use of WithSize()/WithMinSize() detected! + +The domain failed trying to find enough values that satisfy the constraints. +The minimum size requested is %u and we could only find %u elements. + +Please verify that the inner domain can provide enough values. +)", + min_size, actual_size)); + } +} + // Used for ChoosePosition(); enum class IncludeEnd { kYes, kNo }; @@ -460,28 +476,17 @@ class AssociativeContainerOfImpl corpus_type val; Grow(val, prng, size, 10000); - if (val.size() < this->min_size()) { - // We tried to make a container with the minimum specified size and we - // could not after a lot of attempts. This could be caused by an - // unsatisfiable domain, such as one where the minimum desired size is - // greater than the number of unique `value_type` values that exist; for - // example, a uint8_t has only 256 possible values, so we can't create - // a std::set whose size is greater than 256, as requested here: - // - // SetOf(Arbitrary()).WithMinSize(300) - // - // Abort the test and inform the user. - AbortInTest(absl::StrFormat(R"( - -[!] Ineffective use of WithSize()/WithMinSize() detected! - -The domain failed trying to find enough values that satisfy the constraints. -The minimum size requested is %u and we could only find %u elements. - -Please verify that the inner domain can provide enough values. -)", - this->min_size(), val.size())); - } + // We tried to make a container with the minimum specified size and we + // could not after a lot of attempts. This could be caused by an + // unsatisfiable domain, such as one where the minimum desired size is + // greater than the number of unique `value_type` values that exist; for + // example, a uint8_t has only 256 possible values, so we can't create + // a std::set whose size is greater than 256, as requested here: + // + // SetOf(Arbitrary()).WithMinSize(300) + // + // Abort the test and inform the user if it failed. + AbortIfIneffectiveSizeFilters(this->min_size(), val.size()); return val; } @@ -583,6 +588,24 @@ class SequenceContainerOfImplBase return val; } + corpus_type InitWithTracker(absl::BitGenRef prng, + TraversalContextWithTotalCount ctx) { + if (ctx.IsFailed() || ctx.IsResourceExhausted()) { + if (this->min_size() > 0) ctx.Fail(); + return corpus_type{}; + } + + if (auto seed = this->MaybeGetRandomSeed(prng)) return *seed; + const size_t size = this->ChooseRandomInitialSize(prng); + corpus_type val; + while (val.size() < size) { + auto elem = this->inner_.InitWithTracker(prng, ctx); + if (ctx.IsFailed()) break; + val.insert(val.end(), std::move(elem)); + } + return val; + } + uint64_t CountNumberOfFields(corpus_type& val) { uint64_t total_weight = 0; for (auto& i : val) { diff --git a/fuzztest/internal/domains/domain.h b/fuzztest/internal/domains/domain.h index 85425e20..57edfb0a 100644 --- a/fuzztest/internal/domains/domain.h +++ b/fuzztest/internal/domains/domain.h @@ -30,6 +30,7 @@ #include "absl/strings/string_view.h" #include "./fuzztest/internal/domains/domain_base.h" #include "./fuzztest/internal/domains/domain_type_erasure.h" // IWYU pragma: export +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/printer.h" #include "./fuzztest/internal/serialization.h" #include "./fuzztest/internal/table_of_recent_compares.h" @@ -165,6 +166,12 @@ class Domain { // values). corpus_type Init(absl::BitGenRef prng) { return inner_->UntypedInit(prng); } + corpus_type InitWithTracker( + absl::BitGenRef prng, + internal::TraversalContextWithTotalCount ctx) { + return inner_->UntypedInitWithTracker(prng, ctx); + } + // Mutate() makes a relatively small modification on `val` of `corpus_type`. // // Used during coverage-guided fuzzing. When `only_shrink` is true, @@ -393,6 +400,7 @@ absl::StatusOr ParseOneReproducerValue( } return domain.GetValue(*corpus); } + } // namespace internal } // namespace fuzztest diff --git a/fuzztest/internal/domains/domain_base.h b/fuzztest/internal/domains/domain_base.h index 704421b9..f12c43f6 100644 --- a/fuzztest/internal/domains/domain_base.h +++ b/fuzztest/internal/domains/domain_base.h @@ -32,6 +32,7 @@ #include "absl/strings/str_format.h" #include "./fuzztest/internal/domains/domain_type_erasure.h" #include "./fuzztest/internal/domains/mutation_metadata.h" // IWYU pragma: export +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/meta.h" #include "./fuzztest/internal/printer.h" #include "./fuzztest/internal/serialization.h" @@ -134,6 +135,12 @@ class DomainBase { return derived().GetValue(derived().GetRandomCorpusValue(prng)); } + corpus_type InitWithTracker( + absl::BitGenRef prng, + ::fuzztest::internal::TraversalContextWithTotalCount ctx) { + return derived().Init(prng); + } + // Default GetValue and FromValue functions for !has_custom_corpus_type // domains. ValueType GetValue(const ValueType& v) const { diff --git a/fuzztest/internal/domains/domain_type_erasure.h b/fuzztest/internal/domains/domain_type_erasure.h index 442af9d3..82b2a4a5 100644 --- a/fuzztest/internal/domains/domain_type_erasure.h +++ b/fuzztest/internal/domains/domain_type_erasure.h @@ -36,6 +36,7 @@ #include "./common/logging.h" #include "./fuzztest/internal/any.h" #include "./fuzztest/internal/domains/mutation_metadata.h" +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/logging.h" #include "./fuzztest/internal/meta.h" #include "./fuzztest/internal/printer.h" @@ -58,6 +59,9 @@ class UntypedDomainConcept { virtual std::unique_ptr UntypedClone() const = 0; virtual GenericDomainCorpusType UntypedInit(absl::BitGenRef) = 0; + virtual GenericDomainCorpusType UntypedInitWithTracker( + absl::BitGenRef, + TraversalContextWithTotalCount) = 0; virtual void UntypedMutate( GenericDomainCorpusType& val, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, @@ -140,6 +144,13 @@ class DomainModel final : public TypedDomainConcept> { domain_.Init(prng)); } + GenericDomainCorpusType UntypedInitWithTracker( + absl::BitGenRef prng, + TraversalContextWithTotalCount ctx) final { + return GenericDomainCorpusType(std::in_place_type, + domain_.InitWithTracker(prng, ctx)); + } + void UntypedMutate(GenericDomainCorpusType& val, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, bool only_shrink) final { diff --git a/fuzztest/internal/domains/filter_impl.h b/fuzztest/internal/domains/filter_impl.h index f37b8f91..132c1492 100644 --- a/fuzztest/internal/domains/filter_impl.h +++ b/fuzztest/internal/domains/filter_impl.h @@ -25,6 +25,7 @@ #include "./common/logging.h" #include "./fuzztest/internal/domains/domain.h" #include "./fuzztest/internal/domains/domain_base.h" +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/logging.h" #include "./fuzztest/internal/serialization.h" @@ -50,6 +51,20 @@ class FilterImpl } } + corpus_type InitWithTracker(absl::BitGenRef prng, + TraversalContextWithTotalCount ctx) { + if (ctx.IsFailed()) return inner_.InitWithTracker(prng, ctx); + if (auto seed = this->MaybeGetRandomSeed(prng)) return *seed; + + auto checkpoint = ctx.Checkpoint(); + while (true) { + auto v = inner_.InitWithTracker(prng, ctx); + if (ctx.IsFailed()) return v; + if (RunFilter(v)) return v; + ctx.Restore(checkpoint); + } + } + void Mutate(corpus_type& val, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, bool only_shrink) { diff --git a/fuzztest/internal/domains/traversal_context.h b/fuzztest/internal/domains/traversal_context.h new file mode 100644 index 00000000..1c60fdc6 --- /dev/null +++ b/fuzztest/internal/domains/traversal_context.h @@ -0,0 +1,165 @@ +// Copyright 2026 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_INTERNAL_DOMAINS_TRAVERSAL_CONTEXT_H_ +#define FUZZTEST_FUZZTEST_INTERNAL_DOMAINS_TRAVERSAL_CONTEXT_H_ + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "./fuzztest/internal/type_support.h" + +namespace fuzztest { +template +class Domain; +} + +namespace fuzztest::internal { + +struct TraversalState { + int depth = 100; // Default max depth + std::optional count; + absl::Status status = absl::OkStatus(); + std::vector error_trace; +}; + +struct TraversalCheckpoint { + int depth; + std::optional count; + absl::Status status; + size_t error_trace_size; +}; + +inline constexpr int kDefaultMaxCount = 1000; + +template +class TraversalContext { + public: + explicit TraversalContext(TraversalState& state) : state_{state} { Enter(); } + + template + TraversalContext(const TraversalContext& other) + : state_{other.state()} { + Enter(); + } + + TraversalContext(const TraversalContext& other) : state_{other.state_} { + Enter(); + } + + ~TraversalContext() { Exit(); } + + bool IsResourceExhausted() const { + return state_.depth < 0 || (state_.count.has_value() && *state_.count < 0); + } + + bool IsFailed() const { return !state_.status.ok(); } + + void Fail() { + if (state_.status.ok()) { + state_.status = absl::ResourceExhaustedError(absl::StrFormat( + "Traversal budget exceeded at %s", GetTypeName())); + } + } + + TraversalCheckpoint Checkpoint() const { + return {state_.depth, state_.count, state_.status, + state_.error_trace.size()}; + } + + void Restore(const TraversalCheckpoint& checkpoint) { + state_.depth = checkpoint.depth; + state_.count = checkpoint.count; + state_.status = checkpoint.status; + state_.error_trace.resize(checkpoint.error_trace_size); + } + + TraversalState& state() const { return state_; } + + protected: + void Enter() { + enter_ok_ = state_.status.ok(); + depth_decremented_ = state_.depth > -1; + if (depth_decremented_) { + state_.depth--; + } + if (state_.count.has_value()) { + *state_.count = std::max(-1, *state_.count - 1); + } + } + + void Exit() { + if (depth_decremented_) { + state_.depth++; + } + if (enter_ok_ && !state_.status.ok()) { + state_.error_trace.push_back(std::string(GetTypeName())); + } + } + + TraversalState& state_; + bool enter_ok_ = false; + bool depth_decremented_ = false; +}; + +template +class TraversalContextWithTotalCount : public TraversalContext { + public: + explicit TraversalContextWithTotalCount(TraversalState& state) + : TraversalContext{state} { + InitCount(); + } + + template + TraversalContextWithTotalCount( + const TraversalContextWithTotalCount& other) + : TraversalContext{other} { + InitCount(); + } + + template + TraversalContextWithTotalCount(const TraversalContext& other) + : TraversalContext{other} { + InitCount(); + } + + TraversalContextWithTotalCount(const TraversalContextWithTotalCount& other) + : TraversalContext{other} { + InitCount(); + } + + ~TraversalContextWithTotalCount() { + if (!enter_has_count_) { + this->state().count = std::nullopt; + } + } + + private: + void InitCount() { + enter_has_count_ = this->state().count.has_value(); + if (!enter_has_count_) { + this->state().count = kDefaultMaxCount - 1; + } + } + bool enter_has_count_ = true; +}; + +} // namespace fuzztest::internal + +#endif // FUZZTEST_FUZZTEST_INTERNAL_DOMAINS_TRAVERSAL_CONTEXT_H_