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>