Report completion of a target together with failed actions

Report the completion of all targets together with the root causes on the build
event stream. To do so, have TargetCompleteEvent and ActionExecutedEvent be
instances of BuildEvent; however, ignore an ActionExecutedEvent in the
BuildEventStreamer if the execution was successful.

By this change we get, for the first time, a build event stream that is naturally
closed, i.e., without Aborted events closing up lose ends. Add a test asserting
this property.

--
Change-Id: Ie90dd52ee80deb0fdabdce1da551935522880a1a
Reviewed-on: https://bazel-review.googlesource.com/#/c/6279
MOS_MIGRATED_REVID=137273002
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index 0478519..1d3dcc9 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -304,6 +304,7 @@
     srcs = glob(["buildeventstream/*.java"]),
     deps = [
         "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
+        "//src/main/java/com/google/devtools/build/lib/causes",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//third_party:guava",
         "//third_party:jsr305",
@@ -504,6 +505,7 @@
     ],
     deps = [
         ":base-util",
+        ":buildeventstream",
         ":collect",
         ":concurrent",
         ":events",
@@ -521,6 +523,7 @@
         ":util",
         ":vfs",
         "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
         "//src/main/java/com/google/devtools/build/lib/causes",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/skyframe",
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java
index 10ec19a..889165c 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java
@@ -14,11 +14,20 @@
 
 package com.google.devtools.build.lib.actions;
 
+import com.google.common.collect.ImmutableList;
+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.GenericBuildEvent;
+import com.google.devtools.build.lib.causes.ActionFailed;
+import com.google.devtools.build.lib.causes.Cause;
+import java.util.Collection;
+
 /**
  * This event is fired during the build, when an action is executed. It contains information about
  * the action: the Action itself, and the output file names its stdout and stderr are recorded in.
  */
-public class ActionExecutedEvent {
+public class ActionExecutedEvent implements BuildEvent {
   private final Action action;
   private final ActionExecutionException exception;
   private final String stdout;
@@ -48,4 +57,37 @@
   public String getStderr() {
     return stderr;
   }
+
+  @Override
+  public BuildEventId getEventId() {
+    Cause cause =
+        new ActionFailed(action.getPrimaryOutput().getPath(), action.getOwner().getLabel());
+    return BuildEventId.fromCause(cause);
+  }
+
+  @Override
+  public Collection<BuildEventId> getChildrenEvents() {
+    return ImmutableList.<BuildEventId>of();
+  }
+
+  @Override
+  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+    BuildEventStreamProtos.ActionExecuted.Builder actionBuilder =
+        BuildEventStreamProtos.ActionExecuted.newBuilder().setSuccess(getException() == null);
+    if (exception.getExitCode() != null) {
+      actionBuilder.setExitCode(exception.getExitCode().getNumericExitCode());
+    }
+    if (stdout != null) {
+      actionBuilder.setStdout(
+          BuildEventStreamProtos.File.newBuilder().setName("stdout").setUri(stdout).build());
+    }
+    if (stderr != null) {
+      actionBuilder.setStdout(
+          BuildEventStreamProtos.File.newBuilder().setName("stderr").setUri(stderr).build());
+    }
+    if (action.getOwner() != null) {
+      actionBuilder.setLabel(action.getOwner().getLabel().toString());
+    }
+    return GenericBuildEvent.protoChaining(this).setAction(actionBuilder.build()).build();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BUILD b/src/main/java/com/google/devtools/build/lib/actions/BUILD
index 92e9a6a..ca91797 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/actions/BUILD
@@ -10,6 +10,7 @@
         "cache/*.java",
     ]),
     deps = [
+        "//src/main/java/com/google/devtools/build/lib:buildeventstream",
         "//src/main/java/com/google/devtools/build/lib:collect",
         "//src/main/java/com/google/devtools/build/lib:concurrent",
         "//src/main/java/com/google/devtools/build/lib:events",
@@ -21,6 +22,7 @@
         "//src/main/java/com/google/devtools/build/lib:unix",
         "//src/main/java/com/google/devtools/build/lib:util",
         "//src/main/java/com/google/devtools/build/lib:vfs",
+        "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
         "//src/main/java/com/google/devtools/build/lib/causes",
         "//src/main/java/com/google/devtools/build/skyframe",
         "//src/main/java/com/google/devtools/common/options",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java
index 6fea59f..ae1327d 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java
@@ -14,18 +14,22 @@
 
 package com.google.devtools.build.lib.analysis;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+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.GenericBuildEvent;
 import com.google.devtools.build.lib.causes.Cause;
 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.Order;
 import com.google.devtools.build.lib.util.Preconditions;
 import com.google.devtools.build.skyframe.SkyValue;
+import java.util.Collection;
 
-/**
- * This event is fired as soon as a target is either built or fails.
- */
-public final class TargetCompleteEvent implements SkyValue {
+/** This event is fired as soon as a target is either built or fails. */
+public final class TargetCompleteEvent implements SkyValue, BuildEvent {
 
   private final ConfiguredTarget target;
   private final NestedSet<Cause> rootCauses;
@@ -69,4 +73,25 @@
   public Iterable<Cause> getRootCauses() {
     return rootCauses;
   }
+
+  @Override
+  public BuildEventId getEventId() {
+    return BuildEventId.targetCompleted(getTarget().getLabel());
+  }
+
+  @Override
+  public Collection<BuildEventId> getChildrenEvents() {
+    ImmutableList.Builder childrenBuilder = ImmutableList.builder();
+    for (Cause cause : getRootCauses()) {
+      childrenBuilder.add(BuildEventId.fromCause(cause));
+    }
+    return childrenBuilder.build();
+  }
+
+  @Override
+  public BuildEventStreamProtos.BuildEvent asStreamProto() {
+    BuildEventStreamProtos.TargetComplete complete =
+        BuildEventStreamProtos.TargetComplete.newBuilder().setSuccess(!failed()).build();
+    return GenericBuildEvent.protoChaining(this).setCompleted(complete).build();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildEventId.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildEventId.java
index 8262cd4..efa7e1c 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildEventId.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildEventId.java
@@ -14,6 +14,7 @@
 
 package com.google.devtools.build.lib.buildeventstream;
 
+import com.google.devtools.build.lib.causes.Cause;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.protobuf.TextFormat;
 import java.io.Serializable;
@@ -100,4 +101,8 @@
     return new BuildEventId(
         BuildEventStreamProtos.BuildEventId.newBuilder().setTargetCompleted(targetId).build());
   }
+
+  public static BuildEventId fromCause(Cause cause) {
+    return new BuildEventId(cause.getIdProto());
+  }
 }
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 5d3ca56..c5593f7 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
@@ -61,12 +61,20 @@
     string label = 1;
   }
 
+  // Identifier of an event reporting that an action was completed (not all
+  // actions are reported, only the onces that can be considered important;
+  // this includes all failed actions).
+  message ActionCompletedId {
+    string primary_output = 1;
+  }
+
   oneof id {
     UnknownBuildEventId unknown = 1;
     ProgressId progress = 2;
     BuildStartedId started = 3;
     PatternExpandedId pattern = 4;
     TargetCompletedId target_completed = 5;
+    ActionCompletedId action_completed = 6;
   }
 }
 
@@ -137,6 +145,47 @@
 message PatternExpanded {
 }
 
+message File {
+  // identifier indicating the nature of the file (e.g., "stdout", "stderr")
+  string name = 1;
+
+  oneof file {
+    // A location where the contents of the file can be found.
+    string uri = 2;
+    // The conents of the file, if they are guaranteed to be short.
+    bytes contents = 3;
+  }
+}
+
+// Payload of the event indicating the completion of an action. The main purpose
+// of posting those events is to provide details on the root cause for a target
+// failing; however, consumers of the build-event protocol should not assume
+// that only failed actions are posted.
+message ActionExecuted {
+  bool success = 1;
+
+  // The exit code of the action, if it is available.
+  int32 exit_code = 2;
+
+  // Location where to find the standard output of the action
+  // (e.g., a file path).
+  File stdout = 3;
+
+  // Location where to find the standard error of the action
+  // (e.g., a file path).
+  File stderr = 4;
+
+  // Optionally, the label of the owner of the action, for reference.
+  string label = 5;
+}
+
+// Payload of the event indicating the completion of a target. The target is
+// specified in the id. If the target failed the root causes are provided as
+// children events.
+message TargetComplete {
+  bool success = 1;
+}
+
 // Message describing a build event. Events will have an identifier that
 // is unique within a given build invocation; they also announce follow-up
 // events as children. More details, which are specific to the kind of event
@@ -150,5 +199,7 @@
     Aborted aborted = 4;
     BuildStarted started = 5;
     PatternExpanded expanded = 6;
+    ActionExecuted action = 7;
+    TargetComplete completed = 8;
   };
 }
