Add a "canExec" method to SpawnActionContext and SpawnRunner.

This is used by the ProxySpawnActionContext to determine at runtime
whether a spawn strategy can execute a given spawn.

Adds the flag --incompatible_list_based_execution_strategy_selection,
which is used to make this an opt-in change that will be managed by the
incompatible flags process.

The flag's GitHub issue is here:
https://github.com/bazelbuild/bazel/issues/7480

RELNOTES[INC]: The flag --incompatible_list_based_execution_strategy_selection
was added and is used to ease the migration to the new style of specifying
execution strategy selection and fallback behavior. The documentation for
this flag is here: https://github.com/bazelbuild/bazel/issues/7480

PiperOrigin-RevId: 234877574
diff --git a/src/main/java/com/google/devtools/build/lib/actions/SpawnActionContext.java b/src/main/java/com/google/devtools/build/lib/actions/SpawnActionContext.java
index 421d8e4..a932a7a 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/SpawnActionContext.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/SpawnActionContext.java
@@ -35,4 +35,7 @@
     SpawnResult result = Iterables.getOnlyElement(exec(spawn, actionExecutionContext));
     return FutureSpawn.immediate(result);
   }
+
+  /** Returns whether this SpawnActionContext supports executing the given Spawn. */
+  boolean canExec(Spawn spawn);
 }
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 40b8887..05d8cb8 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
@@ -163,9 +163,14 @@
     // independently from each other, for example, to run genrules locally and Java compile action
     // in prod. Thus, for SpawnActions, we decide the action context to use not only based on the
     // context class, but also the mnemonic of the action.
+    ExecutionOptions options = request.getOptions(ExecutionOptions.class);
     spawnActionContextMaps =
-        builder.getSpawnActionContextMapsBuilder().build(
-            actionContextProviders, request.getOptions(ExecutionOptions.class).testStrategy);
+        builder
+            .getSpawnActionContextMapsBuilder()
+            .build(
+                actionContextProviders,
+                options.testStrategy,
+                options.incompatibleListBasedExecutionStrategySelection);
   }
 
   Executor getExecutor() throws ExecutorInitException {
diff --git a/src/main/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategy.java
index 1d6913d0..38802b3 100644
--- a/src/main/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategy.java
@@ -316,6 +316,13 @@
     return dynamicExecutionResult.spawnResults();
   }
 
