Refactor StandaloneTestStrategy
This makes it more similar to other non-oss test strategies. The goal is
to share more of the code in order to then implement async test
execution.
PiperOrigin-RevId: 235713015
diff --git a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestResult.java b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestResult.java
index 58d0575..047f3de 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestResult.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestResult.java
@@ -18,18 +18,22 @@
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
+import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
import com.google.devtools.build.lib.view.test.TestStatus.TestResultData;
import java.util.List;
/** Contains information about the results of test execution. */
@AutoValue
public abstract class StandaloneTestResult {
+ public boolean hasPassed() {
+ return testResultDataBuilder().getStatus() == BlazeTestStatus.PASSED;
+ }
/** Returns the SpawnResults created by the test, if any. */
public abstract List<SpawnResult> spawnResults();
/** Returns the TestResultData for the test. */
- public abstract TestResultData testResultData();
+ public abstract TestResultData.Builder testResultDataBuilder();
public abstract BuildEventStreamProtos.TestResult.ExecutionInfo executionInfo();
@@ -49,7 +53,7 @@
public abstract Builder setSpawnResults(List<SpawnResult> spawnResults);
/** Sets the TestResultData for the test. */
- public abstract Builder setTestResultData(TestResultData testResultData);
+ public abstract Builder setTestResultDataBuilder(TestResultData.Builder testResultDataBuilder);
public abstract Builder setExecutionInfo(
BuildEventStreamProtos.TestResult.ExecutionInfo executionInfo);
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 07bb569..21cd23a 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
@@ -95,6 +95,42 @@
public List<SpawnResult> exec(
TestRunnerAction action, ActionExecutionContext actionExecutionContext)
throws ExecException, InterruptedException {
+ StandaloneTestRunnerSpawn testRunnerSpawn =
+ createStandaloneTestRunnerSpawn(action, actionExecutionContext);
+
+ List<TestResultData> failedAttempts = new ArrayList<>();
+ try {
+ int maxAttempts = getTestAttempts(action);
+ StandaloneTestResult standaloneTestResult = testRunnerSpawn.execute();
+ int attempt;
+ for (attempt = 1; !standaloneTestResult.hasPassed() && attempt < maxAttempts; attempt++) {
+ failedAttempts.add(
+ processFailedTestAttempt(
+ attempt, actionExecutionContext, action, standaloneTestResult));
+ standaloneTestResult = testRunnerSpawn.execute();
+ }
+
+ finalizeTest(action, actionExecutionContext, standaloneTestResult, failedAttempts, attempt);
+
+ // TODO(b/62588075): Should we accumulate SpawnResults across test attempts instead of only
+ // returning the last list?
+ return standaloneTestResult.spawnResults();
+ } catch (IOException e) {
+ // Print the stack trace, otherwise the unexpected I/O error is hard to diagnose.
+ // A stack trace could help with bugs like https://github.com/bazelbuild/bazel/issues/4924
+ StringBuilder sb = new StringBuilder();
+ sb.append("Caught I/O exception: ").append(e.getMessage());
+ for (Object s : e.getStackTrace()) {
+ sb.append("\n\t").append(s);
+ }
+ actionExecutionContext.getEventHandler().handle(Event.error(sb.toString()));
+ throw new EnvironmentalExecException("unexpected I/O exception", e);
+ }
+ }
+
+ private StandaloneTestRunnerSpawn createStandaloneTestRunnerSpawn(
+ TestRunnerAction action, ActionExecutionContext actionExecutionContext)
+ throws ExecException, InterruptedException {
Path execRoot = actionExecutionContext.getExecRoot();
Path coverageDir = execRoot.getRelative(action.getCoverageDirectory());
Path runfilesDir =
@@ -138,76 +174,16 @@
/*tools=*/ ImmutableList.<Artifact>of(),
ImmutableList.copyOf(action.getSpawnOutputs()),
localResourceUsage);
-
- TestResultData.Builder dataBuilder = TestResultData.newBuilder();
-
- try {
- int maxAttempts = getTestAttempts(action);
- StandaloneTestResult standaloneTestResult =
- executeTestAttempt(
- action,
- spawn,
- actionExecutionContext,
- execRoot,
- coverageDir,
- tmpDir,
- workingDirectory);
- int attempt;
- for (attempt = 1;
- standaloneTestResult.testResultData().getStatus() != BlazeTestStatus.PASSED
- && attempt < maxAttempts;
- attempt++) {
- processFailedTestAttempt(
- attempt, actionExecutionContext, action, dataBuilder, standaloneTestResult);
- standaloneTestResult =
- executeTestAttempt(
- action,
- spawn,
- actionExecutionContext,
- execRoot,
- coverageDir,
- tmpDir,
- workingDirectory);
- }
- processLastTestAttempt(attempt, dataBuilder, standaloneTestResult.testResultData());
- ImmutableList<Pair<String, Path>> testOutputs =
- action.getTestOutputsMapping(actionExecutionContext.getPathResolver(), execRoot);
- actionExecutionContext
- .getEventHandler()
- .post(
- TestAttempt.forExecutedTestResult(
- action,
- standaloneTestResult.testResultData(),
- attempt,
- testOutputs,
- standaloneTestResult.executionInfo(),
- true));
- finalizeTest(actionExecutionContext, action, dataBuilder.build());
-
- // TODO(b/62588075): Should we accumulate SpawnResults across test attempts instead of only
- // returning the last list?
- return standaloneTestResult.spawnResults();
- } catch (IOException e) {
- // Print the stack trace, otherwise the unexpected I/O error is hard to diagnose.
- // A stack trace could help with bugs like https://github.com/bazelbuild/bazel/issues/4924
- StringBuilder sb = new StringBuilder();
- sb.append("Caught I/O exception: ").append(e.getMessage());
- for (Object s : e.getStackTrace()) {
- sb.append("\n\t").append(s);
- }
- actionExecutionContext.getEventHandler().handle(Event.error(sb.toString()));
- throw new EnvironmentalExecException("unexpected I/O exception", e);
- }
+ return new StandaloneTestRunnerSpawn(
+ action, actionExecutionContext, spawn, tmpDir, coverageDir, workingDirectory, execRoot);
}
- private void processFailedTestAttempt(
+ private TestResultData processFailedTestAttempt(
int attempt,
ActionExecutionContext actionExecutionContext,
TestRunnerAction action,
- TestResultData.Builder dataBuilder,
StandaloneTestResult result)
throws IOException {
- ImmutableList.Builder<Pair<String, Path>> testOutputsBuilder = new ImmutableList.Builder<>();
// Rename outputs
String namePrefix =
FileSystemUtils.removeExtension(action.getTestLog().getExecPath().getBaseName());
@@ -219,6 +195,7 @@
// Get the normal test output paths, and then update them to use "attempt_N" names, and
// attemptDir, before adding them to the outputs.
+ ImmutableList.Builder<Pair<String, Path>> testOutputsBuilder = new ImmutableList.Builder<>();
ImmutableList<Pair<String, Path>> testOutputs =
action.getTestOutputsMapping(actionExecutionContext.getPathResolver(),
actionExecutionContext.getExecRoot());
@@ -249,11 +226,14 @@
testOutputsBuilder.add(Pair.of(testOutput.getFirst(), destinationPath));
}
- // Add the test log to the output
- TestResultData data = result.testResultData();
+ TestResultData.Builder dataBuilder = result.testResultDataBuilder();
+
+ // We add the test log as a failed log here - we know this attempt failed, and we need to keep
+ // this information around for computing the test summary.
dataBuilder.addFailedLogs(testLog.toString());
- dataBuilder.addTestTimes(data.getTestTimes(0));
- dataBuilder.addAllTestProcessTimes(data.getTestProcessTimesList());
+
+ // Add the test log to the output
+ TestResultData data = dataBuilder.build();
actionExecutionContext
.getEventHandler()
.post(
@@ -265,31 +245,7 @@
result.executionInfo(),
false));
processTestOutput(actionExecutionContext, new TestResult(action, data, false), testLog);
- }
-
- private void processLastTestAttempt(
- int attempt, TestResultData.Builder dataBuilder, TestResultData data) {
- dataBuilder.setHasCoverage(data.getHasCoverage());
- dataBuilder.setRemotelyCached(data.getRemotelyCached());
- dataBuilder.setIsRemoteStrategy(data.getIsRemoteStrategy());
- dataBuilder.setStatus(
- data.getStatus() == BlazeTestStatus.PASSED && attempt > 1
- ? BlazeTestStatus.FLAKY
- : data.getStatus());
- dataBuilder.setTestPassed(data.getTestPassed());
- for (int i = 0; i < data.getFailedLogsCount(); i++) {
- dataBuilder.addFailedLogs(data.getFailedLogs(i));
- }
- if (data.getTestPassed()) {
- dataBuilder.setPassedLog(data.getPassedLog());
- }
- dataBuilder.addTestTimes(data.getTestTimes(0));
- dataBuilder.addAllTestProcessTimes(data.getTestProcessTimesList());
- dataBuilder.setStartTimeMillisEpoch(data.getStartTimeMillisEpoch());
- dataBuilder.setRunDurationMillis(data.getRunDurationMillis());
- if (data.hasTestCase()) {
- dataBuilder.setTestCase(data.getTestCase());
- }
+ return data;
}
private Map<String, String> setupEnvironment(
@@ -322,6 +278,16 @@
Closeable streamed = null;
Path testLogPath = actionExecutionContext.getInputPath(action.getTestLog());
+ // We have two protos to represent test attempts:
+ // 1. com.google.devtools.build.lib.view.test.TestStatus.TestResultData represents both failed
+ // attempts and finished tests. Bazel stores this to disk to persist cached test result
+ // information across server restarts.
+ // 2. com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TestResult
+ // represents only individual attempts (failed or not). Bazel reports this as an event to the
+ // Build Event Protocol, but never saves it to disk.
+ //
+ // The TestResult proto is always constructed from a TestResultData instance, either one that is
+ // created right here, or one that is read back from disk.
TestResultData.Builder builder = TestResultData.newBuilder();
long startTime = actionExecutionContext.getClock().currentTimeMillis();
@@ -420,7 +386,10 @@
return StandaloneTestResult.builder()
.setSpawnResults(spawnResults)
- .setTestResultData(builder.build())
+ // We return the TestResultData.Builder rather than the finished TestResultData instance,
+ // as we may have to rename the output files in case the test needs to be rerun (if it
+ // failed here _and_ is marked flaky _and_ the number of flaky attempts is larger than 1).
+ .setTestResultDataBuilder(builder)
.setExecutionInfo(executionInfo.build())
.build();
} catch (IOException e) {
@@ -546,8 +515,32 @@
}
private final void finalizeTest(
- ActionExecutionContext actionExecutionContext, TestRunnerAction action, TestResultData data)
+ TestRunnerAction action,
+ ActionExecutionContext actionExecutionContext,
+ StandaloneTestResult standaloneTestResult,
+ List<TestResultData> failedAttempts,
+ int attempt)
throws IOException, ExecException {
+ TestResultData.Builder dataBuilder = standaloneTestResult.testResultDataBuilder();
+ for (TestResultData failedAttempt : failedAttempts) {
+ dataBuilder.addAllFailedLogs(failedAttempt.getFailedLogsList());
+ dataBuilder.addTestTimes(failedAttempt.getTestTimes(0));
+ dataBuilder.addAllTestProcessTimes(failedAttempt.getTestProcessTimesList());
+ }
+ ImmutableList<Pair<String, Path>> testOutputs =
+ action.getTestOutputsMapping(
+ actionExecutionContext.getPathResolver(), actionExecutionContext.getExecRoot());
+ TestResultData data = dataBuilder.build();
+ actionExecutionContext
+ .getEventHandler()
+ .post(
+ TestAttempt.forExecutedTestResult(
+ action, data, attempt, testOutputs, standaloneTestResult.executionInfo(), true));
+
+ if (dataBuilder.getStatus() == BlazeTestStatus.PASSED && !failedAttempts.isEmpty()) {
+ dataBuilder.setStatus(BlazeTestStatus.FLAKY);
+ }
+ data = dataBuilder.build();
TestResult result = new TestResult(action, data, false);
postTestResult(actionExecutionContext, result);
@@ -569,4 +562,42 @@
Path execRoot, TestRunnerAction action, TestResultData data) {
return new TestResult(action, data, /*cached*/ true, execRoot);
}
+
+ private final class StandaloneTestRunnerSpawn {
+ private final TestRunnerAction testAction;
+ private final ActionExecutionContext actionExecutionContext;
+ private final Spawn spawn;
+ private final Path tmpDir;
+ private final Path coverageDir;
+ private final Path workingDirectory;
+ private final Path execRoot;
+
+ StandaloneTestRunnerSpawn(
+ TestRunnerAction testAction,
+ ActionExecutionContext actionExecutionContext,
+ Spawn spawn,
+ Path tmpDir,
+ Path coverageDir,
+ Path workingDirectory,
+ Path execRoot) {
+ this.testAction = testAction;
+ this.actionExecutionContext = actionExecutionContext;
+ this.spawn = spawn;
+ this.tmpDir = tmpDir;
+ this.coverageDir = coverageDir;
+ this.workingDirectory = workingDirectory;
+ this.execRoot = execRoot;
+ }
+
+ StandaloneTestResult execute() throws InterruptedException, IOException, ExecException {
+ return executeTestAttempt(
+ testAction,
+ spawn,
+ actionExecutionContext,
+ execRoot,
+ coverageDir,
+ tmpDir,
+ workingDirectory);
+ }
+ }
}