diff --git a/doc/domains-reference.md b/doc/domains-reference.md index f8421fdd..e9458092 100644 --- a/doc/domains-reference.md +++ b/doc/domains-reference.md @@ -45,6 +45,7 @@ Specifically, for the following types: `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` or `std::vector`, are supported as long as the inner types are. For example, @@ -62,8 +63,13 @@ cannot have C-style array members (e.g., `int[5]`). 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()`. +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& m, diff --git a/domain_tests/arbitrary_domains_test.cc b/domain_tests/arbitrary_domains_test.cc index 2af1c797..828464ff 100644 --- a/domain_tests/arbitrary_domains_test.cc +++ b/domain_tests/arbitrary_domains_test.cc @@ -54,6 +54,7 @@ using ::testing::Each; using ::testing::Ge; using ::testing::IsEmpty; using ::testing::SizeIs; +using ::testing::UnorderedElementsAre; TEST(BoolTest, Arbitrary) { absl::BitGen bitgen; @@ -704,5 +705,22 @@ TEST(ArbitraryStatusTest, FromValueAndGetValuePreserveCodeAndMessage) { EXPECT_EQ(mapped_status.message(), status.message()); } +TEST(ArbitraryEnumTest, GeneratesAllValues) { + enum class MyTestEnum { kValue0, kValue1, kValue2 }; + + auto domain = Arbitrary(); + absl::BitGen prng; + absl::flat_hash_set 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 9f1bd107..f5155910 100644 --- a/fuzztest/internal/BUILD +++ b/fuzztest/internal/BUILD @@ -41,6 +41,26 @@ cc_test( ], ) +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"], diff --git a/fuzztest/internal/CMakeLists.txt b/fuzztest/internal/CMakeLists.txt index 4b1c837f..0eb75aba 100644 --- a/fuzztest/internal/CMakeLists.txt +++ b/fuzztest/internal/CMakeLists.txt @@ -37,6 +37,28 @@ fuzztest_cc_test( GTest::gmock_main ) +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 diff --git a/fuzztest/internal/domains/BUILD b/fuzztest/internal/domains/BUILD index 732d6757..545fef36 100644 --- a/fuzztest/internal/domains/BUILD +++ b/fuzztest/internal/domains/BUILD @@ -74,6 +74,7 @@ cc_library( "@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 16bc2c67..adf21e85 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 @@ class ArbitraryImpl { ); }; +// Arbitrary for enums. +// See limitations in fuzztest::internal::enum_reflection::GetEnumValues +template +class ArbitraryImpl< + T, std::enable_if_t && !is_protocol_buffer_enum_v>> + : public ElementOfImpl { + static_assert(enum_reflection::HasEnumValuesInRange(), + "Arbitrary() 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(enum_reflection::GetEnumValues()) {} +}; + // 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 00000000..40db8542 --- /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 +#include + +#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(void)" +// If V is not a valid value, it looks like: +// "IsValidEnumValue(void)" +template +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 +constexpr bool HasEnumValuesInRange() { + return ApplyIndex<256>([](auto... I) { + return (IsValidEnumValue(static_cast(I) - 128)>() || + ...); + }); +} + +// Currently only available with Clang, GCC and MSVC. +// Assumes that the enums values are within the range [-128, 127]. +template +std::vector GetEnumValues() { + return ApplyIndex<256>([](auto... I) { + std::vector values; + auto add_if_valid = [&](auto index) { + if constexpr (IsValidEnumValue(static_cast(index) - + 128)>()) { + values.push_back(static_cast(static_cast(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 00000000..cbe311bf --- /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 + +#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 values = GetEnumValues(); + EXPECT_THAT(values, + UnorderedElementsAre(MyTestEnum::kVal0, MyTestEnum::kVal_1, + MyTestEnum::ABC)); +} + +TEST(HasEnumValuesInRange, ReturnsTrueForValidEnum) { + EXPECT_TRUE(HasEnumValuesInRange()); +} + +TEST(HasEnumValuesInRange, ReturnsFalseForOutOfRangeEnum) { + enum class OutOfRangeEnum { kVal = 128 }; + enum class NegOutOfRangeEnum { kVal = -129 }; + + EXPECT_FALSE(HasEnumValuesInRange()); + EXPECT_FALSE(HasEnumValuesInRange()); +} + +} // namespace +} // namespace fuzztest::internal::enum_reflection