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);
+ }
+ }
+}