blob: 14892fcfb6c03d3b43dccbcbfaf8907d9bec5364 [file] [log] [blame]
// Copyright 2018 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.blackbox.framework;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.blackbox.framework.ProcessRunner.ProcessRunnerException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Class for running Bazel process in the working directory of the blackbox test.
*
* <p>Provides customization methods for timeout, environment variables, etc., which modify the
* current instance of <code>BuilderRunner</code>, and also return it for chain calls.
*
* <p>The instance keeps only parameters for the Bazel invocations, like timeout or environment
* variables values, but not the data related to the actual Bazel invocations. That is why the same
* instance can be used to invoke several commands.
*/
public final class BuilderRunner {
private static final long DEFAULT_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(30);
private final Path workDir;
private final Path binaryPath;
private final Map<String, String> env;
private final ExecutorService executorService;
private long timeoutMillis;
private boolean useDefaultRc = true;
private int errorCode = 0;
private List<String> flags;
private boolean shouldFail;
private boolean enableDebug;
/**
* Creates the BuilderRunner
*
* @param workDir working directory of the test
* @param binaryPath {@link Path} to the Bazel executable
* @param defaultTimeoutMillis default timeout in milliseconds to use if the user has not
* specified timeout for Bazel command invocation
* @param env environment variables to be passed to Bazel process (part, common for all Bazel
* invocations)
* @param executorService {@link ExecutorService} to be used by the {@link ProcessRunner}, which
* actually invokes the Bzel command
*/
BuilderRunner(
Path workDir,
Path binaryPath,
long defaultTimeoutMillis,
Map<String, String> env,
ExecutorService executorService) {
Preconditions.checkNotNull(workDir);
Preconditions.checkNotNull(binaryPath);
Preconditions.checkNotNull(env);
Preconditions.checkState(defaultTimeoutMillis > 0, "Expected default timeout to be positive");
this.workDir = workDir;
this.binaryPath = binaryPath;
this.env = Maps.newHashMap(env);
this.executorService = executorService;
this.timeoutMillis = defaultTimeoutMillis;
this.flags = Lists.newArrayList();
}
/**
* Sets environment variable for the Bazel invocation.
*
* @param name name of the variable
* @param value value of variable
* @return this BuildRunner instance
*/
public BuilderRunner withEnv(String name, String value) {
env.put(name, value);
return this;
}
/**
* Sets the expected error code.
*
* @param errorCode the value of the error code
* @return this BuildRunner instance
*/
public BuilderRunner withErrorCode(int errorCode) {
this.errorCode = errorCode;
return this;
}
/**
* Expect Bazel to fail. This method is needed when the exact error code can not be specified.
*
* @return this BuildRunner instance
*/
public BuilderRunner shouldFail() {
this.shouldFail = true;
return this;
}
/**
* Sets timeout value for the Bazel process invocation. If not called, default value is used,
* which is calculated from the test parameters. See {@link
* BlackBoxTestContext#getTestTimeoutMillis()}. If the invocation time exceeds timeout, {@link
* TimeoutException} is thrown.
*
* @param timeoutMillis timeout value in milliseconds
* @return this BuilderRunner instance
*/
public BuilderRunner withTimeout(long timeoutMillis) {
Preconditions.checkState(this.timeoutMillis > 0);
this.timeoutMillis = timeoutMillis;
return this;
}
/**
* Should be used ONLY FOR TESTS DEBUG. Adds "--host_jvm_debug" to the Bazel startup options, so
* that the JVM waits for the debugger process to connect before executing any code.
*
* @return this BuilderRunner instance
*/
public BuilderRunner enableDebug() {
this.enableDebug = true;
return this;
}
/**
* Specifies that Bazel should not pass the default .bazelrc (in the test working directory) as
* parameter
*
* @return this BuilderRunner instance
*/
public BuilderRunner withoutDefaultRc() {
useDefaultRc = false;
return this;
}
/**
* Specifies the flags to pass to Bazel.
*
* <p>We need it as a builder method, so that several consequent Bazel calls with the same set of
* flags could be performed: bazel build --flag1 --flag2 //... bazel info --flag1 --flag2
* bazel-bin
*
* @return this BuilderRunner instance
*/
public BuilderRunner withFlags(String... flags) {
Collections.addAll(this.flags, flags);
return this;
}
/**
* Runs <code>bazel info &lt;parameter&gt;</code> and returns the result. Asserts the process exit
* code. Does not assert that the error stream is empty.
*
* @param parameters - info command parameter (can be omitted) and the Bazel flags. If Bazel was
* invoked with some flags, the same set of flags should be used with info.
* @return ProcessResult with process exit code, strings with stdout and error streams contents
* @throws TimeoutException in case of timeout
* @throws IOException in case of the process startup/interaction problems
* @throws InterruptedException if the current thread is interrupted while waiting
* @throws ProcessRunnerException if the process return code is not zero or error stream is not
* empty when it was expected
*/
public ProcessResult info(String... parameters) throws Exception {
// additional expectations for the info time to be under the default timeout
withTimeout(Math.min(DEFAULT_TIMEOUT_MILLIS, timeoutMillis));
return runBinary("info", parameters);
}
/**
* Runs <code>bazel help</code> and returns the result. Asserts the process exit code. Does not
* assert that the error stream is empty.
*
* @return ProcessResult with process exit code, strings with stdout and error streams contents
* @throws TimeoutException in case of timeout
* @throws IOException in case of the process startup/interaction problems
* @throws InterruptedException if the current thread is interrupted while waiting
* @throws ProcessRunnerException if the process return code is not zero or error stream is not
* empty when it was expected
*/
public ProcessResult help() throws Exception {
// additional expectations for the info time to be under the default timeout
withTimeout(Math.min(DEFAULT_TIMEOUT_MILLIS, timeoutMillis));
return runBinary("help");
}
/**
* Runs <code>bazel test &lt;args&gt;</code> and returns the result. Asserts that the process exit
* code is zero. Does not assert that the error stream is empty.
*
* @param args arguments to pass to test command
* @return ProcessResult with process exit code, strings with stdout and error streams contents
* @throws TimeoutException in case of timeout
* @throws IOException in case of the process startup/interaction problems
* @throws InterruptedException if the current thread is interrupted while waiting
* @throws ProcessRunnerException if the process return code is not zero or error stream is not
* empty when it was expected
*/
public ProcessResult test(String... args) throws Exception {
return runBinary("test", args);
}
/**
* Runs <code>bazel build &lt;args&gt;</code> and returns the result. Asserts that the process
* exit code is zero. Does not assert that the error stream is empty.
*
* @param args arguments to pass to build command
* @return ProcessResult with process exit code, strings with stdout and error streams contents
* @throws TimeoutException in case of timeout
* @throws IOException in case of the process startup/interaction problems
* @throws InterruptedException if the current thread is interrupted while waiting
* @throws ProcessRunnerException if the process return code is not zero or error stream is not
* empty when it was expected
*/
public ProcessResult build(String... args) throws Exception {
return runBinary("build", args);
}
/**
* Runs <code>bazel query &lt;args&gt;</code> and returns the result. Asserts that the process
* exit code is zero. Does not assert that the error stream is empty.
*
* @param args arguments to pass to query command
* @return ProcessResult with process exit code, strings with stdout and error streams contents
* @throws TimeoutException in case of timeout
* @throws IOException in case of the process startup/interaction problems
* @throws InterruptedException if the current thread is interrupted while waiting
* @throws ProcessRunnerException if the process return code is not zero or error stream is not
* empty when it was expected
*/
public ProcessResult query(String... args) throws Exception {
return runBinary("query", args);
}
/**
* Runs <code>bazel run &lt;args&gt;</code> and returns the result. Asserts that the process exit
* code is zero. Does not assert that the error stream is empty.
*
* @param args arguments to pass to run command
* @return ProcessResult with process exit code, strings with stdout and error streams contents
* @throws TimeoutException in case of timeout
* @throws IOException in case of the process startup/interaction problems
* @throws InterruptedException if the current thread is interrupted while waiting
* @throws ProcessRunnerException if the process return code is not zero or error stream is not
* empty when it was expected
*/
public ProcessResult run(String... args) throws Exception {
return runBinary("run", args);
}
/**
* Runs <code>bazel shutdown</code> and returns the result. Asserts that the process exit code is
* zero. Does not assert that the error stream is empty.
*
* @return ProcessResult with process exit code, strings with stdout and error streams contents
* @throws TimeoutException in case of timeout
* @throws IOException in case of the process startup/interaction problems
* @throws InterruptedException if the current thread is interrupted while waiting
* @throws ProcessRunnerException if the process return code is not zero or error stream is not
* empty when it was expected
*/
public ProcessResult shutdown() throws Exception {
// additional expectations for the shutdown time to be under the default timeout
withTimeout(Math.min(DEFAULT_TIMEOUT_MILLIS, timeoutMillis));
return runBinary("shutdown");
}
private ProcessResult runBinary(String command, String... args) throws Exception {
List<String> list = Lists.newArrayList();
if (useDefaultRc) {
Path bazelRc = workDir.resolve(".bazelrc");
if (Files.exists(bazelRc)) {
list.add("--bazelrc");
list.add(bazelRc.toAbsolutePath().toString());
}
}
if (enableDebug) {
list.add("--host_jvm_debug");
// 10 min for debug
list.add("--max_idle_secs=600");
}
list.add(command);
list.addAll(this.flags);
Collections.addAll(list, args);
ProcessParameters parameters =
ProcessParameters.builder()
.setWorkingDirectory(workDir.toFile())
.setName(binaryPath.toString())
.setTimeoutMillis(timeoutMillis)
.setArguments(list)
.setEnvironment(ImmutableMap.copyOf(env))
// bazel writes info messages to error stream, so
// we need to allow the error output stream be not empty
.setExpectedEmptyError(false)
.setExpectedExitCode(errorCode)
.setExpectedToFail(shouldFail)
.build();
return new ProcessRunner(parameters, executorService).runSynchronously();
}
}