com.facebook.buck.testutil.endtoend.EndToEndWorkspace.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.testutil.endtoend.EndToEndWorkspace.java

Source

/*
 * Copyright 2018-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.testutil.endtoend;

import com.facebook.buck.testutil.AbstractWorkspace;
import com.facebook.buck.testutil.PlatformUtils;
import com.facebook.buck.testutil.ProcessResult;
import com.facebook.buck.testutil.TemporaryPaths;
import com.facebook.buck.testutil.TestConsole;
import com.facebook.buck.util.DefaultProcessExecutor;
import com.facebook.buck.util.ExitCode;
import com.facebook.buck.util.ProcessExecutor;
import com.facebook.buck.util.ProcessExecutor.LaunchedProcess;
import com.facebook.buck.util.ProcessExecutor.LaunchedProcessImpl;
import com.facebook.buck.util.ProcessExecutorParams;
import com.facebook.buck.util.ProcessHelper;
import com.facebook.buck.util.environment.EnvVariablesProvider;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

/**
 * {@link EndToEndWorkspace} provides an interface to create temporary workspaces for the use in
 * EndToEndTests. It implements junit {@link TestRule}, and so can be used by:
 *
 * <ul>
 *   <li>In test class, preceding the declaration of the Workspace with @Rule will automatically
 *       create and destroy the workspace before each test.
 *   <li>Calling {@code .setup} and {@code .teardown} before and after testing manually
 * </ul>
 */
public class EndToEndWorkspace extends AbstractWorkspace implements TestRule {
    private TemporaryPaths tempPath = new TemporaryPaths();
    private final ProcessExecutor processExecutor = new DefaultProcessExecutor(new TestConsole());
    private boolean ranWithBuckd = false;
    private final ProcessHelper processHelper = ProcessHelper.getInstance();

    private static final String TESTDATA_DIRECTORY = "testdata";
    private static final Long buildResultTimeoutMS = TimeUnit.SECONDS.toMillis(5);
    private Set<String> addedTemplates = new HashSet<>();

    private PlatformUtils platformUtils = PlatformUtils.getForPlatform();

    /**
     * Constructor for EndToEndWorkspace. Note that setup and teardown should be called in between
     * tests (easy way to do this is to use @Rule)
     */
    public EndToEndWorkspace() {
    }

    /**
     * Used for @Rule functionality so that EndToEndWorkspace can be automatically set up and torn
     * down before every test.
     *
     * @param base statement that contains the test
     * @param description a description of the test
     * @return transformed statement that sets up and tears down before and after the test
     */
    @Override
    public Statement apply(Statement base, Description description) {
        return statement(base);
    }

    /** Sets up TemporaryPaths for the test, and sets the Workspace's root to that path */
    public void setup() throws Throwable {
        this.tempPath.before();
        this.destPath = tempPath.getRoot();
        System.out.println("EndToEndWorkspace created");
    }

    /**
     * Tears down TemporaryPaths, and resets Workspace's roots to avoid polluting execution
     * environment accidentally.
     */
    public void teardown() {
        if (this.ranWithBuckd) {
            try {
                this.runBuckCommand("kill");
            } catch (Exception e) {
                // Nothing particularly better to do than log the error and swallow it
                System.err.println(e.getMessage());
            }
        }
        this.tempPath.after();
        this.destPath = null;
        System.out.println("EndToEndWorkspace torn down");
    }

