blob: f9a032888cb4c2c4205b605a2fc7b2ed22141ffa [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.blackbox.framework;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.util.OS;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Holds JUnit integration tests context, passed to tests from base class.
*
* <p>Provides access to source and work directories, generated and binary files, allows to run
* built binary, creates {@link BuilderRunner} for running Bazel commands
*/
public final class BlackBoxTestContext {
private final Path workDir;
private final Path srcDir;
private final Path tmpDir;
private final Map<String, String> commonEnv;
private final ExecutorService executorService;
private final String productName;
private final Path binaryPath;
private Path genFilesPath;
private Path binFilesPaths;
public BlackBoxTestContext(
String testName,
String productName,
Path binaryPath,
Map<String, String> commonEnv,
ExecutorService executorService)
throws Exception {
assertThat(Files.exists(binaryPath)).isTrue();
this.commonEnv = Maps.newHashMap(commonEnv);
this.productName = productName;
this.binaryPath = binaryPath;
srcDir = getPathFromEnv("TEST_SRCDIR");
tmpDir = getPathFromEnv("TEST_TMPDIR");
workDir = tmpDir.resolve(testName);
Files.createDirectories(workDir);
this.executorService = executorService;
}
/** Returns the product name: bazel */
public String getProductName() {
return productName;
}
/** Returns the working directory of the test */
public Path getWorkDir() {
return workDir;
}
/** Returns the source directory (TEST_SRCDIR) of the test. */
public Path getSrcDir() {
return srcDir;
}
/** Returns the temp directory (TEST_TMPDIR) of the test */
public Path getTmpDir() {
return tmpDir;
}
/**
* Writes <code>lines</code> using ISO_8859_1 into the file, specified by the <code>subPath</code>
* relative to the working directory. Overrides the file if it exists, creates the file if it does
* not exist.
*
* @param subPath path to file relative to working directory
* @param lines lines of text to write. Newlines are added by the method.
* @return Path to the file
* @throws IOException in case if the file can not be created/overridden, or can not be open for
* writing
*/
public Path write(String subPath, String... lines) throws IOException {
return PathUtils.writeFileInDir(workDir, subPath, lines);
}
/**
* Writes <code>lines</code> using ISO_8859_1 into the file, specified by the <code>subPath</code>
* relative to the working directory. Overrides the file if it exists, creates the file if it does
* not exist.
*
* @param subPath path to file relative to working directory
* @param lines lines of text to write. Newlines are added by the method.
* @return Path to the file
* @throws IOException in case if the file can not be created/overridden, or can not be open for
* writing
*/
public Path write(String subPath, List<String> lines) throws IOException {
return PathUtils.writeFileInDir(workDir, subPath, lines);
}
/**
* Writes <code>lines</code> using ISO_8859_1 into the BUILD file in the directory, specified by
* the <code>subPath</code> relative to the working directory. Overrides the file if it exists,
* creates the file if it does not exist.
*
* @param subPathToDir path to directory relative to working directory, where BUILD file should be
* written
* @param lines lines of text to write. Newlines are added by the method.
* @return Path to the file
* @throws IOException in case if the file can not be created/overridden, or can not be open for
* writing
*/
public Path writeBuild(String subPathToDir, String... lines) throws IOException {
String separator = (subPathToDir.endsWith(File.separator) ? "" : File.separator);
return write(subPathToDir + separator + "BUILD", lines);
}
/**
* Reads the lines of the file, specified by the <code>subPath</code> relative to the working
* directory, in ISO_8859_1.
*
* @param subPath path to the file relative to the working directory
* @return list of file lines (without the newline characters)
* @throws IOException if the file does not exist or can not be read
*/
public List<String> read(String subPath) throws IOException {
return PathUtils.readFile(workDir, subPath);
}
/**
* Resolve a path relative to "bazel-genfiles".
*
* <p>Calls <code>bazel info bazel-genfiles</code>, caches the result.
*
* @param bazel the instance of BuilderRunner to run info with
* @param subPathUnderGen path to the file under bazel-gen directory
* @return full path to the resolved file
* @throws Exception if <code>bazel info</code> command fails
*/
public Path resolveGenPath(BuilderRunner bazel, String subPathUnderGen) throws Exception {
if (genFilesPath == null) {
genFilesPath = PathUtils.resolve(workDir, getInfoValue(bazel, productName + "-genfiles"));
}
return PathUtils.resolve(genFilesPath, subPathUnderGen);
}
/**
* Resolve a path relative to "bazel-bin".
*
* <p>Calls <code>bazel info bazel-bin</code>
*
* @param bazel the instance of BuilderRunner to run info with
* @param subPathUnderBin path to the file under bazel-bin directory
* @return full path to the resolved file
* @throws Exception if <code>bazel info</code> command fails
*/
public Path resolveBinPath(BuilderRunner bazel, String subPathUnderBin) throws Exception {
Path binPath = PathUtils.resolve(workDir, getInfoValue(bazel, productName + "-bin"));
return PathUtils.resolve(binPath, subPathUnderBin);
}
/**
* Resolve a path relative to "execution_root". Useful for checking the contents of the generated
* external repositories.
*
* <p>Calls <code>bazel info execution_root</code>
*
* @param bazel the instance of BuilderRunner to run info with
* @param subPathUnderBin path to the file under execution_root directory
* @return full path to the resolved file
* @throws Exception if <code>bazel info</code> command fails
*/
public Path resolveExecRootPath(BuilderRunner bazel, String subPathUnderBin) throws Exception {
Path binPath = PathUtils.resolve(workDir, getInfoValue(bazel, "execution_root"));
return PathUtils.resolve(binPath, subPathUnderBin);
}
private String getInfoValue(BuilderRunner bazel, String key) throws Exception {
String[] parts = bazel.info(key).outString().trim().split(" ");
return parts[parts.length - 1];
}
/**
* Runs the built executable. Calls <code>bazel info</code> to get the information about bazel-bin
* directory location.
*
* @param bazel the instance of BuilderRunner to run info with
* @param subPathUnderBin path to the executable relative to bazel-bin directory
* @param timeoutMillis timeout on the process execution
* @return ProcessResult result of the execution with the process exit code and strings with
* stdout and stderr contents.
* @throws Exception if <code>bazel info</code> command fails or executable invocation fails.
*/
public ProcessResult runBuiltBinary(
BuilderRunner bazel, String subPathUnderBin, long timeoutMillis) throws Exception {
if (OS.WINDOWS.equals(OS.getCurrent()) && !subPathUnderBin.endsWith(".exe")) {
subPathUnderBin += ".exe";
}
Path executable = resolveBinPath(bazel, subPathUnderBin);
assertThat(Files.exists(executable)).isTrue();
assertThat(Files.isRegularFile(executable)).isTrue();
assertThat(Files.isExecutable(executable)).isTrue();
ProcessParameters parameters =
ProcessParameters.builder()
.setWorkingDirectory(workDir.toFile())
.setName(executable.toString())
.setTimeoutMillis(getProcessTimeoutMillis(timeoutMillis))
.build();
return new ProcessRunner(parameters, executorService).runSynchronously();
}
/**
* Creates the instance of BuilderRunner for running Bazel commands in the working directory. see
* {@link BuilderRunner}
*
* @return BuilderRunner interface for running Bazel commands.
*/
public BuilderRunner bazel() {
return new BuilderRunner(
workDir, binaryPath, getProcessTimeoutMillis(-1), commonEnv, executorService);
}
/**
* Runs external binary in the specified working directory. See {@link BuilderRunner}
*
* @param workingDirectory working directory for running the binary
* @param processToRun path to the binary to run
* @param expectEmptyError if <code>true</code>, no text is expected in the error stream,
* otherwise, ProcessRunnerException is thrown.
* @param arguments arguments to pass to the binary
* @return ProcessResult execution result
*/
public ProcessResult runBinary(
Path workingDirectory, String processToRun, boolean expectEmptyError, String... arguments)
throws Exception {
ProcessParameters parameters =
ProcessParameters.builder()
.setWorkingDirectory(workingDirectory.toFile())
.setName(processToRun)
.setTimeoutMillis(getProcessTimeoutMillis(-1))
.setArguments(arguments)
.setExpectedEmptyError(expectEmptyError)
.build();
return new ProcessRunner(parameters, executorService).runSynchronously();
}
/**
* Take the value from environment variable and assert that it is a path, and the file or
* directory, specified by this path, exists.
*
* @param name name of the environment variable
* @return Path to the file where the value of environment variable points
*/
private static Path getPathFromEnv(String name) {
String pathStr = System.getenv(name);
assertThat(pathStr).isNotNull();
Path path = Paths.get(pathStr);
assertThat(Files.exists(path)).isTrue();
return path.toAbsolutePath();
}
/**
* Define the value of the timeout for the Bazel process invoked for the test. Use the value,
* specified by the user, or default test timeout value.
*
* @param timeoutMillis value for the timeout, specified by the user. If the user has not
* specified the value, -1 is passed.
* @return timeout value in milliseconds
*/
private static long getProcessTimeoutMillis(long timeoutMillis) {
if (timeoutMillis > 0) {
return timeoutMillis;
}
return getTestTimeoutMillis();
}
/**
* Determine the timeout of the blackbox test, use information from the environment variable.
*
* @return timeout value in milliseconds
*/
private static long getTestTimeoutMillis() {
String timeout = System.getenv("TEST_TIMEOUT");
if (timeout != null) {
try {
return TimeUnit.SECONDS.toMillis(Integer.parseInt(timeout));
} catch (NumberFormatException e) {
System.out.println("Invalid test timeout value, using default.");
}
}
return TimeUnit.SECONDS.toMillis(900);
}
}