BuildEventStreamer supports status INCOMPLETE (--nokeep_going) and INTERNAL (catastrophe)

  - Add and Report AbortReason.INCOMPLETE when the build is incomplete due to an earlier build failure (--nokeep_going).
  - Report AbortReason.INTERNAL when the build is incomplete due to a catastrophic failure.
  - If multiple BuildEventStreamer AbortReasons are reported then the last one wins (no change), but all reasons are reported the Aborted event description (new behavior).

RELNOTES: none

PiperOrigin-RevId: 248037389
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto b/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto
index 5941665..00d2592 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto
@@ -16,12 +16,12 @@
 
 package build_event_stream;
 
+import "src/main/protobuf/command_line.proto";
+import "src/main/protobuf/invocation_policy.proto";
+
 option java_package = "com.google.devtools.build.lib.buildeventstream";
 option java_outer_classname = "BuildEventStreamProtos";
 
-import "src/main/protobuf/invocation_policy.proto";
-import "src/main/protobuf/command_line.proto";
-
 // Identifier for a build event. It is deliberately structured to also provide
 // information about which build target etc the event is related to.
 //
@@ -49,13 +49,11 @@
 
   // Identifier of an event indicating the beginning of a build; this will
   // normally be the first event.
-  message BuildStartedId {
-  }
+  message BuildStartedId {}
 
   // Identifier on an event indicating the original commandline received by
   // the bazel server.
-  message UnstructuredCommandLineId {
-  }
+  message UnstructuredCommandLineId {}
 
   // Identifier on an event describing the commandline received by Bazel.
   message StructuredCommandLineId {
@@ -67,13 +65,11 @@
   }
 
   // Identifier of an event indicating the workspace status.
-  message WorkspaceStatusId {
-  }
+  message WorkspaceStatusId {}
 
   // Identifier on an event reporting on the options included in the command
   // line, both explicitly and implicitly.
-  message OptionsParsedId {
-  }
+  message OptionsParsedId {}
 
   // Identifier of an event reporting that an external resource was fetched
   // from.
@@ -180,18 +176,15 @@
   }
 
   // Identifier of the BuildFinished event, indicating the end of a build.
-  message BuildFinishedId {
-  }
+  message BuildFinishedId {}
 
   // Identifier of an event providing additional logs/statistics after
   // completion of the build.
-  message BuildToolLogsId {
-  }
+  message BuildToolLogsId {}
 
   // Identifier of an event providing build metrics after completion
   // of the build.
-  message BuildMetricsId {
-  }
+  message BuildMetricsId {}
 
   oneof id {
     UnknownBuildEventId unknown = 1;
@@ -266,6 +259,10 @@
 
     // Target build was skipped (e.g. due to incompatible CPU constraints).
     SKIPPED = 7;
+
+    // Build incomplete due to an earlier build failure (e.g. --keep_going was
+    // set to false causing the build be ended upon failure).
+    INCOMPLETE = 10;
   }
   AbortReason reason = 1;
 
@@ -357,8 +354,7 @@
 // The main information is in the chaining part: the id will contain the
 // target pattern that was expanded and the children id will contain the
 // target or target pattern it was expanded to.
-message PatternExpanded {
-}
+message PatternExpanded {}
 
 // Enumeration type characterizing the size of a test, as specified by the
 // test rule.
@@ -499,7 +495,7 @@
   REMOTE_FAILURE = 6;
   FAILED_TO_BUILD = 7;
   TOOL_HALTED_BEFORE_TESTING = 8;
-};
+}
 
 // Payload on events reporting about individual test action.
 message TestResult {
@@ -695,5 +691,5 @@
     BuildFinished finished = 14;
     BuildToolLogs build_tool_logs = 23;
     BuildMetrics build_metrics = 24;
-  };
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildEventStreamer.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildEventStreamer.java
index 742aa2b..4dbef12 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BuildEventStreamer.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildEventStreamer.java
@@ -40,6 +40,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildCompletingEvent;
 import com.google.devtools.build.lib.buildeventstream.BuildEvent;
 import com.google.devtools.build.lib.buildeventstream.BuildEventId;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.Aborted.AbortReason;
 import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
 import com.google.devtools.build.lib.buildeventstream.BuildEventWithConfiguration;
@@ -66,6 +67,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.function.BiConsumer;
@@ -100,7 +102,10 @@
 
   private final CountingArtifactGroupNamer artifactGroupNamer;
   private OutErrProvider outErrProvider;
