No public description PiperOrigin-RevId: 955890719
diff --git a/domain_tests/BUILD b/domain_tests/BUILD index 0dfd652..c4ecc87 100644 --- a/domain_tests/BUILD +++ b/domain_tests/BUILD
@@ -30,12 +30,10 @@ "@abseil-cpp//absl/random", "@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 +135,7 @@ "@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", ], ) @@ -153,8 +152,10 @@ "@abseil-cpp//absl/random", "@abseil-cpp//absl/random:bit_gen_ref", "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", "@com_google_fuzztest//common:logging", + "@com_google_fuzztest//common:status_macros", "@com_google_fuzztest//fuzztest/internal:logging", "@com_google_fuzztest//fuzztest/internal:meta", "@com_google_fuzztest//fuzztest/internal:serialization", @@ -199,7 +200,9 @@ ":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 +259,7 @@ ":domain_testing", "@abseil-cpp//absl/random", "@com_google_fuzztest//fuzztest:domain_core", + "@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 be59c47..2b09961 100644 --- a/domain_tests/CMakeLists.txt +++ b/domain_tests/CMakeLists.txt
@@ -8,10 +8,8 @@ absl::flat_hash_set absl::random_bit_gen_ref absl::random_random - absl::optional - absl::span absl::status - absl::variant + fuzztest::core_domains_impl fuzztest::domain_core fuzztest::serialization fuzztest::type_support @@ -122,6 +120,8 @@ absl::flat_hash_map absl::flat_hash_set absl::random_random + absl::status + fuzztest::core_domains_impl fuzztest::domain_core fuzztest::table_of_recent_compares GTest::gmock_main @@ -138,9 +138,11 @@ absl::random_random absl::random_bit_gen_ref absl::status + absl::statusor absl::strings fuzztest::domain_core fuzztest::common_logging + fuzztest::status_macros fuzztest::meta fuzztest::serialization fuzztest::test_protobuf_cc_proto @@ -188,6 +190,7 @@ fuzztest::domain_testing absl::algorithm_container absl::random_random + fuzztest::core_domains_impl fuzztest::domain_core GTest::gmock_main ) @@ -247,6 +250,7 @@ DEPS fuzztest::domain_testing absl::random_random + fuzztest::core_domains_impl fuzztest::domain_core GTest::gmock_main ) @@ -281,3 +285,5 @@ fuzztest::table_of_recent_compares GTest::gmock_main ) + +
diff --git a/domain_tests/aggregate_combinators_test.cc b/domain_tests/aggregate_combinators_test.cc index 5ee180d..c5e87ba 100644 --- a/domain_tests/aggregate_combinators_test.cc +++ b/domain_tests/aggregate_combinators_test.cc
@@ -30,6 +30,7 @@ #include "absl/status/status.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" @@ -490,5 +491,63 @@ EXPECT_TRUE(optional_corpus_tuple.has_value()); } +TEST(StructOf, InitWithTraversalCtxUpdatesInitBudget) { + struct Foo { + int a; + double b; + }; + auto domain = StructOf<Foo>(Arbitrary<int>(), Arbitrary<double>()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.init_budget = 10; + + auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + ASSERT_OK(val.status()); + // 1 (root) + 2 (fields: int, double) = 3 decrements + EXPECT_EQ(state.init_budget, 7); +} + +TEST(StructOf, InitWithTraversalCtxPropagatesFailureFromInnerDomain) { + struct Foo { + int a; + std::vector<int> v; + }; + // Inner container cannot be empty. + auto domain = StructOf<Foo>(Arbitrary<int>(), + VectorOf(Arbitrary<int>()).WithMinSize(1)); + + absl::BitGen prng; + domain_implementor::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_budget = 1; + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + EXPECT_FALSE(val.ok()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.message(), + testing::HasSubstr("Traversal budget exceeded")); +} + +TEST(StructOf, InitWithTraversalCtxHandlesPreExistingFailure) { + auto domain = StructOf<MyStruct>(Arbitrary<int>(), Arbitrary<std::string>()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.status = absl::CancelledError("Pre-existing failure"); + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + EXPECT_FALSE(val.ok()); + EXPECT_FALSE(state.status.ok()); + EXPECT_EQ(state.status.message(), "Pre-existing failure"); +} + } // namespace } // namespace fuzztest
diff --git a/domain_tests/container_test.cc b/domain_tests/container_test.cc index c599fa4..98c6dea 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 { @@ -356,8 +357,7 @@ * 1.0 / 4 // to use cmp tables * 1.0 / 2 // to pick the memcmp table * 1.0 / 2 // to pick one of the entries - * 1.0 / 2 // to apply replacement - ; + * 1.0 / 2; // to apply replacement for (int i = 0; i < 1 * IterationsToHitAll(/*num_cases=*/2, hit_probability); ++i) { std::vector<uint8_t> mutant = {10, 11, 12, 13}; @@ -369,5 +369,106 @@ EXPECT_THAT(mutants, Not(Contains(std::vector<uint8_t>{129, 129, 129, 129}))); } +TEST(ContainerTest, + SequenceInitWithTraversalCtxDepthExhaustedWithMinSizeFails) { + auto domain = VectorOf(Arbitrary<int>()).WithMinSize(3); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.depth_budget = 0; + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + EXPECT_FALSE(val.ok()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.message(), + testing::HasSubstr("Traversal budget exceeded")); +} + +TEST(ContainerTest, + SequenceInitWithTraversalCtxDepthExhaustedNoMinSizeSucceeds) { + auto domain = VectorOf(Arbitrary<int>()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.depth_budget = 0; + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + ASSERT_OK(val.status()); + EXPECT_TRUE(val->user_value.empty()); + EXPECT_TRUE(state.status.ok()); +} + +TEST(ContainerTest, SequenceInitWithTraversalCtxUpdatesInitBudget) { + auto domain = VectorOf(Arbitrary<int>()).WithSize(3); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.init_budget = 10; + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + ASSERT_OK(val.status()); + EXPECT_EQ(val->user_value.size(), 3); + // 1 (root) + 3 (elements) = 4 decrements + EXPECT_EQ(state.init_budget, 6); +} + +TEST(ContainerTest, + SequenceInitWithTraversalCtxInitBudgetExhaustedWithMinSizeFails) { + auto domain = VectorOf(Arbitrary<int>()).WithMinSize(3); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.init_budget = 0; // Enter() will decrement to -1 + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + EXPECT_FALSE(val.ok()); + EXPECT_FALSE(state.status.ok()); +} + +TEST(ContainerTest, + SequenceInitWithTraversalCtxInitBudgetExhaustedNoMinSizeSucceeds) { + auto domain = VectorOf(Arbitrary<int>()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.init_budget = 0; + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + ASSERT_OK(val.status()); + EXPECT_TRUE(val->user_value.empty()); + EXPECT_TRUE(state.status.ok()); +} + +TEST(ContainerTest, + SequenceInitWithTraversalCtxPropagatesFailureFromInnerExhaustion) { + // Both the parent and inner vectors must not be empty. + auto domain = + VectorOf(VectorOf(Arbitrary<int>()).WithMinSize(1)).WithMinSize(1); + + absl::BitGen prng; + domain_implementor::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_budget = 1; + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + EXPECT_FALSE(val.ok()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.message(), + testing::HasSubstr("Traversal budget exceeded")); +} + } // namespace } // namespace fuzztest
diff --git a/domain_tests/domain_testing.h b/domain_tests/domain_testing.h index ca2aa3b..61b6e61 100644 --- a/domain_tests/domain_testing.h +++ b/domain_tests/domain_testing.h
@@ -33,10 +33,13 @@ #include "absl/random/bit_gen_ref.h" #include "absl/random/random.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" +#include "./common/fuzztest_status_macros.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" @@ -81,12 +84,12 @@ template <typename T> size_t operator()(const T& v) const { if constexpr (internal::Requires<T>( - [](auto v) -> decltype(v && v.get()) {})) { + [](auto v) -> decltype(v&& v.get()) {})) { // Smart pointers. return v ? absl::HashOf(*v) : 0; } else if constexpr (internal::Requires<T>( - [](auto v) -> decltype(std::isnan( - *std::optional(v))) {})) { + [](auto v) -> decltype(std::isnan(*std::optional( + v))) {})) { auto o = std::optional(v); return !o || std::isnan(*o) ? 0 : absl::Hash<T>{}(*o); } else if constexpr (internal::Requires<T>( @@ -108,12 +111,12 @@ differencer.set_field_comparator(&cmp); return differencer.Compare(a, b); } else if constexpr (internal::Requires<T>( - [](auto v) -> decltype(v && v.get()) {})) { + [](auto v) -> decltype(v&& v.get()) {})) { // Smart pointers. return a ? b && *a == *b : !b; } else if constexpr (internal::Requires<T>( - [](auto v) -> decltype(std::isnan( - *std::optional(v))) {})) { + [](auto v) -> decltype(std::isnan(*std::optional( + v))) {})) { auto oa = std::optional(a), ob = std::optional(b); return a == b || (oa && ob && std::isnan(*oa) && std::isnan(*ob)); } else { @@ -127,6 +130,7 @@ // The Value class keeps the corpus and value types together throughout tests to // simplify their access and mutation. +// TODO(b/535145936): Update to a class and make the values private. template <typename Domain> struct Value { using T = internal::value_type_t<Domain>; @@ -152,6 +156,17 @@ }()), user_value(std::move(user_value)) {} + static absl::StatusOr<Value> BuildWithTraversalCtx( + Domain& domain, absl::BitGenRef prng, + domain_implementor::TraversalState& state) { + FUZZTEST_ASSIGN_OR_RETURN_IF_NOT_OK( + auto corpus, + domain.InitWithTraversalCtx( + prng, domain_implementor::InitTraversalContext<Domain>(state))); + auto user = domain.GetValue(corpus); + return Value{std::move(corpus), std::move(user)}; + } + void Mutate(Domain& domain, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, bool only_shrink) { @@ -222,6 +237,9 @@ } private: + Value(internal::corpus_type_t<Domain> corpus, T user) + : corpus_value(std::move(corpus)), user_value(std::move(user)) {} + // We don't test the printers here, just that we return one. // The printers themselves are tested in type_support_test.cc using Printer = decltype(std::declval<const Domain&>().GetPrinter());
diff --git a/domain_tests/map_filter_combinator_test.cc b/domain_tests/map_filter_combinator_test.cc index ebf66ed..82c80f9 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,72 @@ HasSubstr("Invalid corpus value for the inner domain in Filter()"))); } +TEST(Filter, InitWithTraversalCtxConsumesBudgetOnPredicateFailure) { + int attempts = 0; + // Fails 5 times, then succeeds. + auto domain = + Filter([&attempts](int i) { return ++attempts > 5; }, Arbitrary<int>()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.depth_budget = 100; + state.init_budget = 10; + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + ASSERT_OK(val.status()); + EXPECT_TRUE(state.status.ok()); + // Start: 10 + // Filter wrapper enter: -1 (9) + // 5 failed attempts of Arbitrary: 5 * -1 (4) + // 1 successful attempt of Arbitrary: -1 (3) + EXPECT_EQ(state.init_budget, 3); + EXPECT_EQ(state.depth_budget, 100); +} + +TEST(Filter, InitWithTraversalCtxReturnsEarlyOnPreExistingFailure) { + auto domain = Filter( + [](int i) { + ADD_FAILURE() << "Predicate should not be called"; + return false; + }, + Arbitrary<int>()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.status = absl::CancelledError("Pre-existing failure"); + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + EXPECT_FALSE(val.ok()); + EXPECT_FALSE(state.status.ok()); + EXPECT_EQ(state.status.message(), "Pre-existing failure"); +} + +TEST(Filter, InitWithTraversalCtxPropagatesFailureFromInnerDomain) { + auto domain = Filter( + [](const std::vector<int>& v) { + ADD_FAILURE() << "Predicate should not be called"; + return false; + }, + VectorOf(Arbitrary<int>()).WithMinSize(3)); + + absl::BitGen prng; + domain_implementor::TraversalState state; + // This causes the inner VectorOf initialization to fail due to budget + // exhaustion. + state.init_budget = 0; + + const auto val = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, prng, state); + + EXPECT_FALSE(val.ok()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.message(), + testing::HasSubstr("Traversal budget exceeded")); +} + } // namespace } // namespace fuzztest
diff --git a/domain_tests/recursive_domains_test.cc b/domain_tests/recursive_domains_test.cc index aa5a956..b2bba27 100644 --- a/domain_tests/recursive_domains_test.cc +++ b/domain_tests/recursive_domains_test.cc
@@ -18,14 +18,20 @@ #include <utility> #include <vector> +#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 { +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Not; + struct Tree { int value; std::vector<Tree> children; @@ -105,5 +111,71 @@ "Finalize\\(\\) has been called with an unknown name: typo"); } +TEST(DomainBuilder, RecursiveDomainReachesDepthLimit) { + DomainBuilder builder; + builder.Set<Tree>( + "tree", StructOf<Tree>(InRange(0, 10), ContainerOf<std::vector<Tree>>( + builder.Get<Tree>("tree")) + .WithSize(2))); + Domain<Tree> domain = std::move(builder).Finalize<Tree>("tree"); + + absl::BitGen bitgen; + domain_implementor::TraversalState state; + state.depth_budget = 5; + + const auto tree = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, bitgen, state); + + EXPECT_FALSE(tree.ok()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.message(), HasSubstr("Traversal budget exceeded")); + EXPECT_THAT(state.error_trace, Not(IsEmpty())); +} + +TEST(DomainBuilder, RecursiveDomainReachesInitBudgetLimit) { + DomainBuilder builder; + builder.Set<Tree>( + "tree", StructOf<Tree>(InRange(0, 10), ContainerOf<std::vector<Tree>>( + builder.Get<Tree>("tree")) + .WithSize(2))); + Domain<Tree> domain = std::move(builder).Finalize<Tree>("tree"); + + absl::BitGen bitgen; + domain_implementor::TraversalState state; + state.depth_budget = 100; + state.init_budget = 5; + + const auto tree = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, bitgen, state); + + EXPECT_FALSE(tree.ok()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.message(), HasSubstr("Traversal budget exceeded")); + EXPECT_THAT(state.error_trace, Not(IsEmpty())); +} + +TEST(DomainBuilder, RecursiveDomainWithFilterReachesDepthLimit) { + DomainBuilder builder; + builder.Set<Tree>( + "tree", + Filter([](const Tree& t) { return t.value % 2 == 0; }, + StructOf<Tree>(InRange(0, 10), ContainerOf<std::vector<Tree>>( + builder.Get<Tree>("tree")) + .WithSize(2)))); + Domain<Tree> domain = std::move(builder).Finalize<Tree>("tree"); + + absl::BitGen bitgen; + domain_implementor::TraversalState state; + state.depth_budget = 5; + + const auto tree = + Value<decltype(domain)>::BuildWithTraversalCtx(domain, bitgen, state); + + EXPECT_FALSE(tree.ok()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.message(), HasSubstr("Traversal budget exceeded")); + EXPECT_THAT(state.error_trace, Not(IsEmpty())); +} + } // namespace } // namespace fuzztest
diff --git a/fuzztest/domain_core.h b/fuzztest/domain_core.h index f0a6a3d..4da59f3 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,14 @@ return GetInnerDomain().Init(prng); } + absl::StatusOr<corpus_type> InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext<IndirectDomain> ctx) { + using InnerCtx = domain_implementor::InitTraversalContext<Domain<T>>; + return GetInnerDomain().InitWithTraversalCtx(prng, + InnerCtx::Passthrough(ctx)); + } + void Mutate(corpus_type& val, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, bool only_shrink) { @@ -218,6 +227,13 @@ corpus_type Init(absl::BitGenRef prng) { return inner_.Init(prng); } + absl::StatusOr<corpus_type> InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext<OwningDomain> ctx) { + using InnerCtx = domain_implementor::InitTraversalContext<Domain<T>>; + return inner_.InitWithTraversalCtx(prng, InnerCtx::Passthrough(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 5682ffb..5d571dd 100644 --- a/fuzztest/internal/domains/BUILD +++ b/fuzztest/internal/domains/BUILD
@@ -13,6 +13,7 @@ # limitations under the License. load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") package(default_visibility = ["@com_google_fuzztest//:__subpackages__"]) @@ -52,6 +53,7 @@ "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", @@ -72,6 +74,7 @@ "@abseil-cpp//absl/time", "@abseil-cpp//absl/types:span", "@com_google_fuzztest//common:logging", + "@com_google_fuzztest//common:status_macros", "@com_google_fuzztest//fuzztest:fuzzing_bit_gen", "@com_google_fuzztest//fuzztest/internal:any", "@com_google_fuzztest//fuzztest/internal:enum_reflection", @@ -97,9 +100,11 @@ "@abseil-cpp//absl/random:bit_gen_ref", "@abseil-cpp//absl/random:distributions", "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:str_format", "@com_google_fuzztest//common:logging", + "@com_google_fuzztest//common:status_macros", "@com_google_fuzztest//fuzztest/internal:any", "@com_google_fuzztest//fuzztest/internal:logging", "@com_google_fuzztest//fuzztest/internal:meta", @@ -247,3 +252,13 @@ "@com_google_fuzztest//fuzztest/internal:serialization", ], ) + +cc_test( + name = "traversal_context_test", + srcs = ["traversal_context_test.cc"], + deps = [ + ":core_domains_impl", + "@abseil-cpp//absl/status", + "@googletest//:gtest_main", + ], +)
diff --git a/fuzztest/internal/domains/CMakeLists.txt b/fuzztest/internal/domains/CMakeLists.txt index e9702ff..6fe4833 100644 --- a/fuzztest/internal/domains/CMakeLists.txt +++ b/fuzztest/internal/domains/CMakeLists.txt
@@ -52,6 +52,7 @@ "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" @@ -71,6 +72,7 @@ absl::time absl::span fuzztest::common_logging + fuzztest::status_macros fuzztest::fuzzing_bit_gen fuzztest::any fuzztest::logging @@ -110,6 +112,7 @@ fuzztest::domain_core fuzztest::any fuzztest::common_logging + fuzztest::status_macros fuzztest::meta fuzztest::printer fuzztest::serialization @@ -235,3 +238,14 @@ fuzztest::meta fuzztest::serialization ) + +fuzztest_cc_test( + NAME + traversal_context_test + SRCS + "traversal_context_test.cc" + DEPS + fuzztest::core_domains_impl + absl::status + GTest::gmock_main +)
diff --git a/fuzztest/internal/domains/aggregate_of_impl.h b/fuzztest/internal/domains/aggregate_of_impl.h index cd70ca1..fa03b4a 100644 --- a/fuzztest/internal/domains/aggregate_of_impl.h +++ b/fuzztest/internal/domains/aggregate_of_impl.h
@@ -24,8 +24,12 @@ #include "absl/random/bit_gen_ref.h" #include "absl/random/distributions.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "./common/fuzztest_status_macros.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 +68,32 @@ inner_); } + absl::StatusOr<corpus_type> InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext<AggregateOfImpl> ctx) { + FUZZTEST_RETURN_IF_NOT_OK(ctx.status()); + if (auto seed = this->MaybeGetRandomSeed(prng)) return *std::move(seed); + return ApplyIndex<sizeof...(Inner)>( + [&](auto... Is) -> absl::StatusOr<corpus_type> { + std::tuple<std::optional<corpus_type_t<Inner>>...> results; + absl::Status status = absl::OkStatus(); + auto init_one = [&](auto I) { + auto res = std::get<I>(inner_).InitWithTraversalCtx(prng, ctx); + if (!res.ok()) { + status = std::move(res).status(); + return false; + } + std::get<I>(results) = *std::move(res); + return true; + }; + if (!(init_one(Is) && ...)) { + return status; + } + + return corpus_type{*std::move(std::get<Is>(results))...}; + }); + } + 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 7a8cd45..312f1f4 100644 --- a/fuzztest/internal/domains/container_of_impl.h +++ b/fuzztest/internal/domains/container_of_impl.h
@@ -29,11 +29,14 @@ #include "absl/random/bit_gen_ref.h" #include "absl/random/distributions.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/str_format.h" #include "absl/types/span.h" +#include "./common/fuzztest_status_macros.h" #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" @@ -583,6 +586,34 @@ return val; } + absl::StatusOr<corpus_type> InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext<Derived> ctx) { + FUZZTEST_RETURN_IF_NOT_OK(ctx.status()); + if (ctx.IsResourceExhausted()) { + if (this->min_size() > 0) { + ctx.FailWithBudgetExceeded(); + return ctx.status(); + } + return corpus_type{}; + } + + if (auto seed = this->MaybeGetRandomSeed(prng)) return *std::move(seed); + const size_t size = this->ChooseRandomInitialSize(prng); + corpus_type val; + while (val.size() < size) { + auto elem = this->inner_.InitWithTraversalCtx(prng, ctx); + if (!elem.ok()) { + if (val.size() >= this->min_size()) { + return val; + } + return std::move(elem).status(); + } + 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 85425e2..c9aaeed 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,14 @@ // values). corpus_type Init(absl::BitGenRef prng) { return inner_->UntypedInit(prng); } + absl::StatusOr<corpus_type> InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext<Domain> ctx) { + return inner_->UntypedInitWithTraversalCtx( + prng, domain_implementor::InitTraversalContext< + internal::UntypedDomainConcept>::Passthrough(ctx)); + } + // Mutate() makes a relatively small modification on `val` of `corpus_type`. // // Used during coverage-guided fuzzing. When `only_shrink` is true, @@ -333,6 +342,14 @@ corpus_type Init(absl::BitGenRef prng) { return inner_->UntypedInit(prng); } + absl::StatusOr<corpus_type> InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext<UntypedDomain> ctx) { + return inner_->UntypedInitWithTraversalCtx( + prng, domain_implementor::InitTraversalContext< + internal::UntypedDomainConcept>::Passthrough(ctx)); + } + void Mutate(corpus_type& corpus_value, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, bool only_shrink) {
diff --git a/fuzztest/internal/domains/domain_base.h b/fuzztest/internal/domains/domain_base.h index 704421b..267985c 100644 --- a/fuzztest/internal/domains/domain_base.h +++ b/fuzztest/internal/domains/domain_base.h
@@ -29,9 +29,12 @@ #include "absl/random/bit_gen_ref.h" #include "absl/random/distributions.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/str_format.h" +#include "./common/fuzztest_status_macros.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 +137,12 @@ return derived().GetValue(derived().GetRandomCorpusValue(prng)); } + absl::StatusOr<corpus_type> InitWithTraversalCtx( + absl::BitGenRef prng, InitTraversalContext<Derived> ctx) { + FUZZTEST_RETURN_IF_NOT_OK(ctx.status()); + 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 442af9d..20b1ad4 100644 --- a/fuzztest/internal/domains/domain_type_erasure.h +++ b/fuzztest/internal/domains/domain_type_erasure.h
@@ -32,10 +32,13 @@ #include "absl/functional/function_ref.h" #include "absl/random/bit_gen_ref.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/string_view.h" +#include "./common/fuzztest_status_macros.h" #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 +61,9 @@ virtual std::unique_ptr<UntypedDomainConcept> UntypedClone() const = 0; virtual GenericDomainCorpusType UntypedInit(absl::BitGenRef) = 0; + virtual absl::StatusOr<GenericDomainCorpusType> UntypedInitWithTraversalCtx( + absl::BitGenRef, + domain_implementor::InitTraversalContext<UntypedDomainConcept>) = 0; virtual void UntypedMutate( GenericDomainCorpusType& val, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, @@ -140,6 +146,19 @@ domain_.Init(prng)); } + absl::StatusOr<GenericDomainCorpusType> UntypedInitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext<UntypedDomainConcept> ctx) + final { + FUZZTEST_ASSIGN_OR_RETURN_IF_NOT_OK( + auto res, + domain_.InitWithTraversalCtx( + prng, + domain_implementor::InitTraversalContext<D>::Passthrough(ctx))); + return GenericDomainCorpusType(std::in_place_type<CorpusType>, + std::move(res)); + } + 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 f37b8f9..ae8a71a 100644 --- a/fuzztest/internal/domains/filter_impl.h +++ b/fuzztest/internal/domains/filter_impl.h
@@ -21,11 +21,13 @@ #include "absl/random/bit_gen_ref.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/str_format.h" +#include "./common/fuzztest_status_macros.h" #include "./common/logging.h" #include "./fuzztest/internal/domains/domain.h" #include "./fuzztest/internal/domains/domain_base.h" -#include "./fuzztest/internal/logging.h" +#include "./fuzztest/internal/domains/traversal_context.h" #include "./fuzztest/internal/serialization.h" namespace fuzztest::internal { @@ -50,6 +52,19 @@ } } + absl::StatusOr<corpus_type> InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext<FilterImpl> ctx) { + FUZZTEST_RETURN_IF_NOT_OK(ctx.status()); + if (auto seed = this->MaybeGetRandomSeed(prng)) return *std::move(seed); + + while (true) { + FUZZTEST_ASSIGN_OR_RETURN_IF_NOT_OK( + auto v, inner_.InitWithTraversalCtx(prng, ctx)); + if (RunFilter(v)) return v; + } + } + 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 0000000..edaff80 --- /dev/null +++ b/fuzztest/internal/domains/traversal_context.h
@@ -0,0 +1,149 @@ +// 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 <limits> +#include <string> +#include <vector> + +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "./common/logging.h" +#include "./fuzztest/internal/type_support.h" + +namespace fuzztest::domain_implementor { + +struct TraversalState { + // The maximum node depth during domain initialization and mutation. + int depth_budget = 100; + // The maximum number of added nodes during domain initialization. + int init_budget = 1000; + absl::Status status = absl::OkStatus(); + std::vector<std::string> error_trace; +}; + +template <typename DomainType> +class TraversalContext { + public: + struct PassthroughTag {}; + TraversalContext(TraversalState& state, PassthroughTag) : state_{state} {} + + explicit TraversalContext(TraversalState& state) : state_{state} { Enter(); } + + template <typename OtherDomain> + TraversalContext(const TraversalContext<OtherDomain>& other) + : state_{other.state()} { + Enter(); + } + + TraversalContext(const TraversalContext& other) : state_{other.state_} { + Enter(); + } + + ~TraversalContext() { Exit(); } + + bool IsResourceExhausted() const { + return state_.depth_budget < 0 || state_.init_budget < 0; + } + + bool IsFailed() const { return !state_.status.ok(); } + + void FailWithBudgetExceeded() { + if (state_.status.ok()) { + state_.status = absl::ResourceExhaustedError( + absl::StrFormat("Traversal budget exceeded at %s", + fuzztest::internal::GetTypeName<DomainType>())); + } + } + + TraversalState& state() const { return state_; } + absl::Status status() const { return state_.status; } + + protected: + void Enter() { + entered_ = true; + enter_ok_ = state_.status.ok(); + FUZZTEST_CHECK_GT(state_.depth_budget, std::numeric_limits<int>::min()); + state_.depth_budget--; + } + + void Exit() { + if (entered_) { + state_.depth_budget++; + } + if (enter_ok_ && !state_.status.ok()) { + state_.error_trace.push_back( + std::string(fuzztest::internal::GetTypeName<DomainType>())); + } + } + + TraversalState& state_; + bool enter_ok_ = false; + bool entered_ = false; +}; + +template <typename DomainType> +class InitTraversalContext : public TraversalContext<DomainType> { + public: + using PassthroughTag = + typename InitTraversalContext::TraversalContext::PassthroughTag; + using InitTraversalContext::TraversalContext::TraversalContext; + + template <typename OtherDomain> + static InitTraversalContext Passthrough( + const InitTraversalContext<OtherDomain>& other) { + return InitTraversalContext(other.state(), PassthroughTag{}); + } + + template <typename OtherDomain> + static InitTraversalContext Passthrough( + const TraversalContext<OtherDomain>& other) { + return InitTraversalContext(other.state(), PassthroughTag{}); + } + + explicit InitTraversalContext(TraversalState& state) + : TraversalContext<DomainType>{state} { + Decrement(); + } + + template <typename OtherDomain> + InitTraversalContext(const InitTraversalContext<OtherDomain>& other) + : TraversalContext<DomainType>{other} { + Decrement(); + } + + template <typename OtherDomain> + InitTraversalContext(const TraversalContext<OtherDomain>& other) + : TraversalContext<DomainType>{other} { + Decrement(); + } + + InitTraversalContext(const InitTraversalContext& other) + : TraversalContext<DomainType>{other} { + Decrement(); + } + + private: + void Decrement() { + if (this->state_.init_budget >= 0) { + --this->state_.init_budget; + } + } +}; + +} // namespace fuzztest::domain_implementor + +#endif // FUZZTEST_FUZZTEST_INTERNAL_DOMAINS_TRAVERSAL_CONTEXT_H_
diff --git a/fuzztest/internal/domains/traversal_context_test.cc b/fuzztest/internal/domains/traversal_context_test.cc new file mode 100644 index 0000000..e2c737d --- /dev/null +++ b/fuzztest/internal/domains/traversal_context_test.cc
@@ -0,0 +1,282 @@ +// 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 "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/status/status.h" + +namespace fuzztest::domain_implementor { +namespace { + +struct TestDomain {}; +struct AnotherTestDomain {}; + +TEST(TraversalContextTest, DepthTrackingDecrementsAndRestores) { + TraversalState state; + state.depth_budget = 1; + + EXPECT_TRUE(state.status.ok()); + EXPECT_EQ(state.depth_budget, 1); + + { + TraversalContext<TestDomain> ctx1(state); + EXPECT_EQ(state.depth_budget, 0); + EXPECT_FALSE(ctx1.IsResourceExhausted()); + + { + TraversalContext<TestDomain> ctx2(ctx1); + EXPECT_EQ(state.depth_budget, -1); + EXPECT_TRUE(ctx2.IsResourceExhausted()); + } + EXPECT_EQ(state.depth_budget, 0); + } + EXPECT_EQ(state.depth_budget, 1); +} + +TEST(TraversalContextTest, InitBudgetTrackingDecrementsAndDoesNotRestore) { + TraversalState state; + state.init_budget = 1; + + { + InitTraversalContext<TestDomain> ctx1(state); + EXPECT_EQ(state.init_budget, 0); + EXPECT_FALSE(ctx1.IsResourceExhausted()); + + { + InitTraversalContext<TestDomain> ctx2(ctx1); + EXPECT_EQ(state.init_budget, -1); + EXPECT_TRUE(ctx2.IsResourceExhausted()); + } + } + + EXPECT_EQ(state.init_budget, -1); +} + +TEST(TraversalContextTest, MixedTraversalContextsInitBudgetTracking) { + TraversalState state; + state.init_budget = 2; + + { + InitTraversalContext<TestDomain> ctx1(state); // Init: decrements to 1 + EXPECT_EQ(state.init_budget, 1); + + { + TraversalContext<TestDomain> ctx2(ctx1); // Mutate: does NOT decrement + EXPECT_EQ(state.init_budget, 1); + + { + InitTraversalContext<TestDomain> ctx3(ctx2); // Init: decrements to 0 + EXPECT_EQ(state.init_budget, 0); + } + } + } + EXPECT_EQ(state.init_budget, 0); +} + +TEST(TraversalContextTest, ExhaustedContextFailsOnlyOnExplicitFail) { + TraversalState state; + state.depth_budget = 0; // Next enter will exhaust it + + InitTraversalContext<TestDomain> ctx(state); // depth -1 + EXPECT_TRUE(ctx.IsResourceExhausted()); + EXPECT_FALSE(ctx.IsFailed()); + + // We can choose to fail: + ctx.FailWithBudgetExceeded(); + EXPECT_TRUE(ctx.IsFailed()); + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.status.ToString(), + testing::HasSubstr("Traversal budget exceeded")); +} + +TEST(TraversalContextTest, ExistingInitBudgetIsNotReset) { + TraversalState state; // init_budget defaults to 1000 + EXPECT_EQ(state.init_budget, 1000); + + { + InitTraversalContext<TestDomain> ctx1(state); + EXPECT_EQ(state.init_budget, 999); + + { + InitTraversalContext<AnotherTestDomain> ctx2(ctx1); + EXPECT_EQ(state.init_budget, 998); + } + // ctx2 destructed. No change to init_budget. + EXPECT_EQ(state.init_budget, 998); + } + // ctx1 destructed. No change to init_budget. + EXPECT_EQ(state.init_budget, 998); +} + +TEST(TraversalContextTest, TransitionToInitTraversalContext) { + TraversalState state; // init_budget = 1000, depth_budget = 100 + state.depth_budget = 5; + + TraversalContext<TestDomain> ctx_without_budget(state); + EXPECT_EQ(state.depth_budget, 4); + EXPECT_EQ(state.init_budget, 1000); // Not decremented by TraversalContext + + { + InitTraversalContext<AnotherTestDomain> ctx_with_budget(ctx_without_budget); + EXPECT_EQ(state.depth_budget, 3); // Decremented by TraversalContext base + EXPECT_EQ(state.init_budget, 999); // Decremented by InitTraversalContext + } + // ctx_with_budget destructed. depth_budget is restored, init_budget is NOT. + EXPECT_EQ(state.depth_budget, 4); + EXPECT_EQ(state.init_budget, 999); +} + +struct ExhaustionTestParam { + TraversalState state; + bool expected_exhausted; + bool expected_failed; +}; + +class TraversalContextExhaustionTest + : public testing::TestWithParam<ExhaustionTestParam> {}; + +TEST_P(TraversalContextExhaustionTest, ChecksIsResourceExhaustedAndIsFailed) { + const auto& param = GetParam(); + TraversalState state = param.state; + InitTraversalContext<TestDomain> 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, 1000}, 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<DomainA> ctxA(state); + { + TraversalContext<DomainB> ctxB(ctxA); + { + TraversalContext<DomainC> ctxC(ctxB); + ctxC.FailWithBudgetExceeded(); + } + } + } + + EXPECT_FALSE(state.status.ok()); + EXPECT_THAT(state.error_trace, + testing::ElementsAre(testing::HasSubstr("DomainC"), + testing::HasSubstr("DomainB"), + testing::HasSubstr("DomainA"))); +} + +TEST(TraversalContextTest, DepthGoesBelowMinusOneAndRestores) { + TraversalState state; + state.depth_budget = 1; + + { + // depth 0 + TraversalContext<TestDomain> ctx1(state); + EXPECT_EQ(state.depth_budget, 0); + { + // depth -1 + TraversalContext<TestDomain> ctx2(ctx1); + EXPECT_EQ(state.depth_budget, -1); + { + // depth goes to -2 (no longer capped) + TraversalContext<TestDomain> ctx3(ctx2); + EXPECT_EQ(state.depth_budget, -2); + } // exit ctx3 -> depth becomes -1 + EXPECT_EQ(state.depth_budget, -1); + } // exit ctx2 -> depth becomes 0 + EXPECT_EQ(state.depth_budget, 0); + } // exit ctx1 -> depth becomes 1 + EXPECT_EQ(state.depth_budget, 1); +} + +TEST(TraversalContextTest, InitBudgetCappedAtMinusOne) { + TraversalState state; + state.init_budget = 1; + + { + InitTraversalContext<TestDomain> ctx(state); + EXPECT_EQ(state.init_budget, 0); + } + { + InitTraversalContext<TestDomain> ctx(state); + EXPECT_EQ(state.init_budget, -1); + } + { + InitTraversalContext<TestDomain> ctx(state); + EXPECT_EQ(state.init_budget, -1); + } +} + +TEST(TraversalContextTest, PassthroughDoesNotDecrementBudgets) { + TraversalState state; + state.depth_budget = 5; + state.init_budget = 10; + + { + InitTraversalContext<TestDomain> ctx(state); + EXPECT_EQ(state.depth_budget, 4); + EXPECT_EQ(state.init_budget, 9); + + { + auto passthrough_ctx = + InitTraversalContext<AnotherTestDomain>::Passthrough(ctx); + EXPECT_EQ(state.depth_budget, 4); // Not decremented + EXPECT_EQ(state.init_budget, 9); // Not decremented + } + // passthrough_ctx destructed. No change. + EXPECT_EQ(state.depth_budget, 4); + EXPECT_EQ(state.init_budget, 9); + } + // ctx destructed. depth_budget restored. + EXPECT_EQ(state.depth_budget, 5); + EXPECT_EQ(state.init_budget, 9); +} + +TEST(TraversalContextTest, PassthroughDoesNotAddToErrorTrace) { + TraversalState state; + + { + InitTraversalContext<TestDomain> ctx(state); + + { + auto passthrough_ctx = + InitTraversalContext<AnotherTestDomain>::Passthrough(ctx); + + passthrough_ctx.FailWithBudgetExceeded(); + EXPECT_FALSE(state.status.ok()); + } + // passthrough_ctx destructed. It should not add "AnotherTestDomain" to + // error_trace. + EXPECT_TRUE(state.error_trace.empty()); + } + // ctx destructed. It was active (enter_ok_ is true). + // So it should add "TestDomain" to error_trace because status is now not ok. + EXPECT_THAT(state.error_trace, testing::ElementsAre("TestDomain")); +} + +} // namespace +} // namespace fuzztest::domain_implementor