Refactor ExecException to centralize logic for "messagePrefix" construction. Leads to a minor behavior change for error messages (additional colon). Don't think it's worth RELNOTES.

The fewer Action#describe() calls we have, the easier it will be to move them all to SkyframeActionExecutor#describeAction.

#11151

PiperOrigin-RevId: 336879499
diff --git a/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java b/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
index 218aae2..862337c 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
@@ -463,7 +463,7 @@
   }
 
   @Override
-  public String describe() {
+  public final String describe() {
     String progressMessage = getProgressMessage();
     return progressMessage != null ? progressMessage : defaultProgressMessage();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java b/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java
index 0e490e0..d91302a 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java
@@ -14,11 +14,10 @@
 
 package com.google.devtools.build.lib.actions;
 
-import com.google.common.base.Throwables;
+import com.google.common.flogger.GoogleLogger;
 import com.google.devtools.build.lib.server.FailureDetails;
 import com.google.devtools.build.lib.server.FailureDetails.Execution;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
-import com.google.devtools.build.lib.util.DetailedExitCode;
 import java.io.IOException;
 
 /**
@@ -35,10 +34,13 @@
  * directory or denied file system access.
  */
 public class EnvironmentalExecException extends ExecException {
+  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
+
   private final FailureDetail failureDetail;
 
   public EnvironmentalExecException(IOException cause, FailureDetails.Execution.Code code) {
     super("unexpected I/O exception", cause);
+    logger.atSevere().withCause(cause).log("Unexpected I/O exception");
     this.failureDetail =
         FailureDetail.newBuilder().setExecution(Execution.newBuilder().setCode(code)).build();
   }
@@ -54,15 +56,7 @@
   }
 
   @Override
-  public ActionExecutionException toActionExecutionException(String messagePrefix, Action action) {
-    String message =
-        String.format(
-            "%s failed due to %s%s",
-            messagePrefix,
-            getMessage(),
-            getCause() == null ? "" : ("\n" + Throwables.getStackTraceAsString(getCause())));
-    FailureDetail failureDetailWithPrefix = failureDetail.toBuilder().setMessage(message).build();
-    return new ActionExecutionException(
-        message, action, isCatastrophic(), DetailedExitCode.of(failureDetailWithPrefix));
+  protected FailureDetail getFailureDetail(String message) {
+    return failureDetail.toBuilder().setMessage(message).build();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ExecException.java b/src/main/java/com/google/devtools/build/lib/actions/ExecException.java
index 27bc137..fb63b60 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ExecException.java
@@ -14,6 +14,11 @@
 
 package com.google.devtools.build.lib.actions;
 
+import com.google.common.base.Strings;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.util.DetailedExitCode;
+import com.google.errorprone.annotations.ForOverride;
+
 /**
  * An exception indication that the execution of an action has failed OR could not be attempted OR
  * could not be finished OR had something else wrong.
@@ -81,7 +86,7 @@
    * @param action failed action
    * @return ActionExecutionException object describing the action failure
    */
-  public ActionExecutionException toActionExecutionException(Action action) {
+  public final ActionExecutionException toActionExecutionException(Action action) {
     return toActionExecutionException("", action);
   }
 
@@ -94,6 +99,28 @@
    * @param action failed action
    * @return ActionExecutionException object describing the action failure
    */
-  public abstract ActionExecutionException toActionExecutionException(
-      String messagePrefix, Action action);
+  public final ActionExecutionException toActionExecutionException(
+      String messagePrefix, Action action) {
+    String message =
+        String.format(
+            "%s failed: %s",
+            Strings.isNullOrEmpty(messagePrefix) ? action.describe() : messagePrefix,
+            getMessageForActionExecutionException());
+    return toActionExecutionException(
+        message, action, DetailedExitCode.of(getFailureDetail(message)));
+  }
+
+  @ForOverride
+  protected ActionExecutionException toActionExecutionException(
+      String message, Action action, DetailedExitCode code) {
+    return new ActionExecutionException(message, this, action, isCatastrophic(), code);
+  }
+
+  @ForOverride
+  protected String getMessageForActionExecutionException() {
+    return getMessage();
+  }
+
+  @ForOverride
+  protected abstract FailureDetail getFailureDetail(String message);
 }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/LostInputsExecException.java b/src/main/java/com/google/devtools/build/lib/actions/LostInputsExecException.java
index 0f4eb65..0be3361 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/LostInputsExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/LostInputsExecException.java
@@ -65,18 +65,20 @@
   }
 
   @Override
-  public ActionExecutionException toActionExecutionException(String messagePrefix, Action action) {
-    String message = String.format("%s failed: %s", messagePrefix, getMessage());
-    DetailedExitCode code =
-        DetailedExitCode.of(
-            FailureDetail.newBuilder()
-                .setMessage(message)
-                .setExecution(Execution.newBuilder().setCode(Code.ACTION_INPUT_LOST))
-                .build());
+  protected ActionExecutionException toActionExecutionException(
+      String message, Action action, DetailedExitCode code) {
     return new LostInputsActionExecutionException(
         message, lostInputs, owners, action, /*cause=*/ this, code);
   }
 
+  @Override
+  protected FailureDetail getFailureDetail(String message) {
+    return FailureDetail.newBuilder()
+        .setExecution(Execution.newBuilder().setCode(Code.ACTION_INPUT_LOST))
+        .setMessage(message)
+        .build();
+  }
+
   public void combineAndThrow(LostInputsExecException other) throws LostInputsExecException {
     // This uses a HashMap when merging the two lostInputs maps because key collisions are expected.
     // In contrast, ImmutableMap.Builder treats collisions as errors. Collisions will happen when
diff --git a/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java b/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java
index da5ea0f..149b2db 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java
@@ -239,7 +239,6 @@
   }
 
   String getDetailMessage(
-      String messagePrefix,
       String message,
       boolean catastrophe,
       boolean forciblyRunRemotely);
@@ -378,13 +377,12 @@
 
     @Override
     public String getDetailMessage(
-        String messagePrefix,
         String message,
         boolean catastrophe,
         boolean forciblyRunRemotely) {
       TerminationStatus status = new TerminationStatus(
           exitCode(), status() == Status.TIMEOUT);
-      String reason = " (" + status.toShortString() + ")"; // e.g " (Exit 1)"
+      String reason = "(" + status.toShortString() + ")"; // e.g "(Exit 1)"
       String explanation = Strings.isNullOrEmpty(message) ? "" : ": " + message;
 
       if (!status().isConsideredUserError()) {
@@ -412,7 +410,7 @@
       if (!Strings.isNullOrEmpty(failureMessage)) {
         explanation += " " + failureMessage;
       }
-      return messagePrefix + " failed" + reason + explanation;
+      return reason + explanation;
     }
 
     @Nullable
diff --git a/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java b/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java
index 73365d0..900579a 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java
@@ -16,7 +16,6 @@
 import com.google.devtools.build.lib.server.FailureDetails;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.server.FailureDetails.TestAction;
-import com.google.devtools.build.lib.util.DetailedExitCode;
 
 /** An TestExecException that is related to the failure of a TestAction. */
 public final class TestExecException extends ExecException {
@@ -28,14 +27,10 @@
   }
 
   @Override
-  public ActionExecutionException toActionExecutionException(String messagePrefix, Action action) {
-    String message = String.format("%s: %s", messagePrefix + " failed", getMessage());
-    DetailedExitCode code =
-        DetailedExitCode.of(
-            FailureDetail.newBuilder()
-                .setMessage(message)
-                .setTestAction(TestAction.newBuilder().setCode(detailedCode))
-                .build());
-    return new ActionExecutionException(message, this, action, isCatastrophic(), code);
+  protected FailureDetail getFailureDetail(String message) {
+    return FailureDetail.newBuilder()
+        .setTestAction(TestAction.newBuilder().setCode(detailedCode))
+        .setMessage(message)
+        .build();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java b/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java
index 020aab3..cbfee2f 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java
@@ -15,7 +15,6 @@
 package com.google.devtools.build.lib.actions;
 
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
-import com.google.devtools.build.lib.util.DetailedExitCode;
 
 /**
  * An ExecException that is related to the failure of an Action and therefore very likely the user's
@@ -36,10 +35,7 @@
   }
 
   @Override
-  public ActionExecutionException toActionExecutionException(String messagePrefix, Action action) {
-    String message = String.format("%s failed: %s", messagePrefix, getMessage());
-    FailureDetail failureDetailWithPrefix = failureDetail.toBuilder().setMessage(message).build();
-    return new ActionExecutionException(
-        message, this, action, isCatastrophic(), DetailedExitCode.of(failureDetailWithPrefix));
+  protected FailureDetail getFailureDetail(String message) {
+    return failureDetail.toBuilder().setMessage(message).build();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java
index 8e255ba..fb6f4f2 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnExecException.java
@@ -14,15 +14,13 @@
 package com.google.devtools.build.lib.exec;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Preconditions;
-import com.google.devtools.build.lib.actions.Action;
-import com.google.devtools.build.lib.actions.ActionExecutionException;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.actions.SpawnResult.Status;
-import com.google.devtools.build.lib.util.DetailedExitCode;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 
 /**
  * A specialization of {@link ExecException} that indicates something went wrong when trying to
@@ -38,7 +36,7 @@
     checkArgument(
         !Status.SUCCESS.equals(result.status()),
         "Can't create exception with successful spawn result.");
-    this.result = Preconditions.checkNotNull(result);
+    this.result = checkNotNull(result);
     this.forciblyRunRemotely = forciblyRunRemotely;
   }
 
@@ -46,7 +44,7 @@
   public SpawnExecException(
       String message, SpawnResult result, boolean forciblyRunRemotely, boolean catastrophe) {
     super(message, catastrophe);
-    this.result = Preconditions.checkNotNull(result);
+    this.result = checkNotNull(result);
     this.forciblyRunRemotely = forciblyRunRemotely;
   }
 
@@ -60,13 +58,12 @@
   }
 
   @Override
-  public ActionExecutionException toActionExecutionException(String messagePrefix, Action action) {
-    if (messagePrefix == null) {
-      messagePrefix = action.describe();
-    }
-    String message =
-        result.getDetailMessage(messagePrefix, getMessage(), isCatastrophic(), forciblyRunRemotely);
-    return new ActionExecutionException(
-        message, this, action, isCatastrophic(), DetailedExitCode.of(result.failureDetail()));
+  protected String getMessageForActionExecutionException() {
+    return result.getDetailMessage(getMessage(), isCatastrophic(), forciblyRunRemotely);
+  }
+
+  @Override
+  protected FailureDetail getFailureDetail(String message) {
+    return checkNotNull(result.failureDetail(), this);
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/actions/SpawnResultTest.java b/src/test/java/com/google/devtools/build/lib/actions/SpawnResultTest.java
index fee0304..6baaa8f 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/SpawnResultTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/SpawnResultTest.java
@@ -48,7 +48,7 @@
                     .build())
             .setRunnerName("test")
             .build();
-    assertThat(r.getDetailMessage("", "", false, false))
+    assertThat(r.getDetailMessage("", false, false))
         .contains("(failed due to timeout after 5.00 seconds.)");
   }
 
@@ -64,7 +64,7 @@
                     .build())
             .setRunnerName("test")
             .build();
-    assertThat(r.getDetailMessage("", "", false, false)).contains("(failed due to timeout.)");
+    assertThat(r.getDetailMessage("", false, false)).contains("(failed due to timeout.)");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java b/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java
index 08815f3..f628d13 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java
@@ -28,7 +28,6 @@
 import com.google.common.eventbus.SubscriberExceptionHandler;
 import com.google.devtools.build.lib.actions.Action;
 import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
-import com.google.devtools.build.lib.actions.ActionExecutionException;
 import com.google.devtools.build.lib.actions.ActionGraph;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ExecException;
@@ -95,7 +94,6 @@
 import com.google.devtools.build.lib.testutil.TestUtils;
 import com.google.devtools.build.lib.util.CommandBuilder;
 import com.google.devtools.build.lib.util.CommandUtils;
-import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.LoggingUtil;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.util.io.OutErr;
@@ -137,22 +135,11 @@
     }
 
     @Override
-    public ActionExecutionException toActionExecutionException(
-        String messagePrefix, Action action) {
-      String message = messagePrefix + getMessage();
-      // Append cause.getMessage() if it's different from getMessage(). It typically
-      // isn't but if it is we'd like to surface cause.getMessage() as part of the
-      // exception message.
-      if (getCause() != null && !getMessage().equals(getCause().getMessage())) {
-        message += ": " + getCause().getMessage();
-      }
-      // The detailed code doesn't matter, but it should be well-formed.
-      DetailedExitCode code =
-          DetailedExitCode.of(
-              FailureDetail.newBuilder()
-                  .setSpawn(Spawn.newBuilder().setCode(Code.NON_ZERO_EXIT))
-                  .build());
-      return new ActionExecutionException(message, getCause(), action, true, code);
+    protected FailureDetail getFailureDetail(String message) {
+      return FailureDetail.newBuilder()
+          .setSpawn(Spawn.newBuilder().setCode(Code.NON_ZERO_EXIT))
+          .setMessage(message)
+          .build();
     }
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java
index 46dc051..94a1040 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java
@@ -822,7 +822,7 @@
 
     SpawnResult result = runner.exec(spawn, policy);
     assertThat(result.exitCode()).isEqualTo(ExitCode.REMOTE_ERROR.getNumericExitCode());
-    assertThat(result.getDetailMessage("", "", false, false)).contains("reasons");
+    assertThat(result.getDetailMessage("", false, false)).contains("reasons");
   }
 
   @Test
@@ -842,7 +842,7 @@
 
     SpawnResult result = runner.exec(spawn, policy);
     assertThat(result.exitCode()).isEqualTo(ExitCode.REMOTE_ERROR.getNumericExitCode());
-    assertThat(result.getDetailMessage("", "", false, false)).contains("reasons");
+    assertThat(result.getDetailMessage("", false, false)).contains("reasons");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java
index 074f254..bd11f56 100644
--- a/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java
@@ -315,7 +315,8 @@
   @Test
   public void testVerboseFailures() {
     ExecException e = assertThrows(ExecException.class, () -> run(createSpawn(getFalseCommand())));
-    ActionExecutionException actionExecutionException = e.toActionExecutionException("", null);
+    ActionExecutionException actionExecutionException =
+        e.toActionExecutionException("messagePrefix", null);
     assertWithMessage("got: " + actionExecutionException.getMessage())
         .that(actionExecutionException.getMessage().contains("failed: error executing command"))
         .isTrue();