-  private volatile AbortReason abortReason = AbortReason.UNKNOWN;
+
+  @GuardedBy("this")
+  private final Set<AbortReason> abortReasons = new LinkedHashSet<>();
+
   // Will be set to true if the build was invoked through "bazel test" or "bazel coverage".
   private boolean isTestCommand;
 
@@ -293,7 +298,11 @@
               .map(BuildEvent::getEventId)
               .collect(ImmutableSet.<BuildEventId>toImmutableSet()));
       buildEvent(
-          new AbortedEvent(BuildEventId.buildStartedId(), children.build(), abortReason, ""));
+          new AbortedEvent(
+              BuildEventId.buildStartedId(),
+              children.build(),
+              getLastAbortReason(),
+              getAbortReasonDetails()));
     }
   }
 
@@ -301,7 +310,7 @@
   private synchronized void clearPendingEvents() {
     while (!pendingEvents.isEmpty()) {
       BuildEventId id = pendingEvents.keySet().iterator().next();
-      buildEvent(new AbortedEvent(id, abortReason, ""));
+      buildEvent(new AbortedEvent(id, getLastAbortReason(), getAbortReasonDetails()));
     }
   }
 
@@ -319,7 +328,7 @@
       }
       for (BuildEventId id : ids) {
         if (!dontclear.contains(id)) {
-          post(new AbortedEvent(id, abortReason, ""));
+          post(new AbortedEvent(id, getLastAbortReason(), getAbortReasonDetails()));
         }
       }
     }
@@ -339,7 +348,7 @@
     }
     closed = true;
     if (reason != null) {
-      abortReason = reason;
+      addAbortReason(reason);
     }
 
     if (finalEventsToCome == null) {
@@ -403,17 +412,17 @@
 
   @Subscribe
   public void buildInterrupted(BuildInterruptedEvent event) {
-    abortReason = AbortReason.USER_INTERRUPTED;
+    addAbortReason(AbortReason.USER_INTERRUPTED);
   }
 
   @Subscribe
   public void noAnalyze(NoAnalyzeEvent event) {
-    abortReason = AbortReason.NO_ANALYZE;
+    addAbortReason(AbortReason.NO_ANALYZE);
   }
 
   @Subscribe
   public void noExecution(NoExecutionEvent event) {
-    abortReason = AbortReason.NO_BUILD;
+    addAbortReason(AbortReason.NO_BUILD);
   }
 
   @Subscribe
@@ -469,8 +478,15 @@
       buildEvent(freedEvent);
     }
 
