| /* |
| * 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); |
| } |
| } |