diff --git a/domain_tests/BUILD b/domain_tests/BUILD index 0dfd652b4..c4ecc87c2 100644 --- a/domain_tests/BUILD +++ b/domain_tests/BUILD @@ -30,12 +30,10 @@ cc_test( "@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 @@ 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", ], ) @@ -153,8 +152,10 @@ cc_library( "@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 @@ 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 +259,7 @@ 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", ], ) diff --git a/domain_tests/CMakeLists.txt b/domain_tests/CMakeLists.txt index be59c4763..2b099613f 100644 --- a/domain_tests/CMakeLists.txt +++ b/domain_tests/CMakeLists.txt @@ -8,10 +8,8 @@ fuzztest_cc_test( 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 @@ fuzztest_cc_test( 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 @@ fuzztest_cc_library( 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_cc_test( fuzztest::domain_testing absl::algorithm_container absl::random_random + fuzztest::core_domains_impl fuzztest::domain_core GTest::gmock_main ) @@ -247,6 +250,7 @@ fuzztest_cc_test( DEPS fuzztest::domain_testing absl::random_random + fuzztest::core_domains_impl fuzztest::domain_core GTest::gmock_main ) @@ -281,3 +285,5 @@ fuzztest_cc_test( 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 5ee180d4d..c5e87ba17 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 @@ TEST(TupleOf, DomainWithCustomPairCorpusType) { EXPECT_TRUE(optional_corpus_tuple.has_value()); } +TEST(StructOf, InitWithTraversalCtxUpdatesInitBudget) { + struct Foo { + int a; + double b; + }; + auto domain = StructOf(Arbitrary(), Arbitrary()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.init_budget = 10; + + auto val = + Value::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 v; + }; + // Inner container cannot be empty. + auto domain = StructOf(Arbitrary(), + VectorOf(Arbitrary()).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::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(Arbitrary(), Arbitrary()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.status = absl::CancelledError("Pre-existing failure"); + + const auto val = + Value::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 c599fa48e..98c6dea54 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 @@ TEST(Container, ValidatesMemoryDictionaryMutationForInnerDomain) { * 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 mutant = {10, 11, 12, 13}; @@ -369,5 +369,106 @@ TEST(Container, ValidatesMemoryDictionaryMutationForInnerDomain) { EXPECT_THAT(mutants, Not(Contains(std::vector{129, 129, 129, 129}))); } +TEST(ContainerTest, + SequenceInitWithTraversalCtxDepthExhaustedWithMinSizeFails) { + auto domain = VectorOf(Arbitrary()).WithMinSize(3); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.depth_budget = 0; + + const auto val = + Value::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()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.depth_budget = 0; + + const auto val = + Value::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()).WithSize(3); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.init_budget = 10; + + const auto val = + Value::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()).WithMinSize(3); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.init_budget = 0; // Enter() will decrement to -1 + + const auto val = + Value::BuildWithTraversalCtx(domain, prng, state); + + EXPECT_FALSE(val.ok()); + EXPECT_FALSE(state.status.ok()); +} + +TEST(ContainerTest, + SequenceInitWithTraversalCtxInitBudgetExhaustedNoMinSizeSucceeds) { + auto domain = VectorOf(Arbitrary()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.init_budget = 0; + + const auto val = + Value::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()).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::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 ca2aa3b91..d5df74ae9 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/logging.h" +#include "./common/status_macros.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 @@ struct Hash { template size_t operator()(const T& v) const { if constexpr (internal::Requires( - [](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( - [](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{}(*o); } else if constexpr (internal::Requires( @@ -108,12 +111,12 @@ struct Eq { differencer.set_field_comparator(&cmp); return differencer.Compare(a, b); } else if constexpr (internal::Requires( - [](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( - [](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 @@ using Set = absl::flat_hash_set; // 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 struct Value { using T = internal::value_type_t; @@ -152,6 +156,17 @@ struct Value { }()), user_value(std::move(user_value)) {} + static absl::StatusOr BuildWithTraversalCtx( + Domain& domain, absl::BitGenRef prng, + domain_implementor::TraversalState& state) { + ASSIGN_OR_RETURN_IF_NOT_OK( + auto corpus, + domain.InitWithTraversalCtx( + prng, domain_implementor::InitTraversalContext(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 @@ struct Value { } private: + Value(internal::corpus_type_t 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().GetPrinter()); diff --git a/domain_tests/map_filter_combinator_test.cc b/domain_tests/map_filter_combinator_test.cc index ebf66ed1b..82c80f9a5 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 @@ TEST(Filter, ValidationRejectsInvalidValue) { 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()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.depth_budget = 100; + state.init_budget = 10; + + const auto val = + Value::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()); + + absl::BitGen prng; + domain_implementor::TraversalState state; + state.status = absl::CancelledError("Pre-existing failure"); + + const auto val = + Value::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& v) { + ADD_FAILURE() << "Predicate should not be called"; + return false; + }, + VectorOf(Arbitrary()).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::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 aa5a95665..b2bba27ab 100644 --- a/domain_tests/recursive_domains_test.cc +++ b/domain_tests/recursive_domains_test.cc @@ -18,14 +18,20 @@ #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 { +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Not; + struct Tree { int value; std::vector children; @@ -105,5 +111,71 @@ 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; + domain_implementor::TraversalState state; + state.depth_budget = 5; + + const auto tree = + Value::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", StructOf(InRange(0, 10), ContainerOf>( + builder.Get("tree")) + .WithSize(2))); + Domain domain = std::move(builder).Finalize("tree"); + + absl::BitGen bitgen; + domain_implementor::TraversalState state; + state.depth_budget = 100; + state.init_budget = 5; + + const auto tree = + Value::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", + 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; + domain_implementor::TraversalState state; + state.depth_budget = 5; + + const auto tree = + Value::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 f0a6a3d63..4da59f3b5 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 @@ class DomainBuilder { return GetInnerDomain().Init(prng); } + absl::StatusOr InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext ctx) { + using InnerCtx = domain_implementor::InitTraversalContext>; + 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 @@ class DomainBuilder { corpus_type Init(absl::BitGenRef prng) { return inner_.Init(prng); } + absl::StatusOr InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext ctx) { + using InnerCtx = domain_implementor::InitTraversalContext>; + 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 5682ffb8c..5d571dd93 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 @@ 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", @@ -72,6 +74,7 @@ cc_library( "@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 @@ cc_library( "@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 @@ cc_library( "@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 e9702ff9d..6fe483308 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" @@ -71,6 +72,7 @@ fuzztest_cc_library( absl::time absl::span fuzztest::common_logging + fuzztest::status_macros fuzztest::fuzzing_bit_gen fuzztest::any fuzztest::logging @@ -110,6 +112,7 @@ fuzztest_cc_library( fuzztest::domain_core fuzztest::any fuzztest::common_logging + fuzztest::status_macros fuzztest::meta fuzztest::printer fuzztest::serialization @@ -235,3 +238,14 @@ fuzztest_cc_library( 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 cd70ca1f0..ac9159e5e 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/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 @@ class AggregateOfImpl inner_); } + absl::StatusOr InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext ctx) { + RETURN_IF_NOT_OK(ctx.status()); + if (auto seed = this->MaybeGetRandomSeed(prng)) return *std::move(seed); + return ApplyIndex( + [&](auto... Is) -> absl::StatusOr { + std::tuple>...> results; + absl::Status status = absl::OkStatus(); + auto init_one = [&](auto I) { + auto res = std::get(inner_).InitWithTraversalCtx(prng, ctx); + if (!res.ok()) { + status = std::move(res).status(); + return false; + } + std::get(results) = *std::move(res); + return true; + }; + if (!(init_one(Is) && ...)) { + return status; + } + + return corpus_type{*std::move(std::get(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 7a8cd4568..60be1623b 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/logging.h" +#include "./common/status_macros.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 @@ class SequenceContainerOfImplBase return val; } + absl::StatusOr InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext ctx) { + 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 85425e20b..c9aaeed11 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 @@ class Domain { // values). corpus_type Init(absl::BitGenRef prng) { return inner_->UntypedInit(prng); } + absl::StatusOr InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext 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 @@ class UntypedDomain { corpus_type Init(absl::BitGenRef prng) { return inner_->UntypedInit(prng); } + absl::StatusOr InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext 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 704421b97..32199adcc 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/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 @@ class DomainBase { return derived().GetValue(derived().GetRandomCorpusValue(prng)); } + absl::StatusOr InitWithTraversalCtx( + absl::BitGenRef prng, InitTraversalContext ctx) { + 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 442af9d39..1377ae79e 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/logging.h" +#include "./common/status_macros.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 @@ class UntypedDomainConcept { virtual std::unique_ptr UntypedClone() const = 0; virtual GenericDomainCorpusType UntypedInit(absl::BitGenRef) = 0; + virtual absl::StatusOr UntypedInitWithTraversalCtx( + absl::BitGenRef, + domain_implementor::InitTraversalContext) = 0; virtual void UntypedMutate( GenericDomainCorpusType& val, absl::BitGenRef prng, const domain_implementor::MutationMetadata& metadata, @@ -140,6 +146,19 @@ class DomainModel final : public TypedDomainConcept> { domain_.Init(prng)); } + absl::StatusOr UntypedInitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext ctx) + final { + ASSIGN_OR_RETURN_IF_NOT_OK( + auto res, + domain_.InitWithTraversalCtx( + prng, + domain_implementor::InitTraversalContext::Passthrough(ctx))); + return GenericDomainCorpusType(std::in_place_type, + 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 f37b8f913..46978f269 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/logging.h" +#include "./common/status_macros.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 @@ class FilterImpl } } + absl::StatusOr InitWithTraversalCtx( + absl::BitGenRef prng, + domain_implementor::InitTraversalContext ctx) { + RETURN_IF_NOT_OK(ctx.status()); + if (auto seed = this->MaybeGetRandomSeed(prng)) return *std::move(seed); + + while (true) { + 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 000000000..edaff80e2 --- /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 +#include +#include + +#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 error_trace; +}; + +template +class TraversalContext { + public: + struct PassthroughTag {}; + TraversalContext(TraversalState& state, PassthroughTag) : state_{state} {} + + 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_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())); + } + } + + 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::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())); + } + } + + TraversalState& state_; + bool enter_ok_ = false; + bool entered_ = false; +}; + +template +class InitTraversalContext : public TraversalContext { + public: + using PassthroughTag = + typename InitTraversalContext::TraversalContext::PassthroughTag; + using InitTraversalContext::TraversalContext::TraversalContext; + + template + static InitTraversalContext Passthrough( + const InitTraversalContext& other) { + return InitTraversalContext(other.state(), PassthroughTag{}); + } + + template + static InitTraversalContext Passthrough( + const TraversalContext& other) { + return InitTraversalContext(other.state(), PassthroughTag{}); + } + + explicit InitTraversalContext(TraversalState& state) + : TraversalContext{state} { + Decrement(); + } + + template + InitTraversalContext(const InitTraversalContext& other) + : TraversalContext{other} { + Decrement(); + } + + template + InitTraversalContext(const TraversalContext& other) + : TraversalContext{other} { + Decrement(); + } + + InitTraversalContext(const InitTraversalContext& other) + : TraversalContext{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 000000000..e2c737d9d --- /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 ctx1(state); + EXPECT_EQ(state.depth_budget, 0); + EXPECT_FALSE(ctx1.IsResourceExhausted()); + + { + TraversalContext 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 ctx1(state); + EXPECT_EQ(state.init_budget, 0); + EXPECT_FALSE(ctx1.IsResourceExhausted()); + + { + InitTraversalContext 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 ctx1(state); // Init: decrements to 1 + EXPECT_EQ(state.init_budget, 1); + + { + TraversalContext ctx2(ctx1); // Mutate: does NOT decrement + EXPECT_EQ(state.init_budget, 1); + + { + InitTraversalContext 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 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 ctx1(state); + EXPECT_EQ(state.init_budget, 999); + + { + InitTraversalContext 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 ctx_without_budget(state); + EXPECT_EQ(state.depth_budget, 4); + EXPECT_EQ(state.init_budget, 1000); // Not decremented by TraversalContext + + { + InitTraversalContext 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 {}; + +TEST_P(TraversalContextExhaustionTest, ChecksIsResourceExhaustedAndIsFailed) { + const auto& param = GetParam(); + TraversalState state = param.state; + InitTraversalContext 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 ctxA(state); + { + TraversalContext ctxB(ctxA); + { + TraversalContext 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 ctx1(state); + EXPECT_EQ(state.depth_budget, 0); + { + // depth -1 + TraversalContext ctx2(ctx1); + EXPECT_EQ(state.depth_budget, -1); + { + // depth goes to -2 (no longer capped) + TraversalContext 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 ctx(state); + EXPECT_EQ(state.init_budget, 0); + } + { + InitTraversalContext ctx(state); + EXPECT_EQ(state.init_budget, -1); + } + { + InitTraversalContext ctx(state); + EXPECT_EQ(state.init_budget, -1); + } +} + +TEST(TraversalContextTest, PassthroughDoesNotDecrementBudgets) { + TraversalState state; + state.depth_budget = 5; + state.init_budget = 10; + + { + InitTraversalContext ctx(state); + EXPECT_EQ(state.depth_budget, 4); + EXPECT_EQ(state.init_budget, 9); + + { + auto passthrough_ctx = + InitTraversalContext::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 ctx(state); + + { + auto passthrough_ctx = + InitTraversalContext::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