-    if (event instanceof BuildCompleteEvent && isCrash((BuildCompleteEvent) event)) {
-      abortReason = AbortReason.INTERNAL;
+    if (event instanceof BuildCompleteEvent) {
+      BuildCompleteEvent buildCompleteEvent = (BuildCompleteEvent) event;
+      if (isCrash(buildCompleteEvent)) {
+        addAbortReason(AbortReason.INTERNAL);
+      } else if (isCatastrophe(buildCompleteEvent)) {
+        addAbortReason(AbortReason.INTERNAL);
+      } else if (isIncomplete(buildCompleteEvent)) {
+        addAbortReason(AbortReason.INCOMPLETE);
+      }
     }
 
     if (event instanceof BuildCompletingEvent) {
@@ -492,6 +508,16 @@
     return event.getResult().getUnhandledThrowable() != null;
   }
 
+  private static boolean isCatastrophe(BuildCompleteEvent event) {
+    return event.getResult().wasCatastrophe();
+  }
+
+  private boolean isIncomplete(BuildCompleteEvent event) {
+    return !event.getResult().getSuccess()
+        && !event.getResult().wasCatastrophe()
+        && event.getResult().getStopOnFirstFailure();
+  }
+
   private synchronized BuildEvent flushStdoutStderrEvent(String out, String err) {
     BuildEvent updateEvent = ProgressEvent.progressUpdate(progressCount, out, err);
     progressCount++;
@@ -713,6 +739,34 @@
     return halfCloseFuturesMap;
   }
 
+  /**
+   * Stores the abort reason for later reporting on BEP pending events. In case of multiple abort
+   * reasons:
+   *
+   * <ul>
+   *   <li>Only the most recent reason will be reported as the main AbortReason in BEP.
+   *   <li>All previous AbortReason will appear in Aborted#getDescription message.
+   * </ul>
+   */
+  private synchronized void addAbortReason(BuildEventStreamProtos.Aborted.AbortReason reason) {
+    abortReasons.add(reason);
+  }
+
+  /** @return the most recent AbortReason or UNKNOWN if no value was set. */
+  private synchronized AbortReason getLastAbortReason() {
+    return abortReasons.isEmpty() ? AbortReason.UNKNOWN : Iterables.getLast(abortReasons);
+  }
+
+  /**
+   * @return Detailed message explaining the most recent AbortReason (and possibly previous
+   *     reasons).
+   */
+  private synchronized String getAbortReasonDetails() {
+    return abortReasons.size() <= 1
+        ? ""
+        : String.format("Multiple abort reasons reported: %s", abortReasons);
+  }
+
   /** A builder for {@link BuildEventStreamer}. */
   public static class Builder {
     private Set<BuildEventTransport> buildEventTransports;
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
index 645f89e..6da3ca7 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
@@ -48,6 +48,7 @@
 import com.google.devtools.build.lib.buildeventstream.BuildEventId;
 import com.google.devtools.build.lib.buildeventstream.BuildEventProtocolOptions;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.Aborted.AbortReason;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.NamedSetOfFilesId;
 import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
 import com.google.devtools.build.lib.buildeventstream.BuildEventTransportClosedEvent;
@@ -59,10 +60,12 @@
 import com.google.devtools.build.lib.buildeventstream.transports.BuildEventStreamOptions;
 import com.google.devtools.build.lib.buildtool.BuildResult;
 import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.NoAnalyzeEvent;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetView;
 import com.google.devtools.build.lib.testutil.FoundationTestCase;
+import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -79,6 +82,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.LockSupport;
+import javax.annotation.Nullable;
 import org.apache.commons.lang.time.StopWatch;
 import org.junit.After;
 import org.junit.Before;
@@ -1178,4 +1182,141 @@
     assertThat(transportedEvents).contains(SUCCESSFUL_ACTION_EXECUTED_EVENT);
     assertThat(transportedEvents).contains(failedActionExecutedEvent);
   }
+
+  @Test
+  public void testBuildIncomplete() {
+    BuildEventId buildEventId = testId("abort_expected");
+    BuildEvent startEvent =
+        new GenericBuildEvent(
+            BuildEventId.buildStartedId(),
+            ImmutableSet.of(
+                buildEventId, ProgressEvent.INITIAL_PROGRESS_UPDATE, BuildEventId.buildFinished()));
+    BuildCompleteEvent buildCompleteEvent =
+        buildCompleteEvent(ExitCode.BUILD_FAILURE, true, null, false);
+
+    streamer.buildEvent(startEvent);
+    streamer.buildEvent(buildCompleteEvent);
+    streamer.close();
+
+    BuildEventStreamProtos.BuildEvent aborted = getBepEvent(buildEventId);
+    assertThat(aborted).isNotNull();
+    assertThat(aborted.hasAborted()).isNotNull();
+    assertThat(aborted.getAborted().getReason()).isEqualTo(AbortReason.INCOMPLETE);
+    assertThat(aborted.getAborted().getDescription()).isEmpty();
+  }
+
+  @Test
+  public void testBuildCrash() {
+    BuildEventId buildEventId = testId("abort_expected");
+    BuildEvent startEvent =
+        new GenericBuildEvent(
+            BuildEventId.buildStartedId(),
+            ImmutableSet.of(
+                buildEventId, ProgressEvent.INITIAL_PROGRESS_UPDATE, BuildEventId.buildFinished()));
+    BuildCompleteEvent buildCompleteEvent =
+        buildCompleteEvent(ExitCode.BUILD_FAILURE, true, new RuntimeException(), false);
+
+    streamer.buildEvent(startEvent);
+    streamer.buildEvent(buildCompleteEvent);
+    streamer.close();
+
+    BuildEventStreamProtos.BuildEvent aborted = getBepEvent(buildEventId);
+    assertThat(aborted).isNotNull();
+    assertThat(aborted.hasAborted()).isNotNull();
+    assertThat(aborted.getAborted().getReason()).isEqualTo(AbortReason.INTERNAL);
+    assertThat(aborted.getAborted().getDescription()).isEmpty();
+  }
+
+  @Test
+  public void testBuildCatastrophe() {
+    BuildEventId buildEventId = testId("abort_expected");
+    BuildEvent startEvent =
+        new GenericBuildEvent(
+            BuildEventId.buildStartedId(),
+            ImmutableSet.of(
+                buildEventId, ProgressEvent.INITIAL_PROGRESS_UPDATE, BuildEventId.buildFinished()));
+    BuildCompleteEvent buildCompleteEvent =
+        buildCompleteEvent(ExitCode.BUILD_FAILURE, true, null, true);
+
+    streamer.buildEvent(startEvent);
+    streamer.buildEvent(buildCompleteEvent);
+    streamer.close();
+
+    BuildEventStreamProtos.BuildEvent aborted = getBepEvent(buildEventId);
+    assertThat(aborted).isNotNull();
+    assertThat(aborted.hasAborted()).isNotNull();
+    assertThat(aborted.getAborted().getReason()).isEqualTo(AbortReason.INTERNAL);
+    assertThat(aborted.getAborted().getDescription()).isEmpty();
+  }
+
+  @Test
+  public void testStreamAbortedWithTimeout() {
+    BuildEventId buildEventId = testId("abort_expected");
+    BuildEvent startEvent =
+        new GenericBuildEvent(
+            BuildEventId.buildStartedId(),
+            ImmutableSet.of(
+                buildEventId, ProgressEvent.INITIAL_PROGRESS_UPDATE, BuildEventId.buildFinished()));
+
+    streamer.buildEvent(startEvent);
+    streamer.close(AbortReason.TIME_OUT);
+
+    BuildEventStreamProtos.BuildEvent aborted0 = getBepEvent(buildEventId);
+    assertThat(aborted0).isNotNull();
+    assertThat(aborted0.hasAborted()).isNotNull();
+    assertThat(aborted0.getAborted().getReason()).isEqualTo(AbortReason.TIME_OUT);
+    assertThat(aborted0.getAborted().getDescription()).isEmpty();
+
+    BuildEventStreamProtos.BuildEvent aborted1 = getBepEvent(BuildEventId.buildFinished());
+    assertThat(aborted1).isNotNull();
+    assertThat(aborted1.hasAborted()).isNotNull();
+    assertThat(aborted1.getAborted().getReason()).isEqualTo(AbortReason.TIME_OUT);
+    assertThat(aborted1.getAborted().getDescription()).isEmpty();
+  }
+
+  @Test
+  public void testBuildFailureMultipleReasons() {
+    BuildEventId buildEventId = testId("abort_expected");
+    BuildEvent startEvent =
+        new GenericBuildEvent(
+            BuildEventId.buildStartedId(),
+            ImmutableSet.of(
+                buildEventId, ProgressEvent.INITIAL_PROGRESS_UPDATE, BuildEventId.buildFinished()));
+    BuildCompleteEvent buildCompleteEvent =
+        buildCompleteEvent(ExitCode.BUILD_FAILURE, false, new RuntimeException(), false);
+
+    streamer.buildEvent(startEvent);
+    streamer.noAnalyze(new NoAnalyzeEvent());
+    streamer.buildEvent(buildCompleteEvent);
+    streamer.close();
+
+    BuildEventStreamProtos.BuildEvent aborted = getBepEvent(buildEventId);
+    assertThat(aborted).isNotNull();
+    assertThat(aborted.hasAborted()).isNotNull();
+    assertThat(aborted.getAborted().getReason()).isEqualTo(AbortReason.INTERNAL);
+    assertThat(aborted.getAborted().getDescription())
+        .isEqualTo("Multiple abort reasons reported: [NO_ANALYZE, INTERNAL]");
+  }
+
+  @Nullable
+  private BuildEventStreamProtos.BuildEvent getBepEvent(BuildEventId buildEventId) {
+    return transport.getEventProtos().stream()
+        .filter(e -> e.getId().equals(buildEventId.asStreamProto()))
+        .findFirst()
+        .orElse(null);
+  }
+
+  private BuildCompleteEvent buildCompleteEvent(
+      ExitCode exitCode, boolean stopOnFailure, Throwable crash, boolean catastrophe) {
+    BuildResult result = new BuildResult(0);
+    result.setExitCondition(exitCode);
+    result.setStopOnFirstFailure(stopOnFailure);
+    if (catastrophe) {
+      result.setCatastrophe();
+    }
+    if (crash != null) {
+      result.setUnhandledThrowable(crash);
+    }
+    return new BuildCompleteEvent(result);
+  }
 }