+  @Override
+  public boolean canExec(Spawn spawn) {
+    return remoteStrategy.canExec(spawn)
+        || workerStrategy.canExec(spawn)
+        || localStrategy.canExec(spawn);
+  }
+
   private void moveFileOutErr(ActionExecutionContext actionExecutionContext, FileOutErr outErr)
       throws IOException {
     if (outErr.getOutputPath().exists()) {
diff --git a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
index e95413b..9759999 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
@@ -77,6 +77,11 @@
   }
 
   @Override
+  public boolean canExec(Spawn spawn) {
+    return spawnRunner.canExec(spawn);
+  }
+
+  @Override
   public List<SpawnResult> exec(
       Spawn spawn,
       ActionExecutionContext actionExecutionContext,
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
index 88e36d5..e3099fc 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
@@ -26,10 +26,10 @@
 import com.google.devtools.common.options.BoolOrEnumConverter;
 import com.google.devtools.common.options.Converters.AssignmentToListOfValuesConverter;
 import com.google.devtools.common.options.Converters.CommaSeparatedNonEmptyOptionListConverter;
-import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionDocumentationCategory;
 import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionMetadataTag;
 import com.google.devtools.common.options.Options;
 import com.google.devtools.common.options.OptionsBase;
 import com.google.devtools.common.options.OptionsParsingException;
@@ -58,6 +58,18 @@
   public static final ExecutionOptions DEFAULTS = Options.getDefaults(ExecutionOptions.class);
 
   @Option(
+      name = "incompatible_list_based_execution_strategy_selection",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
+      effectTags = {OptionEffectTag.EXECUTION},
+      metadataTags = {
+        OptionMetadataTag.INCOMPATIBLE_CHANGE,
+        OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
+      },
+      help = "See https://github.com/bazelbuild/bazel/issues/7480")
+  public boolean incompatibleListBasedExecutionStrategySelection;
+
+  @Option(
       name = "spawn_strategy",
       defaultValue = "",
       converter = CommaSeparatedNonEmptyOptionListConverter.class,
@@ -73,7 +85,7 @@
   @Option(
       name = "genrule_strategy",
       defaultValue = "",
-      converter = CommaSeparatedOptionListConverter.class,
+      converter = CommaSeparatedNonEmptyOptionListConverter.class,
       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
       effectTags = {OptionEffectTag.UNKNOWN},
       help =
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ProxySpawnActionContext.java b/src/main/java/com/google/devtools/build/lib/exec/ProxySpawnActionContext.java
index 7464e6e..f337b12 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/ProxySpawnActionContext.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/ProxySpawnActionContext.java
@@ -22,21 +22,27 @@
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.NullEventHandler;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /** Proxy that looks up the right SpawnActionContext for a spawn during {@link #exec}. */
 public final class ProxySpawnActionContext implements SpawnActionContext {
 
   private final SpawnActionContextMaps spawnActionContextMaps;
+  private final boolean listBasedExecutionStrategySelection;
 
   /**
    * Creates a new {@link ProxySpawnActionContext}.
    *
    * @param spawnActionContextMaps The {@link SpawnActionContextMaps} to use to decide which {@link
    *     SpawnActionContext} should execute a given {@link Spawn} during {@link #exec}.
+   * @param listBasedExecutionStrategySelection
    */
-  public ProxySpawnActionContext(SpawnActionContextMaps spawnActionContextMaps) {
+  public ProxySpawnActionContext(
+      SpawnActionContextMaps spawnActionContextMaps, boolean listBasedExecutionStrategySelection) {
     this.spawnActionContextMaps = spawnActionContextMaps;
+    this.listBasedExecutionStrategySelection = listBasedExecutionStrategySelection;
   }
 
   @Override
@@ -44,8 +50,9 @@
       throws ExecException, InterruptedException {
     List<SpawnActionContext> strategies = resolve(spawn, actionExecutionContext.getEventHandler());
 
-    // For now, we only support executing with the first strategy in the list. Later versions of
-    // this code will add some smartness to pick the best out of the list.
+    // Because the strategies are ordered by preference, we can execute the spawn with the best
+    // possible one by simply filtering out the ones that can't execute it and then picking the
+    // first one from the remaining strategies in the list.
     return strategies.get(0).exec(spawn, actionExecutionContext);
   }
 
@@ -54,8 +61,9 @@
       throws ExecException, InterruptedException {
     List<SpawnActionContext> strategies = resolve(spawn, actionExecutionContext.getEventHandler());
 
-    // For now, we only support executing with the first strategy in the list. Later versions of
-    // this code will add some smartness to pick the best out of the list.
+    // Because the strategies are ordered by preference, we can execute the spawn with the best
+    // possible one by simply filtering out the ones that can't execute it and then picking the
+    // first one from the remaining strategies in the list.
     return strategies.get(0).execMaybeAsync(spawn, actionExecutionContext);
   }
 
@@ -72,6 +80,13 @@
     List<SpawnActionContext> strategies =
         spawnActionContextMaps.getSpawnActionContexts(spawn, eventHandler);
 
+    if (listBasedExecutionStrategySelection) {
+      strategies =
+          strategies.stream()
+              .filter(spawnActionContext -> spawnActionContext.canExec(spawn))
+              .collect(Collectors.toList());
+    }
+
     if (strategies.isEmpty()) {
       throw new UserExecException(
           String.format(
@@ -82,4 +97,10 @@
 
     return strategies;
   }
+
+  @Override
+  public boolean canExec(Spawn spawn) {
+    return spawnActionContextMaps.getSpawnActionContexts(spawn, NullEventHandler.INSTANCE).stream()
+        .anyMatch(spawnActionContext -> spawnActionContext.canExec(spawn));
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/RegexFilterAssignmentConverter.java b/src/main/java/com/google/devtools/build/lib/exec/RegexFilterAssignmentConverter.java
index 96610bc..2824104 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/RegexFilterAssignmentConverter.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/RegexFilterAssignmentConverter.java
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.exec;
 
 import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.util.RegexFilter;
 import com.google.devtools.build.lib.util.RegexFilter.RegexFilterConverter;
@@ -36,6 +37,16 @@
           "Must be in the form of a 'regex=value[,value]' assignment");
     }
     List<String> value = splitter.splitToList(input.substring(pos + 1));
+    if (value.contains("")) {
+      // If the list contains exactly the empty string, it means an empty value was passed and we
+      // should instead return an empty list.
+      if (value.size() == 1) {
+        value = ImmutableList.of();
+      } else {
+        throw new OptionsParsingException(
+            "Values must not contain empty strings or leading / trailing commas");
+      }
+    }
     RegexFilter filter = new RegexFilterConverter().convert(input.substring(0, pos));
     return Maps.immutableEntry(filter, value);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnActionContextMaps.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnActionContextMaps.java
index 2d7918c..b0fd288 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SpawnActionContextMaps.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnActionContextMaps.java
@@ -73,14 +73,17 @@
   private final ImmutableSortedMap<String, List<SpawnActionContext>> mnemonicToSpawnStrategiesMap;
   private final ImmutableList<ActionContext> strategies;
   private final ImmutableList<RegexFilterSpawnActionContext> spawnStrategyRegexList;
+  private final boolean listBasedExecutionStrategySelection;
 
   private SpawnActionContextMaps(
       ImmutableSortedMap<String, List<SpawnActionContext>> mnemonicToSpawnStrategiesMap,
       ImmutableList<ActionContext> strategies,
-      ImmutableList<RegexFilterSpawnActionContext> spawnStrategyRegexList) {
+      ImmutableList<RegexFilterSpawnActionContext> spawnStrategyRegexList,
+      boolean listBasedExecutionStrategySelection) {
     this.mnemonicToSpawnStrategiesMap = mnemonicToSpawnStrategiesMap;
     this.strategies = strategies;
     this.spawnStrategyRegexList = spawnStrategyRegexList;
+    this.listBasedExecutionStrategySelection = listBasedExecutionStrategySelection;
   }
 
   /**
@@ -121,7 +124,9 @@
       }
       contextMap.put(context.getClass(), context);
     }
-    contextMap.put(SpawnActionContext.class, new ProxySpawnActionContext(this));
+    contextMap.put(
+        SpawnActionContext.class,
+        new ProxySpawnActionContext(this, listBasedExecutionStrategySelection));
     return ImmutableMap.copyOf(contextMap);
   }
 
@@ -185,7 +190,8 @@
     return new SpawnActionContextMaps(
         ImmutableSortedMap.copyOf(spawnStrategyMnemonicMap, String.CASE_INSENSITIVE_ORDER),
         ImmutableList.copyOf(strategies),
-        ImmutableList.of());
+        ImmutableList.of(),
+        false);
   }
 
   /** A stored entry for a {@link RegexFilter} to {@code strategy} mapping. */
@@ -238,7 +244,9 @@
 
     /** Builds a {@link SpawnActionContextMaps} instance. */
     public SpawnActionContextMaps build(
-        ImmutableList<ActionContextProvider> actionContextProviders, String testStrategyValue)
+        ImmutableList<ActionContextProvider> actionContextProviders,
+        String testStrategyValue,
+        boolean listBasedExecutionStrategySelection)
         throws ExecutorInitException {
       StrategyConverter strategyConverter = new StrategyConverter(actionContextProviders);
 
@@ -250,7 +258,19 @@
 
       for (String mnemonic : strategyByMnemonicMap.keySet()) {
         ImmutableList.Builder<SpawnActionContext> contexts = ImmutableList.builder();
-        for (String strategy : strategyByMnemonicMap.get(mnemonic)) {
+        Set<String> strategiesForMnemonic = strategyByMnemonicMap.get(mnemonic);
+        if (strategiesForMnemonic.size() > 1 && !listBasedExecutionStrategySelection) {
+          String flagName =
+              mnemonic.isEmpty() ? "--spawn_strategy=" : ("--strategy=" + mnemonic + "=");
+          throw new ExecutorInitException(
+              "Flag "
+                  + flagName
+                  + Joiner.on(',').join(strategiesForMnemonic)
+                  + " contains a list of strategies to use, but "
+                  + "--incompatible_list_based_execution_strategy_selection was not enabled.",
+              ExitCode.COMMAND_LINE_ERROR);
+        }
+        for (String strategy : strategiesForMnemonic) {
           SpawnActionContext context =
               strategyConverter.getStrategy(SpawnActionContext.class, strategy);
           if (context == null) {
@@ -284,7 +304,18 @@
 
       for (RegexFilterStrategy entry : strategyByRegexpBuilder.build()) {
         ImmutableList.Builder<SpawnActionContext> contexts = ImmutableList.builder();
-        for (String strategy : entry.strategy()) {
+        List<String> strategiesForRegex = entry.strategy();
+        if (strategiesForRegex.size() > 1 && !listBasedExecutionStrategySelection) {
+          throw new ExecutorInitException(
+              "Flag --strategy_regexp="
+                  + entry.regexFilter().toString()
+                  + "="
+                  + Joiner.on(',').join(strategiesForRegex)
+                  + " contains a list of strategies to use, but "
+                  + "--incompatible_list_based_execution_strategy_selection was not enabled.",
+              ExitCode.COMMAND_LINE_ERROR);
+        }
+        for (String strategy : strategiesForRegex) {
           SpawnActionContext context =
               strategyConverter.getStrategy(SpawnActionContext.class, strategy);
           if (context == null) {
@@ -310,7 +341,10 @@
       strategies.add(context);
 
       return new SpawnActionContextMaps(
-          spawnStrategyMap.build(), strategies.build(), spawnStrategyRegexList.build());
+          spawnStrategyMap.build(),
+          strategies.build(),
+          spawnStrategyRegexList.build(),
+          listBasedExecutionStrategySelection);
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java
index 0d70563..131915e 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java
@@ -232,6 +232,9 @@
   SpawnResult exec(Spawn spawn, SpawnExecutionContext context)
       throws InterruptedException, IOException, ExecException;
 
+  /** Returns whether this SpawnRunner supports executing the given Spawn. */
+  boolean canExec(Spawn spawn);
+
   /* Name of the SpawnRunner. */
   String getName();
 }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
index 80770b2..04f975d 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
@@ -154,6 +154,11 @@
     }
   }
 
+  @Override
+  public boolean canExec(Spawn spawn) {
+    return true;
+  }
+
   protected Path createActionTemp(Path execRoot) throws IOException {
     return execRoot.getRelative(
         java.nio.file.Files.createTempDirectory(
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
index 85d7f08..55d8c58 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
@@ -279,6 +279,11 @@
     }
   }
 
+  @Override
+  public boolean canExec(Spawn spawn) {
+    return Spawns.mayBeExecutedRemotely(spawn);
+  }
+
   private void maybeWriteParamFilesLocally(Spawn spawn) throws IOException {
     if (!executionOptions.materializeParamFiles) {
       return;
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
index 19dc642..6bccf16 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.actions.SpawnResult.Status;
+import com.google.devtools.build.lib.actions.Spawns;
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.exec.BinTools;
 import com.google.devtools.build.lib.exec.ExecutionOptions;
@@ -78,6 +79,11 @@
     }
   }
 
+  @Override
+  public boolean canExec(Spawn spawn) {
+    return Spawns.mayBeSandboxed(spawn);
+  }
+
   // TODO(laszlocsomor): refactor this class to make `actuallyExec`'s contract clearer: the caller
   // of `actuallyExec` should not depend on `actuallyExec` calling `runSpawn` because it's easy to
   // forget to do so in `actuallyExec`'s implementations.
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
index 95ca264..7f48615 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
@@ -26,7 +26,6 @@
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnActionContext;
 import com.google.devtools.build.lib.actions.SpawnResult;
-import com.google.devtools.build.lib.actions.Spawns;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
 import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
 import com.google.devtools.build.lib.buildtool.buildevent.BuildInterruptedEvent;
@@ -307,12 +306,17 @@
     @Override
     public SpawnResult exec(Spawn spawn, SpawnExecutionContext context)
         throws InterruptedException, IOException, ExecException {
-      if (!Spawns.mayBeSandboxed(spawn)) {
-        return fallbackSpawnRunner.exec(spawn, context);
-      } else {
+      if (sandboxSpawnRunner.canExec(spawn)) {
         return sandboxSpawnRunner.exec(spawn, context);
+      } else {
+        return fallbackSpawnRunner.exec(spawn, context);
       }
     }
+
+    @Override
+    public boolean canExec(Spawn spawn) {
+      return sandboxSpawnRunner.canExec(spawn) || fallbackSpawnRunner.canExec(spawn);
+    }
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java
index 1c8a345..c3ed426 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java
@@ -121,6 +121,11 @@
     return actuallyExec(spawn, context);
   }
 
+  @Override
+  public boolean canExec(Spawn spawn) {
+    return Spawns.supportsWorkers(spawn);
+  }
+
   private SpawnResult actuallyExec(Spawn spawn, SpawnExecutionContext context)
       throws ExecException, IOException, InterruptedException {
     if (Iterables.isEmpty(spawn.getToolFiles())) {
diff --git a/src/main/java/com/google/devtools/common/options/Converters.java b/src/main/java/com/google/devtools/common/options/Converters.java
index a691bd4..6874c7e 100644
--- a/src/main/java/com/google/devtools/common/options/Converters.java
+++ b/src/main/java/com/google/devtools/common/options/Converters.java
@@ -268,6 +268,12 @@
       List<String> result =
           input.isEmpty() ? ImmutableList.of() : ImmutableList.copyOf(splitter.split(input));
       if (!allowEmptyValues && result.contains("")) {
+        // If the list contains exactly the empty string, it means an empty value was passed and we
+        // should instead return an empty list.
+        if (result.size() == 1) {
+          return ImmutableList.of();
+        }
+
         throw new OptionsParsingException(
             "Empty values are not allowed as part of this " + getTypeDescription());
       }
diff --git a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
index 1f3454b..cd24422 100644
--- a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
@@ -102,6 +102,11 @@
     }
 
     @Override
+    public boolean canExec(Spawn spawn) {
+      return true;
+    }
+
+    @Override
     public List<SpawnResult> exec(
         Spawn spawn,
         ActionExecutionContext actionExecutionContext,
diff --git a/src/test/java/com/google/devtools/build/lib/exec/SpawnActionContextMapsTest.java b/src/test/java/com/google/devtools/build/lib/exec/SpawnActionContextMapsTest.java
index a676256..d757799 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/SpawnActionContextMapsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/SpawnActionContextMapsTest.java
@@ -74,7 +74,7 @@
   public void duplicateMnemonics_bothGetStored() throws Exception {
     builder.strategyByMnemonicMap().put("Spawn1", "ac1");
     builder.strategyByMnemonicMap().put("Spawn1", "ac2");
-    SpawnActionContextMaps maps = builder.build(PROVIDERS, "actest");
+    SpawnActionContextMaps maps = builder.build(PROVIDERS, "actest", true);
     List<SpawnActionContext> result =
         maps.getSpawnActionContexts(mockSpawn("Spawn1", null), reporter);
     assertThat(result).containsExactly(ac1, ac2);
@@ -84,7 +84,7 @@
   public void emptyStrategyFallsBackToEmptyMnemonicNotToDefault() throws Exception {
     builder.strategyByMnemonicMap().put("Spawn1", "");
     builder.strategyByMnemonicMap().put("", "ac2");
-    SpawnActionContextMaps maps = builder.build(PROVIDERS, "actest");
+    SpawnActionContextMaps maps = builder.build(PROVIDERS, "actest", false);
     List<SpawnActionContext> result =
         maps.getSpawnActionContexts(mockSpawn("Spawn1", null), reporter);
     assertThat(result).containsExactly(ac2);
@@ -94,7 +94,7 @@
   public void multipleRegexps_firstMatchWins() throws Exception {
     builder.addStrategyByRegexp(converter.convert("foo"), ImmutableList.of("ac1"));
     builder.addStrategyByRegexp(converter.convert("foo/bar"), ImmutableList.of("ac2"));
-    SpawnActionContextMaps maps = builder.build(PROVIDERS, "actest");
+    SpawnActionContextMaps maps = builder.build(PROVIDERS, "actest", false);
 
     List<SpawnActionContext> result =
         maps.getSpawnActionContexts(mockSpawn(null, "Doing something with foo/bar/baz"), reporter);
@@ -106,7 +106,7 @@
   public void regexpAndMnemonic_regexpWins() throws Exception {
     builder.strategyByMnemonicMap().put("Spawn1", "ac1");
     builder.addStrategyByRegexp(converter.convert("foo/bar"), ImmutableList.of("ac2"));
-    SpawnActionContextMaps maps = builder.build(PROVIDERS, "actest");
+    SpawnActionContextMaps maps = builder.build(PROVIDERS, "actest", false);
 
     List<SpawnActionContext> result =
         maps.getSpawnActionContexts(
@@ -138,6 +138,11 @@
         throws ExecException, InterruptedException {
       throw new UnsupportedOperationException();
     }
+
+    @Override
+    public boolean canExec(Spawn spawn) {
+      return true;
+    }
   }
 
   @ExecutionStrategy(contextType = SpawnActionContext.class, name = "ac2")
@@ -147,6 +152,11 @@
         throws ExecException, InterruptedException {
       throw new UnsupportedOperationException();
     }
+
+    @Override
+    public boolean canExec(Spawn spawn) {
+      return true;
+    }
   }
 
   @ExecutionStrategy(contextType = TestActionContext.class, name = "actest")
diff --git a/src/test/shell/integration/execution_strategies_test.sh b/src/test/shell/integration/execution_strategies_test.sh
index 934c098..6cf70ad 100755
--- a/src/test/shell/integration/execution_strategies_test.sh
+++ b/src/test/shell/integration/execution_strategies_test.sh
@@ -44,27 +44,37 @@
 source "$(rlocation "io_bazel/src/test/shell/integration_test_setup.sh")" \
   || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
 
+# Tests that you have to opt-in to list based strategy selection via an incompatible flag.
+function test_incompatible_flag_required() {
+  bazel build --spawn_strategy=worker,local --debug_print_action_contexts &> $TEST_log || true
+  expect_log "incompatible_list_based_execution_strategy_selection was not enabled"
+}
+
 # Tests that you can set the spawn strategy flags to a list of strategies.
 function test_multiple_strategies() {
-  bazel build --spawn_strategy=worker,local --debug_print_action_contexts &> $TEST_log || fail
+  bazel build --incompatible_list_based_execution_strategy_selection \
+      --spawn_strategy=worker,local --debug_print_action_contexts &> $TEST_log || fail
   # Can't test for exact strategy names here, because they differ between platforms and products.
   expect_log "\"\" = \[.*, .*\]"
 }
 
 # Tests that Bazel catches an invalid strategy list that has an empty string as an element.
 function test_empty_strategy_in_list_is_forbidden() {
-  bazel build --spawn_strategy=worker,,local --debug_print_action_contexts &> $TEST_log || true
+  bazel build --incompatible_list_based_execution_strategy_selection \
+      --spawn_strategy=worker,,local --debug_print_action_contexts &> $TEST_log || true
   expect_log "--spawn_strategy=worker,,local: Empty values are not allowed as part of this comma-separated list of options"
 }
 
 # Test that when you set a strategy to the empty string, it gets removed from the map of strategies
 # and thus results in the default strategy being used (the one set via --spawn_strategy=).
 function test_empty_strategy_means_default() {
-  bazel build --spawn_strategy=worker,local --strategy=FooBar=local \
+  bazel build --incompatible_list_based_execution_strategy_selection \
+      --spawn_strategy=worker,local --strategy=FooBar=local \
       --debug_print_action_contexts &> $TEST_log || fail
   expect_log "\"FooBar\" = "
 
-  bazel build --spawn_strategy=worker,local --strategy=FooBar=local --strategy=FooBar= \
+  bazel build --incompatible_list_based_execution_strategy_selection \
+      --spawn_strategy=worker,local --strategy=FooBar=local --strategy=FooBar= \
       --debug_print_action_contexts &> $TEST_log || fail
   expect_not_log "\"FooBar\" = "
 }