diff --git a/src/main/java/com/google/devtools/build/lib/causes/ActionFailed.java b/src/main/java/com/google/devtools/build/lib/causes/ActionFailed.java
index 85d47ee..1b7b008 100644
--- a/src/main/java/com/google/devtools/build/lib/causes/ActionFailed.java
+++ b/src/main/java/com/google/devtools/build/lib/causes/ActionFailed.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.causes;
 
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.vfs.Path;
 
@@ -38,4 +39,14 @@
   public Label getLabel() {
     return label;
   }
+
+  @Override
+  public BuildEventStreamProtos.BuildEventId getIdProto() {
+    return BuildEventStreamProtos.BuildEventId.newBuilder()
+        .setActionCompleted(
+            BuildEventStreamProtos.BuildEventId.ActionCompletedId.newBuilder()
+                .setPrimaryOutput(path.toString())
+                .build())
+        .build();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/causes/BUILD b/src/main/java/com/google/devtools/build/lib/causes/BUILD
index 699391c..b03924e 100644
--- a/src/main/java/com/google/devtools/build/lib/causes/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/causes/BUILD
@@ -10,6 +10,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//src/main/java/com/google/devtools/build/lib:vfs",
+        "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
     ],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/causes/Cause.java b/src/main/java/com/google/devtools/build/lib/causes/Cause.java
