Refactor TestAttempt event posting code

For flaky tests, Bazel may have cached information about multiple test attempts. In that case, we might want to post all of them on a subsequent cache hit, rather than posting only the passing attempt.

We currently subclass TestResult inside Google, which overrides the new getCachedTestAttempts method.

PiperOrigin-RevId: 196491575
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestResult.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestResult.java
index 37ddad8..a8ef2ac 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestResult.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestResult.java
@@ -15,16 +15,19 @@
 package com.google.devtools.build.lib.analysis.test;
 
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.exec.TestAttempt;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
 import com.google.devtools.build.lib.view.test.TestStatus.TestResultData;
 import java.util.Collection;
+import java.util.List;
 import javax.annotation.Nullable;
 
 /**
@@ -89,6 +92,22 @@
   }
 
   /**
+   * Returns the list of locally cached test attempts. This method must only be called if {@link
+   * #isCached} returns <code>true</code>.
+   */
+  public List<TestAttempt> getCachedTestAttempts() {
+    Preconditions.checkState(isCached());
+    return ImmutableList.of(
+        TestAttempt.fromCachedTestResult(
+            testAction,
+            data,
+            1,
+            getFiles(),
+            BuildEventStreamProtos.TestResult.ExecutionInfo.getDefaultInstance(),
+            /*lastAttempt=*/ true));
+  }
+
+  /**
    * @return Coverage data artifact, if available and null otherwise.
    */
   public Path getCoverageData() {
@@ -106,10 +125,6 @@
     return testAction.getCacheStatusArtifact();
   }
 
-  public BuildEventStreamProtos.TestResult.ExecutionInfo getExecutionInfo() {
-    return BuildEventStreamProtos.TestResult.ExecutionInfo.getDefaultInstance();
-  }
-
   /**
    * Gets the test name in a user-friendly format.
    * Will generally include the target name and shard number, if applicable.
@@ -150,7 +165,8 @@
    * @return Collection of files created by the test, tagged by their name indicating usage (e.g.,
    *     "test.log").
    */
