| // 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.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.common.io.LineReader; |
| import com.google.devtools.build.lib.util.StringUtilities; |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.Consumer; |
| import java.util.logging.Logger; |
| import java.util.stream.Collectors; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Helper class for running Bazel process as external process from JUnit tests. Can be used to run |
| * arbitrary external process and explore the results. |
| */ |
| public final class ProcessRunner { |
| private static final Logger logger = Logger.getLogger(ProcessRunner.class.getName()); |
| private final ProcessParameters parameters; |
| private final ExecutorService executorService; |
| |
| /** |
| * Creates ProcessRunner |
| * |
| * @param parameters process parameters like executable name, arguments, timeout etc |
| * @param executorService to use for process output/error streams reading; intentionally passed as |
| * a parameter so we can use the thread pool to speed up. Should be multi-threaded, as two |
| * separate tasks are submitted, to read from output and error streams. |
| * <p>SuppressWarnings: WeakerAccess - suppress the warning about constructor being public: |
| * the class is intended to be used outside the package. (IDE currently marks the possibility |
| * for the constructor to be package-private because the current usages are only inside the |
| * package, but it is going to change) |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public ProcessRunner(ProcessParameters parameters, ExecutorService executorService) { |
| this.parameters = parameters; |
| this.executorService = executorService; |
| } |
| |
| public ProcessResult runSynchronously() throws Exception { |
| ImmutableList<String> args = parameters.arguments(); |
| final List<String> commandParts = new ArrayList<>(args.size() + 1); |
| commandParts.add(parameters.name()); |
| commandParts.addAll(args); |
| |
| logger.info("Running: " + commandParts.stream().collect(Collectors.joining(" "))); |
| |
| ProcessBuilder processBuilder = new ProcessBuilder(commandParts); |
| processBuilder.directory(parameters.workingDirectory()); |
| parameters.environment().ifPresent(map -> processBuilder.environment().putAll(map)); |
| |
| parameters.redirectOutput().ifPresent(path -> processBuilder.redirectOutput(path.toFile())); |
| parameters.redirectError().ifPresent(path -> processBuilder.redirectError(path.toFile())); |
| |
| Process process = processBuilder.start(); |
| |
| try (ProcessStreamReader outReader = |
| parameters.redirectOutput().isPresent() |
| ? null |
| : createReader(process.getInputStream(), ">> "); |
| ProcessStreamReader errReader = |
| parameters.redirectError().isPresent() |
| ? null |
| : createReader(process.getErrorStream(), "ERROR: ")) { |
| |
| long timeoutMillis = parameters.timeoutMillis(); |
| if (!process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS)) { |
| throw new TimeoutException( |
| String.format( |
| "%s timed out after %d seconds (%d millis)", |
| parameters.name(), timeoutMillis / 1000, timeoutMillis)); |
| } |
| |
| List<String> err = |
| errReader != null |
| ? errReader.get() |
| : Files.readAllLines(parameters.redirectError().get()); |
| List<String> out = |
| outReader != null |
| ? outReader.get() |
| : Files.readAllLines(parameters.redirectOutput().get()); |
| |
| int exitValue = process.exitValue(); |
| boolean expectedToFail = parameters.expectedToFail() || parameters.expectedExitCode() != 0; |
| if ((exitValue == 0) == expectedToFail) { |
| throw new ProcessRunnerException( |
| String.format( |
| "Expected to %s, but %s.\nError: %s\nOutput: %s", |
| expectedToFail ? "fail" : "succeed", |
| exitValue == 0 ? "succeeded" : "failed", |
| StringUtilities.joinLines(err), |
| StringUtilities.joinLines(out))); |
| } |
| // We want to check the exact exit code if it was explicitly set to something; |
| // we already checked the variant when it is equal to zero above. |
| if (parameters.expectedExitCode() != 0 && parameters.expectedExitCode() != exitValue) { |
| throw new ProcessRunnerException( |
| String.format( |
| "Expected exit code %d, but found %d.\nError: %s\nOutput: %s", |
| parameters.expectedExitCode(), |
| exitValue, |
| StringUtilities.joinLines(err), |
| StringUtilities.joinLines(out))); |
| } |
| |
| if (parameters.expectedEmptyError()) { |
| if (!err.isEmpty()) { |
| throw new ProcessRunnerException( |
| "Expected empty error stream, but found: " + StringUtilities.joinLines(err)); |
| } |
| } |
| return ProcessResult.create(exitValue, out, err); |
| } finally { |
| process.destroy(); |
| } |
| } |
| |
| private ProcessStreamReader createReader(InputStream stream, String prefix) { |
| return new ProcessStreamReader(executorService, stream, s -> logger.fine(prefix + s)); |
| } |
| |
| /** Specific runtime exception for external process errors */ |
| public static class ProcessRunnerException extends RuntimeException { |
| ProcessRunnerException(String message) { |
| super(message); |
| } |
| } |
| |
| private static class ProcessStreamReader implements AutoCloseable { |
| |
| private final InputStream stream; |
| private final Future<List<String>> future; |
| private final AtomicReference<IOException> exception = new AtomicReference<>(); |
| |
| private ProcessStreamReader( |
| ExecutorService executorService, |
| InputStream stream, |
| @Nullable Consumer<String> logConsumer) { |
| this.stream = stream; |
| future = |
| executorService.submit( |
| () -> { |
| final List<String> lines = Lists.newArrayList(); |
| try (BufferedReader reader = |
| new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { |
| LineReader lineReader = new LineReader(reader); |
| String line; |
| while ((line = lineReader.readLine()) != null) { |
| if (logConsumer != null) { |
| logConsumer.accept(line); |
| } |
| lines.add(line); |
| } |
| } catch (IOException e) { |
| exception.set(e); |
| } |
| return lines; |
| }); |
| } |
| |
| public List<String> get() |
| throws InterruptedException, ExecutionException, TimeoutException, IOException { |
| try { |
| List<String> lines = future.get(15, TimeUnit.SECONDS); |
| if (exception.get() != null) { |
| throw exception.get(); |
| } |
| return lines; |
| } finally { |
| // if future is timed out |
| stream.close(); |
| } |
| } |
| |
| @Override |
| public void close() throws Exception { |
| stream.close(); |
| } |
| } |
| } |