    private Statement statement(Statement base) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                setup();
                try {
                    base.evaluate();
                } finally {
                    teardown();
                }
            }
        };
    }

    private ImmutableMap<String, String> overrideSystemEnvironment(boolean buckdEnabled,
            ImmutableMap<String, String> environmentOverrides) {
        ImmutableMap.Builder<String, String> environmentBuilder = ImmutableMap.builder();

        // Ensure that we make it look like we're not running from within buck. This is fine since
        // we're running in a completely different directory. Also make sure that NO_BUCKD is only
        // set if buckdEnabled is false
        ImmutableSet<String> keysToRemove = ImmutableSet.of("BUCK_ROOT_BUILD_ID", "NO_BUCKD");
        for (Map.Entry<String, String> entry : EnvVariablesProvider.getSystemEnv().entrySet()) {
            if (keysToRemove.contains(entry.getKey())) {
                continue;
            }
            environmentBuilder.put(entry.getKey(), entry.getValue());
        }
        if (!buckdEnabled) {
            environmentBuilder.put("NO_BUCKD", "1");
        }

        Map<String, String> finalEnvironment = new HashMap<>(environmentBuilder.build());
        finalEnvironment.putAll(environmentOverrides);
        return ImmutableMap.copyOf(finalEnvironment);
    }

    /**
     * Runs Buck with the specified list of command-line arguments, and the current system environment
     * variables.
     *
     * @param args to pass to {@code buck}, so that could be {@code ["build", "//path/to:target"]},
     *     {@code ["project"]}, etc.
     * @return the result of running Buck, which includes the exit code, stdout, and stderr.
     */
    @Override
    public ProcessResult runBuckCommand(String... args) throws Exception {
        ImmutableMap<String, String> environment = ImmutableMap.of();
        return runBuckCommand(environment, args);
    }

    /**
     * Launches Buck process (non-blocking) with the specified list of command-line arguments, and the
     * current system environment variables.
     *
     * @param args to pass to {@code buck}, so that could be {@code ["build", "//path/to:target"]},
     *     {@code ["project"]}, etc.
     * @return the launched buck process
     */
    public LaunchedProcess launchBuckCommandProcess(String... args) throws Exception {
        ImmutableMap<String, String> environment = ImmutableMap.of();
        return launchBuckCommandProcess(environment, args);
    }

    /**
     * Runs Buck with the specified list of command-line arguments, and the current system environment
     * variables.
     *
     * @param buckdEnabled determines whether the command is run with buckdEnabled or not
     * @param args to pass to {@code buck}, so that could be {@code ["build", "//path/to:target"]},
     *     {@code ["project"]}, etc.
     * @return the result of running Buck, which includes the exit code, stdout, and stderr.
     */
    public ProcessResult runBuckCommand(boolean buckdEnabled, String... args) throws Exception {
        ImmutableMap<String, String> environment = ImmutableMap.of();
        String[] templates = new String[] {};
        return runBuckCommand(buckdEnabled, environment, templates, args);
    }

    /**
     * Launches Buck process (non-blocking) with the specified list of command-line arguments, and the
     * current system environment variables.
     *
     * @param buckdEnabled determines whether the command is run with buckdEnabled or not
     * @param args to pass to {@code buck}, so that could be {@code ["build", "//path/to:target"]},
     *     {@code ["project"]}, etc.
     * @return the launched buck process
     */
    public LaunchedProcess launchBuckCommandProcess(boolean buckdEnabled, String... args) throws Exception {
        ImmutableMap<String, String> environment = ImmutableMap.of();
        String[] templates = new String[] {};
        return launchBuckCommandProcess(buckdEnabled, environment, templates, args);
    }

    /**
     * Runs Buck with the specified list of command-line arguments with the given map of environment
     * variables as overrides of the current system environment.
     *
     * @param environmentOverrides set of environment variables to override
     * @param args to pass to {@code buck}, so that could be {@code ["build", "//path/to:target"]},
     *     {@code ["project"]}, etc.
     * @return the result of running Buck, which includes the exit code, stdout, and stderr.
     */
    @Override
    public ProcessResult runBuckCommand(ImmutableMap<String, String> environmentOverrides, String... args)
            throws Exception {
        String[] templates = new String[] {};
        return runBuckCommand(false, environmentOverrides, templates, args);
    }

    /**
     * Launches Buck process (non-blocking) with the specified list of command-line arguments with the
     * given map of environment variables as overrides of the current system environment.
     *
     * @param environmentOverrides set of environment variables to override
     * @param args to pass to {@code buck}, so that could be {@code ["build", "//path/to:target"]},
     *     {@code ["project"]}, etc.
     * @return the launched buck process
     */
    public LaunchedProcess launchBuckCommandProcess(ImmutableMap<String, String> environmentOverrides,
            String... args) throws Exception {
        String[] templates = new String[] {};
        return launchBuckCommandProcess(false, environmentOverrides, templates, args);
    }

    /**
     * Runs buck given command-line arguments and environment variables as overrides of the current
     * system environment based on a given EndToEndTestDescriptor
     *
     * @param testDescriptor provides buck command arguments/environment variables
     * @return the result of running Buck, which includes the exit code, stdout, and stderr.
     */
    public ProcessResult runBuckCommand(EndToEndTestDescriptor testDescriptor) throws Exception {
        return runBuckCommand(testDescriptor.getBuckdEnabled(),
                ImmutableMap.copyOf(testDescriptor.getVariableMap()), testDescriptor.getTemplateSet(),
                testDescriptor.getFullCommand());
    }

    /**
     * Launches buck process (non-blocking) given command-line arguments and environment variables as
     * overrides of the current system environment based on a given EndToEndTestDescriptor
     *
     * @param testDescriptor provides buck command arguments/environment variables
     * @return the launched buck process
     */
    public LaunchedProcess launchBuckCommandProcess(EndToEndTestDescriptor testDescriptor) throws Exception {
        return launchBuckCommandProcess(testDescriptor.getBuckdEnabled(),
                ImmutableMap.copyOf(testDescriptor.getVariableMap()), testDescriptor.getTemplateSet(),
                testDescriptor.getFullCommand());
    }

    /**
     * Runs Buck with the specified list of command-line arguments with the given map of environment
     * variables as overrides of the current system environment. This command is blocking.
     *
     * @param buckdEnabled determines whether the command is run with buckdEnabled or not
     * @param environmentOverrides set of environment variables to override
     * @param templates is an array of premade templates to add to the workspace before running the
     *     command.
     * @param args to pass to {@code buck}, so that could be {@code ["build", "//path/to:target"]},
     *     {@code ["project"]}, etc.
     * @return the result of running Buck, which includes the exit code, stdout, and stderr.
     */
    public ProcessResult runBuckCommand(boolean buckdEnabled, ImmutableMap<String, String> environmentOverrides,
            String[] templates, String... args) throws Exception {
        System.out.println("Running buck command: " + String.join(" ", args));
        for (String template : templates) {
            this.addPremadeTemplate(template);
        }
        ImmutableList.Builder<String> commandBuilder = platformUtils.getBuckCommandBuilder();
        List<String> command = commandBuilder.addAll(ImmutableList.copyOf(args)).build();
        ranWithBuckd = ranWithBuckd || buckdEnabled;
        return runCommand(buckdEnabled, environmentOverrides, command, Optional.empty());
    }

    /**
     * Launches a Buck process (non-blocking) with the specified list of command-line arguments with
     * the given map of environment variables as overrides of the current system environment.
     *
     * @param buckdEnabled determines whether the command is run with buckdEnabled or not
     * @param environmentOverrides set of environment variables to override
     * @param templates is an array of premade templates to add to the workspace before running the
     *     command.
     * @param args to pass to {@code buck}, so that could be {@code ["build", "//path/to:target"]},
     *     {@code ["project"]}, etc.
     * @return the launched buck process
     */
    public LaunchedProcess launchBuckCommandProcess(boolean buckdEnabled,
            ImmutableMap<String, String> environmentOverrides, String[] templates, String... args)
            throws Exception {
        System.out.println("Launching buck command: " + String.join(" ", args));
        for (String template : templates) {
            this.addPremadeTemplate(template);
        }
        ImmutableList.Builder<String> commandBuilder = platformUtils.getBuckCommandBuilder();
        List<String> command = commandBuilder.addAll(ImmutableList.copyOf(args)).build();
        ranWithBuckd = ranWithBuckd || buckdEnabled;
        return launchCommandProcess(buckdEnabled, environmentOverrides, command);
    }

    private int getLaunchedPid(LaunchedProcess launchedProcess) {
        Preconditions.checkState(launchedProcess instanceof LaunchedProcessImpl);
        Process p = ((LaunchedProcessImpl) launchedProcess).process;
        return Math.toIntExact(processHelper.getPid(p));
    }

    /** Sends interrupt signal to the given launched process */
    public void sendSigInt(LaunchedProcess launchedProcess) throws IOException {
        Runtime.getRuntime().exec(String.format("kill -SIGINT %d", getLaunchedPid(launchedProcess)));
    }

    /**
     * Waits for given launched process to terminate, with 5 second timeout
     *
     * @return whether the process has terminated or not
     */
    public boolean waitForProcess(LaunchedProcess launchedProcess) throws InterruptedException {
        return !processExecutor.waitForLaunchedProcessWithTimeout(launchedProcess, 5000, Optional.empty())
                .isTimedOut();
    }

    /**
     * Runs a given built command and buckdEnabled/environmentOverride settings, and returns a {@link
     * ProcessResult} If an empty timeoutMS is given, then the command will have no timeout.
     */
    private ProcessResult runCommand(boolean buckdEnabled, ImmutableMap<String, String> environmentOverrides,
            List<String> command, Optional<Long> timeoutMS) throws Exception {
        ImmutableMap<String, String> environment = overrideSystemEnvironment(buckdEnabled, environmentOverrides);
        ProcessExecutorParams params = ProcessExecutorParams.builder().setCommand(command)
                .setEnvironment(environment).setDirectory(destPath.toAbsolutePath()).build();
        ProcessExecutor.Result result = processExecutor.launchAndExecute(params, /* context */ ImmutableMap.of(),
                /* options */ ImmutableSet.of(), /* stdin */ Optional.empty(), timeoutMS,
                /* timeoutHandler */ Optional.empty());
        ExitCode exitCode = Arrays.stream(ExitCode.values()).filter(e -> e.getCode() == result.getExitCode())
                .findAny().orElse(ExitCode.BUILD_ERROR);
        return new ProcessResult(exitCode, result.getStdout().orElse(""), result.getStderr().orElse(""),
                result.isTimedOut());
    }

    /**
     * Launches a given built command with buckdEnabled/environmentOverrides settings, and returns a
     * {@link com.facebook.buck.util.ProcessExecutor.LaunchedProcess}. This command does not block.
     */
    public LaunchedProcess launchCommandProcess(boolean buckdEnabled,
            ImmutableMap<String, String> environmentOverrides, List<String> command) throws Exception {
        ImmutableMap<String, String> environment = overrideSystemEnvironment(buckdEnabled, environmentOverrides);
        ProcessExecutorParams params = ProcessExecutorParams.builder().setCommand(command)
                .setEnvironment(environment).setDirectory(destPath.toAbsolutePath()).build();
        return processExecutor.launchProcess(params);
    }

    /** Replaces platform-specific placeholders configurations with their appropriate replacements */
    private void postAddPlatformConfiguration() {
        platformUtils.checkAssumptions();
    }

    /**
     * Runs the process that is built by the given fully qualified target name. The program should be
     * already built to run it.
     */
    public ProcessResult runBuiltResult(String target, String... args) throws Exception {
        String fullTarget = EndToEndHelper.getProperBuildTarget(target);
        ProcessResult targetsResult = runBuckCommand(ranWithBuckd, "targets", fullTarget, "--show-output");
        if (targetsResult.getExitCode().getCode() > 0) {
            throw new RuntimeException(
                    "buck targets command getting outputPath failed: " + targetsResult.getStderr());
        }
        String[] targetsOutput = targetsResult.getStdout().split(" ");
        if (targetsOutput.length != 2) {
            throw new IllegalStateException(
                    "Expect to receive single target and path pair from buck targets --show-output, got:"
                            + targetsResult.getStdout());
        }
        String outputPath = targetsOutput[1].replaceAll("[ \n]", "");
        ImmutableList.Builder<String> commandBuilder = platformUtils.getCommandBuilder();
        List<String> command = commandBuilder.add(outputPath).addAll(ImmutableList.copyOf(args)).build();
        return runCommand(ranWithBuckd, ImmutableMap.<String, String>builder().build(), command,
                Optional.of(buildResultTimeoutMS));
    }

    /**
     * Copies the template directory of a premade template, contained in endtoend/testdata, stripping
     * the suffix from files with suffix .fixture, and not copying files with suffix .expected
     */
    public void addPremadeTemplate(String templateName) throws Exception {
        if (addedTemplates.contains(templateName)) {
            return; // already added template
        }
        addedTemplates.add(templateName);
        URL testDataResource = getClass().getResource(TESTDATA_DIRECTORY);
        // If we're running this test in IJ, then this path doesn't exist. Fall back to one that does
        if (testDataResource != null && testDataResource.getPath().contains(".jar")) {
            this.addTemplateToWorkspace(testDataResource, templateName);
        } else {
            Path templatePath = FileSystems.getDefault().getPath("test", "com", "facebook", "buck", "testutil",
                    "endtoend", TESTDATA_DIRECTORY, templateName);
            addTemplateToWorkspace(templatePath);
        }
        postAddPlatformConfiguration();
    }
}