index 4fb491a..fba24ad 100644
--- a/src/main/java/com/google/devtools/build/lib/causes/Cause.java
+++ b/src/main/java/com/google/devtools/build/lib/causes/Cause.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.causes;
 
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.cmdline.Label;
 
 /**
@@ -22,4 +23,7 @@
 
   /** Return the label associated with the failure. */
   Label getLabel();
+
+  /** Return the event id for the cause in the format of the build event protocol. */
+  BuildEventStreamProtos.BuildEventId getIdProto();
 }
diff --git a/src/main/java/com/google/devtools/build/lib/causes/LabelCause.java b/src/main/java/com/google/devtools/build/lib/causes/LabelCause.java
index 240bbb5..0450f83 100644
--- a/src/main/java/com/google/devtools/build/lib/causes/LabelCause.java
+++ b/src/main/java/com/google/devtools/build/lib/causes/LabelCause.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.causes;
 
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.cmdline.Label;
 
 /** Class describing a {@link Cause} that can uniquely be described by a {@link Label}. */
@@ -32,4 +33,17 @@
   public Label getLabel() {
     return label;
   }
+
+  @Override
+  public BuildEventStreamProtos.BuildEventId getIdProto() {
+    BuildEventStreamProtos.BuildEventId.TargetCompletedId.newBuilder()
+        .setLabel(label.toString())
+        .build();
+    return BuildEventStreamProtos.BuildEventId.newBuilder()
+        .setTargetCompleted(
+            BuildEventStreamProtos.BuildEventId.TargetCompletedId.newBuilder()
+                .setLabel(label.toString())
+                .build())
+        .build();
+  }
 }
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 6329d41..819e2f3 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
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Sets;
 import com.google.common.eventbus.Subscribe;
+import com.google.devtools.build.lib.actions.ActionExecutedEvent;
 import com.google.devtools.build.lib.analysis.NoBuildEvent;
 import com.google.devtools.build.lib.buildeventstream.AbortedEvent;
 import com.google.devtools.build.lib.buildeventstream.BuildEvent;
@@ -146,6 +147,13 @@
 
   @Subscribe
   public void buildEvent(BuildEvent event) {
+    if (event instanceof ActionExecutedEvent) {
+      // We ignore events about action executions if the execution succeeded.
+      if (((ActionExecutedEvent) event).getException() == null) {
+        return;
+      }
+    }
+
     post(event);
   }
 }