/*
 * 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.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
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;

/**
 * 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)
        .setEnvironment(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.absent(),
          testRuleTimeoutMs,
          /* timeOutHandler */ Optional.absent());
    } 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.or(-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;
  }

  public ImmutableMap<String, String> getEnv() {
    return env.isPresent() ? env.get() : null;
  }

  public Path getExitCode() {
    return exitCode;
  }

  public Path getOutput() {
    return output;
  }

  @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();
  }
}

@@ -3304,16 +3304,47 @@
 irectory
+.transform(filesystem::resolve)
 )%0A      

Fix (at least) Go tests