Open-source BlazeRuntimeWrapper and BuildIntegrationTestCase.

These classes are used by some tests that should be open-sourced.

PiperOrigin-RevId: 251217041
diff --git a/src/BUILD b/src/BUILD
index e3f5d64..68961d0 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -435,6 +435,7 @@
         "//src/main/cpp:srcs",
         "//src/main/java/com/google/devtools/build/docgen:srcs",
         "//src/main/java/com/google/devtools/build/lib:srcs",
+        "//src/main/java/com/google/devtools/build/lib/includescanning:srcs",
         "//src/main/java/com/google/devtools/build/lib/network:srcs",
         "//src/main/java/com/google/devtools/build/skydoc:srcs",
         "//src/main/java/com/google/devtools/build/skyframe:srcs",
diff --git a/src/main/java/com/google/devtools/build/lib/includescanning/BUILD b/src/main/java/com/google/devtools/build/lib/includescanning/BUILD
new file mode 100644
index 0000000..0ee8f14
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/includescanning/BUILD
@@ -0,0 +1,37 @@
+package(
+    default_visibility = ["//src:__subpackages__"],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["*"]),
+)
+
+java_library(
+    name = "includescanning",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib:build-base",
+        "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/main/java/com/google/devtools/build/lib:io",
+        "//src/main/java/com/google/devtools/build/lib:packages-internal",
+        "//src/main/java/com/google/devtools/build/lib:resource-converter",
+        "//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/actions:localhost_capacity",
+        "//src/main/java/com/google/devtools/build/lib/analysis/platform",
+        "//src/main/java/com/google/devtools/build/lib/cmdline",
+        "//src/main/java/com/google/devtools/build/lib/concurrent",
+        "//src/main/java/com/google/devtools/build/lib/profiler",
+        "//src/main/java/com/google/devtools/build/lib/rules/cpp",
+        "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
+        "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//src/main/java/com/google/devtools/build/lib/vfs:output_service",
+        "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/java/com/google/devtools/build/skyframe",
+        "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
+        "//src/main/java/com/google/devtools/common/options",
+        "//third_party:guava",
+        "//third_party:jsr305",
+    ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
index 15976e8..5d38918 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
@@ -15,9 +15,44 @@
     srcs = glob(["util/*.java"]),
     deps = [
         "//src/main/java/com/google/devtools/build/lib:bazel-main",
+        "//src/main/java/com/google/devtools/build/lib:build-base",
+        "//src/main/java/com/google/devtools/build/lib:build-request-options",
+        "//src/main/java/com/google/devtools/build/lib:command-utils",
+        "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/main/java/com/google/devtools/build/lib:exitcode-external",
+        "//src/main/java/com/google/devtools/build/lib:io",
+        "//src/main/java/com/google/devtools/build/lib:keep-going-option",
+        "//src/main/java/com/google/devtools/build/lib:loading-phase-threads-option",
+        "//src/main/java/com/google/devtools/build/lib:logging-util",
+        "//src/main/java/com/google/devtools/build/lib:out-err",
+        "//src/main/java/com/google/devtools/build/lib:packages-internal",
         "//src/main/java/com/google/devtools/build/lib:runtime",
+        "//src/main/java/com/google/devtools/build/lib:util",
+        "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/java/com/google/devtools/build/lib/cmdline",
+        "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
+        "//src/main/java/com/google/devtools/build/lib/exec/local:options",
+        "//src/main/java/com/google/devtools/build/lib/includescanning",
+        "//src/main/java/com/google/devtools/build/lib/network:connectivity",
+        "//src/main/java/com/google/devtools/build/lib/sandbox",
+        "//src/main/java/com/google/devtools/build/lib/shell",
+        "//src/main/java/com/google/devtools/build/lib/standalone",
+        "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//src/main/java/com/google/devtools/build/lib/vfs:output_service",
+        "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/java/com/google/devtools/common/options:invocation_policy",
+        "//src/main/java/com/google/devtools/common/options:options_internal",
+        "//src/main/protobuf:invocation_policy_java_proto",
+        "//src/test/java/com/google/devtools/build/lib:actions_testutil",
+        "//src/test/java/com/google/devtools/build/lib:analysis_testutil",
+        "//src/test/java/com/google/devtools/build/lib:foundations_testutil",
+        "//src/test/java/com/google/devtools/build/lib:integration_testutil",
         "//src/test/java/com/google/devtools/build/lib:packages_testutil",
         "//src/test/java/com/google/devtools/build/lib:testutil",
+        "//src/test/java/com/google/devtools/build/lib/skyframe:testutil",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
     ],
 )
 
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/BlazeRuntimeWrapper.java b/src/test/java/com/google/devtools/build/lib/buildtool/util/BlazeRuntimeWrapper.java
new file mode 100644
index 0000000..5a02509
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/BlazeRuntimeWrapper.java
@@ -0,0 +1,384 @@
+// 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.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.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.network.ConnectivityModule;
+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.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandEventHandler;
+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.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;
+import java.util.function.Consumer;
+
+/**
+ * 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 Consumer<EventBus> eventBusReceiver;
+
+  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,
+                ExecutionOptions.class,
+                LocalExecutionOptions.class,
+                CommonCommandOptions.class,
+                ClientOptions.class,
+                LoadingOptions.class,
+                AnalysisOptions.class,
+                KeepGoingOption.class,
+                LoadingPhaseThreadsOption.class,
+                PackageCacheOptions.class,
+                StarlarkSemanticsOptions.class,
+                BlazeCommandEventHandler.Options.class,
+                ConnectivityModule.ConnectivityOptions.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.newOptionsParser(options);
+  }
+
+  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;
+  }
+
+  /**
+   * If called with a non-null argument, all new EventBuses are posted on creation to the given
+   * receiver.
+   */
+  public void setEventBusReceiver(Consumer<EventBus> receiver) {
+    eventBusReceiver = receiver;
+  }
+
+  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 {
+      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);
+      }
+      if (eventBusReceiver != null) {
+        eventBusReceiver.accept(env.getEventBus());
+      }
+      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.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 {
+        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);
+        getSkyframeExecutor().notifyCommandComplete(env.getReporter());
+      }
+    } finally {
+      System.setOut(origSystemOut);
+      System.setErr(origSystemErr);
+    }
+  }
+
+  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;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java b/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java
new file mode 100644
index 0000000..28c9f40
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java
@@ -0,0 +1,787 @@
+// 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 static com.google.common.base.Throwables.throwIfUnchecked;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.eventbus.SubscriberExceptionContext;
+import com.google.common.eventbus.SubscriberExceptionHandler;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.ServerDirectories;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.skylark.StarlarkTransition.TransitionException;
+import com.google.devtools.build.lib.analysis.util.AnalysisMock;
+import com.google.devtools.build.lib.analysis.util.AnalysisTestUtil;
+import com.google.devtools.build.lib.analysis.util.AnalysisTestUtil.DummyWorkspaceStatusActionContext;
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.buildtool.BuildResult;
+import com.google.devtools.build.lib.buildtool.BuildTool;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.events.NullEventHandler;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.exec.BinTools;
+import com.google.devtools.build.lib.exec.ExecutorBuilder;
+import com.google.devtools.build.lib.includescanning.IncludeScanningModule;
+import com.google.devtools.build.lib.integration.util.IntegrationMock;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.util.MockToolsConfig;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.BlazeServerStartupOptions;
+import com.google.devtools.build.lib.runtime.BlazeWorkspace;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.runtime.WorkspaceBuilder;
+import com.google.devtools.build.lib.shell.AbnormalTerminationException;
+import com.google.devtools.build.lib.shell.Command;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue.Injected;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils;
+import com.google.devtools.build.lib.standalone.StandaloneModule;
+import com.google.devtools.build.lib.testutil.Suite;
+import com.google.devtools.build.lib.testutil.TestConstants;
+import com.google.devtools.build.lib.testutil.TestConstants.InternalTestExecutionMode;
+import com.google.devtools.build.lib.testutil.TestFileOutErr;
+import com.google.devtools.build.lib.testutil.TestSpec;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.util.CommandBuilder;
+import com.google.devtools.build.lib.util.CommandUtils;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.util.io.RecordingOutErr;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.util.FileSystems;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * A base class for integration tests that use the {@link BuildTool}. These tests basically run a
+ * little build and check what happens.
+ *
+ * <p>All integration tests are at least size medium.
+ */
+@TestSpec(size = Suite.MEDIUM_TESTS)
+public abstract class BuildIntegrationTestCase {
+
+  /** Thrown when an integration test case fails. */
+  public static class IntegrationTestExecException extends ExecException {
+    public IntegrationTestExecException(String message) {
+      super(message);
+    }
+
+    public IntegrationTestExecException(String message, Throwable cause) {
+      super(message, cause);
+    }
+
+    @Override
+    public ActionExecutionException toActionExecutionException(
+        String messagePrefix, boolean verboseFailures, Action action) {
+      return new ActionExecutionException(messagePrefix + getMessage(), getCause(), action, true);
+    }
+  }
+
+  protected FileSystem fileSystem;
+  protected EventCollectionApparatus events = new EventCollectionApparatus();
+  protected OutErr outErr = OutErr.SYSTEM_OUT_ERR;
+  protected Path testRoot;
+  protected ServerDirectories serverDirectories;
+  protected BlazeDirectories directories;
+  protected MockToolsConfig mockToolsConfig;
+  protected BinTools binTools;
+
+  protected BlazeRuntimeWrapper runtimeWrapper;
+  protected Path outputBase;
+  protected String outputBaseName = "outputBase";
+
+  private Path workspace;
+  protected RecordingExceptionHandler subscriberException = new RecordingExceptionHandler();
+
+  @Before
+  public final void createFilesAndMocks() throws Exception  {
+    runPriorToBeforeMethods();
+    events.setFailFast(false);
+    // TODO(mschaller): This will ignore any attempt by Blaze modules to provide a filesystem;
+    // consider something better.
+    this.fileSystem = createFileSystem();
+    this.testRoot = createTestRoot(fileSystem);
+
+    outputBase = testRoot.getRelative(outputBaseName);
+    outputBase.createDirectoryAndParents();
+    workspace = testRoot.getRelative(getDesiredWorkspaceRelative());
+    beforeCreatingWorkspace(workspace);
+    workspace.createDirectoryAndParents();
+    serverDirectories = createServerDirectories();
+    directories =
+        new BlazeDirectories(
+            serverDirectories,
+            workspace,
+            /* defaultSystemJavabase= */ null,
+            TestConstants.PRODUCT_NAME);
+    binTools =
+        IntegrationMock.get().getIntegrationBinTools(fileSystem, directories);
+    mockToolsConfig = new MockToolsConfig(workspace, realFileSystem());
+    setupMockTools();
+    createRuntimeWrapper();
+  }
+
+  protected ServerDirectories createServerDirectories() {
+    return new ServerDirectories(
+        /*installBase=*/ outputBase,
+        /*outputBase=*/ outputBase,
+        /*outputUserRoot=*/ outputBase,
+        /*execRootBase=*/ outputBase.getRelative("execroot"),
+        // Arbitrary install base hash.
+        /*installMD5=*/ "83bc4458738962b9b77480bac76164a9");
+  }
+
+  protected void createRuntimeWrapper() throws Exception {
+    runtimeWrapper =
+        new BlazeRuntimeWrapper(
+            events,
+            serverDirectories,
+            directories,
+            binTools,
+            getRuntimeBuilder().setEventBusExceptionHandler(subscriberException)) {
+          @Override
+          protected void finalizeBuildResult(BuildResult result) {
+            finishBuildResult(result);
+          }
+        };
+    setupOptions();
+  }
+
+  protected void runPriorToBeforeMethods() throws Exception {
+    // Allows tests such as SkyframeIntegrationInvalidationTest to execute code before all @Before
+    // methods are being run.
+  }
+
+  @After
+  public final void cleanUp() throws Exception  {
+    if (subscriberException.getException() != null) {
+      throwIfUnchecked(subscriberException.getException());
+      throw new RuntimeException(subscriberException.getException());
+    }
+    LoggingUtil.installRemoteLoggerForTesting(null);
+    testRoot.deleteTreesBelow(); // (comment out during debugging)
+  }
+
+  /**
+   * A helper class that can be used to record exceptions that occur on the event bus, by passing an
+   * instance of it to BlazeRuntime#setEventBusExceptionHandler.
+   */
+  public static final class RecordingExceptionHandler implements SubscriberExceptionHandler {
+    private Throwable exception;
+
+    @Override
+    public void handleException(Throwable exception, SubscriberExceptionContext context) {
+      System.err.println("subscriber exception: ");
+      exception.printStackTrace();
+      if (this.exception == null) {
+        this.exception = exception;
+      }
+    }
+
+    public Throwable getException() {
+      return exception;
+    }
+  }
+
+  /**
+   * Returns the relative path (from {@code testRoot}) to the desired workspace. This method may be
+   * called in {@link #createFilesAndMocks}, so overrides this method should not use any variables
+   * that may not have been initialized yet.
+   */
+  protected PathFragment getDesiredWorkspaceRelative() {
+    return PathFragment.create("workspace");
+  }
+
+  protected InternalTestExecutionMode getInternalTestExecutionMode() {
+    return InternalTestExecutionMode.NORMAL;
+  }
+
+  /**
+   * Called in #setUp before creating the workspace directory. Subclasses should override this
+   * if they want to a non-standard filesystem setup, e.g. introduce symlinked directories.
+   */
+  protected void beforeCreatingWorkspace(@SuppressWarnings("unused") Path workspace)
+      throws Exception {}
+
+  protected void finishBuildResult(@SuppressWarnings("unused") BuildResult result) {}
+
+  protected boolean realFileSystem() {
+    return true;
+  }
+
+  protected FileSystem createFileSystem() throws Exception {
+    return FileSystems.getNativeFileSystem();
+  }
+
+  protected Path createTestRoot(FileSystem fileSystem) {
+    return fileSystem.getPath(TestUtils.tmpDir());
+  }
+
+  // This is only here to support HaskellNonIntegrationTest. You should not call or override this
+  // method.
+  protected void setupMockTools() throws IOException {
+    // (Almost) every integration test calls BuildView.doLoadingPhase, which loads the default
+    // crosstool, etc.  So we create these package here.
+    AnalysisMock.get().setupMockClient(mockToolsConfig);
+  }
+
+  protected BlazeModule getBuildInfoModule() {
+    return new BlazeModule() {
+      @Override
+      public void workspaceInit(
+          BlazeRuntime runtime, BlazeDirectories directories, WorkspaceBuilder builder) {
+        builder.setWorkspaceStatusActionFactory(
+            new AnalysisTestUtil.DummyWorkspaceStatusActionFactory());
+      }
+
+      @Override
+      public void executorInit(
+          CommandEnvironment env, BuildRequest request, ExecutorBuilder builder) {
+        builder.addActionContext(new DummyWorkspaceStatusActionContext());
+      }
+    };
+  }
+
+  // There should be one getSpawnModule at a time, as we lack infrastructure to decide from
+  // which Module to take the SpawnActionContext for specific actions.
+  protected BlazeModule getSpawnModule() {
+    return new StandaloneModule();
+  }
+
+  /** Gets a module containing rules (by default, using the TestRuleClassProvider) */
+  protected BlazeModule getRulesModule() {
+    return TestRuleModule.getModule();
+  }
+
+  private BlazeModule getNoResolvedFileModule() {
+    return new BlazeModule() {
+      @Override
+      public ImmutableList<Injected> getPrecomputedValues() {
+        return ImmutableList.of(
+            PrecomputedValue.injected(
+                RepositoryDelegatorFunction.RESOLVED_FILE_INSTEAD_OF_WORKSPACE, Optional.absent()));
+      }
+    };
+  }
+
+  protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
+    OptionsParser startupOptionsParser = OptionsParser.newOptionsParser(getStartupOptionClasses());
+    startupOptionsParser.parse(getStartupOptions());
+    return new BlazeRuntime.Builder()
+        .setFileSystem(fileSystem)
+        .setProductName(TestConstants.PRODUCT_NAME)
+        .setStartupOptionsProvider(startupOptionsParser)
+        .addBlazeModule(getNoResolvedFileModule())
+        .addBlazeModule(getSpawnModule())
+        .addBlazeModule(new IncludeScanningModule())
+        .addBlazeModule(getBuildInfoModule())
+        .addBlazeModule(getRulesModule());
+  }
+
+  protected List<String> getStartupOptions() {
+    return ImmutableList.of();
+  }
+
+  protected ImmutableList<Class<? extends OptionsBase>> getStartupOptionClasses() {
+    return ImmutableList.of(BlazeServerStartupOptions.class);
+  }
+
+  protected void setupOptions() throws Exception {
+    runtimeWrapper.resetOptions();
+
+    runtimeWrapper.addOptions(
+        // Set visibility to public so that test cases don't have to bother
+        // with visibility declarations
+        "--default_visibility=public",
+
+        // Don't show progress messages unless we need to, to keep the noise down.
+        "--noshow_progress",
+
+        // Don't use ijars, because we don't have the executable in these tests
+        "--nouse_ijars");
+
+    runtimeWrapper.addOptions("--experimental_extended_sanity_checks");
+    runtimeWrapper.addOptions(TestConstants.PRODUCT_SPECIFIC_FLAGS);
+  }
+
+  protected void resetOptions() {
+    runtimeWrapper.resetOptions();
+  }
+
+  protected void addOptions(String... args) throws Exception {
+    runtimeWrapper.addOptions(args);
+  }
+
+  protected OptionsParser createOptionsParser() {
+    return runtimeWrapper.createOptionsParser();
+  }
+
+  protected Action getGeneratingAction(Artifact artifact) {
+    ActionAnalysisMetadata action = getActionGraph().getGeneratingAction(artifact);
+
+    if (action != null) {
+      Preconditions.checkState(
+          action instanceof Action,
+          "%s is not a proper Action object",
+          action.prettyPrint());
+      return (Action) action;
+    } else {
+      return null;
+    }
+  }
+  /**
+   * Returns the path to the executable that label "target" identifies.
+   *
+   * <p>Assumes that the specified target is executable, i.e. defines getExecutable; use {@link
+   * #getArtifacts} instead if this is not the case.
+   *
+   * @param target the label of the target whose executable location is requested.
+   */
+  protected Path getExecutableLocation(String target)
+      throws LabelSyntaxException, NoSuchPackageException, NoSuchTargetException,
+          InterruptedException, TransitionException, InvalidConfigurationException {
+    return getExecutable(getConfiguredTarget(target)).getPath();
+  }
+
+  /**
+   * Given a label (which has typically, but not necessarily, just been built), returns the
+   * collection of files that it produces.
+   *
+   * @param target the label of the target whose artifacts are requested.
+   */
+  protected Iterable<Artifact> getArtifacts(String target)
+      throws LabelSyntaxException, NoSuchPackageException, NoSuchTargetException,
+          InterruptedException, TransitionException, InvalidConfigurationException {
+    return getFilesToBuild(getConfiguredTarget(target));
+  }
+
+  /**
+   * Given a label (which has typically, but not necessarily, just been built), returns the
+   * configured target for it using the request configuration.
+   *
+   * @param target the label of the requested target.
+   */
+  protected ConfiguredTarget getConfiguredTarget(String target)
+      throws LabelSyntaxException, NoSuchPackageException, NoSuchTargetException,
+          InterruptedException, TransitionException, InvalidConfigurationException {
+    getPackageManager()
+        .getTarget(events.reporter(), Label.parseAbsolute(target, ImmutableMap.of()));
+    return getSkyframeExecutor()
+        .getConfiguredTargetForTesting(events.reporter(), label(target), getTargetConfiguration());
+  }
+
+  protected ConfiguredTarget getConfiguredTarget(
+      ExtendedEventHandler eventHandler, Label label, BuildConfiguration config)
+      throws TransitionException, InvalidConfigurationException {
+    return getSkyframeExecutor().getConfiguredTargetForTesting(eventHandler, label, config);
+  }
+
+  /**
+   * Gets all the already computed configured targets.
+   */
+  protected Iterable<ConfiguredTarget> getAllConfiguredTargets() {
+    return SkyframeExecutorTestUtils.getAllExistingConfiguredTargets(getSkyframeExecutor());
+  }
+
+  /** Gets an existing configured target. */
+  protected ConfiguredTarget getExistingConfiguredTarget(String target)
+      throws InterruptedException, LabelSyntaxException {
+    ConfiguredTarget existingConfiguredTarget =
+        SkyframeExecutorTestUtils.getExistingConfiguredTarget(
+            getSkyframeExecutor(), label(target), getTargetConfiguration());
+    assertWithMessage(target).that(existingConfiguredTarget).isNotNull();
+    return existingConfiguredTarget;
+  }
+
+  protected BuildConfigurationCollection getConfigurationCollection() {
+    return runtimeWrapper.getConfigurationCollection();
+  }
+
+  /**
+   * Returns the target configuration for the most recent build, as created in Blaze's master
+   * configuration creation phase.
+   *
+   * <p>Tries to find the configuration used by all of the top-level targets in the last invocation.
+   * If they used multiple different configurations, or if none of them had a configuration, then
+   * falls back to the base top-level configuration.
+   */
+  protected BuildConfiguration getTargetConfiguration() throws InterruptedException {
+    BuildConfiguration baseConfiguration =
+        Iterables.getOnlyElement(getConfigurationCollection().getTargetConfigurations());
+    BuildResult result = getResult();
+    if (result == null) {
+      return baseConfiguration;
+    }
+    Set<BuildConfiguration> topLevelTargetConfigurations =
+        result
+            .getActualTargets()
+            .stream()
+            .map((ct) -> getConfiguration(ct))
+            .filter((config) -> config != null)
+            .collect(toImmutableSet());
+    if (topLevelTargetConfigurations.size() != 1) {
+      return baseConfiguration;
+    }
+    return Iterables.getOnlyElement(topLevelTargetConfigurations);
+  }
+
+  protected BuildConfiguration getHostConfiguration() {
+    return getConfigurationCollection().getHostConfiguration();
+  }
+
+  protected TopLevelArtifactContext getTopLevelArtifactContext() {
+    return getRequest().getTopLevelArtifactContext();
+  }
+
+  /**
+   * Convenience wrapper around buildTool.syncPackageCache() and buildTool.build() that creates and
+   * executes a BuildRequest. Returns the BuildRequest on success (it is also subsequently
+   * accessible via {@link #getRequest}, even in case of abnormal termination). Also redirects the
+   * output from the reporter's event handler to go to this.OutErr during the build, and redirects
+   * System.out/System.err to go via the reporter (and hence to this.OutErr) during the build.
+   */
+  public BuildResult buildTarget(String... targets) throws Exception {
+    events.setOutErr(this.outErr);
+    runtimeWrapper.executeBuild(Arrays.asList(targets));
+    return runtimeWrapper.getLastResult();
+  }
+
+  /**
+   * Create a BuildRequest for the specified list of targets, using the
+   * currently-installed request options.
+   */
+  protected BuildRequest createRequest(String... targets) throws Exception {
+    return createNewRequest("BuildIntegrationTestCase", targets);
+  }
+
+  /**
+   * Creates a BuildRequest for either blaze build or blaze analyze, using the
+   * currently-installed request options.
+   * @param commandName blaze build or analyze command
+   * @param targets the targets to be built
+   */
+  protected BuildRequest createNewRequest(String commandName, String... targets) throws Exception {
+    runtimeWrapper.initializeOptionsParser();
+    return runtimeWrapper.createRequest(commandName, Arrays.asList(targets));
+  }
+
+  /**
+   * Utility function: parse a string as a label.
+   */
+  protected static Label label(String labelString) throws LabelSyntaxException {
+    return Label.parseAbsolute(labelString, ImmutableMap.of());
+  }
+
+  protected String run(Artifact executable, String... arguments) throws Exception {
+    Map<String, String> environment = null;
+    return run(executable.getPath(), null, environment, arguments);
+  }
+
+  /**
+   * This runs an executable using the executor instance configured for
+   * this test.
+   */
+  protected String run(Path executable, String... arguments) throws Exception {
+    Map<String, String> environment = null;
+    return run(executable, null, environment, arguments);
+  }
+
+  protected String run(Path executable, Path workingDirectory, String... arguments)
+      throws ExecException, InterruptedException {
+    return run(executable, workingDirectory, null, arguments);
+  }
+
+  protected String run(
+      Path executable, Path workingDirectory, Map<String, String> environment, String... arguments)
+      throws ExecException, InterruptedException {
+    RecordingOutErr outErr = new RecordingOutErr();
+    try {
+      run(executable, workingDirectory, outErr, environment, arguments);
+    } catch (ExecException e) {
+      throw new IntegrationTestExecException(
+          "failed to execute '"
+              + executable.getPathString()
+              + "'\n----- captured stdout:\n"
+              + outErr.outAsLatin1()
+              + "\n----- captured stderr:"
+              + outErr.errAsLatin1()
+              + "\n----- Reason",
+          e.getCause());
+    }
+
+    return outErr.outAsLatin1();
+  }
+
+  protected void run(Path executable, OutErr outErr, String... arguments) throws Exception {
+    run(executable, null, outErr, null, arguments);
+  }
+
+  private void run(
+      Path executable,
+      Path workingDirectory,
+      OutErr outErr,
+      Map<String, String> environment,
+      String... arguments)
+      throws ExecException, InterruptedException {
+    if (workingDirectory == null) {
+      workingDirectory = directories.getWorkspace();
+    }
+    List<String> argv = Lists.newArrayList(arguments);
+    argv.add(0, executable.toString());
+    Map<String, String> env =
+        (environment != null ? environment : getTargetConfiguration().getLocalShellEnvironment());
+    TestFileOutErr testOutErr = new TestFileOutErr();
+    try {
+      execute(workingDirectory, env, argv, testOutErr, /* verboseFailures= */ false);
+    } finally {
+      testOutErr.dumpOutAsLatin1(outErr.getOutputStream());
+      testOutErr.dumpErrAsLatin1(outErr.getErrorStream());
+    }
+  }
+
+  /**
+   * Writes a number of lines of text to a source file using Latin-1 encoding.
+   *
+   * @param relativePath the path relative to the workspace root.
+   * @param lines the lines of text to write to the file.
+   * @return the path of the created file.
+   * @throws IOException if the file could not be written.
+   */
+  public Path write(String relativePath, String... lines) throws IOException {
+    Path path = getWorkspace().getRelative(relativePath);
+    return writeAbsolute(path, lines);
+  }
+
+  /**
+   * Same as {@link #write}, but with an absolute path.
+   */
+  protected Path writeAbsolute(Path path, String... lines) throws IOException {
+    FileSystemUtils.writeIsoLatin1(path, lines);
+    return path;
+  }
+
+  /** Equivalent to {@code ln -s <target> <relativeLinkPath>}. */
+  protected void createSymlink(String target, String relativeLinkPath) throws IOException {
+    getWorkspace().getRelative(relativeLinkPath).createSymbolicLink(PathFragment.create(target));
+  }
+
+  /**
+   * The TimestampGranularityMonitor operates on the files created by the
+   * request and thus does not help here. Calling this method ensures that files
+   * we modify as part of the test environment are considered as changed.
+   */
+  protected static void waitForTimestampGranularity() throws Exception {
+    // Ext4 has a nanosecond granularity. Empirically, tmpfs supports ~5ms increments on
+    // Ubuntu Trusty.
+    Thread.sleep(10 /*ms*/);
+  }
+
+  /**
+   * Fork/exec/wait the specified command.  A utility method for subclasses.
+   */
+  protected String exec(String... argv) throws CommandException {
+    return new String(new Command(argv).execute().getStdout());
+  }
+
+  /**
+   * Performs a local direct spawn execution given spawn information broken out
+   * into individual arguments. Directs standard out/err to {@code outErr}.
+   *
+   * @param workingDirectory the directory from which to execute the subprocess
+   * @param environment the environment map to provide to the subprocess. If
+   *        null, the environment is inherited from the parent process.
+   * @param argv the argument vector including the command itself
+   * @param outErr the out+err stream pair to receive stdout and stderr from the
+   *        subprocess
+   * @throws ExecException if any kind of abnormal termination or command
+   *         exception occurs
+   */
+  public static void execute(
+      Path workingDirectory,
+      Map<String, String> environment,
+      List<String> argv,
+      FileOutErr outErr,
+      boolean verboseFailures)
+      throws ExecException {
+    Command command =
+        new CommandBuilder()
+            .addArgs(argv)
+            .setEnv(environment)
+            .setWorkingDir(workingDirectory)
+            .build();
+    try {
+      command.execute(outErr.getOutputStream(), outErr.getErrorStream());
+    } catch (AbnormalTerminationException e) { // non-zero exit or signal or I/O problem
+      IntegrationTestExecException e2 =
+          new IntegrationTestExecException(CommandUtils.describeCommandFailure(verboseFailures, e));
+      e2.initCause(e); // We don't pass cause=e to the ExecException constructor
+      // since we don't want it to contribute to the exception
+      // message again; it's already in describeCommandFailure().
+      throw e2;
+    } catch (CommandException e) {
+      IntegrationTestExecException e2 =
+          new IntegrationTestExecException(CommandUtils.describeCommandFailure(verboseFailures, e));
+      e2.initCause(e); // We don't pass cause=e to the ExecException constructor
+      // since we don't want it to contribute to the exception
+      // message again; it's already in describeCommandFailure().
+      throw e2;
+    }
+  }
+
+  protected String readContentAsLatin1String(Artifact artifact) throws IOException {
+    return new String(FileSystemUtils.readContentAsLatin1(artifact.getPath()));
+  }
+
+  /**
+   * Given a collection of Artifacts, returns a corresponding set of strings of
+   * the form "<root> <relpath>", such as "bin x/libx.a".  Such strings make
+   * assertions easier to write.
+   *
+   * <p>The returned set preserves the order of the input.
+   */
+  protected Set<String> artifactsToStrings(Iterable<Artifact> artifacts) {
+    return AnalysisTestUtil.artifactsToStrings(getConfigurationCollection(), artifacts);
+  }
+
+  protected ActionsTestUtil actionsTestUtil() {
+    return new ActionsTestUtil(getActionGraph());
+  }
+
+  protected Artifact getExecutable(TransitiveInfoCollection target) {
+    return target.getProvider(FilesToRunProvider.class).getExecutable();
+  }
+
+  protected NestedSet<Artifact> getFilesToBuild(TransitiveInfoCollection target) {
+    return target.getProvider(FileProvider.class).getFilesToBuild();
+  }
+
+  protected final BuildConfiguration getConfiguration(ConfiguredTarget ct) {
+    return getSkyframeExecutor()
+        .getConfiguration(NullEventHandler.INSTANCE, ct.getConfigurationKey());
+  }
+
+  /**
+   * Returns the BuildRequest of the last call to buildTarget().
+   */
+  protected BuildRequest getRequest() {
+    return runtimeWrapper.getLastRequest();
+  }
+
+  /**
+   * Returns the BuildResultof the last call to buildTarget().
+   */
+  protected BuildResult getResult() {
+    return runtimeWrapper.getLastResult();
+  }
+
+  /**
+   * Returns the {@link BlazeRuntime} in use.
+   */
+  protected BlazeRuntime getRuntime() {
+    return runtimeWrapper.getRuntime();
+  }
+
+  protected BlazeWorkspace getBlazeWorkspace() {
+    return runtimeWrapper.getRuntime().getWorkspace();
+  }
+
+  protected ConfiguredTargetAndData getConfiguredTargetAndTarget(
+      ExtendedEventHandler eventHandler, Label label, BuildConfiguration config)
+      throws TransitionException, InvalidConfigurationException {
+    return getSkyframeExecutor().getConfiguredTargetAndDataForTesting(eventHandler, label, config);
+  }
+
+  protected ActionGraph getActionGraph() {
+    return getSkyframeExecutor().getActionGraph(events.reporter());
+  }
+
+  protected CommandEnvironment getCommandEnvironment() {
+    return runtimeWrapper.getCommandEnvironment();
+  }
+
+  public SkyframeExecutor getSkyframeExecutor() {
+    return runtimeWrapper.getSkyframeExecutor();
+  }
+
+  protected PackageManager getPackageManager() {
+    return getSkyframeExecutor().getPackageManager();
+  }
+
+  protected Path getOutputBase() {
+    return outputBase;
+  }
+
+  protected Path getWorkspace() {
+    return workspace;
+  }
+
+  /**
+   * Assertion-checks that the expected error was reported,
+   */
+  protected void assertContainsError(String expectedError) {
+    for (Event error : events.errors()) {
+      if (error.getMessage().contains(expectedError)) {
+        return;
+      }
+    }
+    fail("didn't find expected error: \"" + expectedError + "\"");
+  }
+}