blob: b75366a4c464e3f4a09bcfee776cf285a0f216e7 [file] [log] [blame]
// Copyright 2019 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.buildtool.util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.analysis.AnalysisOptions;
import com.google.devtools.build.lib.analysis.AnalysisPhaseCompleteEvent;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.ServerDirectories;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
import com.google.devtools.build.lib.buildeventstream.BuildEventProtocolOptions;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
import com.google.devtools.build.lib.buildtool.BuildResult;
import com.google.devtools.build.lib.buildtool.BuildTool;
import com.google.devtools.build.lib.clock.JavaClock;
import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
import com.google.devtools.build.lib.exec.BinTools;
import com.google.devtools.build.lib.exec.ExecutionOptions;
import com.google.devtools.build.lib.exec.local.LocalExecutionOptions;
import com.google.devtools.build.lib.packages.StarlarkSemanticsOptions;
import com.google.devtools.build.lib.pkgcache.LoadingOptions;
import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.SilentCloseable;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.BlazeCommandResult;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.ClientOptions;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.CommandStartEvent;
import com.google.devtools.build.lib.runtime.CommonCommandOptions;
import com.google.devtools.build.lib.runtime.GotOptionsEvent;
import com.google.devtools.build.lib.runtime.KeepGoingOption;
import com.google.devtools.build.lib.runtime.LoadingPhaseThreadsOption;
import com.google.devtools.build.lib.runtime.UiOptions;
import com.google.devtools.build.lib.runtime.commands.BuildCommand;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
import com.google.devtools.build.lib.sandbox.SandboxOptions;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.vfs.OutputService;
import com.google.devtools.common.options.InvocationPolicyEnforcer;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A wrapper for {@link BlazeRuntime} for testing purposes that makes it possible to exercise
* (most) of the build machinery in integration tests. Note that {@code BlazeCommandDispatcher}
* is not exercised here.
*/
public class BlazeRuntimeWrapper {
private final BlazeRuntime runtime;
private CommandEnvironment env;
private final EventCollectionApparatus events;
private boolean commandCreated;
private BuildRequest lastRequest;
private BuildResult lastResult;
private BuildConfigurationCollection configurations;
private ImmutableSet<ConfiguredTarget> topLevelTargets;
private OptionsParser optionsParser;
private ImmutableList.Builder<String> optionsToParse = new ImmutableList.Builder<>();
private final List<Object> eventBusSubscribers = new ArrayList<>();
public BlazeRuntimeWrapper(
EventCollectionApparatus events, ServerDirectories serverDirectories,
BlazeDirectories directories, BinTools binTools, BlazeRuntime.Builder builder)
throws Exception {
this.events = events;
runtime =
builder
.setServerDirectories(serverDirectories)
.addBlazeModule(
new BlazeModule() {
@Override
public void beforeCommand(CommandEnvironment env) {
// This only does something interesting for tests that create their own
// BlazeCommandDispatcher. :-(
if (BlazeRuntimeWrapper.this.env != env) {
BlazeRuntimeWrapper.this.env = env;
BlazeRuntimeWrapper.this.lastRequest = null;
BlazeRuntimeWrapper.this.lastResult = null;
resetOptions();
env.getEventBus().register(this);
}
}
@Subscribe
public void analysisPhaseComplete(AnalysisPhaseCompleteEvent e) {
topLevelTargets = ImmutableSet.copyOf(e.getTopLevelTargets());
}
})
.addBlazeModule(
new BlazeModule() {
@Override
public void beforeCommand(CommandEnvironment env) {
BlazeRuntimeWrapper.this.events.initExternal(env.getReporter());
}
})
.build();
runtime.initWorkspace(directories, binTools);
optionsParser = createOptionsParser();
}
@Command(name = "build", builds = true, help = "", shortDescription = "")
private static class DummyBuildCommand {}
public OptionsParser createOptionsParser() {
Set<Class<? extends OptionsBase>> options =
new HashSet<>(
ImmutableList.of(
BuildRequestOptions.class,
BuildEventProtocolOptions.class,
ExecutionOptions.class,
LocalExecutionOptions.class,
CommonCommandOptions.class,
ClientOptions.class,
LoadingOptions.class,
AnalysisOptions.class,
KeepGoingOption.class,
LoadingPhaseThreadsOption.class,
PackageCacheOptions.class,
StarlarkSemanticsOptions.class,
UiOptions.class,
SandboxOptions.class));
for (BlazeModule module : runtime.getBlazeModules()) {
Iterables.addAll(options, module.getCommonCommandOptions());
Iterables.addAll(
options, module.getCommandOptions(DummyBuildCommand.class.getAnnotation(Command.class)));
}
options.addAll(runtime.getRuleClassProvider().getConfigurationOptions());
return OptionsParser.builder().optionsClasses(options).build();
}
private void enforceTestInvocationPolicy(OptionsParser parser) {
InvocationPolicyEnforcer optionsPolicyEnforcer =
new InvocationPolicyEnforcer(runtime.getModuleInvocationPolicy());
try {
optionsPolicyEnforcer.enforce(parser);
} catch (OptionsParsingException e) {
throw new IllegalStateException(e);
}
}
public BlazeRuntime getRuntime() {
return runtime;
}
/** Registers the given {@code subscriber} with the {@link EventBus} before each command. */
public void registerSubscriber(Object subscriber) {
eventBusSubscribers.add(subscriber);
}
public final CommandEnvironment newCommand() throws Exception {
return newCommand(BuildCommand.class);
}
/** Creates a new command environment; executeBuild does this automatically if you do not. */
public final CommandEnvironment newCommand(Class<? extends BlazeCommand> buildCommand)
throws Exception {
initializeOptionsParser();
commandCreated = true;
if (env != null) {
runtime.afterCommand(env, BlazeCommandResult.exitCode(ExitCode.SUCCESS));
}
if (optionsParser == null) {
throw new IllegalArgumentException("The options parser must be initialized before creating "
+ "a new command environment");
}
env =
runtime
.getWorkspace()
.initCommand(
buildCommand.getAnnotation(Command.class), optionsParser, new ArrayList<>());
return env;
}
/**
* Returns the command environment. You must call {@link #newCommand()} before calling this
* method.
*/
public CommandEnvironment getCommandEnvironment() {
// In these tests, calls to the CommandEnvironment are not always in correct order; this is OK
// for unit tests. So return an environment here, that has a forced command id to allow tests to
// stay simple.
try {
env.getCommandId();
} catch (IllegalArgumentException e) {
// Ignored, as we know that tests deviate from normal calling order.
}
return env;
}
public SkyframeExecutor getSkyframeExecutor() {
return runtime.getWorkspace().getSkyframeExecutor();
}
public void resetOptions() {
optionsToParse = new ImmutableList.Builder<>();
}
public void addOptions(String... args) {
addOptions(ImmutableList.copyOf(args));
}
public void addOptions(List<String> args) {
optionsToParse.addAll(args);
}
public ImmutableList<String> getOptions() {
return optionsToParse.build();
}
public <O extends OptionsBase> O getOptions(Class<O> optionsClass) {
return optionsParser.getOptions(optionsClass);
}
protected void finalizeBuildResult(@SuppressWarnings("unused") BuildResult request) {}
/**
* Initializes a new options parser, parsing all the options set by {@link
* #addOptions(String...)}.
*/
public void initializeOptionsParser() throws OptionsParsingException {
// Create the options parser and parse all the options collected so far
optionsParser = createOptionsParser();
optionsParser.parse(optionsToParse.build());
// Enforce the test invocation policy once the options have been added
enforceTestInvocationPolicy(optionsParser);
}
public void executeBuild(List<String> targets) throws Exception {
if (!commandCreated) {
// If you didn't create a command we do it for you
newCommand();
}
commandCreated = false;
BuildTool buildTool = new BuildTool(env);
PrintStream origSystemOut = System.out;
PrintStream origSystemErr = System.err;
try {
Profiler.instance()
.start(
ImmutableSet.of(),
/* stream= */ null,
/* format= */ null,
/* outputBase= */ null,
/* buildID= */ null,
/* recordAllDurations= */ false,
new JavaClock(),
/* execStartTimeNanos= */ 42,
/* enabledCpuUsageProfiling= */ false,
/* slimProfile= */ false,
/* enableActionCountProfile= */ false,
/* includePrimaryOutput= */ false);
OutErr outErr = env.getReporter().getOutErr();
System.setOut(new PrintStream(outErr.getOutputStream(), /*autoflush=*/ true));
System.setErr(new PrintStream(outErr.getErrorStream(), /*autoflush=*/ true));
// This cannot go into newCommand, because we hook up the EventCollectionApparatus as a
// module, and after that ran, further changes to the apparatus aren't reflected on the
// reporter.
for (BlazeModule module : getRuntime().getBlazeModules()) {
module.beforeCommand(env);
}
EventBus eventBus = env.getEventBus();
for (Object subscriber : eventBusSubscribers) {
eventBus.register(subscriber);
}
env.getEventBus()
.post(
new GotOptionsEvent(
getRuntime().getStartupOptionsProvider(),
optionsParser,
InvocationPolicy.getDefaultInstance()));
// This roughly mimics what BlazeRuntime#beforeCommand does in practice.
env.throwPendingException();
// In this test we are allowed to omit the beforeCommand; so force setting of a command
// id in the CommandEnvironment, as we will need it in a moment even though we deviate from
// normal calling order.
try {
env.getCommandId();
} catch (IllegalArgumentException e) {
// Ignored, as we know the test deviates from normal calling order.
}
OutputService outputService = null;
BlazeModule outputModule = null;
for (BlazeModule module : runtime.getBlazeModules()) {
OutputService moduleService = module.getOutputService();
if (moduleService != null) {
if (outputService != null) {
throw new IllegalStateException(
String.format(
"More than one module (%s and %s) returns an output service",
module.getClass(), outputModule.getClass()));
}
outputService = moduleService;
outputModule = module;
}
}
getSkyframeExecutor().setOutputService(outputService);
env.setOutputServiceForTesting(outputService);
env.getEventBus()
.post(
new CommandStartEvent(
"build",
env.getCommandId(),
env.getBuildRequestId(),
env.getClientEnv(),
env.getWorkingDirectory(),
env.getDirectories(),
0));
lastRequest = createRequest("build", targets);
lastResult = new BuildResult(lastRequest.getStartTime());
boolean success = false;
for (BlazeModule module : getRuntime().getBlazeModules()) {
env.getSkyframeExecutor().injectExtraPrecomputedValues(module.getPrecomputedValues());
}
try {
try (SilentCloseable c = Profiler.instance().profile("setupPackageCache")) {
env.setupPackageCache(lastRequest);
}
buildTool.buildTargets(lastRequest, lastResult, null);
success = true;
} finally {
env
.getTimestampGranularityMonitor()
.waitForTimestampGranularity(lastRequest.getOutErr());
this.configurations = lastResult.getBuildConfigurationCollection();
finalizeBuildResult(lastResult);
buildTool.stopRequest(
lastResult,
null,
success ? ExitCode.SUCCESS : ExitCode.BUILD_FAILURE,
/*startSuspendCount=*/ 0);
getSkyframeExecutor().notifyCommandComplete(env.getReporter());
}
} finally {
System.setOut(origSystemOut);
System.setErr(origSystemErr);
Profiler.instance().stop();
}
}
public BuildRequest createRequest(String commandName, List<String> targets) {
return BuildRequest.create(
commandName,
optionsParser,
null,
targets,
env.getReporter().getOutErr(),
env.getCommandId(),
runtime.getClock().currentTimeMillis());
}
public BuildRequest getLastRequest() {
return lastRequest;
}
public BuildResult getLastResult() {
return lastResult;
}
public BuildConfigurationCollection getConfigurationCollection() {
return configurations;
}
public ImmutableSet<ConfiguredTarget> getTopLevelTargets() {
return topLevelTargets;
}
}