Allow test strategies to treat exclusive tests as regular parallel tests.

PiperOrigin-RevId: 292337914
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisResult.java b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisResult.java
index 01a490b..c4aedce 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisResult.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisResult.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.actions.ActionGraph;
 import com.google.devtools.build.lib.actions.PackageRoots;
 import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
@@ -167,4 +168,27 @@
   public ImmutableSortedSet<String> getNonSymlinkedDirectoriesUnderExecRoot() {
     return nonSymlinkedDirectoriesUnderExecRoot;
   }
+
+  /**
+   * Returns an equivalent {@link AnalysisResult}, except with exclusive tests treated as parallel
+   * tests.
+   */
+  public AnalysisResult withExclusiveTestsAsParallelTests() {
+    return new AnalysisResult(
+        configurations,
+        targetsToBuild,
+        aspects,
+        targetsToTest,
+        targetsToSkip,
+        error,
+        actionGraph,
+        topLevelArtifactsToOwnerLabels,
+        Sets.union(parallelTests, exclusiveTests).immutableCopy(),
+        /*exclusiveTests=*/ ImmutableSet.of(),
+        topLevelContext,
+        packageRoots,
+        workspaceName,
+        topLevelTargetsWithConfigs,
+        nonSymlinkedDirectoriesUnderExecRoot);
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionContext.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionContext.java
index 3ce4bac..a68ea8c 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionContext.java
@@ -38,6 +38,17 @@
   /** Returns whether test_keep_going is enabled. */
   boolean isTestKeepGoing();
 
+  /**
+   * Returns {@code true} to indicate that exclusive tests should be treated as regular parallel
+   * tests.
+   *
+   * <p>Returning {@code true} may make sense for certain forced remote test execution strategies
+   * where running tests in sequence would be wasteful.
+   */
+  default boolean forceParallelTestExecution() {
+    return false;
+  }
+
   /** Creates a cached test result. */
   TestResult newCachedTestResult(Path execRoot, TestRunnerAction action, TestResultData cached)
       throws IOException;
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
index c667362..d625154 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.analysis.AnalysisResult;
 import com.google.devtools.build.lib.analysis.BuildInfoEvent;
 import com.google.devtools.build.lib.analysis.BuildView;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
 import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.DummyEnvironment;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
@@ -38,6 +39,7 @@
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.OutputFilter;
 import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
 import com.google.devtools.build.lib.pkgcache.LoadingFailedException;
 import com.google.devtools.build.lib.profiler.ProfilePhase;
 import com.google.devtools.build.lib.profiler.Profiler;
@@ -147,6 +149,22 @@
       // execution phase.
       executionTool = new ExecutionTool(env, request);
       if (request.getBuildOptions().performAnalysisPhase) {
+
+        if (!analysisResult.getExclusiveTests().isEmpty()
+            && executionTool.getTestActionContext().forceParallelTestExecution()) {
+          String testStrategy = request.getOptions(ExecutionOptions.class).testStrategy;
+          for (ConfiguredTarget test : analysisResult.getExclusiveTests()) {
+            getReporter()
+                .handle(
+                    Event.warn(
+                        test.getLabel()
+                            + " is tagged exclusive, but --test_strategy="
+                            + testStrategy
+                            + " forces parallel test execution."));
+          }
+          analysisResult = analysisResult.withExclusiveTestsAsParallelTests();
+        }
+
         result.setBuildConfigurationCollection(analysisResult.getConfigurationCollection());
         result.setActualTargets(analysisResult.getTargetsToBuild());
         result.setTestTargets(analysisResult.getTargetsToTest());
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
index fc343e4..ce536e2 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
@@ -202,6 +202,10 @@
     }
   }
 
+  TestActionContext getTestActionContext() {
+    return spawnActionContextMaps.getContext(TestActionContext.class);
+  }
+
   /**
    * Performs the execution phase (phase 3) of the build, in which the Builder is applied to the
    * action graph to bring the targets up to date. (This function will return prior to