Refactor FuzzTest custom printer extension points.

The source code printing extension point `FuzzTestPrintSourceCode` is changed to
be consistent with `AbslStringify`. The custom printer auto-detection is
simplified: a type is considered to have a custom printer if it has either
`AbslStringify` or `FuzzTestPrintSourceCode` (or both). `AbslStringify` is used
for human-readable mode, and `FuzzTestPrintSourceCode` is used for the
source-code mode. Either is a fallback if the other extension point is missing.

This change required updates in the XLS code, most of which are straightforward.
One thing worth pointing out is that I moved the printing function for
`std::vector<std::vector<Value>>` from `ir_fuzz_test_library` to the `value`
library, where it fits more naturally.

PiperOrigin-RevId: 868685539
diff --git a/doc/domains-reference.md b/doc/domains-reference.md
index 929f7ad..a656179 100644
--- a/doc/domains-reference.md
+++ b/doc/domains-reference.md
@@ -905,7 +905,7 @@
     expression. This is used in the auto-generated regression tests to recreate
     the exact value that caused the failure. This mode is purely best-effort.
 
-### Customizing the human-readable printer:
+### Customizing the human-readable printer
 
 The simplest and most recommended way to implement custom printing for your
 types is to implement `AbslStringify`. This hooks into Abseil's string
@@ -923,26 +923,45 @@
 };
 ```
 
-### Customizing the source code printer:
+### Customizing the source code printer
 
-To provide a custom source code printer, you can implement the
-`FuzzTestPrintSourceCode` function for your type:
+To provide a custom source code representation (which is used in regression
+tests), you can implement the `FuzzTestPrintSourceCode` function for your type.
+For example:
 
 ```c++
