| // 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); |
| } |
| } |