/* * 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.io; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import com.facebook.buck.config.Config; import com.facebook.buck.config.ConfigBuilder; import com.facebook.buck.io.ProjectFilesystem.CopySourceMode; import com.facebook.buck.testutil.integration.TemporaryPaths; import com.facebook.buck.testutil.integration.ZipInspector; import com.facebook.buck.zip.Unzip; import com.facebook.buck.zip.ZipConstants; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Ordering; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.Date; import java.util.EnumSet; import java.util.Enumeration; import java.util.Optional; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.archivers.zip.ZipUtil; import org.hamcrest.Matchers; import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; public class ProjectFilesystemTest { @Rule public TemporaryPaths tmp = new TemporaryPaths(); private ProjectFilesystem filesystem; @Before public void setUp() throws InterruptedException { filesystem = new ProjectFilesystem(tmp.getRoot()); } @Test public void testIsFile() throws IOException { tmp.newFolder("foo"); tmp.newFile("foo/bar.txt"); assertTrue(filesystem.isFile(Paths.get("foo/bar.txt"))); assertFalse(filesystem.isFile(Paths.get("i_do_not_exist"))); assertFalse( "foo/ is a directory, but not an ordinary file", filesystem.isFile(Paths.get("foo"))); } @Test public void testSetLastModifiedTime() throws IOException { Path path = tmp.newFile("somefile"); filesystem.setLastModifiedTime(path, FileTime.fromMillis(0)); assertEquals(Files.getLastModifiedTime(path).toMillis(), 0); } @Test public void testIsDirectory() throws IOException { Path dir = tmp.newFolder("src"); Path file = tmp.newFile("BUCK"); assertTrue(filesystem.isDirectory(dir)); assertFalse(filesystem.isDirectory(file)); } @Test public void testMkdirsCanCreateNestedFolders() throws IOException { filesystem.mkdirs(new File("foo/bar/baz").toPath()); assertTrue(Files.isDirectory(tmp.getRoot().resolve("foo/bar/baz"))); } @Test public void testCreateNewFile() throws IOException { Path path = Paths.get("somefile"); filesystem.createNewFile(path); assertTrue(Files.exists(tmp.getRoot().toAbsolutePath().resolve(path))); } @Test public void testCreateParentDirs() throws IOException { Path pathRelativeToProjectRoot = Paths.get("foo/bar/baz.txt"); filesystem.createParentDirs(pathRelativeToProjectRoot); assertTrue(Files.isDirectory(tmp.getRoot().resolve("foo"))); assertTrue(Files.isDirectory(tmp.getRoot().resolve("foo/bar"))); assertFalse( "createParentDirs() should create directories, but not the leaf/file part of the path.", Files.exists(tmp.getRoot().resolve("foo/bar/baz.txt"))); } @Test(expected = NullPointerException.class) public void testReadFirstLineRejectsNullString() { filesystem.readFirstLine(/* pathRelativeToProjectRoot */ (String) null); } @Test(expected = NullPointerException.class) public void testReadFirstLineRejectsNullPath() { filesystem.readFirstLine(/* pathRelativeToProjectRoot */ (Path) null); } @Test public void testReadFirstLineToleratesNonExistentFile() { assertEquals(Optional.empty(), filesystem.readFirstLine("foo.txt")); } @Test public void testReadFirstLineWithEmptyFile() throws IOException { Path emptyFile = tmp.newFile("foo.txt"); Files.write(emptyFile, new byte[0]); assertTrue(Files.isRegularFile(emptyFile)); assertEquals(Optional.empty(), filesystem.readFirstLine("foo.txt")); } @Test public void testReadFirstLineFromMultiLineFile() throws IOException { Path multiLineFile = tmp.newFile("foo.txt"); Files.write(multiLineFile, "foo\nbar\nbaz\n".getBytes(UTF_8)); assertEquals(Optional.of("foo"), filesystem.readFirstLine("foo.txt")); } @Test public void testGetFileSize() throws IOException { Path wordsFile = tmp.newFile("words.txt"); String content = "Here\nare\nsome\nwords.\n"; Files.write(wordsFile, content.getBytes(UTF_8)); assertEquals(content.length(), filesystem.getFileSize(Paths.get("words.txt"))); } @Test(expected = IOException.class) public void testGetFileSizeThrowsForNonExistentFile() throws IOException { filesystem.getFileSize(Paths.get("words.txt")); } @Test public void testWriteLinesToPath() throws IOException { Iterable<String> lines = ImmutableList.of("foo", "bar", "baz"); filesystem.writeLinesToPath(lines, Paths.get("lines.txt")); String contents = new String(Files.readAllBytes(tmp.getRoot().resolve("lines.txt")), UTF_8); assertEquals("foo\nbar\nbaz\n", contents); } @Test public void testWriteBytesToPath() throws IOException { String content = "Hello, World!"; byte[] bytes = content.getBytes(UTF_8); filesystem.writeBytesToPath(bytes, Paths.get("hello.txt")); assertEquals( content, new String(Files.readAllBytes(tmp.getRoot().resolve("hello.txt")), UTF_8)); } @Test public void testCopyToPath() throws IOException { InputStream inputStream = new ByteArrayInputStream("Hello, world!".getBytes(UTF_8)); filesystem.copyToPath(inputStream, Paths.get("bytes.txt")); assertEquals( "The bytes on disk should match those from the InputStream.", "Hello, world!", new String(Files.readAllBytes(tmp.getRoot().resolve("bytes.txt")), UTF_8)); } @Test public void testCopyToPathWithOptions() throws IOException { InputStream inputStream = new ByteArrayInputStream("hello!".getBytes(UTF_8)); filesystem.copyToPath(inputStream, Paths.get("replace_me.txt")); inputStream = new ByteArrayInputStream("hello again!".getBytes(UTF_8)); filesystem.copyToPath( inputStream, Paths.get("replace_me.txt"), StandardCopyOption.REPLACE_EXISTING); assertEquals( "The bytes on disk should match those from the second InputStream.", "hello again!", new String(Files.readAllBytes(tmp.getRoot().resolve("replace_me.txt")), UTF_8)); } @Test public void testCopyFolder() throws IOException { // Build up a directory of dummy files. tmp.newFolder("src"); tmp.newFolder("src/com"); tmp.newFolder("src/com/example"); tmp.newFolder("src/com/example/foo"); tmp.newFile("src/com/example/foo/Foo.java"); tmp.newFile("src/com/example/foo/package.html"); tmp.newFolder("src/com/example/bar"); tmp.newFile("src/com/example/bar/Bar.java"); tmp.newFile("src/com/example/bar/package.html"); // Copy the contents of src/ to dest/. tmp.newFolder("dest"); filesystem.copyFolder(Paths.get("src"), Paths.get("dest")); assertTrue(Files.exists(tmp.getRoot().resolve("dest/com/example/foo/Foo.java"))); assertTrue(Files.exists(tmp.getRoot().resolve("dest/com/example/foo/package.html"))); assertTrue(Files.exists(tmp.getRoot().resolve("dest/com/example/bar/Bar.java"))); assertTrue(Files.exists(tmp.getRoot().resolve("dest/com/example/bar/package.html"))); } @Test public void testCopyFolderAndContents() throws IOException { // Build up a directory of dummy files. tmp.newFolder("src"); tmp.newFolder("src/com"); tmp.newFolder("src/com/example"); tmp.newFolder("src/com/example/foo"); tmp.newFile("src/com/example/foo/Foo.java"); tmp.newFile("src/com/example/foo/package.html"); tmp.newFolder("src/com/example/bar"); tmp.newFile("src/com/example/bar/Bar.java"); tmp.newFile("src/com/example/bar/package.html"); // Copy the contents of src/ to dest/ (including src itself). tmp.newFolder("dest"); filesystem.copy(Paths.get("src"), Paths.get("dest"), CopySourceMode.DIRECTORY_AND_CONTENTS); assertTrue(Files.exists(tmp.getRoot().resolve("dest/src/com/example/foo/Foo.java"))); assertTrue(Files.exists(tmp.getRoot().resolve("dest/src/com/example/foo/package.html"))); assertTrue(Files.exists(tmp.getRoot().resolve("dest/src/com/example/bar/Bar.java"))); assertTrue(Files.exists(tmp.getRoot().resolve("dest/src/com/example/bar/package.html"))); } @Test public void testCopyFile() throws IOException { tmp.newFolder("foo"); Path file = tmp.newFile("foo/bar.txt"); String content = "Hello, World!"; Files.write(file, content.getBytes(UTF_8)); filesystem.copyFile(Paths.get("foo/bar.txt"), Paths.get("foo/baz.txt")); assertEquals( content, new String(Files.readAllBytes(tmp.getRoot().resolve("foo/baz.txt")), UTF_8)); } @Test public void testDeleteFileAtPath() throws IOException { Path path = Paths.get("foo.txt"); Path file = tmp.newFile(path.toString()); assertTrue(Files.exists(file)); filesystem.deleteFileAtPath(path); assertFalse(Files.exists(file)); } @Test public void testWalkFileTreeWhenProjectRootIsNotWorkingDir() throws IOException { tmp.newFolder("dir"); tmp.newFile("dir/file.txt"); tmp.newFolder("dir/dir2"); tmp.newFile("dir/dir2/file2.txt"); final ImmutableList.Builder<String> fileNames = ImmutableList.builder(); filesystem.walkRelativeFileTree( Paths.get("dir"), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { fileNames.add(file.getFileName().toString()); return FileVisitResult.CONTINUE; } }); assertThat(fileNames.build(), containsInAnyOrder("file.txt", "file2.txt")); } @Test public void testWalkFileTreeWhenProjectRootIsWorkingDir() throws InterruptedException, IOException { ProjectFilesystem projectFilesystem = new ProjectFilesystem(Paths.get(".").toAbsolutePath()); final ImmutableList.Builder<String> fileNames = ImmutableList.builder(); Path pathRelativeToProjectRoot = Paths.get("test/com/facebook/buck/io/testdata/directory_traversal_ignore_paths"); projectFilesystem.walkRelativeFileTree( pathRelativeToProjectRoot, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { fileNames.add(file.getFileName().toString()); return FileVisitResult.CONTINUE; } }); assertThat( fileNames.build(), containsInAnyOrder("file", "a_file", "b_file", "b_c_file", "b_d_file")); } @Test public void testWalkFileTreeFollowsSymlinks() throws IOException { tmp.newFolder("dir"); tmp.newFile("dir/file.txt"); Files.createSymbolicLink(tmp.getRoot().resolve("linkdir"), tmp.getRoot().resolve("dir")); final ImmutableList.Builder<Path> filePaths = ImmutableList.builder(); filesystem.walkRelativeFileTree( Paths.get(""), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { filePaths.add(file); return FileVisitResult.CONTINUE; } }); assertThat( filePaths.build(), containsInAnyOrder(Paths.get("dir/file.txt"), Paths.get("linkdir/file.txt"))); } @Test public void testGetFilesUnderPath() throws IOException { tmp.newFile("file1"); tmp.newFolder("dir1"); tmp.newFile("dir1/file2"); tmp.newFolder("dir1/dir2"); tmp.newFile("dir1/dir2/file3"); assertThat( filesystem.getFilesUnderPath( Paths.get("dir1"), x -> true, EnumSet.noneOf(FileVisitOption.class)), containsInAnyOrder(Paths.get("dir1/file2"), Paths.get("dir1/dir2/file3"))); assertThat( filesystem.getFilesUnderPath( Paths.get("dir1"), Paths.get("dir1/dir2/file3")::equals, EnumSet.noneOf(FileVisitOption.class)), containsInAnyOrder(Paths.get("dir1/dir2/file3"))); assertThat( filesystem.getFilesUnderPath(Paths.get("dir1")), containsInAnyOrder(Paths.get("dir1/file2"), Paths.get("dir1/dir2/file3"))); assertThat( filesystem.getFilesUnderPath(Paths.get("dir1"), Paths.get("dir1/file2")::equals), containsInAnyOrder(Paths.get("dir1/file2"))); } @Test public void testCreateZipPreservesExecutablePermissions() throws IOException { // Create a empty executable file. Path exe = tmp.newFile("test.exe"); MoreFiles.makeExecutable(exe); // Archive it into a zipfile using `ProjectFileSystem.createZip`. Path zipFile = tmp.getRoot().resolve("test.zip"); filesystem.createZip(ImmutableList.of(exe), zipFile); // Now unpack the archive (using apache's common-compress, as it preserves // executable permissions) and verify that the archive entry has executable // permissions. try (ZipFile zip = new ZipFile(zipFile.toFile())) { Enumeration<ZipArchiveEntry> entries = zip.getEntries(); assertTrue(entries.hasMoreElements()); ZipArchiveEntry entry = entries.nextElement(); Set<PosixFilePermission> permissions = MorePosixFilePermissions.fromMode(entry.getExternalAttributes() >> 16); assertTrue(permissions.contains(PosixFilePermission.OWNER_EXECUTE)); assertFalse(entries.hasMoreElements()); } } @Test public void testCreateZipIgnoresMtimes() throws IOException { // Create a empty executable file. Path exe = tmp.newFile("foo"); // Archive it into a zipfile using `ProjectFileSystem.createZip`. Path zipFile = tmp.getRoot().resolve("test.zip"); filesystem.createZip(ImmutableList.of(exe), zipFile); // Iterate over each of the entries, expecting to see all zeros in the time fields. Date dosEpoch = new Date(ZipUtil.dosToJavaTime(ZipConstants.DOS_FAKE_TIME)); try (ZipInputStream is = new ZipInputStream(Files.newInputStream(zipFile))) { for (ZipEntry entry = is.getNextEntry(); entry != null; entry = is.getNextEntry()) { assertEquals(entry.getName(), dosEpoch, new Date(entry.getTime())); } } } @Test public void testCreateReadOnlyFileSetsPermissions() throws IOException { Assume.assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")); Path path = Paths.get("hello.txt"); ImmutableSet<PosixFilePermission> permissions = ImmutableSet.of( PosixFilePermission.OWNER_READ, PosixFilePermission.GROUP_READ, PosixFilePermission.OTHERS_READ); filesystem.writeContentsToPath( "hello world", path, PosixFilePermissions.asFileAttribute(permissions)); // The umask may restrict the actual permissions on the filesystem: // https://fburl.com/26569549 // So the best we can do is to check that the actual permissions are a // strict subset of the expected permissions. PosixFileAttributes attrs = filesystem.readAttributes(path, PosixFileAttributes.class); assertTrue(permissions.containsAll(attrs.permissions())); } @Test public void testCreateZip() throws IOException { tmp.newFolder("foo"); tmp.newFile("foo/bar.txt"); tmp.newFile("foo/baz.txt"); Path output = tmp.newFile("out.zip"); filesystem.createZip( ImmutableList.of(Paths.get("foo/bar.txt"), Paths.get("foo/baz.txt")), output); ZipInspector zipInspector = new ZipInspector(output); assertEquals(ImmutableSet.of("foo/bar.txt", "foo/baz.txt"), zipInspector.getZipFileEntries()); } @Test public void testCreateZipWithEmptyDir() throws IOException { tmp.newFolder("foo"); tmp.newFile("foo/bar.txt"); tmp.newFile("foo/baz.txt"); tmp.newFolder("empty"); Path output = tmp.newFile("out.zip"); filesystem.createZip( ImmutableList.of(Paths.get("foo/bar.txt"), Paths.get("foo/baz.txt"), Paths.get("empty")), output); ZipInspector zipInspector = new ZipInspector(output); assertEquals( ImmutableSet.of("foo/bar.txt", "foo/baz.txt", "empty/"), zipInspector.getZipFileEntries()); } @Test public void testGetZipContents() throws IOException { tmp.newFolder("foo"); tmp.newFile("foo/bar.txt"); tmp.newFile("foo/baz.txt"); Path output = tmp.newFile("out.zip"); filesystem.createZip( ImmutableList.of(Paths.get("foo/bar.txt"), Paths.get("foo/baz.txt")), output); ImmutableCollection<Path> actualContents = ImmutableSortedSet.copyOf(Unzip.getZipMembers(filesystem.resolve(output))); assertEquals( ImmutableSortedSet.of(Paths.get("foo/bar.txt"), Paths.get("foo/baz.txt")), actualContents); } @Test public void testIsSymLinkReturnsTrueForSymLink() throws IOException { Path rootPath = tmp.getRoot(); Files.createSymbolicLink(rootPath.resolve("foo"), rootPath.resolve("bar")); assertTrue(filesystem.isSymLink(Paths.get("foo"))); } @Test public void testIsSymLinkReturnsFalseForFile() throws IOException { tmp.newFile("foo"); assertFalse(filesystem.isSymLink(Paths.get("foo"))); } @Test public void testIsSymLinkReturnsFalseForNotExistent() throws IOException { assertFalse(filesystem.isSymLink(Paths.get("foo"))); } @Test public void testSortedDirectoryContents() throws IOException { tmp.newFolder("foo"); Path a = tmp.newFile("foo/a.txt"); Files.setLastModifiedTime(a, FileTime.fromMillis(1000)); Path b = tmp.newFile("foo/b.txt"); Files.setLastModifiedTime(b, FileTime.fromMillis(2000)); Path c = tmp.newFile("foo/c.txt"); Files.setLastModifiedTime(c, FileTime.fromMillis(3000)); tmp.newFile("foo/non-matching"); assertEquals( ImmutableSet.of(c, b, a), filesystem.getMtimeSortedMatchingDirectoryContents(Paths.get("foo"), "*.txt")); } @Test public void testExtractIgnorePaths() throws InterruptedException, IOException { Config config = ConfigBuilder.createFromText("[project]", "ignore = .git, foo, bar/, baz//, a/b/c"); Path rootPath = tmp.getRoot(); ProjectFilesystem filesystem = new ProjectFilesystem(rootPath, config); ImmutableSet<Path> ignorePaths = FluentIterable.from(filesystem.getIgnorePaths()) .filter(input -> input.getType() == PathOrGlobMatcher.Type.PATH) .transform(PathOrGlobMatcher::getPath) .toSet(); assertThat( ImmutableSortedSet.copyOf(Ordering.natural(), ignorePaths), equalTo( ImmutableSortedSet.of( filesystem.getBuckPaths().getBuckOut(), filesystem.getBuckPaths().getTrashDir(), Paths.get(".idea"), Paths.get(System.getProperty(ProjectFilesystem.BUCK_BUCKD_DIR_KEY, ".buckd")), filesystem.getBuckPaths().getCacheDir(), Paths.get(".git"), Paths.get("foo"), Paths.get("bar"), Paths.get("baz"), Paths.get("a/b/c")))); } @Test public void testExtractIgnorePathsWithCacheDir() throws InterruptedException, IOException { Config config = ConfigBuilder.createFromText("[cache]", "dir = cache_dir"); Path rootPath = tmp.getRoot(); ImmutableSet<Path> ignorePaths = FluentIterable.from(new ProjectFilesystem(rootPath, config).getIgnorePaths()) .filter(input -> input.getType() == PathOrGlobMatcher.Type.PATH) .transform(PathOrGlobMatcher::getPath) .toSet(); assertThat( "Cache directory should be in set of ignored paths", ignorePaths, Matchers.hasItem(Paths.get("cache_dir"))); } @Test public void ignoredPathsShouldBeIgnoredWhenWalkingTheFilesystem() throws InterruptedException, IOException { Config config = ConfigBuilder.createFromText("[project]", "ignore = **/*.orig"); ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot(), config); Files.createDirectories(tmp.getRoot().resolve("foo/bar")); filesystem.touch(Paths.get("foo/bar/cake.txt")); filesystem.touch(Paths.get("foo/bar/cake.txt.orig")); final ImmutableSet.Builder<String> allPaths = ImmutableSet.builder(); filesystem.walkRelativeFileTree( tmp.getRoot(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { allPaths.add(file.toString()); return FileVisitResult.CONTINUE; } }); ImmutableSet<String> found = allPaths.build(); assertTrue(found.contains(Paths.get("foo/bar/cake.txt").toString())); assertFalse(found.contains(Paths.get("foo/bar/cake.txt.orig").toString())); } @Test public void twoProjectFilesystemsWithSameIgnoreGlobsShouldBeEqual() throws InterruptedException, IOException { Config config = ConfigBuilder.createFromText("[project]", "ignore = **/*.orig"); Path rootPath = tmp.getRoot(); assertThat( "Two ProjectFilesystems with same glob in ignore should be equal", new ProjectFilesystem(rootPath, config), equalTo(new ProjectFilesystem(rootPath, config))); } @Test public void getPathReturnsPathWithCorrectFilesystem() throws InterruptedException, IOException { FileSystem vfs = Jimfs.newFileSystem(Configuration.unix()); Path root = vfs.getPath("/root"); Files.createDirectories(root); assertEquals(vfs, new ProjectFilesystem(root).getPath("bar").getFileSystem()); assertEquals(vfs.getPath("bar"), new ProjectFilesystem(root).getPath("bar")); } }