blob: b65e29bd180173566d57ee121085890070d9cbec [file] [log] [blame]
/*
* Copyright 2012-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.integration;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.facebook.buck.cli.Main;
import com.facebook.buck.util.CapturingPrintStream;
import com.facebook.buck.util.MoreFiles;
import com.facebook.buck.util.MoreStrings;
import com.facebook.buck.util.environment.Platform;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import com.martiansoftware.nailgun.NGContext;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import javax.annotation.Nullable;
/**
* {@link ProjectWorkspace} is a directory that contains a Buck project, complete with build files.
* <p>
* When {@link #setUp()} is invoked, the project files are cloned from a directory of testdata into
* a tmp directory according to the following rule:
* <ul>
* <li>Files with the {@code .expected} extension will not be copied.
* </ul>
* After {@link #setUp()} is invoked, the test should invoke Buck in that directory. As this is an
* integration test, we expect that files will be written as a result of invoking Buck.
* <p>
* After Buck has been run, invoke {@link #verify()} to verify that Buck wrote the correct files.
* For each file in the testdata directory with the {@code .expected} extension, {@link #verify()}
* will check that a file with the same relative path (but without the {@code .expected} extension)
* exists in the tmp directory. If not, {@link org.junit.Assert#fail()} will be invoked.
*/
public class ProjectWorkspace {
private static final String EXPECTED_SUFFIX = ".expected";
private static final Function<Path, Path> BUILD_FILE_RENAME = new Function<Path, Path>() {
@Override
@Nullable
public Path apply(Path path) {
String fileName = path.getFileName().toString();
if (fileName.endsWith(EXPECTED_SUFFIX)) {
return null;
} else {
return path;
}
}
};
private boolean isSetUp = false;
private final Path templatePath;
private final File destDir;
private final Path destPath;
/**
* @param templateDir The directory that contains the template version of the project.
* @param temporaryFolder The directory where the clone of the template directory should be
* written. By requiring a {@link TemporaryFolder} rather than a {@link File}, we can ensure
* that JUnit will clean up the test correctly.
*/
public ProjectWorkspace(File templateDir, DebuggableTemporaryFolder temporaryFolder) {
Preconditions.checkNotNull(templateDir);
Preconditions.checkNotNull(temporaryFolder);
this.templatePath = templateDir.toPath();
this.destDir = temporaryFolder.getRoot();
this.destPath = destDir.toPath();
}
/**
* This will copy the template directory, renaming files named {@code BUCK.test} to {@code BUCK}
* in the process. Files whose names end in {@code .expected} will not be copied.
*/
public void setUp() throws IOException {
MoreFiles.copyRecursively(templatePath, destPath, BUILD_FILE_RENAME);
if (Platform.detect() == Platform.WINDOWS) {
// Hack for symlinks on Windows.
SimpleFileVisitor<Path> copyDirVisitor = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
// On Windows, symbolic links from git repository are checked out as normal files
// containing a one-line path. In order to distinguish them, paths are read and pointed
// files are trued to locate. Once the pointed file is found, it will be copied to target.
// On NTFS length of path must be greater than 0 and less than 4096.
if (attrs.size() > 0 && attrs.size() <= 4096) {
File file = path.toFile();
String linkTo = Files.toString(file, Charsets.UTF_8);
File linkToFile = new File(templatePath.toFile(), linkTo);
if (linkToFile.isFile()) {
java.nio.file.Files.copy(
linkToFile.toPath(), path, StandardCopyOption.REPLACE_EXISTING);
} else if (linkToFile.isDirectory()) {
if (!file.delete()) {
throw new IOException();
}
MoreFiles.copyRecursively(linkToFile.toPath(), path);
}
}
return FileVisitResult.CONTINUE;
}
};
java.nio.file.Files.walkFileTree(destPath, copyDirVisitor);
}
isSetUp = true;
}
/**
* Runs Buck with the specified list of command-line arguments.
* @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(String... args) throws IOException {
assertTrue("setUp() must be run before this method is invoked", isSetUp);
CapturingPrintStream stdout = new CapturingPrintStream();
CapturingPrintStream stderr = new CapturingPrintStream();
Main main = new Main(stdout, stderr);
int exitCode = main.runMainWithExitCode(destDir, Optional.<NGContext>absent(), args);
return new ProcessResult(exitCode,
stdout.getContentsAsString(Charsets.UTF_8),
stderr.getContentsAsString(Charsets.UTF_8));
}
/**
* @return the {@link File} that corresponds to the {@code pathRelativeToProjectRoot}.
*/
public File getFile(String pathRelativeToProjectRoot) {
return new File(destDir, pathRelativeToProjectRoot);
}
public String getFileContents(String pathRelativeToProjectRoot) throws IOException {
return Files.toString(getFile(pathRelativeToProjectRoot), Charsets.UTF_8);
}
/**
* @return the specified path resolved against the root of this workspace.
*/
public Path resolve(Path pathRelativeToWorkspaceRoot) {
return destPath.resolve(pathRelativeToWorkspaceRoot);
}
/** The result of running {@code buck} from the command line. */
public static class ProcessResult {
private final int exitCode;
private final String stdout;
private final String stderr;
private ProcessResult(int exitCode, String stdout, String stderr) {
this.exitCode = exitCode;
this.stdout = Preconditions.checkNotNull(stdout);
this.stderr = Preconditions.checkNotNull(stderr);
}
/**
* Returns the exit code from the process.
* <p>
* Currently, this method is private because, in practice, any time a client might want to use
* it, it is more appropriate to use {@link #assertExitCode(String, int)} instead. If a valid
* use case arises, then we should make this getter public.
*/
private int getExitCode() {
return exitCode;
}
public String getStdout() {
return stdout;
}
public String getStderr() {
return stderr;
}
public void assertExitCode(int exitCode) {
assertExitCode(null, exitCode);
}
public void assertExitCode(@Nullable String message, int exitCode) {
if (exitCode == getExitCode()) {
return;
}
String failureMessage = String.format(
"Expected exit code %d but was %d.", exitCode, getExitCode());
if (message != null) {
failureMessage = message + " " + failureMessage;
}
System.err.println("=== " + failureMessage + " ===");
System.err.println("=== STDERR ===");
System.err.println(getStderr());
System.err.println("=== STDOUT ===");
System.err.println(getStdout());
fail(failureMessage);
}
}
/**
* For every file in the template directory whose name ends in {@code .expected}, checks that an
* equivalent file has been written in the same place under the destination directory.
*/
public void verify() throws IOException {
SimpleFileVisitor<Path> copyDirVisitor = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String fileName = file.getFileName().toString();
if (fileName.endsWith(EXPECTED_SUFFIX)) {
// Get File for the file that should be written, but without the ".expected" suffix.
Path generatedFileWithSuffix = destPath.resolve(templatePath.relativize(file));
File directory = generatedFileWithSuffix.getParent().toFile();
File observedFile = new File(directory, Files.getNameWithoutExtension(fileName));
if (!observedFile.isFile()) {
fail("Expected file " + observedFile + " could not be found.");
}
String expectedFileContent = Files.toString(file.toFile(), Charsets.UTF_8);
String observedFileContent = Files.toString(observedFile, Charsets.UTF_8);
observedFileContent = observedFileContent.replace("\r\n", "\n");
String cleanPathToObservedFile = MoreStrings.withoutSuffix(
templatePath.relativize(file).toString(), EXPECTED_SUFFIX);
assertEquals(
String.format(
"In %s, expected content of %s to match that of %s.",
cleanPathToObservedFile,
expectedFileContent,
observedFileContent),
expectedFileContent,
observedFileContent);
}
return FileVisitResult.CONTINUE;
}
};
java.nio.file.Files.walkFileTree(templatePath, copyDirVisitor);
}
}