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