-void FuzzTestPrintSourceCode(const YourValueType& v, std::ostream* os) {
-  // Write a valid C++ expression that recreates `v` into the output stream.
-  *os << "my_namespace::CreateMyObject(" << v.GetId() << ").WithName(\"" << v.name << "\")";
-}
+class MyObject {
+ public:
+  static MyObject Make(int id, std::string name) {
+    return MyObject{id, std::move(name)};
+  }
+
+  template <typename Sink>
+  friend void FuzzTestPrintSourceCode(Sink& sink, const MyObject& v) {
+    absl::Format(&sink, "MyObject::Make(%d, \"%s\")", v.id_, v.name_);
+  }
+
+ private:
+  MyObject(int id, std::string name) : id_{id}, name_{std::move(name)} {}
+
+  int id_ = 0;
+  std::string name_;
+};
 ```
 
-FuzzTest will call this function to generate the reproduction code for a test
-failure.
+If you define only one of `AbslStringify` and `FuzzTestPrintSourceCode`,
+FuzzTest will use the defined function for both the human-readable and the
+source-code mode. If you define both, FuzzTest will use `AbslStringify` for the
+human-readable mode, and `FuzzTestPrintSourceCode` for the source-code mode.
+Note that this fallback behavior primarily applies to non-aggregate types. For
+aggregate types (like simple structs), FuzzTest prefers field-level printing for
+the mode that doesn't have a custom extension point.
 
 NOTE: FuzzTest does not validate the output. You are responsible for ensuring
-the function prints a valid C++ expression that correctly recreates the original
-value.
+that in the source-code mode, the function prints a valid C++ expression that
+correctly recreates the original value.
 
-NOTE: FuzzTest uses C++ name resolution to find `FuzzTestPrintSourceCode` (and
-`AbslStringify` for that matter). This function should generally be declared
-either at the same place as the printed struct/class or in the same translation
-unit as the fuzz test.
+NOTE: FuzzTest uses ADL resolution to find `AbslStringify` and
+`FuzzTestPrintSourceCode`. These functions should generally be declared either
+at the same place as the printed struct/class or in the same translation unit as
+the fuzz test.
diff --git a/e2e_tests/BUILD b/e2e_tests/BUILD
index 0bbdc18..fc0e23f 100644
--- a/e2e_tests/BUILD
+++ b/e2e_tests/BUILD
@@ -72,7 +72,6 @@
         "@com_google_fuzztest//common:temp_dir",
         "@com_google_fuzztest//fuzztest/internal:escaping",
         "@com_google_fuzztest//fuzztest/internal:io",
-        "@com_google_fuzztest//fuzztest/internal:logging",
         "@com_google_fuzztest//fuzztest/internal:printer",
         "@com_google_fuzztest//fuzztest/internal:serialization",
         "@com_google_fuzztest//fuzztest/internal:subprocess",
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc
index 3ccbcc1..bd54ea7 100644
--- a/e2e_tests/functional_test.cc
+++ b/e2e_tests/functional_test.cc
@@ -38,7 +38,6 @@
 #include "./e2e_tests/test_binary_util.h"
 #include "./fuzztest/internal/escaping.h"
 #include "./fuzztest/internal/io.h"
-#include "./fuzztest/internal/logging.h"
 #include "./fuzztest/internal/printer.h"
 #include "./fuzztest/internal/serialization.h"
 #include "./fuzztest/internal/subprocess.h"
@@ -449,14 +448,15 @@
   auto [status, std_out, std_err] =
       Run("MySuite.CustomSourceCodePrinterCorrectlyPrintsValue");
   ExpectTargetAbort(status, std_err);
-  // This is the argument to the output domain.
+  // Human-readable mode.
   EXPECT_THAT_LOG(
       std_err,
-      HasSubstr("MyCustomPrintableTestType::BuildWithValue(\"abcd\")"));
-  // This is the argument to the input domain.
-  EXPECT_THAT_LOG(std_err, Not(HasSubstr("argument 0: \"abcd\"")));
+      HasSubstr("argument 0: MyCustomPrintableTestType{val=\"abcd\"}"));
+  // Source code mode.
   EXPECT_THAT_LOG(
-      std_err, HasSubstr("argument 0: MyCustomPrintableTestType - input=abcd"));
+      std_err, HasReproducerTest(
+                   "MySuite", "CustomSourceCodePrinterCorrectlyPrintsValue",
+                   "MyCustomPrintableTestType::BuildWithValue\\(\"abcd\"\\)"));
 }
 
 TEST_F(UnitTestModeTest, PrintsVeryLongInputsTrimmed) {
diff --git a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
index be0739e..0a066d0 100644
--- a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
+++ b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
@@ -692,53 +692,30 @@
         },
         Just(3)));
 
-namespace {
 struct MyCustomPrintableTestType {
  public:
-  static MyCustomPrintableTestType BuildWithValue(std::string inp) {
-    return MyCustomPrintableTestType(inp + "_foo", inp + "_bar");
-  }
-  absl::string_view foo() const { return foo_; }
-  absl::string_view bar() const { return bar_; }
-  bool correctly_built() const {
-    auto ends_with = [](absl::string_view s, absl::string_view suffix) {
-      return s.size() >= suffix.size() &&
-             s.substr(s.size() - suffix.size()) == suffix;
-    };
-    return ends_with(foo(), "_foo") && ends_with(bar(), "_bar") &&
-           foo().substr(0, foo().size() - 4) ==
-               bar().substr(0, bar().size() - 4);
+  static MyCustomPrintableTestType BuildWithValue(std::string val) {
+    return MyCustomPrintableTestType{std::move(val)};
   }
 
   template <typename Sink>
   friend void AbslStringify(Sink& sink, const MyCustomPrintableTestType& v) {
-    if (v.correctly_built()) {
-      absl::Format(&sink, "MyCustomPrintableTestType - input=%s",
-                   v.foo().substr(0, v.foo().size() - 4));
-      return;
-    }
-    absl::Format(&sink, "MyCustomPrintableTestType - foo=%s bar=%s", v.foo(),
-                 v.bar());
+    absl::Format(&sink, "MyCustomPrintableTestType{val=\"%s\"}", v.val_);
+  }
+
+  template <typename Sink>
+  friend void FuzzTestPrintSourceCode(Sink& sink,
+                                      const MyCustomPrintableTestType& v) {
+    absl::Format(&sink, "MyCustomPrintableTestType::BuildWithValue(\"%s\")",
+                 v.val_);
   }
 
  private:
-  MyCustomPrintableTestType(std::string foo, std::string bar)
-      : foo_(foo), bar_(bar) {}
-  std::string foo_;
-  std::string bar_;
+  MyCustomPrintableTestType(std::string val) : val_{std::move(val)} {}
+
+  std::string val_;
 };
-void FuzzTestPrintSourceCode(const MyCustomPrintableTestType& v,  // NOLINT
-                             std::ostream* os) {
-  if (v.correctly_built()) {
-    *os << "MyCustomPrintableTestType::BuildWithValue(\""
-        << v.foo().substr(0, v.foo().size() - 4) << "\")";
-    return;
-  }
-  // NB Not actually valid constructor.
-  *os << "MyCustomPrintableTestType{.foo=\"" << v.foo() << "\", .bar=\""
-      << v.bar() << "\"}";
-}
-}  // namespace
+
 void CustomSourceCodePrinterCorrectlyPrintsValue(
     const MyCustomPrintableTestType& v) {
   std::abort();
diff --git a/fuzztest/internal/BUILD b/fuzztest/internal/BUILD
index ce749d4..abb0a3f 100644
--- a/fuzztest/internal/BUILD
+++ b/fuzztest/internal/BUILD
@@ -547,6 +547,7 @@
     deps = [
         ":meta",
         ":printer",
+        "@abseil-cpp//absl/base:nullability",
         "@abseil-cpp//absl/debugging:symbolize",
         "@abseil-cpp//absl/functional:function_ref",
         "@abseil-cpp//absl/numeric:int128",
diff --git a/fuzztest/internal/CMakeLists.txt b/fuzztest/internal/CMakeLists.txt
index 147ae51..1a674bc 100644
--- a/fuzztest/internal/CMakeLists.txt
+++ b/fuzztest/internal/CMakeLists.txt
@@ -550,6 +550,7 @@
   DEPS
     fuzztest::meta
     fuzztest::printer
+    absl::nullability
     absl::symbolize
     absl::function_ref
     absl::int128
diff --git a/fuzztest/internal/type_support.h b/fuzztest/internal/type_support.h
index 0630836..cc30636 100644
--- a/fuzztest/internal/type_support.h
+++ b/fuzztest/internal/type_support.h
@@ -20,8 +20,6 @@
 #include <cstddef>
 #include <cstdint>
 #include <limits>
-#include <ostream>
-#include <sstream>
 #include <string>
 #include <string_view>
 #include <tuple>
@@ -29,6 +27,7 @@
 #include <utility>
 #include <vector>
 
+#include "absl/base/nullability.h"
 #include "absl/debugging/symbolize.h"
 #include "absl/functional/function_ref.h"
 #include "absl/numeric/int128.h"
@@ -58,13 +57,12 @@
 // value.
 // It implements a good printer for common known types and fallbacks to an
 // "unknown" printer to prevent compile time errors.
-template <typename T, bool kAllowCustomSourcePrinter = true>
+template <typename T>
 decltype(auto) AutodetectTypePrinter();
 
-// Returns true iff type `T` has a known printer that isn't UnknownPrinter for
-// the given mode.
+// Returns true iff type `T` has a known printer that isn't UnknownPrinter.
 template <typename T>
-constexpr bool HasKnownPrinter(domain_implementor::PrintMode mode);
+constexpr bool HasKnownPrinter();
 
 // If `prefix` is present in `name`, consume everything until the rightmost
 // occurrence of `prefix` and return true. Otherwise, return false.
@@ -439,8 +437,7 @@
       }
       case domain_implementor::PrintMode::kSourceCode:
         if constexpr (!HasFunctionName<Mapper>() &&
-                      HasKnownPrinter<decltype(value)>(
-                          domain_implementor::PrintMode::kSourceCode)) {
+                      HasKnownPrinter<decltype(value)>()) {
           if (map_fn_name.empty()) {
             // Fall back on printing the user value if the mapping function is
             // unknown (e.g. a lambda) and the value has a useful printer.
@@ -489,31 +486,6 @@
   }
 };
 
-struct AutodetectAggregatePrinter {
-  template <typename T>
-  void PrintUserValue(const T& v, domain_implementor::RawSink out,
-                      domain_implementor::PrintMode mode) {
-    if (mode == domain_implementor::PrintMode::kHumanReadable) {
-      // In human-readable mode, prefer formatting with Abseil if possible.
-      if constexpr (has_absl_stringify_v<T>) {
-        absl::Format(out, "%v", v);
-        return;
-      }
-    }
-    std::tuple bound = DetectBindAggregate(v);
-    const auto print_one = [&](auto I) {
-      if (I > 0) absl::Format(out, ", ");
-      AutodetectTypePrinter<
-          std::remove_reference_t<std::tuple_element_t<I, decltype(bound)>>>()
-          .PrintUserValue(std::get<I>(bound), out, mode);
-    };
-    absl::Format(out, "%s{", GetTypeNameIfUserDefined<T>());
-    ApplyIndex<std::tuple_size_v<decltype(bound)>>(
-        [&](auto... Is) { (print_one(Is), ...); });
-    absl::Format(out, "}");
-  }
-};
-
 struct DurationPrinter {
   void PrintUserValue(const absl::Duration duration,
                       domain_implementor::RawSink out,
@@ -571,6 +543,92 @@
   }
 };
 
+// A wrapper around `RawSink` that can be passed to the user's
+// `FuzzTestPrintSourceCode` function, a FTADLE extension point for custom
+// printers. This is so that the function can look more like `AbslStringify`,
+// which is parameterized by the sink type and passes a pointer to the sink to
+// `absl::Format()`.
+struct RawSinkWrapper {
+  domain_implementor::RawSink& raw_sink;
+
+  friend void AbslFormatFlush(RawSinkWrapper* absl_nonnull sink,
+                              absl::string_view part) {
+    absl::Format(sink->raw_sink, "%s", part);
+  }
+};
+
+template <typename T, typename = void>
+struct HasCustomSourceCodePrinter : std::false_type {};
+
+template <typename T>
+struct HasCustomSourceCodePrinter<
+    T, std::enable_if_t<std::is_void<decltype(FuzzTestPrintSourceCode(
+           std::declval<RawSinkWrapper&>(), std::declval<const T&>()))>::value>>
+    : std::true_type {};
+
+template <typename T>
+inline constexpr bool has_custom_source_code_printer_v =
+    HasCustomSourceCodePrinter<T>::value;
+
+template <typename T>
+inline constexpr bool has_custom_printer_v =
+    has_absl_stringify_v<T> || has_custom_source_code_printer_v<T>;
+
+struct AutodetectAggregatePrinter {
+  template <typename T>
+  void PrintUserValue(const T& v, domain_implementor::RawSink out,
+                      domain_implementor::PrintMode mode) {
+    if (mode == domain_implementor::PrintMode::kHumanReadable) {
+      // In human-readable mode, prefer formatting with Abseil if possible.
+      if constexpr (has_absl_stringify_v<T>) {
+        absl::Format(out, "%v", v);
+        return;
+      }
+    } else {
+      // In source-code mode, prefer custom source-code printer if possible.
+      if constexpr (has_custom_source_code_printer_v<T>) {
+        RawSinkWrapper sink{out};
+        FuzzTestPrintSourceCode(sink, v);
+        return;
+      }
+    }
+    std::tuple bound = DetectBindAggregate(v);
+    const auto print_one = [&](auto I) {
+      if (I > 0) absl::Format(out, ", ");
+      AutodetectTypePrinter<
+          std::remove_reference_t<std::tuple_element_t<I, decltype(bound)>>>()
+          .PrintUserValue(std::get<I>(bound), out, mode);
+    };
+    absl::Format(out, "%s{", GetTypeNameIfUserDefined<T>());
+    ApplyIndex<std::tuple_size_v<decltype(bound)>>(
+        [&](auto... Is) { (print_one(Is), ...); });
+    absl::Format(out, "}");
+  }
+};
+
+template <typename T, typename = std::enable_if_t<has_custom_printer_v<T>>>
+struct CustomPrinter {
+  void PrintUserValue(const T& v, domain_implementor::RawSink out,
+                      domain_implementor::PrintMode mode) {
+    RawSinkWrapper sink{out};
+    if (mode == domain_implementor::PrintMode::kHumanReadable) {
+      // Prefer AbslStringify, fall back on source code printer.
+      if constexpr (has_absl_stringify_v<T>) {
+        absl::Format(out, "%v", v);
+      } else {
+        FuzzTestPrintSourceCode(sink, v);
+      }
+    } else {
+      // Prefer source code printer, fall back on AbslStringify.
+      if constexpr (has_custom_source_code_printer_v<T>) {
+        FuzzTestPrintSourceCode(sink, v);
+      } else {
+        absl::Format(out, "%v", v);
+      }
+    }
+  }
+};
+
 struct UnknownPrinter {
   template <typename T>
   void PrintUserValue(const T& v, domain_implementor::RawSink out,
@@ -592,31 +650,13 @@
   }
 };
 
-template <typename T, typename = void>
-struct HasCustomSourceCodePrinter : std::false_type {};
-
 template <typename T>
-struct HasCustomSourceCodePrinter<
-    T, std::enable_if_t<std::is_void<decltype(FuzzTestPrintSourceCode(
-           std::declval<const T&>(), std::declval<std::ostream*>()))>::value>>
-    : std::true_type {};
-
-template <typename T>
-inline constexpr bool has_custom_source_code_printer_v =
-    HasCustomSourceCodePrinter<T>::value;
-
-struct CustomPrinter {
-  template <typename T>
-  void PrintUserValue(const T& v, domain_implementor::RawSink out,
-                      domain_implementor::PrintMode mode);
-};
-
-template <typename T, bool kAllowCustomSourcePrinter>
 decltype(auto) AutodetectTypePrinter() {
-  if constexpr (kAllowCustomSourcePrinter &&
-                has_custom_source_code_printer_v<T>) {
-    return CustomPrinter{};
-  } else if constexpr (is_protocol_buffer_enum_v<T>) {
+  // The order of these checks somewhat matters. Most of the concrete types have
+  // AbslStringify, so they should come first not to be captured by the custom
+  // printer case. The aggregate case is also more specific than the custom
+  // printer case, so it needs to come before.
+  if constexpr (is_protocol_buffer_enum_v<T>) {
     return ProtobufEnumPrinter<const google::protobuf::EnumDescriptor*>{
         google::protobuf::GetEnumDescriptor<T>()};
   } else if constexpr (std::numeric_limits<T>::is_integer ||
@@ -632,43 +672,23 @@
     return MonostatePrinter{};
   } else if constexpr (is_protocol_buffer_v<T>) {
     return ProtobufPrinter{};
-  } else if constexpr (is_bindable_aggregate_v<T>) {
-    return AutodetectAggregatePrinter{};
   } else if constexpr (std::is_same_v<T, absl::Duration>) {
     return DurationPrinter{};
   } else if constexpr (std::is_same_v<T, absl::Time>) {
     return TimePrinter{};
+  } else if constexpr (is_bindable_aggregate_v<T>) {
+    return AutodetectAggregatePrinter{};
+  } else if constexpr (has_custom_printer_v<T>) {
+    return CustomPrinter<T>{};
   } else {
     return UnknownPrinter{};
   }
 }
 
 template <typename T>
-void CustomPrinter::PrintUserValue(const T& v, domain_implementor::RawSink out,
-                                   domain_implementor::PrintMode mode) {
-  if (mode == domain_implementor::PrintMode::kSourceCode) {
-    std::ostringstream oss;
-    FuzzTestPrintSourceCode(v, &oss);
-    absl::Format(out, "%s", std::move(oss).str());
-  } else {
-    // Fallback for non-source-code.
-    auto printer =
-        AutodetectTypePrinter<T, /*kAllowCustomSourcePrinter=*/false>();
-    printer.PrintUserValue(v, out, mode);
-  }
-}
-
-template <typename T>
-constexpr bool HasKnownPrinter(domain_implementor::PrintMode mode) {
-  if (mode == domain_implementor::PrintMode::kSourceCode) {
-    return !std::is_convertible_v<
-        decltype(AutodetectTypePrinter<T,
-                                       /*kAllowCustomSourcePrinter=*/true>()),
-        UnknownPrinter>;
-  }
-  return !std::is_convertible_v<
-      decltype(AutodetectTypePrinter<T, /*kAllowCustomSourcePrinter=*/false>()),
-      UnknownPrinter>;
+constexpr bool HasKnownPrinter() {
+  return !std::is_convertible_v<decltype(AutodetectTypePrinter<T>()),
+                                UnknownPrinter>;
 }
 
 }  // namespace fuzztest::internal
diff --git a/fuzztest/internal/type_support_test.cc b/fuzztest/internal/type_support_test.cc
index bdd5587..56b2f07 100644
--- a/fuzztest/internal/type_support_test.cc
+++ b/fuzztest/internal/type_support_test.cc
@@ -488,35 +488,57 @@
   EXPECT_THAT(TestPrintValue(UserDefinedEmpty{}), Each("UserDefinedEmpty{}"));
 }
 
-struct AggregateStructWithNoAbslStringify {
+struct AggregateWithoutCustomPrinter {
   int i = 1;
   std::pair<std::string, std::string> nested = {"Foo", "Bar"};
 };
 
-struct AggregateStructWithAbslStringify {
+struct AggregateWithAbslStringify {
   int i = 1;
   std::pair<std::string, std::string> nested = {"Foo", "Bar"};
 
   template <typename Sink>
-  friend void AbslStringify(Sink& sink,
-                            const AggregateStructWithAbslStringify& s) {
+  friend void AbslStringify(Sink& sink, const AggregateWithAbslStringify& s) {
     absl::Format(&sink, "value={%d, {%s, %s}}", s.i, s.nested.first,
                  s.nested.second);
   }
 };
 
+struct AggregateWithCustomSourceCodePrinter {
+  int i = 1;
+  std::pair<std::string, std::string> nested = {"Foo", "Bar"};
+
+  static AggregateWithCustomSourceCodePrinter Make(int i, std::string fst,
+                                                   std::string snd) {
+    return AggregateWithCustomSourceCodePrinter{
+        i, {std::move(fst), std::move(snd)}};
+  }
+
+  template <typename Sink>
+  friend void FuzzTestPrintSourceCode(
+      Sink& sink, const AggregateWithCustomSourceCodePrinter& s) {
+    absl::Format(
+        &sink, "AggregateWithCustomSourceCodePrinter::Make(%d, \"%s\", \"%s\")",
+        s.i, s.nested.first, s.nested.second);
+  }
+};
+
 TEST(AutodetectAggregateTest, Printer) {
   // MonostateTest handles empty tuple and array.
 
   EXPECT_THAT(TestPrintValue(std::tuple{123}), Each("{123}"));
   EXPECT_THAT(TestPrintValue(std::pair{123, 456}), Each("{123, 456}"));
   EXPECT_THAT(TestPrintValue(std::array{123, 456}), Each("{123, 456}"));
-  EXPECT_THAT(TestPrintValue(AggregateStructWithNoAbslStringify{}),
-              Each(R"(AggregateStructWithNoAbslStringify{1, {"Foo", "Bar"}})"));
+  EXPECT_THAT(TestPrintValue(AggregateWithoutCustomPrinter{}),
+              Each(R"(AggregateWithoutCustomPrinter{1, {"Foo", "Bar"}})"));
+  EXPECT_THAT(TestPrintValue(AggregateWithAbslStringify{}),
+              ElementsAre("value={1, {Foo, Bar}}",
+                          R"(AggregateWithAbslStringify{1, {"Foo", "Bar"}})"));
   EXPECT_THAT(
-      TestPrintValue(AggregateStructWithAbslStringify{}),
-      ElementsAre("value={1, {Foo, Bar}}",
-                  R"(AggregateStructWithAbslStringify{1, {"Foo", "Bar"}})"));
+      TestPrintValue(AggregateWithCustomSourceCodePrinter{}),
+      ElementsAre(
+          R"(AggregateWithCustomSourceCodePrinter{1, {"Foo", "Bar"}})",
+          R"(AggregateWithCustomSourceCodePrinter::Make(1, "Foo", "Bar"))"));
 }
 
 TEST(DurationTest, Printer) {
@@ -553,36 +575,104 @@
                           "absl::UnixEpoch() + absl::Seconds(-1290000)"));
 }
 
-struct NonAggregateStructWithNoAbslStringify {
-  NonAggregateStructWithNoAbslStringify() : i(1), nested("Foo", "Bar") {}
-  int i;
-  std::pair<std::string, std::string> nested;
+class ClassWithoutCustomPrinter {
+ private:
+  // Needs a private member so that it isn't monostate or a bindable aggregate.
+  [[maybe_unused]] int a_ = 0;
 };
 
-struct NonAggregateStructWithAbslStringify {
-  NonAggregateStructWithAbslStringify() : i(1), nested("Foo", "Bar") {}
-  int i;
-  std::pair<std::string, std::string> nested;
+static_assert(!is_monostate_v<ClassWithoutCustomPrinter>);
+static_assert(!is_bindable_aggregate_v<ClassWithoutCustomPrinter>);
+
+class ClassWithAbslStringify {
+ public:
+  ClassWithAbslStringify(int a, std::string b) : a_{a}, b_{std::move(b)} {}
 
   template <typename Sink>
-  friend void AbslStringify(Sink& sink,
-                            const NonAggregateStructWithAbslStringify& s) {
-    absl::Format(&sink, "value={%d, {%s, %s}}", s.i, s.nested.first,
-                 s.nested.second);
+  friend void AbslStringify(Sink& sink, const ClassWithAbslStringify& v) {
+    absl::Format(&sink, "value={%d, \"%s\"}", v.a_, v.b_);
   }
+
+ private:
+  int a_ = 0;
+  std::string b_;
 };
 
-TEST(UnprintableTest, Printer) {
-  EXPECT_THAT(TestPrintValue(NonAggregateStructWithNoAbslStringify{}),
+class ClassWithCustomSourceCodePrinter {
+ public:
+  static ClassWithCustomSourceCodePrinter Make(int a, std::string b) {
+    return ClassWithCustomSourceCodePrinter{a, std::move(b)};
+  }
+
+  template <typename Sink>
+  friend void FuzzTestPrintSourceCode(
+      Sink& sink, const ClassWithCustomSourceCodePrinter& v) {
+    absl::Format(&sink, "ClassWithCustomSourceCodePrinter::Make(%d, \"%s\")",
+                 v.a_, v.b_);
+  }
+
+ private:
+  ClassWithCustomSourceCodePrinter(int a, std::string b)
+      : a_{a}, b_{std::move(b)} {}
+
+  int a_ = 0;
+  std::string b_;
+};
+
+class ClassWithAbslStringifyAndCustomSourceCodePrinter {
+ public:
+  static ClassWithAbslStringifyAndCustomSourceCodePrinter Make(int a,
+                                                               std::string b) {
+    return ClassWithAbslStringifyAndCustomSourceCodePrinter{a, std::move(b)};
+  }
+
+  template <typename Sink>
+  friend void AbslStringify(
+      Sink& sink, const ClassWithAbslStringifyAndCustomSourceCodePrinter& v) {
+    absl::Format(&sink, "value={a=%d, b=\"%s\"}", v.a_, v.b_);
+  }
+
+  template <typename Sink>
+  friend void FuzzTestPrintSourceCode(
+      Sink& sink, const ClassWithAbslStringifyAndCustomSourceCodePrinter& v) {
+    absl::Format(
+        &sink,
+        "ClassWithAbslStringifyAndCustomSourceCodePrinter::Make(%d, \"%s\")",
+        v.a_, v.b_);
+  }
+
+ private:
+  ClassWithAbslStringifyAndCustomSourceCodePrinter(int a, std::string b)
+      : a_{a}, b_{std::move(b)} {}
+
+  int a_ = 0;
+  std::string b_;
+};
+
+TEST(HasCustomPrinterTest, CorrectlyDetectsCustomPrinters) {
+  static_assert(has_custom_printer_v<ClassWithAbslStringify>);
+  static_assert(has_custom_printer_v<ClassWithCustomSourceCodePrinter>);
+  static_assert(
+      has_custom_printer_v<ClassWithAbslStringifyAndCustomSourceCodePrinter>);
+  static_assert(!has_custom_printer_v<ClassWithoutCustomPrinter>);
+}
+
+TEST(CustomPrinterTest, PrintsValuesCorrectly) {
+  EXPECT_THAT(TestPrintValue(ClassWithAbslStringify{1, "foo"}),
+              Each("value={1, \"foo\"}"));
+  EXPECT_THAT(TestPrintValue(ClassWithCustomSourceCodePrinter::Make(1, "foo")),
+              Each("ClassWithCustomSourceCodePrinter::Make(1, \"foo\")"));
+  EXPECT_THAT(
+      TestPrintValue(
+          ClassWithAbslStringifyAndCustomSourceCodePrinter::Make(1, "foo")),
+      ElementsAre("value={a=1, b=\"foo\"}",
+                  "ClassWithAbslStringifyAndCustomSourceCodePrinter::Make(1, "
+                  "\"foo\")"));
+}
+
+TEST(UnprintableTest, PrintsUnprintableValues) {
+  EXPECT_THAT(TestPrintValue(ClassWithoutCustomPrinter{}),
               Each("<unprintable value>"));
-  EXPECT_THAT(TestPrintValue(NonAggregateStructWithAbslStringify{}),
-              ElementsAre("value={1, {Foo, Bar}}", "<unprintable value>"));
-  EXPECT_THAT(
-      TestPrintValue(std::vector<NonAggregateStructWithNoAbslStringify>{}),
-      Each("<unprintable value>"));
-  EXPECT_THAT(
-      TestPrintValue(std::vector<NonAggregateStructWithAbslStringify>{}),
-      Each("<unprintable value>"));
 }
 
 }  // namespace