/* * Copyright 2013-present Facebook, Inc. * * 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.facebook.buck.step; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.ProcessExecutorParams; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** Abstract implementation of {@link Step} that ... */ public abstract class AbstractTestStep implements Step { private final String name; private final ProjectFilesystem filesystem; private final Optional<Path> workingDirectory; private final ImmutableList<String> command; private final Optional<ImmutableMap<String, String>> env; private final Path exitCode; private final Path output; private final Optional<Long> testRuleTimeoutMs; public AbstractTestStep( String name, ProjectFilesystem filesystem, Optional<Path> workingDirectory, ImmutableList<String> command, Optional<ImmutableMap<String, String>> env, Path exitCode, Optional<Long> testRuleTimeoutMs, Path output) { this.name = name; this.filesystem = filesystem; this.workingDirectory = workingDirectory; this.command = command; this.env = env; this.exitCode = exitCode; this.testRuleTimeoutMs = testRuleTimeoutMs; this.output = output; } @Override public StepExecutionResult execute(ExecutionContext context) throws InterruptedException { // Build the process, redirecting output to the provided output file. In general, // it's undesirable that both stdout and stderr are being redirected to the same // input stream. However, due to the nature of OS pipe buffering, we can't really // maintain the natural interleaving of multiple output streams in a way that we // can correctly associate both stdout/stderr streams to the one correct test out // of the many that ran. So, our best bet is to just combine them all into stdout, // so they get properly interleaved with the test start and end messages that we // use when we parse the test output. Map<String, String> environment = new HashMap<>(System.getenv()); if (env.isPresent()) { environment.putAll(env.get()); } ProcessExecutorParams params = ProcessExecutorParams.builder() .setCommand(command) .setDirectory(workingDirectory.map(filesystem::resolve)) .setEnvironment(ImmutableMap.copyOf(environment)) .setRedirectOutput(ProcessBuilder.Redirect.to(filesystem.resolve(output).toFile())) .setRedirectErrorStream(true) .build(); ProcessExecutor.Result result; try { // Run the test process, saving the exit code. ProcessExecutor executor = context.getProcessExecutor(); ImmutableSet<ProcessExecutor.Option> options = ImmutableSet.of(ProcessExecutor.Option.EXPECTING_STD_OUT); result = executor.launchAndExecute( params, options, /* stdin */ Optional.empty(), testRuleTimeoutMs, /* timeOutHandler */ Optional.empty()); } catch (IOException e) { context.logError(e, "Error starting command %s", command); return StepExecutionResult.ERROR; } if (result.isTimedOut()) { throw new HumanReadableException( "Timed out after %d ms running test command %s", testRuleTimeoutMs.orElse(-1L), command); } // Since test binaries return a non-zero exit code when unittests fail, save the exit code // to a file rather than signalling a step failure. try (FileOutputStream fileOut = new FileOutputStream(filesystem.resolve(exitCode).toFile()); ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) { objectOut.writeInt(result.getExitCode()); } catch (IOException e) { context.logError(e, "Error saving exit code to %s", exitCode); return StepExecutionResult.ERROR; } return StepExecutionResult.SUCCESS; } @Override public String getShortName() { return name; } @Override public String getDescription(ExecutionContext context) { return name; } public ImmutableList<String> getCommand() { return command; } @VisibleForTesting public Optional<ImmutableMap<String, String>> getEnv() { return env; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof AbstractTestStep)) { return false; } AbstractTestStep that = (AbstractTestStep) o; if (!command.equals(that.command)) { return false; } if (!exitCode.equals(that.exitCode)) { return false; } if (!output.equals(that.output)) { return false; } return true; } @Override public int hashCode() { return Objects.hashCode(command, exitCode, output); } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("command", command) .add("env", env) .add("exitCode", exitCode) .add("output", output) .toString(); } }