Add support for scoped enumerations in CHECK_XX().
This change allows values of a scoped enum (that is, an `enum class` or `enum struct`) to be passed to CHECK_EQ and its relatives (CHECK_NE, DCHECK_GE, etc). Before this change, this was not possible because enum class values could not be printed without a custom operator<< or AbslStringify implementation. This change adds support by converting the scoped-enum values to their underlying types for printing purposes when a CHECK_XX fails. If a scoped enum already has operator<< or AbslStringify defined, those methods are still preferred.
One design detail: enums can have char as their underlying type. For consistency with unscoped enums, a char-backed scoped enum is printed as an ASCII character when its value is in the printable ASCII range (decimal 32 to 126).
PiperOrigin-RevId: 771129686
Change-Id: I4d0ba0f4e1dc1264df4ee8d0230d3dbd02581c9b
diff --git a/absl/log/check_test_impl.inc b/absl/log/check_test_impl.inc
index 7a0000e..37226a3 100644
--- a/absl/log/check_test_impl.inc
+++ b/absl/log/check_test_impl.inc
@@ -22,6 +22,8 @@
#error ABSL_TEST_CHECK must be defined for these tests to work.
#endif
+#include <cstdint>
+#include <limits>
#include <ostream>
#include <string>
@@ -40,6 +42,7 @@
using ::testing::AllOf;
using ::testing::AnyOf;
+using ::testing::ContainsRegex;
using ::testing::HasSubstr;
using ::testing::Not;
@@ -638,9 +641,8 @@
EXPECT_DEATH(
ABSL_TEST_CHECK_EQ(p, nullptr),
AnyOf(
- HasSubstr("Check failed: p == nullptr (0000000000001234 vs. (null))"),
- HasSubstr("Check failed: p == nullptr (0x1234 vs. (null))")
- ));
+ HasSubstr("Check failed: p == nullptr (0000000000001234 vs. (null))"),
+ HasSubstr("Check failed: p == nullptr (0x1234 vs. (null))")));
}
// An uncopyable object with operator<<.
@@ -670,6 +672,273 @@
HasSubstr("Check failed: v1 == v2 (Uncopyable{1} vs. Uncopyable{2})"));
}
+enum class ScopedEnum { kValue1 = 1, kValue2 = 2 };
+
+TEST(CHECKTest, TestScopedEnumComparisonChecks) {
+ ABSL_TEST_CHECK_EQ(ScopedEnum::kValue1, ScopedEnum::kValue1);
+ ABSL_TEST_CHECK_NE(ScopedEnum::kValue1, ScopedEnum::kValue2);
+ ABSL_TEST_CHECK_LT(ScopedEnum::kValue1, ScopedEnum::kValue2);
+ ABSL_TEST_CHECK_LE(ScopedEnum::kValue1, ScopedEnum::kValue2);
+ ABSL_TEST_CHECK_GT(ScopedEnum::kValue2, ScopedEnum::kValue1);
+ ABSL_TEST_CHECK_GE(ScopedEnum::kValue2, ScopedEnum::kValue2);
+ ABSL_TEST_DCHECK_EQ(ScopedEnum::kValue1, ScopedEnum::kValue1);
+ ABSL_TEST_DCHECK_NE(ScopedEnum::kValue1, ScopedEnum::kValue2);
+ ABSL_TEST_DCHECK_LT(ScopedEnum::kValue1, ScopedEnum::kValue2);
+ ABSL_TEST_DCHECK_LE(ScopedEnum::kValue1, ScopedEnum::kValue2);
+ ABSL_TEST_DCHECK_GT(ScopedEnum::kValue2, ScopedEnum::kValue1);
+ ABSL_TEST_DCHECK_GE(ScopedEnum::kValue2, ScopedEnum::kValue2);
+
+ // Check that overloads work correctly with references as well.
+ const ScopedEnum x = ScopedEnum::kValue1;
+ const ScopedEnum& x_ref = x;
+ ABSL_TEST_CHECK_EQ(x, x_ref);
+ ABSL_TEST_CHECK_EQ(x_ref, x_ref);
+}
+
+#if GTEST_HAS_DEATH_TEST
+TEST(CHECKDeathTest, TestScopedEnumCheckFailureMessagePrintsIntegerValues) {
+ const auto e1 = ScopedEnum::kValue1;
+ const auto e2 = ScopedEnum::kValue2;
+ EXPECT_DEATH(ABSL_TEST_CHECK_EQ(e1, e2),
+ ContainsRegex(R"re(Check failed:.*\(1 vs. 2\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_NE(e1, e1),
+ ContainsRegex(R"re(Check failed:.*\(1 vs. 1\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_GT(e1, e1),
+ ContainsRegex(R"re(Check failed:.*\(1 vs. 1\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_GE(e1, e2),
+ ContainsRegex(R"re(Check failed:.*\(1 vs. 2\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_LT(e2, e2),
+ ContainsRegex(R"re(Check failed:.*\(2 vs. 2\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_LE(e2, e1),
+ ContainsRegex(R"re(Check failed:.*\(2 vs. 1\))re"));
+
+ const auto& e1_ref = e1;
+ EXPECT_DEATH(ABSL_TEST_CHECK_NE(e1_ref, e1),
+ ContainsRegex(R"re(Check failed:.*\(1 vs. 1\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_NE(e1_ref, e1_ref),
+ ContainsRegex(R"re(Check failed:.*\(1 vs. 1\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_EQ(e2, e1_ref),
+ ContainsRegex(R"re(Check failed:.*\(2 vs. 1\))re"));
+
+#ifndef NDEBUG
+ EXPECT_DEATH(ABSL_TEST_DCHECK_EQ(e2, e1),
+ ContainsRegex(R"re(Check failed:.*\(2 vs. 1\))re"));
+#else
+ // DHECK_EQ is not evaluated in non-debug mode.
+ ABSL_TEST_DCHECK_EQ(e2, e1);
+#endif // NDEBUG
+}
+#endif // GTEST_HAS_DEATH_TEST
+
+enum class ScopedInt8Enum : int8_t {
+ kValue1 = 1,
+ kValue2 = 66 // Printable ascii value 'B'.
+};
+
+TEST(CHECKDeathTest, TestScopedInt8EnumCheckFailureMessagePrintsCharValues) {
+ const auto e1 = ScopedInt8Enum::kValue1;
+ const auto e2 = ScopedInt8Enum::kValue2;
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_EQ(e1, e2),
+ ContainsRegex(R"re(Check failed:.*\(signed char value 1 vs. 'B'\))re"));
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_NE(e1, e1),
+ ContainsRegex(
+ R"re(Check failed:.*\(signed char value 1 vs. signed char value 1\))re"));
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_GT(e1, e1),
+ ContainsRegex(
+ R"re(Check failed:.*\(signed char value 1 vs. signed char value 1\))re"));
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_GE(e1, e2),
+ ContainsRegex(R"re(Check failed:.*\(signed char value 1 vs. 'B'\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_LT(e2, e2),
+ ContainsRegex(R"re(Check failed:.*\('B' vs. 'B'\))re"));
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_LE(e2, e1),
+ ContainsRegex(R"re(Check failed:.*\('B' vs. signed char value 1\))re"));
+}
+
+enum class ScopedUnsignedEnum : uint16_t {
+ kValue1 = std::numeric_limits<uint16_t>::min(),
+ kValue2 = std::numeric_limits<uint16_t>::max()
+};
+
+TEST(CHECKDeathTest,
+ TestScopedUnsignedEnumCheckFailureMessagePrintsCorrectValues) {
+ const auto e1 = ScopedUnsignedEnum::kValue1;
+ const auto e2 = ScopedUnsignedEnum::kValue2;
+ EXPECT_DEATH(ABSL_TEST_CHECK_EQ(e1, e2),
+ ContainsRegex(R"re(Check failed:.*\(0 vs. 65535\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_NE(e1, e1),
+ ContainsRegex(R"re(Check failed:.*\(0 vs. 0\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_GT(e1, e1),
+ ContainsRegex(R"re(Check failed:.*\(0 vs. 0\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_GE(e1, e2),
+ ContainsRegex(R"re(Check failed:.*\(0 vs. 65535\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_LT(e1, e1),
+ ContainsRegex(R"re(Check failed:.*\(0 vs. 0\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_LE(e2, e1),
+ ContainsRegex(R"re(Check failed:.*\(65535 vs. 0\))re"));
+}
+
+enum class ScopedInt64Enum : int64_t {
+ kMin = std::numeric_limits<int64_t>::min(),
+ kMax = std::numeric_limits<int64_t>::max(),
+};
+
+// Tests that int64-backed enums are printed correctly even for very large and
+// very small values.
+TEST(CHECKDeathTest, TestScopedInt64EnumCheckFailureMessage) {
+ const auto min = ScopedInt64Enum::kMin;
+ const auto max = ScopedInt64Enum::kMax;
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_EQ(max, min),
+ ContainsRegex(
+ "Check failed:.*9223372036854775807 vs. -9223372036854775808"));
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_NE(max, max),
+ ContainsRegex(
+ "Check failed:.*9223372036854775807 vs. 9223372036854775807"));
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_GT(min, min),
+ ContainsRegex(
+ "Check failed:.*-9223372036854775808 vs. -9223372036854775808"));
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_GE(min, max),
+ ContainsRegex(
+ R"(Check failed:.*-9223372036854775808 vs. 9223372036854775807)"));
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_LT(max, max),
+ ContainsRegex(
+ R"(Check failed:.*9223372036854775807 vs. 9223372036854775807)"));
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_LE(max, min),
+ ContainsRegex(
+ R"(Check failed:.*9223372036854775807 vs. -9223372036854775808)"));
+}
+
+enum class ScopedBoolEnum : bool {
+ kFalse,
+ kTrue,
+};
+
+TEST(CHECKDeathTest, TestScopedBoolEnumCheckFailureMessagePrintsCorrectValues) {
+ const auto t = ScopedBoolEnum::kTrue;
+ const auto f = ScopedBoolEnum::kFalse;
+ EXPECT_DEATH(ABSL_TEST_CHECK_EQ(t, f),
+ ContainsRegex(R"re(Check failed:.*\(1 vs. 0\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_NE(f, f),
+ ContainsRegex(R"re(Check failed:.*\(0 vs. 0\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_GT(f, f),
+ ContainsRegex(R"re(Check failed:.*\(0 vs. 0\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_GE(f, t),
+ ContainsRegex(R"re(Check failed:.*\(0 vs. 1\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_LT(t, t),
+ ContainsRegex(R"re(Check failed:.*\(1 vs. 1\))re"));
+ EXPECT_DEATH(ABSL_TEST_CHECK_LE(t, f),
+ ContainsRegex(R"re(Check failed:.*\(1 vs. 0\))re"));
+}
+
+enum class ScopedEnumWithAbslStringify {
+ kValue1 = 1,
+ kValue2 = 2,
+ kValue3 = 3
+};
+
+template <typename Sink>
+void AbslStringify(Sink& sink, ScopedEnumWithAbslStringify v) {
+ switch (v) {
+ case ScopedEnumWithAbslStringify::kValue1:
+ sink.Append("AbslStringify: kValue1");
+ break;
+ case ScopedEnumWithAbslStringify::kValue2:
+ sink.Append("AbslStringify: kValue2");
+ break;
+ case ScopedEnumWithAbslStringify::kValue3:
+ sink.Append("AbslStringify: kValue3");
+ break;
+ }
+}
+
+#if GTEST_HAS_DEATH_TEST
+TEST(CHECKDeathTest, TestScopedEnumUsesAbslStringify) {
+ EXPECT_DEATH(ABSL_TEST_CHECK_EQ(ScopedEnumWithAbslStringify::kValue1,
+ ScopedEnumWithAbslStringify::kValue2),
+ ContainsRegex("Check failed:.*AbslStringify: kValue1 vs. "
+ "AbslStringify: kValue2"));
+}
+#endif // GTEST_HAS_DEATH_TEST
+
+enum class ScopedEnumWithOutputOperator {
+ kValue1 = 1,
+ kValue2 = 2,
+};
+
+std::ostream& operator<<(std::ostream& os, ScopedEnumWithOutputOperator v) {
+ switch (v) {
+ case ScopedEnumWithOutputOperator::kValue1:
+ os << "OutputOperator: kValue1";
+ break;
+ case ScopedEnumWithOutputOperator::kValue2:
+ os << "OutputOperator: kValue2";
+ break;
+ }
+ return os;
+}
+
+#if GTEST_HAS_DEATH_TEST
+TEST(CHECKDeathTest, TestOutputOperatorIsUsedForScopedEnum) {
+ EXPECT_DEATH(ABSL_TEST_CHECK_EQ(ScopedEnumWithOutputOperator::kValue1,
+ ScopedEnumWithOutputOperator::kValue2),
+ ContainsRegex("Check failed:.*OutputOperator: kValue1 vs. "
+ "OutputOperator: kValue2"));
+}
+#endif // GTEST_HAS_DEATH_TEST
+
+enum class ScopedEnumWithAbslStringifyAndOutputOperator {
+ kValue1 = 1,
+ kValue2 = 2,
+};
+
+template <typename Sink>
+void AbslStringify(Sink& sink, ScopedEnumWithAbslStringifyAndOutputOperator v) {
+ switch (v) {
+ case ScopedEnumWithAbslStringifyAndOutputOperator::kValue1:
+ sink.Append("AbslStringify: kValue1");
+ break;
+ case ScopedEnumWithAbslStringifyAndOutputOperator::kValue2:
+ sink.Append("AbslStringify: kValue2");
+ break;
+ }
+}
+
+std::ostream& operator<<(std::ostream& os,
+ ScopedEnumWithAbslStringifyAndOutputOperator v) {
+ switch (v) {
+ case ScopedEnumWithAbslStringifyAndOutputOperator::kValue1:
+ os << "OutputOperator: kValue1";
+ break;
+ case ScopedEnumWithAbslStringifyAndOutputOperator::kValue2:
+ os << "OutputOperator: kValue2";
+ break;
+ }
+ return os;
+}
+
+#if GTEST_HAS_DEATH_TEST
+
+// Test that, if operator<< and AbslStringify are both defined for a scoped
+// enum, streaming takes precedence over AbslStringify.
+TEST(CHECKDeathTest, TestScopedEnumPrefersOutputOperatorOverAbslStringify) {
+ EXPECT_DEATH(
+ ABSL_TEST_CHECK_EQ(ScopedEnumWithAbslStringifyAndOutputOperator::kValue1,
+ ScopedEnumWithAbslStringifyAndOutputOperator::kValue2),
+ ContainsRegex("Check failed:.*OutputOperator: kValue1 vs. "
+ "OutputOperator: kValue2"));
+}
+#endif // GTEST_HAS_DEATH_TEST
+
} // namespace absl_log_internal
// NOLINTEND(misc-definitions-in-headers)
diff --git a/absl/log/internal/check_op.h b/absl/log/internal/check_op.h
index 17afded..d7b55f6 100644
--- a/absl/log/internal/check_op.h
+++ b/absl/log/internal/check_op.h
@@ -298,12 +298,11 @@
// This overload triggers when the call is ambiguous.
// It means that T is either one from this list or printed as one from this
-// list. Eg an enum that decays to `int` for printing.
+// list. Eg an unscoped enum that decays to `int` for printing.
// We ask the overload set to give us the type we want to convert it to.
template <typename T>
-decltype(detect_specialization::operator<<(std::declval<std::ostream&>(),
- std::declval<const T&>()))
-Detect(char);
+decltype(detect_specialization::operator<<(
+ std::declval<std::ostream&>(), std::declval<const T&>())) Detect(char);
// A sink for AbslStringify which redirects everything to a std::ostream.
class StringifySink {
@@ -344,6 +343,35 @@
std::enable_if_t<HasAbslStringify<T>::value,
StringifyToStreamWrapper<T>>
Detect(...); // Ellipsis has lowest preference when int passed.
+
+// is_streamable is true for types that have an output stream operator<<.
+template <class T, class = void>
+struct is_streamable : std::false_type {};
+
+template <class T>
+struct is_streamable<T, std::void_t<decltype(std::declval<std::ostream&>()
+ << std::declval<T>())>>
+ : std::true_type {};
+
+// This overload triggers when T is a scoped enum that has not defined an output
+// stream operator (operator<<) or AbslStringify. It causes the enum value to be
+// converted to a type that can be streamed. For consistency with other enums, a
+// scoped enum backed by a bool or char is converted to its underlying type, and
+// one backed by another integer is converted to (u)int64_t.
+template <typename T>
+std::enable_if_t<
+ std::conjunction_v<
+ std::is_enum<T>, std::negation<std::is_convertible<T, int>>,
+ std::negation<is_streamable<T>>, std::negation<HasAbslStringify<T>>>,
+ std::conditional_t<
+ std::is_same_v<std::underlying_type_t<T>, bool> ||
+ std::is_same_v<std::underlying_type_t<T>, char> ||
+ std::is_same_v<std::underlying_type_t<T>, signed char> ||
+ std::is_same_v<std::underlying_type_t<T>, unsigned char>,
+ std::underlying_type_t<T>,
+ std::conditional_t<std::is_signed_v<std::underlying_type_t<T>>, int64_t,
+ uint64_t>>>
+Detect(...);
} // namespace detect_specialization
template <typename T>