-  public Collection<Pair<String, Path>> getFiles() {
+  private Collection<Pair<String, Path>> getFiles() {
+    // TODO(ulfjack): Cache the set of generated files in the TestResultData.
     return testAction.getTestOutputsMapping(execRoot);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
index c1248a6..91d8297 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
@@ -165,7 +165,7 @@
       actionExecutionContext
           .getEventHandler()
           .post(
-              new TestAttempt(
+              TestAttempt.forExecutedTestResult(
                   action,
                   standaloneTestResult.executionInfo(),
                   attempt,
@@ -242,7 +242,7 @@
     actionExecutionContext
         .getEventHandler()
         .post(
-            new TestAttempt(
+            TestAttempt.forExecutedTestResult(
                 action,
                 result.executionInfo(),
                 attempt,
diff --git a/src/main/java/com/google/devtools/build/lib/exec/TestAttempt.java b/src/main/java/com/google/devtools/build/lib/exec/TestAttempt.java
index a7a4db6..f32336c 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/TestAttempt.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/TestAttempt.java
@@ -15,9 +15,9 @@
 package com.google.devtools.build.lib.exec;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.actions.Artifact;
-import com.google.devtools.build.lib.analysis.test.TestResult;
 import com.google.devtools.build.lib.analysis.test.TestRunnerAction;
 import com.google.devtools.build.lib.buildeventstream.BuildEventConverters;
 import com.google.devtools.build.lib.buildeventstream.BuildEventId;
@@ -34,6 +34,9 @@
 import java.util.List;
 
 /** This event is raised whenever an individual test attempt is completed. */
+// TODO(ulfjack): This class should be in the same package as the TestResult class, and TestSummary
+// should live there, too. It's depended upon by TestRunnerAction / TestActionContext, which
+// suggests that it should live in the analysis.test package.
 public class TestAttempt implements BuildEventWithOrderConstraint {
 
   private final TestRunnerAction testAction;
@@ -54,11 +57,11 @@
    * @param testAction The test that was run.
    * @param attempt The number of the attempt for this action.
    */
-  public TestAttempt(
+  private TestAttempt(
       boolean cachedLocally,
       TestRunnerAction testAction,
       BuildEventStreamProtos.TestResult.ExecutionInfo executionInfo,
-      Integer attempt,
+      int attempt,
       BlazeTestStatus status,
       long startTimeMillis,
       long durationMillis,
@@ -66,72 +69,62 @@
       List<String> testWarnings,
       boolean lastAttempt) {
     this.testAction = testAction;
-    this.executionInfo = executionInfo;
+    this.executionInfo = Preconditions.checkNotNull(executionInfo);
     this.attempt = attempt;
-    this.status = status;
+    this.status = Preconditions.checkNotNull(status);
     this.cachedLocally = cachedLocally;
     this.startTimeMillis = startTimeMillis;
     this.durationMillis = durationMillis;
-    this.files = files;
-    this.testWarnings = testWarnings;
+    this.files = Preconditions.checkNotNull(files);
+    this.testWarnings = Preconditions.checkNotNull(testWarnings);
     this.lastAttempt = lastAttempt;
   }
 
-  public TestAttempt(
-      boolean cachedLocally,
-      TestRunnerAction testAction,
-      Integer attempt,
-      BlazeTestStatus status,
-      long startTimeMillis,
-      long durationMillis,
-      Collection<Pair<String, Path>> files,
-      List<String> testWarnings,
-      boolean lastAttempt) {
-    this(cachedLocally, testAction,
-        BuildEventStreamProtos.TestResult.ExecutionInfo.getDefaultInstance(), attempt, status,
-        startTimeMillis, durationMillis, files, testWarnings, lastAttempt);
-  }
-
-  public TestAttempt(
+  /**
+   * Creates a test attempt result instance for a test that was not locally cached; it may have been
+   * locally executed, remotely executed, or remotely cached.
+   */
+  public static TestAttempt forExecutedTestResult(
       TestRunnerAction testAction,
       BuildEventStreamProtos.TestResult.ExecutionInfo executionInfo,
-      Integer attempt,
+      int attempt,
       BlazeTestStatus status,
       long startTimeMillis,
       long durationMillis,
       Collection<Pair<String, Path>> files,
       List<String> testWarnings,
       boolean lastAttempt) {
-    this(false, testAction, executionInfo, attempt, status, startTimeMillis, durationMillis, files,
-        testWarnings, lastAttempt);
+    return new TestAttempt(
+        false,
+        testAction,
+        executionInfo,
+        attempt,
+        status,
+        startTimeMillis,
+        durationMillis,
+        files,
+        testWarnings,
+        lastAttempt);
   }
 
-  public TestAttempt(
+  public static TestAttempt fromCachedTestResult(
       TestRunnerAction testAction,
-      Integer attempt,
-      BlazeTestStatus status,
-      long startTimeMillis,
-      long durationMillis,
+      TestResultData attemptData,
+      int attempt,
       Collection<Pair<String, Path>> files,
-      List<String> testWarnings,
+      BuildEventStreamProtos.TestResult.ExecutionInfo executionInfo,
       boolean lastAttempt) {
-    this(testAction,  BuildEventStreamProtos.TestResult.ExecutionInfo.getDefaultInstance(), attempt,
-        status, startTimeMillis, durationMillis, files, testWarnings, lastAttempt);
-  }
-
-  public static TestAttempt fromCachedTestResult(TestResult result) {
-    TestResultData data = result.getData();
     return new TestAttempt(
         true,
-        result.getTestAction(),
-        result.getExecutionInfo(),
-        1,
-        data.getStatus(),
-        data.getStartTimeMillisEpoch(),
-        data.getRunDurationMillis(),
-        result.getFiles(),
-        result.getData().getWarningList(),
-        true);
+        testAction,
+        executionInfo,
+        attempt,
+        attemptData.getStatus(),
+        attemptData.getStartTimeMillisEpoch(),
+        attemptData.getRunDurationMillis(),
+        files,
+        attemptData.getWarningList(),
+        lastAttempt);
   }
 
   @VisibleForTesting
@@ -154,6 +147,11 @@
     return status;
   }
 
+  @VisibleForTesting
+  public boolean isCachedLocally() {
+    return cachedLocally;
+  }
+
   @Override
   public BuildEventId getEventId() {
     return BuildEventId.testResult(
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java b/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java
index 8b021c5..9e851aa 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java
@@ -127,10 +127,11 @@
     ConfiguredTargetKey targetLabel =
         ConfiguredTargetKey.of(testOwner.getLabel(), result.getTestAction().getConfiguration());
 
-    // If a test result was cached, then no attempts for that test were actually
-    // executed. Hence report that fact as a cached attempt.
+    // If a test result was cached, then post the cached attempts to the event bus.
     if (result.isCached()) {
-      eventBus.post(TestAttempt.fromCachedTestResult(result));
+      for (TestAttempt attempt : result.getCachedTestAttempts()) {
+        eventBus.post(attempt);
+      }
     }
 
     TestSummary finalTestSummary = null;
@@ -142,7 +143,7 @@
         // This situation is likely to happen if --notest_keep_going is set with multiple targets.
         return;
       }
-     
+
       summary = analyzer.incrementalAnalyze(summary, result);
 
       // If all runs are processed, the target is finished and ready to report.