Arbitrary Enum Domain PiperOrigin-RevId: 918473334
diff --git a/doc/domains-reference.md b/doc/domains-reference.md index f8421fd..e945809 100644 --- a/doc/domains-reference.md +++ b/doc/domains-reference.md
@@ -45,6 +45,7 @@ `absl::Duration`, `absl::Time`. - [Abseil status types](https://abseil.io/docs/cpp/guides/status): `absl::StatusCode`, `absl::Status` (without support for payloads). +- Enum types: `enum class Color { kRed, kGreen, kBlue };` Composite or container types, like `std::optional<T>` or `std::vector<T>`, are supported as long as the inner types are. For example, @@ -62,8 +63,13 @@ TIP: If your struct doesn't satisfy the requirements for `Arbitrary`, you can construct a domain for it using `Map`, `ReversibleMap`, or `FlatMap`. +Enum types are only supported for `clang`, `gcc`, and `msvc`. +Automatic reflection only detects values in the range `[-128, 127]`. +Values outside this range will not be generated by `Arbitrary<T>()`. +At least one value must be in this range to avoid a compile-time error. + Recall that `Arbitrary` is the default input domain, which means that you can -fuzz a function like below without a `.WithDomains()` clause: +fuzz a function like below without a `.WithDomains()` clause. ```c++ void MyProperty(const absl::flat_hash_map<uint32, MyProtoMessage>& m,
diff --git a/domain_tests/arbitrary_domains_test.cc b/domain_tests/arbitrary_domains_test.cc index 2af1c79..828464f 100644 --- a/domain_tests/arbitrary_domains_test.cc +++ b/domain_tests/arbitrary_domains_test.cc
@@ -54,6 +54,7 @@ using ::testing::Ge; using ::testing::IsEmpty; using ::testing::SizeIs; +using ::testing::UnorderedElementsAre; TEST(BoolTest, Arbitrary) { absl::BitGen bitgen; @@ -704,5 +705,22 @@ EXPECT_EQ(mapped_status.message(), status.message()); } +TEST(ArbitraryEnumTest, GeneratesAllValues) { + enum class MyTestEnum { kValue0, kValue1, kValue2 }; + + auto domain = Arbitrary<MyTestEnum>(); + absl::BitGen prng; + absl::flat_hash_set<MyTestEnum> found; + + const int max_iterations = IterationsToHitAll(3, 1.0 / 3); + for (int i = 0; i < max_iterations && found.size() < 3; ++i) { + found.insert(domain.GetRandomValue(prng)); + } + + EXPECT_THAT(found, + UnorderedElementsAre(MyTestEnum::kValue0, MyTestEnum::kValue1, + MyTestEnum::kValue2)); +} + } // namespace } // namespace fuzztest
diff --git a/fuzztest/internal/BUILD b/fuzztest/internal/BUILD index 9f1bd10..f515591 100644 --- a/fuzztest/internal/BUILD +++ b/fuzztest/internal/BUILD
@@ -42,6 +42,26 @@ ) cc_library( + name = "enum_reflection", + hdrs = ["enum_reflection.h"], + deps = [ + ":meta", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + ], +) + +cc_test( + name = "enum_reflection_test", + srcs = ["enum_reflection_test.cc"], + deps = [ + ":enum_reflection", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest_main", + ], +) + +cc_library( name = "centipede_adaptor", srcs = ["centipede_adaptor.cc"], hdrs = ["centipede_adaptor.h"],
diff --git a/fuzztest/internal/CMakeLists.txt b/fuzztest/internal/CMakeLists.txt index 4b1c837..0eb75ab 100644 --- a/fuzztest/internal/CMakeLists.txt +++ b/fuzztest/internal/CMakeLists.txt
@@ -39,6 +39,28 @@ fuzztest_cc_library( NAME + enum_reflection + HDRS + "enum_reflection.h" + DEPS + absl::strings + absl::string_view + fuzztest::meta +) + +fuzztest_cc_test( + NAME + enum_reflection_test + SRCS + "enum_reflection_test.cc" + DEPS + fuzztest::enum_reflection + GTest::gmock_main + absl::string_view +) + +fuzztest_cc_library( + NAME compatibility_mode HDRS "compatibility_mode.h"
diff --git a/fuzztest/internal/domains/BUILD b/fuzztest/internal/domains/BUILD index 732d675..545fef3 100644 --- a/fuzztest/internal/domains/BUILD +++ b/fuzztest/internal/domains/BUILD
@@ -74,6 +74,7 @@ "@com_google_fuzztest//common:logging", "@com_google_fuzztest//fuzztest:fuzzing_bit_gen", "@com_google_fuzztest//fuzztest/internal:any", + "@com_google_fuzztest//fuzztest/internal:enum_reflection", "@com_google_fuzztest//fuzztest/internal:logging", "@com_google_fuzztest//fuzztest/internal:meta", "@com_google_fuzztest//fuzztest/internal:printer",
diff --git a/fuzztest/internal/domains/arbitrary_impl.h b/fuzztest/internal/domains/arbitrary_impl.h index 16bc2c6..adf21e8 100644 --- a/fuzztest/internal/domains/arbitrary_impl.h +++ b/fuzztest/internal/domains/arbitrary_impl.h
@@ -50,6 +50,7 @@ #include "./fuzztest/internal/domains/special_values.h" #include "./fuzztest/internal/domains/value_mutation_helpers.h" #include "./fuzztest/internal/domains/variant_of_impl.h" +#include "./fuzztest/internal/enum_reflection.h" #include "./fuzztest/internal/meta.h" #include "./fuzztest/internal/serialization.h" #include "./fuzztest/internal/status.h" @@ -66,6 +67,21 @@ ); }; +// Arbitrary for enums. +// See limitations in fuzztest::internal::enum_reflection::GetEnumValues +template <typename T> +class ArbitraryImpl< + T, std::enable_if_t<std::is_enum_v<T> && !is_protocol_buffer_enum_v<T>>> + : public ElementOfImpl<T> { + static_assert(enum_reflection::HasEnumValuesInRange<T>(), + "Arbitrary<T>() for enums requires at least one value in the " + "supported range [-128, 127] for automatic reflection. " + "Please use ElementOf directly with explicit values."); + + public: + ArbitraryImpl() : ElementOfImpl<T>(enum_reflection::GetEnumValues<T>()) {} +}; + // Arbitrary for monostate. // // For monostate types with a default constructor, just give the single value.
diff --git a/fuzztest/internal/enum_reflection.h b/fuzztest/internal/enum_reflection.h new file mode 100644 index 0000000..40db854 --- /dev/null +++ b/fuzztest/internal/enum_reflection.h
@@ -0,0 +1,102 @@ +// 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_ENUM_REFLECTION_H_ +#define FUZZTEST_FUZZTEST_INTERNAL_DOMAINS_ENUM_REFLECTION_H_ + +#include <cstddef> +#include <vector> + +#include "absl/strings/string_view.h" +#include "absl/strings/strip.h" +#include "./fuzztest/internal/meta.h" + +namespace fuzztest::internal::enum_reflection { + +constexpr bool IsValidCharacter(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || c == '_'; +} + +constexpr bool IsDigit(char c) { return c >= '0' && c <= '9'; } + +// Checks if `name` ends with `compiler_suffix`. After removing it, checks if +// the remaining string ends with a valid identifier (valid enum value) rather +// than a numeric literal (invalid value). +constexpr bool IsValidEnumValueSuffix(absl::string_view name, + absl::string_view compiler_suffix) { + if (!absl::ConsumeSuffix(&name, compiler_suffix)) return false; + + size_t i = name.size(); + while (i > 0 && IsValidCharacter(name[i - 1])) { + --i; + } + return i < name.size() && !IsDigit(name[i]); +} + +// When the template parameter V is equal to a valid enum value, +// compilers replace the signature macro with a string containing the enum name. +// +// For Clang/GCC, __PRETTY_FUNCTION__ ends in something like: +// "[E = ns::MyEnum, V = ns::MyEnum::kRed]" +// If V is not a valid value, the suffix looks like: +// "[E = ns::MyEnum, V = (ns::MyEnum)5]". +// +// For MSVC, __FUNCSIG__ ends in something like: +// "IsValidEnumValue<enum ns::MyEnum,ns::MyEnum::kRed>(void)" +// If V is not a valid value, it looks like: +// "IsValidEnumValue<enum ns::MyEnum,5>(void)" +template <typename E, E V> +constexpr bool IsValidEnumValue() { +#if defined(__clang__) || defined(__GNUC__) + constexpr absl::string_view kCompilerSuffix = "]"; + return IsValidEnumValueSuffix( + {__PRETTY_FUNCTION__, sizeof(__PRETTY_FUNCTION__) - 1}, kCompilerSuffix); +#elif defined(_MSC_VER) + constexpr absl::string_view kCompilerSuffix = ">(void)"; + return IsValidEnumValueSuffix({__FUNCSIG__, sizeof(__FUNCSIG__) - 1}, + kCompilerSuffix); +#else +#error "Enum reflection is only supported on Clang, GCC, and MSVC" +#endif +} + +template <typename E> +constexpr bool HasEnumValuesInRange() { + return ApplyIndex<256>([](auto... I) { + return (IsValidEnumValue<E, static_cast<E>(static_cast<int>(I) - 128)>() || + ...); + }); +} + +// Currently only available with Clang, GCC and MSVC. +// Assumes that the enums values are within the range [-128, 127]. +template <typename E> +std::vector<E> GetEnumValues() { + return ApplyIndex<256>([](auto... I) { + std::vector<E> values; + auto add_if_valid = [&](auto index) { + if constexpr (IsValidEnumValue<E, static_cast<E>(static_cast<int>(index) - + 128)>()) { + values.push_back(static_cast<E>(static_cast<int>(index) - 128)); + } + }; + (add_if_valid(I), ...); + return values; + }); +} + +} // namespace fuzztest::internal::enum_reflection + +#endif // FUZZTEST_FUZZTEST_INTERNAL_DOMAINS_ENUM_REFLECTION_H_
diff --git a/fuzztest/internal/enum_reflection_test.cc b/fuzztest/internal/enum_reflection_test.cc new file mode 100644 index 0000000..cbe311b --- /dev/null +++ b/fuzztest/internal/enum_reflection_test.cc
@@ -0,0 +1,67 @@ +// 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/enum_reflection.h" + +#include <vector> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/strings/string_view.h" + +namespace fuzztest::internal::enum_reflection { +namespace { + +using ::testing::UnorderedElementsAre; + +TEST(IsValidEnumValue, WithValidSuffixes) { + EXPECT_TRUE(IsValidEnumValueSuffix("... V = kGreen]", "]")); + EXPECT_TRUE(IsValidEnumValueSuffix("... V = Green]", "]")); + EXPECT_TRUE(IsValidEnumValueSuffix("... V = _Value_123]", "]")); + EXPECT_TRUE(IsValidEnumValueSuffix("... V = fuzztest::Color::kGreen]", "]")); +} + +TEST(IsValidEnumValue, WithInvalidSuffixes) { + EXPECT_FALSE(IsValidEnumValueSuffix("... V = kGreen", "]")); // Missing ] + EXPECT_FALSE(IsValidEnumValueSuffix("", "]")); // Empty + EXPECT_FALSE(IsValidEnumValueSuffix("]", "]")); // Only ] + EXPECT_FALSE(IsValidEnumValueSuffix("... V = 42]", "]")); // Ends with number + EXPECT_FALSE( + IsValidEnumValueSuffix("... V = (Color)2]", "]")); // Ends with number + EXPECT_FALSE(IsValidEnumValueSuffix("... V = ]", "]")); // Empty suffix +} + +enum class MyTestEnum { kVal0 = 0, kVal_1 = 2, ABC = 3 }; + +TEST(GetEnumValues, ReturnsValidValues) { + std::vector<MyTestEnum> values = GetEnumValues<MyTestEnum>(); + EXPECT_THAT(values, + UnorderedElementsAre(MyTestEnum::kVal0, MyTestEnum::kVal_1, + MyTestEnum::ABC)); +} + +TEST(HasEnumValuesInRange, ReturnsTrueForValidEnum) { + EXPECT_TRUE(HasEnumValuesInRange<MyTestEnum>()); +} + +TEST(HasEnumValuesInRange, ReturnsFalseForOutOfRangeEnum) { + enum class OutOfRangeEnum { kVal = 128 }; + enum class NegOutOfRangeEnum { kVal = -129 }; + + EXPECT_FALSE(HasEnumValuesInRange<OutOfRangeEnum>()); + EXPECT_FALSE(HasEnumValuesInRange<NegOutOfRangeEnum>()); +} + +} // namespace +} // namespace fuzztest::internal::enum_reflection