Move `SpawnController` and `ControllableActionStrategyModule` to open source.

Minor cleanups on `SpawnController` methods: Only take a single `SpawnResult` in `ExecResult#of` since no callers need to set up multiple results. Rename `ExecResult#ofException` to distinguish the override that takes an exception.

PiperOrigin-RevId: 454256692
Change-Id: I89f1957611a6756d153fb945954126ef1687795e
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/BUILD b/src/test/java/com/google/devtools/build/lib/testutil/BUILD
index ba183ca..1109da3 100644
--- a/src/test/java/com/google/devtools/build/lib/testutil/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/testutil/BUILD
@@ -100,6 +100,31 @@
 )
 
 java_library(
+    name = "controllable_action_strategy_module",
+    srcs = ["ControllableActionStrategyModule.java"],
+    deps = [
+        ":spawn_controller",
+        "//src/main/java/com/google/devtools/build/lib:runtime",
+        "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/java/com/google/devtools/build/lib/exec:spawn_strategy_registry",
+        "//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception",
+        "//third_party:guava",
+    ],
+)
+
+java_library(
+    name = "spawn_controller",
+    srcs = ["SpawnController.java"],
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/java/com/google/devtools/build/lib/util:crash_failure_details",
+        "//src/main/protobuf:failure_details_java_proto",
+        "//third_party:guava",
+        "//third_party:jsr305",
+    ],
+)
+
+java_library(
     name = "test_interrupting_bug_reporter",
     srcs = ["TestInterruptingBugReporter.java"],
     deps = [
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/ControllableActionStrategyModule.java b/src/test/java/com/google/devtools/build/lib/testutil/ControllableActionStrategyModule.java
new file mode 100644
index 0000000..aac2e4f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/ControllableActionStrategyModule.java
@@ -0,0 +1,56 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.testutil;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.SpawnStrategy;
+import com.google.devtools.build.lib.exec.SpawnStrategyRegistry;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.util.AbruptExitException;
+
+/**
+ * A {@link BlazeModule} that uses {@link SpawnController} to inject custom behavior.
+ *
+ * <p>The identifiers of strategies to make controllable are passed to the constructor. These
+ * strategies are expected to already exist in the {@link SpawnStrategyRegistry.Builder} when {@link
+ * #registerSpawnStrategies} is called, so the modules responsible for registering them should be
+ * added to the runtime builder <em>before</em> the {@code ControllableActionStrategyModule}. Each
+ * strategy corresponding to a specified identifier is replaced in the {@link
+ * SpawnStrategyRegistry.Builder} with a {@linkplain SpawnController#wrap controllable wrapper}.
+ */
+public final class ControllableActionStrategyModule extends BlazeModule {
+
+  private final SpawnController spawnController;
+  private final ImmutableList<String> identifiers;
+
+  public ControllableActionStrategyModule(SpawnController spawnController, String... identifiers) {
+    checkArgument(identifiers.length > 0, "No identifiers given");
+    this.spawnController = checkNotNull(spawnController);
+    this.identifiers = ImmutableList.copyOf(identifiers);
+  }
+
+  @Override
+  public void registerSpawnStrategies(
+      SpawnStrategyRegistry.Builder registryBuilder, CommandEnvironment env)
+      throws AbruptExitException {
+    for (String identifier : identifiers) {
+      SpawnStrategy delegate = registryBuilder.toStrategy(identifier, getClass().getSimpleName());
+      registryBuilder.registerStrategy(spawnController.wrap(delegate), identifier);
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/SpawnController.java b/src/test/java/com/google/devtools/build/lib/testutil/SpawnController.java
new file mode 100644
index 0000000..dcc2b45
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/SpawnController.java
@@ -0,0 +1,196 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.testutil;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimaps;
+import com.google.devtools.build.lib.actions.ActionContext.ActionContextRegistry;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnResult;
+import com.google.devtools.build.lib.actions.SpawnStrategy;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.util.CrashFailureDetails;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Test utility that allows for controlling the behavior of spawns by using {@link SpawnShim}.
+ *
+ * <p>To install in integration tests, use {@link ControllableActionStrategyModule}.
+ */
+public final class SpawnController {
+
+  /** The means of controlling {@link SpawnStrategy#exec} calls. */
+  public interface SpawnShim {
+    ExecResult getExecResult(Spawn spawn, ActionExecutionContext context)
+        throws IOException, InterruptedException;
+  }
+
+  /**
+   * Represents the desired behavior of a {@link SpawnStrategy#exec} call. Instances represent one
+   * of the following:
+   *
+   * <ul>
+   *   <li>A {@link SpawnResult}, created with {@link #of}.
+   *   <li>An {@link ExecException}, created with {@link #ofException}.
+   *   <li>A delegate to the underlying {@link SpawnStrategy}, created with {@link #delegate}.
+   * </ul>
+   */
+  public static final class ExecResult {
+    private static final ExecResult DELEGATE =
+        new ExecResult(/*execException=*/ null, /*spawnResult=*/ null);
+
+    @Nullable private final ExecException execException;
+    @Nullable private final SpawnResult spawnResult;
+
+    private ExecResult(@Nullable ExecException execException, @Nullable SpawnResult spawnResult) {
+      this.execException = execException;
+      this.spawnResult = spawnResult;
+    }
+
+    /** Override the action by returning the provided {@link SpawnResult}. */
+    public static ExecResult of(SpawnResult spawnResult) {
+      return new ExecResult(/*execException=*/ null, checkNotNull(spawnResult));
+    }
+
+    /** Override the action by throwing the provided {@link ExecException}. */
+    public static ExecResult ofException(ExecException execException) {
+      return new ExecResult(checkNotNull(execException), /*spawnResult=*/ null);
+    }
+
+    /** Do not override the action. Allow the underlying {@link SpawnStrategy} to handle it. */
+    public static ExecResult delegate() {
+      return DELEGATE;
+    }
+  }
+
+  private final List<String> executedSpawnDescriptions =
+      Collections.synchronizedList(new ArrayList<>());
+
+  private final ListMultimap<String, SpawnShim> spawnShims =
+      Multimaps.synchronizedListMultimap(LinkedListMultimap.create());
+
+  /**
+   * Returns a list of all executed spawn descriptions seen by strategies created via {@link #wrap}
+   * (in order) since the last call to {@link #clearExecutedSpawnDescriptions}.
+   */
+  public ImmutableList<String> getExecutedSpawnDescriptions() {
+    return ImmutableList.copyOf(executedSpawnDescriptions);
+  }
+
+  /** Clears the list of executed spawn descriptions. */
+  public void clearExecutedSpawnDescriptions() {
+    executedSpawnDescriptions.clear();
+  }
+
+  /**
+   * Injects custom spawn behavior for {@linkplain #wrap controllable strategies}.
+   *
+   * <p>The given {@link SpawnShim} is enqueued for a single execution of a spawn with the given
+   * description. When a matching spawn is seen, an associated {@link SpawnShim} is dequeued and
+   * used in a FIFO manner. If there are no matching shims enqueued, the delegate strategy is used.
+   */
+  public void addSpawnShim(String spawnDescription, SpawnShim spawnShim) {
+    spawnShims.put(spawnDescription, spawnShim);
+  }
+
+  /**
+   * Creates a new {@link SpawnStrategy} that picks up custom behavior added via {@link
+   * #addSpawnShim} and delegates to the given {@code delegate} if necessary.
+   */
+  SpawnStrategy wrap(SpawnStrategy delegate) {
+    return new ControllableSpawnStrategy(delegate);
+  }
+
+  /**
+   * Checks that all spawn shims added via {@link #addSpawnShim} have been consumed, throwing an
+   * {@link IllegalStateException} if any remain.
+   *
+   * <p>This can be used to verify that shims were configured correctly.
+   */
+  public void verifyAllShimsConsumed() {
+    checkState(spawnShims.isEmpty(), "Remaining spawn shims: %s", spawnShims);
+  }
+
+  private final class ControllableSpawnStrategy implements SpawnStrategy {
+    private final SpawnStrategy delegate;
+
+    ControllableSpawnStrategy(SpawnStrategy delegate) {
+      this.delegate = checkNotNull(delegate);
+    }
+
+    @Override
+    public ImmutableList<SpawnResult> exec(
+        Spawn spawn, ActionExecutionContext actionExecutionContext)
+        throws ExecException, InterruptedException {
+      String description = spawn.getResourceOwner().describe();
+      executedSpawnDescriptions.add(description);
+
+      List<SpawnShim> events = spawnShims.get(description);
+      if (!events.isEmpty()) {
+        ExecResult execResult;
+        try {
+          execResult = events.remove(0).getExecResult(spawn, actionExecutionContext);
+        } catch (IOException e) {
+          throw new SpawnShimException(e, description);
+        }
+        if (execResult.execException != null) {
+          throw execResult.execException;
+        }
+        if (execResult.spawnResult != null) {
+          return ImmutableList.of(execResult.spawnResult);
+        }
+      }
+
+      return delegate.exec(spawn, actionExecutionContext);
+    }
+
+    @Override
+    public boolean canExec(Spawn spawn, ActionContextRegistry actionContextRegistry) {
+      return delegate.canExec(spawn, actionContextRegistry);
+    }
+  }
+
+  private static final class SpawnShimException extends ExecException {
+    private final String description;
+
+    SpawnShimException(IOException e, String description) {
+      super(e);
+      this.description = description;
+    }
+
+    @Override
+    protected String getMessageForActionExecutionException() {
+      return "In a test shim, failed to determine ExecException for action: "
+          + description
+          + ": "
+          + getCause().getMessage();
+    }
+
+    @Override
+    protected FailureDetail getFailureDetail(String message) {
+      return CrashFailureDetails.forThrowable(this);
+    }
+  }
+}