Include exception classes in Crash-type FailureDetails.message text

Provides better at-a-glance summaries of crashes when exceptions lack
message text.

RELNOTES: None.
PiperOrigin-RevId: 318324099
diff --git a/src/main/java/com/google/devtools/build/lib/util/CrashFailureDetails.java b/src/main/java/com/google/devtools/build/lib/util/CrashFailureDetails.java
index 6629132..0b68223 100644
--- a/src/main/java/com/google/devtools/build/lib/util/CrashFailureDetails.java
+++ b/src/main/java/com/google/devtools/build/lib/util/CrashFailureDetails.java
@@ -57,17 +57,22 @@
                     : Crash.Code.CRASH_UNKNOWN);
     addCause(crashBuilder, throwable, Sets.newIdentityHashSet());
     return FailureDetail.newBuilder()
-        .setMessage("Crashed: " + joinCauseMessages(crashBuilder))
+        .setMessage("Crashed: " + joinSummarizedCauses(crashBuilder))
         .setCrash(crashBuilder)
         .build();
   }
 
-  private static String joinCauseMessages(Crash.Builder crashBuilder) {
+  private static String joinSummarizedCauses(Crash.Builder crashBuilder) {
     return crashBuilder.getCausesOrBuilderList().stream()
-        .map(ThrowableOrBuilder::getMessage)
+        .map(CrashFailureDetails::summarizeCause)
         .collect(Collectors.joining(", "));
   }
 
+  private static String summarizeCause(ThrowableOrBuilder throwableOrBuilder) {
+    return String.format(
+        "(%s) %s", throwableOrBuilder.getThrowableClass(), throwableOrBuilder.getMessage());
+  }
+
   private static void addCause(
       Crash.Builder crashBuilder, Throwable throwable, Set<Object> addedThrowables) {
     addedThrowables.add(throwable);
diff --git a/src/test/java/com/google/devtools/build/lib/bugreport/BugReportTest.java b/src/test/java/com/google/devtools/build/lib/bugreport/BugReportTest.java
index 5598b79..992075e 100644
--- a/src/test/java/com/google/devtools/build/lib/bugreport/BugReportTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bugreport/BugReportTest.java
@@ -47,6 +47,8 @@
 @RunWith(JUnit4.class)
 public class BugReportTest {
 
+  private static final String TEST_EXCEPTION_NAME =
+      "com.google.devtools.build.lib.bugreport.BugReportTest$TestException";
   @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
 
   @After
@@ -82,15 +84,15 @@
     FailureDetail failureDetail =
         FailureDetail.parseFrom(
             Files.readAllBytes(failureDetailFilePath), ExtensionRegistry.getEmptyRegistry());
-    assertThat(failureDetail.getMessage()).isEqualTo("Crashed: myMessage");
+    assertThat(failureDetail.getMessage())
+        .isEqualTo(String.format("Crashed: (%s) myMessage", TEST_EXCEPTION_NAME));
     assertThat(failureDetail.hasCrash()).isTrue();
     Crash crash = failureDetail.getCrash();
     assertThat(crash.getCode()).isEqualTo(Code.CRASH_UNKNOWN);
     assertThat(crash.getCausesList()).hasSize(1);
     Throwable cause = crash.getCauses(0);
     assertThat(cause.getMessage()).isEqualTo("myMessage");
-    assertThat(cause.getThrowableClass())
-        .isEqualTo("com.google.devtools.build.lib.bugreport.BugReportTest$TestException");
+    assertThat(cause.getThrowableClass()).isEqualTo(TEST_EXCEPTION_NAME);
     assertThat(cause.getStackTraceCount()).isAtLeast(1);
     assertThat(cause.getStackTrace(0))
         .contains(
diff --git a/src/test/java/com/google/devtools/build/lib/util/CrashFailureDetailsTest.java b/src/test/java/com/google/devtools/build/lib/util/CrashFailureDetailsTest.java
index f919992..63f3fea 100644
--- a/src/test/java/com/google/devtools/build/lib/util/CrashFailureDetailsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/util/CrashFailureDetailsTest.java
@@ -31,6 +31,9 @@
 @RunWith(JUnit4.class)
 public class CrashFailureDetailsTest {
 
+  private static final String TEST_EXCEPTION_NAME =
+      "com.google.devtools.build.lib.util.CrashFailureDetailsTest$TestException";
+
   @Test
   public void nestedThrowables() {
     // This test confirms that throwables' details are recorded: their messages, types, stack
@@ -40,7 +43,11 @@
         CrashFailureDetails.forThrowable(
             functionForStackFrameTests_A(functionForStackFrameTests_B()));
 
-    assertThat(failureDetail.getMessage()).isEqualTo("Crashed: myMessage_A, myMessage_B");
+    assertThat(failureDetail.getMessage())
+        .isEqualTo(
+            String.format(
+                "Crashed: (%s) myMessage_A, (%s) myMessage_B",
+                TEST_EXCEPTION_NAME, TEST_EXCEPTION_NAME));
     assertThat(failureDetail.hasCrash()).isTrue();
     Crash crash = failureDetail.getCrash();
     assertThat(crash.getCode()).isEqualTo(Code.CRASH_UNKNOWN);
@@ -49,8 +56,7 @@
 
     FailureDetails.Throwable outerCause = crash.getCauses(0);
     assertThat(outerCause.getMessage()).isEqualTo("myMessage_A");
-    assertThat(outerCause.getThrowableClass())
-        .isEqualTo("com.google.devtools.build.lib.util.CrashFailureDetailsTest$TestException");
+    assertThat(outerCause.getThrowableClass()).isEqualTo(TEST_EXCEPTION_NAME);
     assertThat(outerCause.getStackTraceCount()).isAtLeast(2);
     assertThat(outerCause.getStackTrace(0))
         .contains(
@@ -61,8 +67,7 @@
 
     FailureDetails.Throwable innerCause = crash.getCauses(1);
     assertThat(innerCause.getMessage()).isEqualTo("myMessage_B");
-    assertThat(innerCause.getThrowableClass())
-        .isEqualTo("com.google.devtools.build.lib.util.CrashFailureDetailsTest$TestException");
+    assertThat(innerCause.getThrowableClass()).isEqualTo(TEST_EXCEPTION_NAME);
     assertThat(innerCause.getStackTraceCount()).isAtLeast(2);
     assertThat(innerCause.getStackTrace(0))
         .contains(