blob: dcc2b45988c80b77d29d324caaadd23f0698302a [file] [log] [blame]
// 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);
}
}
}