diff options
Diffstat (limited to 'src/test/java/com/google/devtools/build/lib/vfs')
27 files changed, 7653 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemConcurrencyTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemConcurrencyTest.java new file mode 100644 index 0000000000..c90de18082 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemConcurrencyTest.java @@ -0,0 +1,97 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import com.google.devtools.build.lib.testutil.TestUtils; +import com.google.devtools.build.lib.vfs.util.FileSystems; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.IOException; + +/** + * (Slow) tests of FileSystem under concurrency. + * + * These tests are nondeterministic but provide good coverage nonetheless. + */ +@RunWith(JUnit4.class) +public class FileSystemConcurrencyTest { + + Path workingDir; + + @Before + public void setUp() throws Exception { + FileSystem testFS = FileSystems.initDefaultAsNative(); + + // Resolve symbolic links in the temp dir: + workingDir = testFS.getPath(new File(TestUtils.tmpDir()).getCanonicalPath()); + } + + @Test + public void testConcurrentSymlinkModifications() throws Exception { + final Path xFile = workingDir.getRelative("file"); + FileSystemUtils.createEmptyFile(xFile); + + final Path xLinkToFile = workingDir.getRelative("link"); + + // "Boxed" for pass-by-reference. + final boolean[] run = { true }; + final IOException[] exception = { null }; + Thread createThread = new Thread() { + @Override + public void run() { + while (run[0]) { + if (!xLinkToFile.exists()) { + try { + xLinkToFile.createSymbolicLink(xFile); + } catch (IOException e) { + exception[0] = e; + return; + } + } + } + } + }; + Thread deleteThread = new Thread() { + @Override + public void run() { + while (run[0]) { + if (xLinkToFile.exists(Symlinks.NOFOLLOW)) { + try { + xLinkToFile.delete(); + } catch (IOException e) { + exception[0] = e; + return; + } + } + } + } + }; + createThread.start(); + deleteThread.start(); + Thread.sleep(1000); + run[0] = false; + createThread.join(0); + deleteThread.join(0); + + if (exception[0] != null) { + throw exception[0]; + } + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java new file mode 100644 index 0000000000..b6e88d8976 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java @@ -0,0 +1,1356 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; +import com.google.devtools.build.lib.testutil.MoreAsserts; +import com.google.devtools.build.lib.testutil.TestUtils; +import com.google.devtools.build.lib.util.Fingerprint; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class handles the generic tests that any filesystem must pass. + * + * <p>Each filesystem-test should inherit from this class, thereby obtaining + * all the tests. + */ +public abstract class FileSystemTest { + + private long savedTime; + protected FileSystem testFS; + protected boolean supportsSymlinks; + protected Path workingDir; + + // Some useful examples of various kinds of files (mnemonic: "x" = "eXample") + protected Path xNothing; + protected Path xFile; + protected Path xNonEmptyDirectory; + protected Path xNonEmptyDirectoryFoo; + protected Path xEmptyDirectory; + + @Before + public void setUp() throws Exception { + testFS = getFreshFileSystem(); + workingDir = testFS.getPath(getTestTmpDir()); + cleanUpWorkingDirectory(workingDir); + supportsSymlinks = testFS.supportsSymbolicLinks(); + + // % ls -lR + // -rw-rw-r-- xFile + // drwxrwxr-x xNonEmptyDirectory + // -rw-rw-r-- xNonEmptyDirectory/foo + // drwxrwxr-x xEmptyDirectory + + xNothing = absolutize("xNothing"); + xFile = absolutize("xFile"); + xNonEmptyDirectory = absolutize("xNonEmptyDirectory"); + xNonEmptyDirectoryFoo = xNonEmptyDirectory.getChild("foo"); + xEmptyDirectory = absolutize("xEmptyDirectory"); + + FileSystemUtils.createEmptyFile(xFile); + xNonEmptyDirectory.createDirectory(); + FileSystemUtils.createEmptyFile(xNonEmptyDirectoryFoo); + xEmptyDirectory.createDirectory(); + } + + @After + public void tearDown() throws Exception { + destroyFileSystem(testFS); + } + + /** + * Returns an instance of the file system to test. + */ + protected abstract FileSystem getFreshFileSystem() throws IOException; + + protected boolean isSymbolicLink(File file) { + return com.google.devtools.build.lib.unix.FilesystemUtils.isSymbolicLink(file); + } + + protected void setWritable(File file) throws IOException { + com.google.devtools.build.lib.unix.FilesystemUtils.setWritable(file); + } + + protected void setExecutable(File file) throws IOException { + com.google.devtools.build.lib.unix.FilesystemUtils.setExecutable(file); + } + + private static final Pattern STAT_SUBDIR_ERROR = Pattern.compile("(.*) \\(Not a directory\\)"); + + // Test that file is not present, using statIfFound. Base implementation throws an exception, but + // subclasses may override statIfFound to return null, in which case their tests should override + // this method. + @SuppressWarnings("unused") // Subclasses may throw. + protected void expectNotFound(Path path) throws IOException { + try { + assertNull(path.statIfFound()); + } catch (IOException e) { + // May be because of a non-directory path component. Parse exception to check this. + Matcher matcher = STAT_SUBDIR_ERROR.matcher(e.getMessage()); + if (!matcher.matches() || !path.getPathString().startsWith(matcher.group(1))) { + // Throw if this doesn't match what an ENOTDIR error looks like. + throw e; + } + } + } + + /** + * Removes all stuff from the test filesystem. + */ + protected void destroyFileSystem(FileSystem fileSystem) throws IOException { + Preconditions.checkArgument(fileSystem.equals(workingDir.getFileSystem())); + cleanUpWorkingDirectory(workingDir); + } + + /** + * Cleans up the working directory by removing everything. + */ + protected void cleanUpWorkingDirectory(Path workingPath) + throws IOException { + if (workingPath.exists()) { + removeEntireDirectory(workingPath.getPathFile()); // uses java.io.File! + } + FileSystemUtils.createDirectoryAndParents(workingPath); + } + + /** + * This function removes an entire directory and all of its contents. + * Much like rm -rf directoryToRemove + */ + protected void removeEntireDirectory(File directoryToRemove) + throws IOException { + // make sure that we do not remove anything outside the test directory + Path testDirPath = testFS.getPath(getTestTmpDir()); + if (!testFS.getPath(directoryToRemove.getAbsolutePath()).startsWith(testDirPath)) { + throw new IOException("trying to remove files outside of the testdata directory"); + } + // Some tests set the directories read-only and/or non-executable, so + // override that: + setWritable(directoryToRemove); + setExecutable(directoryToRemove); + + File[] files = directoryToRemove.listFiles(); + if (files != null) { + for (File currentFile : files) { + boolean isSymbolicLink = isSymbolicLink(currentFile); + if (!isSymbolicLink && currentFile.isDirectory()) { + removeEntireDirectory(currentFile); + } else { + if (!isSymbolicLink) { + setWritable(currentFile); + } + if (!currentFile.delete()) { + throw new IOException("Failed to delete '" + currentFile + "'"); + } + } + } + } + if (!directoryToRemove.delete()) { + throw new IOException("Failed to delete '" + directoryToRemove + "'"); + } + } + + /** + * Returns the directory to use as the FileSystem's working directory. + * Canonicalized to make tests hermetic against symbolic links in TEST_TMPDIR. + */ + protected final String getTestTmpDir() throws IOException { + return new File(TestUtils.tmpDir()).getCanonicalPath() + "/testdir"; + } + + /** + * Indirection to create links so we can test FileSystems that do not support + * link creation. For example, JavaFileSystemTest overrides this method + * and creates the link with an alternate FileSystem. + */ + protected void createSymbolicLink(Path link, Path target) throws IOException { + createSymbolicLink(link, target.asFragment()); + } + + /** + * Indirection to create links so we can test FileSystems that do not support + * link creation. For example, JavaFileSystemTest overrides this method + * and creates the link with an alternate FileSystem. + */ + protected void createSymbolicLink(Path link, PathFragment target) throws IOException { + link.createSymbolicLink(target); + } + + /** + * Indirection to setReadOnly(false) on FileSystems that do not + * support setReadOnly(false). For example, JavaFileSystemTest overrides this + * method and makes the Path writable with an alternate FileSystem. + */ + protected void makeWritable(Path target) throws IOException { + target.setWritable(true); + } + + /** + * Indirection to {@link Path#setExecutable(boolean)} on FileSystems that do + * not support setExecutable. For example, JavaFileSystemTest overrides this + * method and makes the Path executable with an alternate FileSystem. + */ + protected void setExecutable(Path target, boolean mode) throws IOException { + target.setExecutable(mode); + } + + // TODO(bazel-team): (2011) Put in a setLastModifiedTime into the various objects + // and clobber the current time of the object we're currently handling. + // Otherwise testing the thing might get a little hard, depending on the clock. + void storeReferenceTime(long timeToMark) { + savedTime = timeToMark; + } + + boolean isLaterThanreferenceTime(long testTime) { + return (savedTime <= testTime); + } + + Path getTestFile() throws IOException { + Path tempPath = absolutize("test-file"); + FileSystemUtils.createEmptyFile(tempPath); + return tempPath; + } + + protected Path absolutize(String relativePathName) { + return workingDir.getRelative(relativePathName); + } + + // Here the tests begin. + + @Test + public void testIsFileForNonexistingPath() { + Path nonExistingPath = testFS.getPath("/something/strange"); + assertFalse(nonExistingPath.isFile()); + } + + @Test + public void testIsDirectoryForNonexistingPath() { + Path nonExistingPath = testFS.getPath("/something/strange"); + assertFalse(nonExistingPath.isDirectory()); + } + + @Test + public void testIsLinkForNonexistingPath() { + Path nonExistingPath = testFS.getPath("/something/strange"); + assertFalse(nonExistingPath.isSymbolicLink()); + } + + @Test + public void testExistsForNonexistingPath() throws Exception { + Path nonExistingPath = testFS.getPath("/something/strange"); + assertFalse(nonExistingPath.exists()); + expectNotFound(nonExistingPath); + } + + @Test + public void testBadPermissionsThrowsExceptionOnStatIfFound() throws Exception { + Path inaccessible = absolutize("inaccessible"); + inaccessible.createDirectory(); + Path child = inaccessible.getChild("child"); + FileSystemUtils.createEmptyFile(child); + inaccessible.setExecutable(false); + assertFalse(child.exists()); + try { + child.statIfFound(); + fail(); + } catch (IOException expected) { + // Expected. + } + } + + @Test + public void testStatIfFoundReturnsNullForChildOfNonDir() throws Exception { + Path foo = absolutize("foo"); + foo.createDirectory(); + Path nonDir = foo.getRelative("bar"); + FileSystemUtils.createEmptyFile(nonDir); + assertNull(nonDir.getRelative("file").statIfFound()); + } + + // The following tests check the handling of the current working directory. + @Test + public void testCreatePathRelativeToWorkingDirectory() { + Path relativeCreatedPath = absolutize("some-file"); + Path expectedResult = workingDir.getRelative(new PathFragment("some-file")); + + assertEquals(expectedResult, relativeCreatedPath); + } + + // The following tests check the handling of the root directory + @Test + public void testRootIsDirectory() { + Path rootPath = testFS.getPath("/"); + assertTrue(rootPath.isDirectory()); + } + + @Test + public void testRootHasNoParent() { + Path rootPath = testFS.getPath("/"); + assertNull(rootPath.getParentDirectory()); + } + + // The following functions test the creation of files/links/directories. + @Test + public void testFileExists() throws Exception { + Path someFile = absolutize("some-file"); + FileSystemUtils.createEmptyFile(someFile); + assertTrue(someFile.exists()); + assertNotNull(someFile.statIfFound()); + } + + @Test + public void testFileIsFile() throws Exception { + Path someFile = absolutize("some-file"); + FileSystemUtils.createEmptyFile(someFile); + assertTrue(someFile.isFile()); + } + + @Test + public void testFileIsNotDirectory() throws Exception { + Path someFile = absolutize("some-file"); + FileSystemUtils.createEmptyFile(someFile); + assertFalse(someFile.isDirectory()); + } + + @Test + public void testFileIsNotSymbolicLink() throws Exception { + Path someFile = absolutize("some-file"); + FileSystemUtils.createEmptyFile(someFile); + assertFalse(someFile.isSymbolicLink()); + } + + @Test + public void testDirectoryExists() throws Exception { + Path someDirectory = absolutize("some-dir"); + someDirectory.createDirectory(); + assertTrue(someDirectory.exists()); + assertNotNull(someDirectory.statIfFound()); + } + + @Test + public void testDirectoryIsDirectory() throws Exception { + Path someDirectory = absolutize("some-dir"); + someDirectory.createDirectory(); + assertTrue(someDirectory.isDirectory()); + } + + @Test + public void testDirectoryIsNotFile() throws Exception { + Path someDirectory = absolutize("some-dir"); + someDirectory.createDirectory(); + assertFalse(someDirectory.isFile()); + } + + @Test + public void testDirectoryIsNotSymbolicLink() throws Exception { + Path someDirectory = absolutize("some-dir"); + someDirectory.createDirectory(); + assertFalse(someDirectory.isSymbolicLink()); + } + + @Test + public void testSymbolicFileLinkExists() throws Exception { + if (supportsSymlinks) { + Path someLink = absolutize("some-link"); + someLink.createSymbolicLink(xFile); + assertTrue(someLink.exists()); + assertNotNull(someLink.statIfFound()); + } + } + + @Test + public void testSymbolicFileLinkIsSymbolicLink() throws Exception { + if (supportsSymlinks) { + Path someLink = absolutize("some-link"); + someLink.createSymbolicLink(xFile); + assertTrue(someLink.isSymbolicLink()); + } + } + + @Test + public void testSymbolicFileLinkIsFile() throws Exception { + if (supportsSymlinks) { + Path someLink = absolutize("some-link"); + someLink.createSymbolicLink(xFile); + assertTrue(someLink.isFile()); + } + } + + @Test + public void testSymbolicFileLinkIsNotDirectory() throws Exception { + if (supportsSymlinks) { + Path someLink = absolutize("some-link"); + someLink.createSymbolicLink(xFile); + assertFalse(someLink.isDirectory()); + } + } + + @Test + public void testSymbolicDirLinkExists() throws Exception { + if (supportsSymlinks) { + Path someLink = absolutize("some-link"); + someLink.createSymbolicLink(xEmptyDirectory); + assertTrue(someLink.exists()); + assertNotNull(someLink.statIfFound()); + } + } + + @Test + public void testSymbolicDirLinkIsSymbolicLink() throws Exception { + if (supportsSymlinks) { + Path someLink = absolutize("some-link"); + someLink.createSymbolicLink(xEmptyDirectory); + assertTrue(someLink.isSymbolicLink()); + } + } + + @Test + public void testSymbolicDirLinkIsDirectory() throws Exception { + if (supportsSymlinks) { + Path someLink = absolutize("some-link"); + someLink.createSymbolicLink(xEmptyDirectory); + assertTrue(someLink.isDirectory()); + } + } + + @Test + public void testSymbolicDirLinkIsNotFile() throws Exception { + if (supportsSymlinks) { + Path someLink = absolutize("some-link"); + someLink.createSymbolicLink(xEmptyDirectory); + assertFalse(someLink.isFile()); + } + } + + @Test + public void testChildOfNonDirectory() throws Exception { + Path somePath = absolutize("file-name"); + FileSystemUtils.createEmptyFile(somePath); + Path childOfNonDir = somePath.getChild("child"); + assertFalse(childOfNonDir.exists()); + expectNotFound(childOfNonDir); + } + + @Test + public void testCreateDirectoryIsEmpty() throws Exception { + Path newPath = xEmptyDirectory.getChild("new-dir"); + newPath.createDirectory(); + assertEquals(newPath.getDirectoryEntries().size(), 0); + } + + @Test + public void testCreateDirectoryIsOnlyChildInParent() throws Exception { + Path newPath = xEmptyDirectory.getChild("new-dir"); + newPath.createDirectory(); + assertEquals(1, newPath.getParentDirectory().getDirectoryEntries().size()); + assertThat(newPath.getParentDirectory().getDirectoryEntries()).containsExactly(newPath); + } + + @Test + public void testCreateDirectories() throws Exception { + Path newPath = absolutize("new-dir/sub/directory"); + assertTrue(FileSystemUtils.createDirectoryAndParents(newPath)); + } + + @Test + public void testCreateDirectoriesIsDirectory() throws Exception { + Path newPath = absolutize("new-dir/sub/directory"); + FileSystemUtils.createDirectoryAndParents(newPath); + assertTrue(newPath.isDirectory()); + } + + @Test + public void testCreateDirectoriesIsNotFile() throws Exception { + Path newPath = absolutize("new-dir/sub/directory"); + FileSystemUtils.createDirectoryAndParents(newPath); + assertFalse(newPath.isFile()); + } + + @Test + public void testCreateDirectoriesIsNotSymbolicLink() throws Exception { + Path newPath = absolutize("new-dir/sub/directory"); + FileSystemUtils.createDirectoryAndParents(newPath); + assertFalse(newPath.isSymbolicLink()); + } + + @Test + public void testCreateDirectoriesIsEmpty() throws Exception { + Path newPath = absolutize("new-dir/sub/directory"); + FileSystemUtils.createDirectoryAndParents(newPath); + assertEquals(newPath.getDirectoryEntries().size(), 0); + } + + @Test + public void testCreateDirectoriesIsOnlyChildInParent() throws Exception { + Path newPath = absolutize("new-dir/sub/directory"); + FileSystemUtils.createDirectoryAndParents(newPath); + assertEquals(1, newPath.getParentDirectory().getDirectoryEntries().size()); + assertThat(newPath.getParentDirectory().getDirectoryEntries()).containsExactly(newPath); + } + + @Test + public void testCreateEmptyFileIsEmpty() throws Exception { + Path newPath = xEmptyDirectory.getChild("new-file"); + FileSystemUtils.createEmptyFile(newPath); + + assertEquals(newPath.getFileSize(), 0); + } + + @Test + public void testCreateFileIsOnlyChildInParent() throws Exception { + Path newPath = xEmptyDirectory.getChild("new-file"); + FileSystemUtils.createEmptyFile(newPath); + assertEquals(1, newPath.getParentDirectory().getDirectoryEntries().size()); + assertThat(newPath.getParentDirectory().getDirectoryEntries()).containsExactly(newPath); + } + + // The following functions test the behavior if errors occur during the + // creation of files/links/directories. + @Test + public void testCreateDirectoryWhereDirectoryAlreadyExists() throws Exception { + assertFalse(xEmptyDirectory.createDirectory()); + } + + @Test + public void testCreateDirectoryWhereFileAlreadyExists() { + try { + xFile.createDirectory(); + fail(); + } catch (IOException e) { + assertEquals(xFile + " (File exists)", e.getMessage()); + } + } + + @Test + public void testCannotCreateDirectoryWithoutExistingParent() throws Exception { + Path newPath = testFS.getPath("/deep/new-dir"); + try { + newPath.createDirectory(); + fail(); + } catch (FileNotFoundException e) { + MoreAsserts.assertEndsWith(" (No such file or directory)", e.getMessage()); + } + } + + @Test + public void testCannotCreateDirectoryWithReadOnlyParent() throws Exception { + xEmptyDirectory.setWritable(false); + Path xChildOfReadonlyDir = xEmptyDirectory.getChild("x"); + try { + xChildOfReadonlyDir.createDirectory(); + fail(); + } catch (IOException e) { + assertEquals(xChildOfReadonlyDir + " (Permission denied)", e.getMessage()); + } + } + + @Test + public void testCannotCreateFileWithoutExistingParent() throws Exception { + Path newPath = testFS.getPath("/non-existing-dir/new-file"); + try { + FileSystemUtils.createEmptyFile(newPath); + fail(); + } catch (FileNotFoundException e) { + MoreAsserts.assertEndsWith(" (No such file or directory)", e.getMessage()); + } + } + + @Test + public void testCannotCreateFileWithReadOnlyParent() throws Exception { + xEmptyDirectory.setWritable(false); + Path xChildOfReadonlyDir = xEmptyDirectory.getChild("x"); + try { + FileSystemUtils.createEmptyFile(xChildOfReadonlyDir); + fail(); + } catch (IOException e) { + assertEquals(xChildOfReadonlyDir + " (Permission denied)", e.getMessage()); + } + } + + @Test + public void testCannotCreateFileWithinFile() throws Exception { + Path newFilePath = absolutize("some-file"); + FileSystemUtils.createEmptyFile(newFilePath); + Path wrongPath = absolutize("some-file/new-file"); + try { + FileSystemUtils.createEmptyFile(wrongPath); + fail(); + } catch (IOException e) { + MoreAsserts.assertEndsWith(" (Not a directory)", e.getMessage()); + } + } + + @Test + public void testCannotCreateDirectoryWithinFile() throws Exception { + Path newFilePath = absolutize("some-file"); + FileSystemUtils.createEmptyFile(newFilePath); + Path wrongPath = absolutize("some-file/new-file"); + try { + wrongPath.createDirectory(); + fail(); + } catch (IOException e) { + MoreAsserts.assertEndsWith(" (Not a directory)", e.getMessage()); + } + } + + // Test directory contents + @Test + public void testCreateMultipleChildren() throws Exception { + Path theDirectory = absolutize("foo/"); + theDirectory.createDirectory(); + Path newPath1 = absolutize("foo/new-file-1"); + Path newPath2 = absolutize("foo/new-file-2"); + Path newPath3 = absolutize("foo/new-file-3"); + + FileSystemUtils.createEmptyFile(newPath1); + FileSystemUtils.createEmptyFile(newPath2); + FileSystemUtils.createEmptyFile(newPath3); + + assertThat(theDirectory.getDirectoryEntries()).containsExactly(newPath1, newPath2, newPath3); + } + + @Test + public void testGetDirectoryEntriesThrowsExceptionWhenRunOnFile() throws Exception { + try { + xFile.getDirectoryEntries(); + fail("No Exception thrown."); + } catch (IOException ex) { + if (ex instanceof FileNotFoundException) { + fail("The method should throw an object of class IOException."); + } + assertEquals(xFile + " (Not a directory)", ex.getMessage()); + } + } + + @Test + public void testGetDirectoryEntriesThrowsExceptionForNonexistingPath() { + Path somePath = testFS.getPath("/non-existing-path"); + try { + somePath.getDirectoryEntries(); + fail("FileNotFoundException not thrown."); + } catch (Exception x) { + assertEquals(somePath + " (No such file or directory)", x.getMessage()); + } + } + + // Test the removal of items + @Test + public void testDeleteDirectory() throws Exception { + assertTrue(xEmptyDirectory.delete()); + } + + @Test + public void testDeleteDirectoryIsNotDirectory() throws Exception { + xEmptyDirectory.delete(); + assertFalse(xEmptyDirectory.isDirectory()); + } + + @Test + public void testDeleteDirectoryParentSize() throws Exception { + int parentSize = workingDir.getDirectoryEntries().size(); + xEmptyDirectory.delete(); + assertEquals(workingDir.getDirectoryEntries().size(), parentSize - 1); + } + + @Test + public void testDeleteFile() throws Exception { + assertTrue(xFile.delete()); + } + + @Test + public void testDeleteFileIsNotFile() throws Exception { + xFile.delete(); + assertFalse(xEmptyDirectory.isFile()); + } + + @Test + public void testDeleteFileParentSize() throws Exception { + int parentSize = workingDir.getDirectoryEntries().size(); + xFile.delete(); + assertEquals(workingDir.getDirectoryEntries().size(), parentSize - 1); + } + + @Test + public void testDeleteRemovesCorrectFile() throws Exception { + Path newPath1 = xEmptyDirectory.getChild("new-file-1"); + Path newPath2 = xEmptyDirectory.getChild("new-file-2"); + Path newPath3 = xEmptyDirectory.getChild("new-file-3"); + + FileSystemUtils.createEmptyFile(newPath1); + FileSystemUtils.createEmptyFile(newPath2); + FileSystemUtils.createEmptyFile(newPath3); + + assertTrue(newPath2.delete()); + assertThat(xEmptyDirectory.getDirectoryEntries()).containsExactly(newPath1, newPath3); + } + + @Test + public void testDeleteNonExistingDir() throws Exception { + Path path = xEmptyDirectory.getRelative("non-existing-dir"); + assertFalse(path.delete()); + } + + @Test + public void testDeleteNotADirectoryPath() throws Exception { + Path path = xFile.getChild("new-file"); + assertFalse(path.delete()); + } + + // Here we test the situations where delete should throw exceptions. + @Test + public void testDeleteNonEmptyDirectoryThrowsException() throws Exception { + try { + xNonEmptyDirectory.delete(); + fail(); + } catch (IOException e) { + assertEquals(xNonEmptyDirectory + " (Directory not empty)", e.getMessage()); + } + } + + @Test + public void testDeleteNonEmptyDirectoryNotDeletedDirectory() throws Exception { + try { + xNonEmptyDirectory.delete(); + fail(); + } catch (IOException e) { + // Expected + } + + assertTrue(xNonEmptyDirectory.isDirectory()); + } + + @Test + public void testDeleteNonEmptyDirectoryNotDeletedFile() throws Exception { + try { + xNonEmptyDirectory.delete(); + fail(); + } catch (IOException e) { + // Expected + } + + assertTrue(xNonEmptyDirectoryFoo.isFile()); + } + + @Test + public void testCannotRemoveRoot() { + Path rootDirectory = testFS.getRootDirectory(); + try { + rootDirectory.delete(); + fail(); + } catch (IOException e) { + String msg = e.getMessage(); + assertTrue(String.format("got %s want EBUSY or ENOTEMPTY", msg), + msg.endsWith(" (Directory not empty)") + || msg.endsWith(" (Device or resource busy)") + || msg.endsWith(" (Is a directory)")); // Happens on OS X. + } + } + + // Test the date functions + @Test + public void testCreateFileChangesTimeOfDirectory() throws Exception { + storeReferenceTime(workingDir.getLastModifiedTime()); + Path newPath = absolutize("new-file"); + FileSystemUtils.createEmptyFile(newPath); + assertTrue(isLaterThanreferenceTime(workingDir.getLastModifiedTime())); + } + + @Test + public void testRemoveFileChangesTimeOfDirectory() throws Exception { + Path newPath = absolutize("new-file"); + FileSystemUtils.createEmptyFile(newPath); + storeReferenceTime(workingDir.getLastModifiedTime()); + newPath.delete(); + assertTrue(isLaterThanreferenceTime(workingDir.getLastModifiedTime())); + } + + // This test is a little bit strange, as we cannot test the progression + // of the time directly. As the Java time and the OS time are slightly different. + // Therefore, we first create an unrelated file to get a notion + // of the current OS time and use that as a baseline. + @Test + public void testCreateFileTimestamp() throws Exception { + Path syncFile = absolutize("sync-file"); + FileSystemUtils.createEmptyFile(syncFile); + + Path newFile = absolutize("new-file"); + storeReferenceTime(syncFile.getLastModifiedTime()); + FileSystemUtils.createEmptyFile(newFile); + assertTrue(isLaterThanreferenceTime(newFile.getLastModifiedTime())); + } + + @Test + public void testCreateDirectoryTimestamp() throws Exception { + Path syncFile = absolutize("sync-file"); + FileSystemUtils.createEmptyFile(syncFile); + + Path newPath = absolutize("new-dir"); + storeReferenceTime(syncFile.getLastModifiedTime()); + assertTrue(newPath.createDirectory()); + assertTrue(isLaterThanreferenceTime(newPath.getLastModifiedTime())); + } + + @Test + public void testWriteChangesModifiedTime() throws Exception { + storeReferenceTime(xFile.getLastModifiedTime()); + FileSystemUtils.writeContentAsLatin1(xFile, "abc19"); + assertTrue(isLaterThanreferenceTime(xFile.getLastModifiedTime())); + } + + @Test + public void testGetLastModifiedTimeThrowsExceptionForNonexistingPath() throws Exception { + Path newPath = testFS.getPath("/non-existing-dir"); + try { + newPath.getLastModifiedTime(); + fail("FileNotFoundException not thrown!"); + } catch (FileNotFoundException x) { + assertEquals(newPath + " (No such file or directory)", x.getMessage()); + } + } + + // Test file size + @Test + public void testFileSizeThrowsExceptionForNonexistingPath() throws Exception { + Path newPath = testFS.getPath("/non-existing-file"); + try { + newPath.getFileSize(); + fail("FileNotFoundException not thrown."); + } catch (FileNotFoundException e) { + assertEquals(newPath + " (No such file or directory)", e.getMessage()); + } + } + + @Test + public void testFileSizeAfterWrite() throws Exception { + String testData = "abc19"; + + FileSystemUtils.writeContentAsLatin1(xFile, testData); + assertEquals(testData.length(), xFile.getFileSize()); + } + + // Testing the input/output routines + @Test + public void testFileWriteAndReadAsLatin1() throws Exception { + String testData = "abc19"; + + FileSystemUtils.writeContentAsLatin1(xFile, testData); + String resultData = new String(FileSystemUtils.readContentAsLatin1(xFile)); + + assertEquals(testData,resultData); + } + + @Test + public void testInputAndOutputStreamEOF() throws Exception { + OutputStream outStream = xFile.getOutputStream(); + outStream.write(1); + outStream.close(); + + InputStream inStream = xFile.getInputStream(); + inStream.read(); + assertEquals(-1, inStream.read()); + inStream.close(); + } + + @Test + public void testInputAndOutputStream() throws Exception { + OutputStream outStream = xFile.getOutputStream(); + for (int i = 33; i < 126; i++) { + outStream.write(i); + } + outStream.close(); + + InputStream inStream = xFile.getInputStream(); + for (int i = 33; i < 126; i++) { + int readValue = inStream.read(); + assertEquals(i,readValue); + } + inStream.close(); + } + + @Test + public void testInputAndOutputStreamAppend() throws Exception { + OutputStream outStream = xFile.getOutputStream(); + for (int i = 33; i < 126; i++) { + outStream.write(i); + } + outStream.close(); + + OutputStream appendOut = xFile.getOutputStream(true); + for (int i = 126; i < 155; i++) { + appendOut.write(i); + } + appendOut.close(); + + InputStream inStream = xFile.getInputStream(); + for (int i = 33; i < 155; i++) { + int readValue = inStream.read(); + assertEquals(i,readValue); + } + inStream.close(); + } + + @Test + public void testInputAndOutputStreamNoAppend() throws Exception { + OutputStream outStream = xFile.getOutputStream(); + outStream.write(1); + outStream.close(); + + OutputStream noAppendOut = xFile.getOutputStream(false); + noAppendOut.close(); + + InputStream inStream = xFile.getInputStream(); + assertEquals(-1, inStream.read()); + inStream.close(); + } + + @Test + public void testGetOutputStreamCreatesFile() throws Exception { + Path newFile = absolutize("does_not_exist_yet.txt"); + + OutputStream out = newFile.getOutputStream(); + out.write(42); + out.close(); + + assertTrue(newFile.isFile()); + } + + @Test + public void testInpuStreamThrowExceptionOnDirectory() throws Exception { + try { + xEmptyDirectory.getOutputStream(); + fail("The Exception was not thrown!"); + } catch (IOException ex) { + assertEquals(xEmptyDirectory + " (Is a directory)", ex.getMessage()); + } + } + + @Test + public void testOutputStreamThrowExceptionOnDirectory() throws Exception { + try { + xEmptyDirectory.getInputStream(); + fail("The Exception was not thrown!"); + } catch (IOException ex) { + assertEquals(xEmptyDirectory + " (Is a directory)", ex.getMessage()); + } + } + + // Test renaming + @Test + public void testCanRenameToUnusedName() throws Exception { + xFile.renameTo(xNothing); + assertFalse(xFile.exists()); + assertTrue(xNothing.isFile()); + } + + @Test + public void testCanRenameFileToExistingFile() throws Exception { + Path otherFile = absolutize("otherFile"); + FileSystemUtils.createEmptyFile(otherFile); + xFile.renameTo(otherFile); // succeeds + assertFalse(xFile.exists()); + assertTrue(otherFile.isFile()); + } + + @Test + public void testCanRenameDirToExistingEmptyDir() throws Exception { + xNonEmptyDirectory.renameTo(xEmptyDirectory); // succeeds + assertFalse(xNonEmptyDirectory.exists()); + assertTrue(xEmptyDirectory.isDirectory()); + assertFalse(xEmptyDirectory.getDirectoryEntries().isEmpty()); + } + + @Test + public void testCantRenameDirToExistingNonEmptyDir() throws Exception { + try { + xEmptyDirectory.renameTo(xNonEmptyDirectory); + fail(); + } catch (IOException e) { + MoreAsserts.assertEndsWith(" (Directory not empty)", e.getMessage()); + } + } + + @Test + public void testCantRenameDirToExistingNonEmptyDirNothingChanged() throws Exception { + try { + xEmptyDirectory.renameTo(xNonEmptyDirectory); + fail(); + } catch (IOException e) { + // Expected + } + + assertTrue(xNonEmptyDirectory.isDirectory()); + assertTrue(xEmptyDirectory.isDirectory()); + assertTrue(xEmptyDirectory.getDirectoryEntries().isEmpty()); + assertFalse(xNonEmptyDirectory.getDirectoryEntries().isEmpty()); + } + + @Test + public void testCantRenameDirToExistingFile() { + try { + xEmptyDirectory.renameTo(xFile); + fail(); + } catch (IOException e) { + assertEquals(xEmptyDirectory + " -> " + xFile + " (Not a directory)", e.getMessage()); + } + } + + @Test + public void testCantRenameDirToExistingFileNothingChanged() { + try { + xEmptyDirectory.renameTo(xFile); + fail(); + } catch (IOException e) { + // Expected + } + + assertTrue(xEmptyDirectory.isDirectory()); + assertTrue(xFile.isFile()); + } + + @Test + public void testCantRenameFileToExistingDir() { + try { + xFile.renameTo(xEmptyDirectory); + fail(); + } catch (IOException e) { + assertEquals(xFile + " -> " + xEmptyDirectory + " (Is a directory)", + e.getMessage()); + } + } + + @Test + public void testCantRenameFileToExistingDirNothingChanged() { + try { + xFile.renameTo(xEmptyDirectory); + fail(); + } catch (IOException e) { + // Expected + } + + assertTrue(xEmptyDirectory.isDirectory()); + assertTrue(xFile.isFile()); + } + + @Test + public void testMoveOnNonExistingFileThrowsException() throws Exception { + Path nonExistingPath = absolutize("non-existing"); + Path targetPath = absolutize("does-not-matter"); + try { + nonExistingPath.renameTo(targetPath); + fail(); + } catch (FileNotFoundException e) { + MoreAsserts.assertEndsWith(" (No such file or directory)", e.getMessage()); + } + } + + // Test the Paths + @Test + public void testGetPathOnlyAcceptsAbsolutePath() { + try { + testFS.getPath("not-absolute"); + fail("The expected Exception was not thrown."); + } catch (IllegalArgumentException ex) { + assertEquals("not-absolute (not an absolute path)", ex.getMessage()); + } + } + + @Test + public void testGetPathOnlyAcceptsAbsolutePathFragment() { + try { + testFS.getPath(new PathFragment("not-absolute")); + fail("The expected Exception was not thrown."); + } catch (IllegalArgumentException ex) { + assertEquals("not-absolute (not an absolute path)", ex.getMessage()); + } + } + + // Test the access permissions + @Test + public void testNewFilesAreWritable() throws Exception { + assertTrue(xFile.isWritable()); + } + + @Test + public void testNewFilesAreReadable() throws Exception { + assertTrue(xFile.isReadable()); + } + + @Test + public void testNewDirsAreWritable() throws Exception { + assertTrue(xEmptyDirectory.isWritable()); + } + + @Test + public void testNewDirsAreReadable() throws Exception { + assertTrue(xEmptyDirectory.isReadable()); + } + + @Test + public void testNewDirsAreExecutable() throws Exception { + assertTrue(xEmptyDirectory.isExecutable()); + } + + @Test + public void testCannotGetExecutableOnNonexistingFile() throws Exception { + try { + xNothing.isExecutable(); + fail("No exception thrown."); + } catch (FileNotFoundException ex) { + assertEquals(xNothing + " (No such file or directory)", ex.getMessage()); + } + } + + @Test + public void testCannotSetExecutableOnNonexistingFile() throws Exception { + try { + xNothing.setExecutable(true); + fail("No exception thrown."); + } catch (FileNotFoundException ex) { + assertEquals(xNothing + " (No such file or directory)", ex.getMessage()); + } + } + + @Test + public void testCannotGetWritableOnNonexistingFile() throws Exception { + try { + xNothing.isWritable(); + fail("No exception thrown."); + } catch (FileNotFoundException ex) { + assertEquals(xNothing + " (No such file or directory)", ex.getMessage()); + } + } + + @Test + public void testCannotSetWritableOnNonexistingFile() throws Exception { + try { + xNothing.setWritable(false); + fail("No exception thrown."); + } catch (FileNotFoundException ex) { + assertEquals(xNothing + " (No such file or directory)", ex.getMessage()); + } + } + + @Test + public void testSetReadableOnFile() throws Exception { + xFile.setReadable(false); + assertFalse(xFile.isReadable()); + xFile.setReadable(true); + assertTrue(xFile.isReadable()); + } + + @Test + public void testSetWritableOnFile() throws Exception { + xFile.setWritable(false); + assertFalse(xFile.isWritable()); + xFile.setWritable(true); + assertTrue(xFile.isWritable()); + } + + @Test + public void testSetExecutableOnFile() throws Exception { + xFile.setExecutable(true); + assertTrue(xFile.isExecutable()); + xFile.setExecutable(false); + assertFalse(xFile.isExecutable()); + } + + @Test + public void testSetExecutableOnDirectory() throws Exception { + setExecutable(xNonEmptyDirectory, false); + + try { + // We can't map names->inodes in a non-executable directory: + xNonEmptyDirectoryFoo.isWritable(); // i.e. stat + fail(); + } catch (IOException e) { + MoreAsserts.assertEndsWith(" (Permission denied)", e.getMessage()); + } + } + + @Test + public void testWritingToReadOnlyFileThrowsException() throws Exception { + xFile.setWritable(false); + try { + FileSystemUtils.writeContent(xFile, "hello, world!".getBytes()); + fail("No exception thrown."); + } catch (IOException e) { + assertEquals(xFile + " (Permission denied)", e.getMessage()); + } + } + + @Test + public void testReadingFromUnreadableFileThrowsException() throws Exception { + FileSystemUtils.writeContent(xFile, "hello, world!".getBytes()); + xFile.setReadable(false); + try { + FileSystemUtils.readContent(xFile); + fail("No exception thrown."); + } catch (IOException e) { + assertEquals(xFile + " (Permission denied)", e.getMessage()); + } + } + + @Test + public void testCannotCreateFileInReadOnlyDirectory() throws Exception { + Path xNonEmptyDirectoryBar = xNonEmptyDirectory.getChild("bar"); + xNonEmptyDirectory.setWritable(false); + + try { + FileSystemUtils.createEmptyFile(xNonEmptyDirectoryBar); + fail("No exception thrown."); + } catch (IOException e) { + assertEquals(xNonEmptyDirectoryBar + " (Permission denied)", e.getMessage()); + } + } + + @Test + public void testCannotCreateDirectoryInReadOnlyDirectory() throws Exception { + Path xNonEmptyDirectoryBar = xNonEmptyDirectory.getChild("bar"); + xNonEmptyDirectory.setWritable(false); + + try { + xNonEmptyDirectoryBar.createDirectory(); + fail("No exception thrown."); + } catch (IOException e) { + assertEquals(xNonEmptyDirectoryBar + " (Permission denied)", e.getMessage()); + } + } + + @Test + public void testCannotMoveIntoReadOnlyDirectory() throws Exception { + Path xNonEmptyDirectoryBar = xNonEmptyDirectory.getChild("bar"); + xNonEmptyDirectory.setWritable(false); + + try { + xFile.renameTo(xNonEmptyDirectoryBar); + fail("No exception thrown."); + } catch (IOException e) { + MoreAsserts.assertEndsWith(" (Permission denied)", e.getMessage()); + } + } + + @Test + public void testCannotMoveFromReadOnlyDirectory() throws Exception { + xNonEmptyDirectory.setWritable(false); + + try { + xNonEmptyDirectoryFoo.renameTo(xNothing); + fail("No exception thrown."); + } catch (IOException e) { + MoreAsserts.assertEndsWith(" (Permission denied)", e.getMessage()); + } + } + + @Test + public void testCannotDeleteInReadOnlyDirectory() throws Exception { + xNonEmptyDirectory.setWritable(false); + + try { + xNonEmptyDirectoryFoo.delete(); + fail("No exception thrown."); + } catch (IOException e) { + assertEquals(xNonEmptyDirectoryFoo + " (Permission denied)", e.getMessage()); + } + } + + @Test + public void testCannotCreatSymbolicLinkInReadOnlyDirectory() throws Exception { + Path xNonEmptyDirectoryBar = xNonEmptyDirectory.getChild("bar"); + xNonEmptyDirectory.setWritable(false); + + if (supportsSymlinks) { + try { + createSymbolicLink(xNonEmptyDirectoryBar, xNonEmptyDirectoryFoo); + fail("No exception thrown."); + } catch (IOException e) { + assertEquals(xNonEmptyDirectoryBar + " (Permission denied)", e.getMessage()); + } + } + } + + @Test + public void testGetMD5DigestForEmptyFile() throws Exception { + Fingerprint fp = new Fingerprint(); + fp.addBytes(new byte[0]); + assertEquals(BaseEncoding.base16().lowerCase().encode(xFile.getMD5Digest()), + fp.hexDigestAndReset()); + } + + @Test + public void testGetMD5Digest() throws Exception { + byte[] buffer = new byte[500000]; + for (int i = 0; i < buffer.length; ++i) { + buffer[i] = 1; + } + FileSystemUtils.writeContent(xFile, buffer); + Fingerprint fp = new Fingerprint(); + fp.addBytes(buffer); + assertEquals(BaseEncoding.base16().lowerCase().encode(xFile.getMD5Digest()), + fp.hexDigestAndReset()); + } + + @Test + public void testStatFailsFastOnNonExistingFiles() throws Exception { + try { + xNothing.stat(); + fail("Expected IOException"); + } catch(IOException e) { + // Do nothing. + } + } + + @Test + public void testStatNullableFailsFastOnNonExistingFiles() throws Exception { + assertNull(xNothing.statNullable()); + } + + @Test + public void testResolveSymlinks() throws Exception { + if (supportsSymlinks) { + createSymbolicLink(xNothing, xFile); + FileSystemUtils.createEmptyFile(xFile); + assertEquals(xFile.asFragment(), testFS.resolveOneLink(xNothing)); + assertEquals(xFile, xNothing.resolveSymbolicLinks()); + } + } + + @Test + public void testResolveNonSymlinks() throws Exception { + if (supportsSymlinks) { + assertEquals(null, testFS.resolveOneLink(xFile)); + assertEquals(xFile, xFile.resolveSymbolicLinks()); + } + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java new file mode 100644 index 0000000000..21ca39b8f0 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java @@ -0,0 +1,878 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.appendWithoutExtension; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.commonAncestor; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.copyFile; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.copyTool; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.createDirectoryAndParents; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.deleteTree; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.deleteTreesBelowNotPrefixed; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.longestPathPrefix; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.plantLinkForest; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.relativePath; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.removeExtension; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.touchFile; +import static com.google.devtools.build.lib.vfs.FileSystemUtils.traverseTree; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.devtools.build.lib.testutil.BlazeTestUtils; +import com.google.devtools.build.lib.testutil.ManualClock; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * This class tests the file system utilities. + */ +@RunWith(JUnit4.class) +public class FileSystemUtilsTest { + private ManualClock clock; + private FileSystem fileSystem; + private Path workingDir; + + @Before + public void setUp() throws Exception { + clock = new ManualClock(); + fileSystem = new InMemoryFileSystem(clock); + workingDir = fileSystem.getPath("/workingDir"); + } + + Path topDir; + Path file1; + Path file2; + Path aDir; + Path file3; + Path innerDir; + Path link1; + Path dirLink; + Path file4; + + /* + * Build a directory tree that looks like: + * top-dir/ + * file-1 + * file-2 + * a-dir/ + * file-3 + * inner-dir/ + * link-1 => file-4 + * dir-link => a-dir + * file-4 + */ + private void createTestDirectoryTree() throws IOException { + topDir = fileSystem.getPath("/top-dir"); + file1 = fileSystem.getPath("/top-dir/file-1"); + file2 = fileSystem.getPath("/top-dir/file-2"); + aDir = fileSystem.getPath("/top-dir/a-dir"); + file3 = fileSystem.getPath("/top-dir/a-dir/file-3"); + innerDir = fileSystem.getPath("/top-dir/a-dir/inner-dir"); + link1 = fileSystem.getPath("/top-dir/a-dir/inner-dir/link-1"); + dirLink = fileSystem.getPath("/top-dir/a-dir/inner-dir/dir-link"); + file4 = fileSystem.getPath("/file-4"); + + topDir.createDirectory(); + FileSystemUtils.createEmptyFile(file1); + FileSystemUtils.createEmptyFile(file2); + aDir.createDirectory(); + FileSystemUtils.createEmptyFile(file3); + innerDir.createDirectory(); + link1.createSymbolicLink(file4); // simple symlink + dirLink.createSymbolicLink(aDir); // creates link loop + FileSystemUtils.createEmptyFile(file4); + } + + private void checkTestDirectoryTreesBelow(Path toPath) throws IOException { + Path copiedFile1 = toPath.getChild("file-1"); + assertTrue(copiedFile1.exists()); + assertTrue(copiedFile1.isFile()); + + Path copiedFile2 = toPath.getChild("file-2"); + assertTrue(copiedFile2.exists()); + assertTrue(copiedFile2.isFile()); + + Path copiedADir = toPath.getChild("a-dir"); + assertTrue(copiedADir.exists()); + assertTrue(copiedADir.isDirectory()); + Collection<Path> aDirEntries = copiedADir.getDirectoryEntries(); + assertEquals(2, aDirEntries.size()); + + Path copiedFile3 = copiedADir.getChild("file-3"); + assertTrue(copiedFile3.exists()); + assertTrue(copiedFile3.isFile()); + + Path copiedInnerDir = copiedADir.getChild("inner-dir"); + assertTrue(copiedInnerDir.exists()); + assertTrue(copiedInnerDir.isDirectory()); + + Path copiedLink1 = copiedInnerDir.getChild("link-1"); + assertTrue(copiedLink1.exists()); + assertTrue(copiedLink1.isSymbolicLink()); + assertEquals(copiedLink1.resolveSymbolicLinks(), file4); + + Path copiedDirLink = copiedInnerDir.getChild("dir-link"); + assertTrue(copiedDirLink.exists()); + assertTrue(copiedDirLink.isSymbolicLink()); + assertEquals(copiedDirLink.resolveSymbolicLinks(), aDir); + } + + // tests + + @Test + public void testChangeModtime() throws IOException { + Path file = fileSystem.getPath("/my-file"); + try { + BlazeTestUtils.changeModtime(file); + fail(); + } catch (FileNotFoundException e) { + /* ok */ + } + FileSystemUtils.createEmptyFile(file); + long prevMtime = file.getLastModifiedTime(); + BlazeTestUtils.changeModtime(file); + assertFalse(prevMtime == file.getLastModifiedTime()); + } + + @Test + public void testCommonAncestor() { + assertEquals(topDir, commonAncestor(topDir, topDir)); + assertEquals(topDir, commonAncestor(file1, file3)); + assertEquals(topDir, commonAncestor(file1, dirLink)); + } + + @Test + public void testRelativePath() throws IOException { + createTestDirectoryTree(); + assertEquals("file-1", relativePath(topDir, file1).getPathString()); + assertEquals(".", relativePath(topDir, topDir).getPathString()); + assertEquals("a-dir/inner-dir/dir-link", relativePath(topDir, dirLink).getPathString()); + assertEquals("../file-4", relativePath(topDir, file4).getPathString()); + assertEquals("../../../file-4", relativePath(innerDir, file4).getPathString()); + } + + private String longestPathPrefixStr(String path, String... prefixStrs) { + Set<PathFragment> prefixes = new HashSet<>(); + for (String prefix : prefixStrs) { + prefixes.add(new PathFragment(prefix)); + } + PathFragment longest = longestPathPrefix(new PathFragment(path), prefixes); + return longest != null ? longest.getPathString() : null; + } + + @Test + public void testLongestPathPrefix() { + assertEquals("A", longestPathPrefixStr("A/b", "A", "B")); // simple parent + assertEquals("A", longestPathPrefixStr("A", "A", "B")); // self + assertEquals("A/B", longestPathPrefixStr("A/B/c", "A", "A/B")); // want longest + assertNull(longestPathPrefixStr("C/b", "A", "B")); // not found in other parents + assertNull(longestPathPrefixStr("A", "A/B", "B")); // not found in child + assertEquals("A/B/C", longestPathPrefixStr("A/B/C/d/e/f.h", "A/B/C", "B/C/d")); + } + + @Test + public void testRemoveExtension_Strings() throws Exception { + assertEquals("foo", removeExtension("foo.c")); + assertEquals("a/foo", removeExtension("a/foo.c")); + assertEquals("a.b/foo", removeExtension("a.b/foo")); + assertEquals("foo", removeExtension("foo")); + assertEquals("foo", removeExtension("foo.")); + } + + @Test + public void testRemoveExtension_Paths() throws Exception { + assertPath("/foo", removeExtension(fileSystem.getPath("/foo.c"))); + assertPath("/a/foo", removeExtension(fileSystem.getPath("/a/foo.c"))); + assertPath("/a.b/foo", removeExtension(fileSystem.getPath("/a.b/foo"))); + assertPath("/foo", removeExtension(fileSystem.getPath("/foo"))); + assertPath("/foo", removeExtension(fileSystem.getPath("/foo."))); + } + + private static void assertPath(String expected, PathFragment actual) { + assertEquals(expected, actual.getPathString()); + } + + private static void assertPath(String expected, Path actual) { + assertEquals(expected, actual.getPathString()); + } + + @Test + public void testReplaceExtension_Path() throws Exception { + assertPath("/foo/bar.baz", + FileSystemUtils.replaceExtension(fileSystem.getPath("/foo/bar"), ".baz")); + assertPath("/foo/bar.baz", + FileSystemUtils.replaceExtension(fileSystem.getPath("/foo/bar.cc"), ".baz")); + assertPath("/foo.baz", FileSystemUtils.replaceExtension(fileSystem.getPath("/foo/"), ".baz")); + assertPath("/foo.baz", + FileSystemUtils.replaceExtension(fileSystem.getPath("/foo.cc/"), ".baz")); + assertPath("/foo.baz", FileSystemUtils.replaceExtension(fileSystem.getPath("/foo"), ".baz")); + assertPath("/foo.baz", FileSystemUtils.replaceExtension(fileSystem.getPath("/foo.cc"), ".baz")); + assertPath("/.baz", FileSystemUtils.replaceExtension(fileSystem.getPath("/.cc"), ".baz")); + assertEquals(null, FileSystemUtils.replaceExtension(fileSystem.getPath("/"), ".baz")); + } + + @Test + public void testReplaceExtension_PathFragment() throws Exception { + assertPath("foo/bar.baz", + FileSystemUtils.replaceExtension(new PathFragment("foo/bar"), ".baz")); + assertPath("foo/bar.baz", + FileSystemUtils.replaceExtension(new PathFragment("foo/bar.cc"), ".baz")); + assertPath("/foo/bar.baz", + FileSystemUtils.replaceExtension(new PathFragment("/foo/bar"), ".baz")); + assertPath("/foo/bar.baz", + FileSystemUtils.replaceExtension(new PathFragment("/foo/bar.cc"), ".baz")); + assertPath("foo.baz", FileSystemUtils.replaceExtension(new PathFragment("foo/"), ".baz")); + assertPath("foo.baz", FileSystemUtils.replaceExtension(new PathFragment("foo.cc/"), ".baz")); + assertPath("/foo.baz", FileSystemUtils.replaceExtension(new PathFragment("/foo/"), ".baz")); + assertPath("/foo.baz", + FileSystemUtils.replaceExtension(new PathFragment("/foo.cc/"), ".baz")); + assertPath("foo.baz", FileSystemUtils.replaceExtension(new PathFragment("foo"), ".baz")); + assertPath("foo.baz", FileSystemUtils.replaceExtension(new PathFragment("foo.cc"), ".baz")); + assertPath("/foo.baz", FileSystemUtils.replaceExtension(new PathFragment("/foo"), ".baz")); + assertPath("/foo.baz", + FileSystemUtils.replaceExtension(new PathFragment("/foo.cc"), ".baz")); + assertPath(".baz", FileSystemUtils.replaceExtension(new PathFragment(".cc"), ".baz")); + assertEquals(null, FileSystemUtils.replaceExtension(new PathFragment("/"), ".baz")); + assertEquals(null, FileSystemUtils.replaceExtension(new PathFragment(""), ".baz")); + assertPath("foo/bar.baz", + FileSystemUtils.replaceExtension(new PathFragment("foo/bar.pony"), ".baz", ".pony")); + assertPath("foo/bar.baz", + FileSystemUtils.replaceExtension(new PathFragment("foo/bar"), ".baz", "")); + assertEquals(null, FileSystemUtils.replaceExtension(new PathFragment(""), ".baz", ".pony")); + assertEquals(null, + FileSystemUtils.replaceExtension(new PathFragment("foo/bar.pony"), ".baz", ".unicorn")); + } + + @Test + public void testAppendWithoutExtension() throws Exception { + assertPath("libfoo-src.jar", + appendWithoutExtension(new PathFragment("libfoo.jar"), "-src")); + assertPath("foo/libfoo-src.jar", + appendWithoutExtension(new PathFragment("foo/libfoo.jar"), "-src")); + assertPath("java/com/google/foo/libfoo-src.jar", + appendWithoutExtension(new PathFragment("java/com/google/foo/libfoo.jar"), "-src")); + assertPath("libfoo.bar-src.jar", + appendWithoutExtension(new PathFragment("libfoo.bar.jar"), "-src")); + assertPath("libfoo-src", + appendWithoutExtension(new PathFragment("libfoo"), "-src")); + assertPath("libfoo-src.jar", + appendWithoutExtension(new PathFragment("libfoo.jar/"), "-src")); + assertPath("libfoo.src.jar", + appendWithoutExtension(new PathFragment("libfoo.jar"), ".src")); + assertEquals(null, appendWithoutExtension(new PathFragment("/"), "-src")); + assertEquals(null, appendWithoutExtension(new PathFragment(""), "-src")); + } + + @Test + public void testReplaceSegments() { + assertPath( + "poo/bar/baz.cc", + FileSystemUtils.replaceSegments(new PathFragment("foo/bar/baz.cc"), "foo", "poo", true)); + assertPath( + "poo/poo/baz.cc", + FileSystemUtils.replaceSegments(new PathFragment("foo/foo/baz.cc"), "foo", "poo", true)); + assertPath( + "poo/foo/baz.cc", + FileSystemUtils.replaceSegments(new PathFragment("foo/foo/baz.cc"), "foo", "poo", false)); + assertPath( + "foo/bar/baz.cc", + FileSystemUtils.replaceSegments(new PathFragment("foo/bar/baz.cc"), "boo", "poo", true)); + } + + @Test + public void testGetWorkingDirectory() { + String userDir = System.getProperty("user.dir"); + + assertEquals(FileSystemUtils.getWorkingDirectory(fileSystem), + fileSystem.getPath(System.getProperty("user.dir", "/"))); + + System.setProperty("user.dir", "/blah/blah/blah"); + assertEquals(FileSystemUtils.getWorkingDirectory(fileSystem), + fileSystem.getPath("/blah/blah/blah")); + + System.setProperty("user.dir", userDir); + } + + @Test + public void testResolveRelativeToFilesystemWorkingDir() { + PathFragment relativePath = new PathFragment("relative/path"); + assertEquals(workingDir.getRelative(relativePath), + workingDir.getRelative(relativePath)); + + PathFragment absolutePath = new PathFragment("/absolute/path"); + assertEquals(fileSystem.getPath(absolutePath), + workingDir.getRelative(absolutePath)); + } + + @Test + public void testTouchFileCreatesFile() throws IOException { + createTestDirectoryTree(); + Path nonExistingFile = fileSystem.getPath("/previously-non-existing"); + assertFalse(nonExistingFile.exists()); + touchFile(nonExistingFile); + + assertTrue(nonExistingFile.exists()); + } + + @Test + public void testTouchFileAdjustsFileTime() throws IOException { + createTestDirectoryTree(); + Path testFile = file4; + long oldTime = testFile.getLastModifiedTime(); + testFile.setLastModifiedTime(42); + touchFile(testFile); + + assertTrue(testFile.getLastModifiedTime() >= oldTime); + } + + @Test + public void testCopyFile() throws IOException { + createTestDirectoryTree(); + Path originalFile = file1; + byte[] content = new byte[] { 'a', 'b', 'c', 23, 42 }; + FileSystemUtils.writeContent(originalFile, content); + + Path copyTarget = file2; + + copyFile(originalFile, copyTarget); + + assertTrue(Arrays.equals(content, FileSystemUtils.readContent(copyTarget))); + } + + @Test + public void testReadContentWithLimit() throws IOException { + createTestDirectoryTree(); + String str = "this is a test of readContentWithLimit method"; + FileSystemUtils.writeContent(file1, StandardCharsets.ISO_8859_1, str); + assertEquals(readStringFromFile(file1, 0), ""); + assertEquals(readStringFromFile(file1, 10), str.substring(0, 10)); + assertEquals(readStringFromFile(file1, 1000000), str); + } + + private String readStringFromFile(Path file, int limit) throws IOException { + byte[] bytes = FileSystemUtils.readContentWithLimit(file, limit); + return new String(bytes, StandardCharsets.ISO_8859_1); + } + + @Test + public void testAppend() throws IOException { + createTestDirectoryTree(); + FileSystemUtils.writeIsoLatin1(file1, "nobody says "); + FileSystemUtils.writeIsoLatin1(file1, "mary had"); + FileSystemUtils.appendIsoLatin1(file1, "a little lamb"); + assertEquals( + "mary had\na little lamb\n", + new String(FileSystemUtils.readContentAsLatin1(file1))); + } + + @Test + public void testCopyFileAttributes() throws IOException { + createTestDirectoryTree(); + Path originalFile = file1; + byte[] content = new byte[] { 'a', 'b', 'c', 23, 42 }; + FileSystemUtils.writeContent(originalFile, content); + file1.setLastModifiedTime(12345L); + file1.setWritable(false); + file1.setExecutable(false); + + Path copyTarget = file2; + copyFile(originalFile, copyTarget); + + assertEquals(12345L, file2.getLastModifiedTime()); + assertFalse(file2.isExecutable()); + assertFalse(file2.isWritable()); + + file1.setWritable(true); + file1.setExecutable(true); + + copyFile(originalFile, copyTarget); + + assertEquals(12345L, file2.getLastModifiedTime()); + assertTrue(file2.isExecutable()); + assertTrue(file2.isWritable()); + + } + + @Test + public void testCopyFileThrowsExceptionIfTargetCantBeDeleted() throws IOException { + createTestDirectoryTree(); + Path originalFile = file1; + byte[] content = new byte[] { 'a', 'b', 'c', 23, 42 }; + FileSystemUtils.writeContent(originalFile, content); + + try { + copyFile(originalFile, aDir); + fail(); + } catch (IOException ex) { + assertEquals("error copying file: couldn't delete destination: " + + aDir + " (Directory not empty)", + ex.getMessage()); + } + } + + @Test + public void testCopyTool() throws IOException { + createTestDirectoryTree(); + Path originalFile = file1; + byte[] content = new byte[] { 'a', 'b', 'c', 23, 42 }; + FileSystemUtils.writeContent(originalFile, content); + + Path copyTarget = copyTool(topDir.getRelative("file-1"), aDir.getRelative("file-1")); + + assertTrue(Arrays.equals(content, FileSystemUtils.readContent(copyTarget))); + assertEquals(file1.isWritable(), copyTarget.isWritable()); + assertEquals(file1.isExecutable(), copyTarget.isExecutable()); + assertEquals(file1.getLastModifiedTime(), copyTarget.getLastModifiedTime()); + } + + @Test + public void testCopyTreesBelow() throws IOException { + createTestDirectoryTree(); + Path toPath = fileSystem.getPath("/copy-here"); + toPath.createDirectory(); + + FileSystemUtils.copyTreesBelow(topDir, toPath); + checkTestDirectoryTreesBelow(toPath); + } + + @Test + public void testCopyTreesBelowWithOverriding() throws IOException { + createTestDirectoryTree(); + Path toPath = fileSystem.getPath("/copy-here"); + toPath.createDirectory(); + toPath.getChild("file-2"); + + FileSystemUtils.copyTreesBelow(topDir, toPath); + checkTestDirectoryTreesBelow(toPath); + } + + @Test + public void testCopyTreesBelowToSubtree() throws IOException { + createTestDirectoryTree(); + try { + FileSystemUtils.copyTreesBelow(topDir, aDir); + fail("Should not be able to copy a directory to a subdir"); + } catch (IllegalArgumentException expected) { + assertEquals("/top-dir/a-dir is a subdirectory of /top-dir", expected.getMessage()); + } + } + + @Test + public void testCopyFileAsDirectoryTree() throws IOException { + createTestDirectoryTree(); + try { + FileSystemUtils.copyTreesBelow(file1, aDir); + fail("Should not be able to copy a file with copyDirectory method"); + } catch (IOException expected) { + assertEquals("/top-dir/file-1 (Not a directory)", expected.getMessage()); + } + } + + @Test + public void testCopyTreesBelowToFile() throws IOException { + createTestDirectoryTree(); + Path copyDir = fileSystem.getPath("/my-dir"); + Path copySubDir = fileSystem.getPath("/my-dir/subdir"); + FileSystemUtils.createDirectoryAndParents(copySubDir); + try { + FileSystemUtils.copyTreesBelow(copyDir, file4); + fail("Should not be able to copy a directory to a file"); + } catch (IOException expected) { + assertEquals("/file-4 (Not a directory)", expected.getMessage()); + } + } + + @Test + public void testCopyTreesBelowFromUnexistingDir() throws IOException { + createTestDirectoryTree(); + + try { + Path unexistingDir = fileSystem.getPath("/unexisting-dir"); + FileSystemUtils.copyTreesBelow(unexistingDir, aDir); + fail("Should not be able to copy from an unexisting path"); + } catch (FileNotFoundException expected) { + assertEquals("/unexisting-dir (No such file or directory)", expected.getMessage()); + } + } + + @Test + public void testTraverseTree() throws IOException { + createTestDirectoryTree(); + + Collection<Path> paths = traverseTree(topDir, new Predicate<Path>() { + @Override + public boolean apply(Path p) { + return !p.getPathString().contains("a-dir"); + } + }); + assertThat(paths).containsExactly(file1, file2); + } + + @Test + public void testTraverseTreeDeep() throws IOException { + createTestDirectoryTree(); + + Collection<Path> paths = traverseTree(topDir, + Predicates.alwaysTrue()); + assertThat(paths).containsExactly(aDir, + file3, + innerDir, + link1, + file1, + file2, + dirLink); + } + + @Test + public void testTraverseTreeLinkDir() throws IOException { + // Use a new little tree for this test: + // top-dir/ + // dir-link2 => linked-dir + // linked-dir/ + // file + topDir = fileSystem.getPath("/top-dir"); + Path dirLink2 = fileSystem.getPath("/top-dir/dir-link2"); + Path linkedDir = fileSystem.getPath("/linked-dir"); + Path linkedDirFile = fileSystem.getPath("/top-dir/dir-link2/file"); + + topDir.createDirectory(); + linkedDir.createDirectory(); + dirLink2.createSymbolicLink(linkedDir); // simple symlink + FileSystemUtils.createEmptyFile(linkedDirFile); // created through the link + + // traverseTree doesn't follow links: + Collection<Path> paths = traverseTree(topDir, Predicates.alwaysTrue()); + assertThat(paths).containsExactly(dirLink2); + + paths = traverseTree(linkedDir, Predicates.alwaysTrue()); + assertThat(paths).containsExactly(fileSystem.getPath("/linked-dir/file")); + } + + @Test + public void testDeleteTreeCommandDeletesTree() throws IOException { + createTestDirectoryTree(); + Path toDelete = topDir; + deleteTree(toDelete); + + assertTrue(file4.exists()); + assertFalse(topDir.exists()); + assertFalse(file1.exists()); + assertFalse(file2.exists()); + assertFalse(aDir.exists()); + assertFalse(file3.exists()); + } + + @Test + public void testDeleteTreeCommandsDeletesUnreadableDirectories() throws IOException { + createTestDirectoryTree(); + Path toDelete = topDir; + + try { + aDir.setReadable(false); + } catch (UnsupportedOperationException e) { + // For file systems that do not support setting readable attribute to + // false, this test is simply skipped. + + return; + } + + deleteTree(toDelete); + assertFalse(topDir.exists()); + assertFalse(aDir.exists()); + + } + + @Test + public void testDeleteTreeCommandDoesNotFollowLinksOut() throws IOException { + createTestDirectoryTree(); + Path toDelete = topDir; + Path outboundLink = fileSystem.getPath("/top-dir/outbound-link"); + outboundLink.createSymbolicLink(file4); + + deleteTree(toDelete); + + assertTrue(file4.exists()); + assertFalse(topDir.exists()); + assertFalse(file1.exists()); + assertFalse(file2.exists()); + assertFalse(aDir.exists()); + assertFalse(file3.exists()); + } + + @Test + public void testDeleteTreesBelowNotPrefixed() throws IOException { + createTestDirectoryTree(); + deleteTreesBelowNotPrefixed(topDir, new String[] { "file-"}); + assertTrue(file1.exists()); + assertTrue(file2.exists()); + assertFalse(aDir.exists()); + } + + @Test + public void testCreateDirectories() throws IOException { + Path mainPath = fileSystem.getPath("/some/where/deep/in/the/hierarchy"); + assertTrue(createDirectoryAndParents(mainPath)); + assertTrue(mainPath.exists()); + assertFalse(createDirectoryAndParents(mainPath)); + } + + @Test + public void testCreateDirectoriesWhenAncestorIsFile() throws IOException { + Path somewhereDeepIn = fileSystem.getPath("/somewhere/deep/in"); + assertTrue(createDirectoryAndParents(somewhereDeepIn.getParentDirectory())); + FileSystemUtils.createEmptyFile(somewhereDeepIn); + Path theHierarchy = somewhereDeepIn.getChild("the-hierarchy"); + try { + createDirectoryAndParents(theHierarchy); + fail(); + } catch (IOException e) { + assertEquals("/somewhere/deep/in (Not a directory)", e.getMessage()); + } + } + + @Test + public void testCreateDirectoriesWhenSymlinkToDir() throws IOException { + Path somewhereDeepIn = fileSystem.getPath("/somewhere/deep/in"); + assertTrue(createDirectoryAndParents(somewhereDeepIn)); + Path realDir = fileSystem.getPath("/real/dir"); + assertTrue(createDirectoryAndParents(realDir)); + + Path theHierarchy = somewhereDeepIn.getChild("the-hierarchy"); + theHierarchy.createSymbolicLink(realDir); + + assertFalse(createDirectoryAndParents(theHierarchy)); + } + + @Test + public void testCreateDirectoriesWhenSymlinkEmbedded() throws IOException { + Path somewhereDeepIn = fileSystem.getPath("/somewhere/deep/in"); + assertTrue(createDirectoryAndParents(somewhereDeepIn)); + Path realDir = fileSystem.getPath("/real/dir"); + assertTrue(createDirectoryAndParents(realDir)); + + Path the = somewhereDeepIn.getChild("the"); + the.createSymbolicLink(realDir); + + Path theHierarchy = somewhereDeepIn.getChild("hierarchy"); + assertTrue(createDirectoryAndParents(theHierarchy)); + } + + PathFragment createPkg(Path rootA, Path rootB, String pkg) throws IOException { + if (rootA != null) { + createDirectoryAndParents(rootA.getRelative(pkg)); + FileSystemUtils.createEmptyFile(rootA.getRelative(pkg).getChild("file")); + } + if (rootB != null) { + createDirectoryAndParents(rootB.getRelative(pkg)); + FileSystemUtils.createEmptyFile(rootB.getRelative(pkg).getChild("file")); + } + return new PathFragment(pkg); + } + + void assertLinksTo(Path fromRoot, Path toRoot, String relpart) throws IOException { + assertTrue(fromRoot.getRelative(relpart).isSymbolicLink()); + assertEquals(toRoot.getRelative(relpart).asFragment(), + fromRoot.getRelative(relpart).readSymbolicLink()); + } + + void assertIsDir(Path root, String relpart) { + assertTrue(root.getRelative(relpart).isDirectory(Symlinks.NOFOLLOW)); + } + + void dumpTree(Path root, PrintStream out) throws IOException { + out.println("\n" + root); + for (Path p : FileSystemUtils.traverseTree(root, Predicates.alwaysTrue())) { + if (p.isDirectory(Symlinks.NOFOLLOW)) { + out.println(" " + p + "/"); + } else if (p.isSymbolicLink()) { + out.println(" " + p + " => " + p.readSymbolicLink()); + } else { + out.println(" " + p + " [" + p.resolveSymbolicLinks() + "]"); + } + } + } + + @Test + public void testPlantLinkForest() throws IOException { + Path rootA = fileSystem.getPath("/A"); + Path rootB = fileSystem.getPath("/B"); + + ImmutableMap<PathFragment, Path> packageRootMap = ImmutableMap.<PathFragment, Path>builder() + .put(createPkg(rootA, rootB, "pkgA"), rootA) + .put(createPkg(rootA, rootB, "dir1/pkgA"), rootA) + .put(createPkg(rootA, rootB, "dir1/pkgB"), rootB) + .put(createPkg(rootA, rootB, "dir2/pkg"), rootA) + .put(createPkg(rootA, rootB, "dir2/pkg/pkg"), rootB) + .put(createPkg(rootA, rootB, "pkgB"), rootB) + .put(createPkg(rootA, rootB, "pkgB/dir/pkg"), rootA) + .put(createPkg(rootA, rootB, "pkgB/pkg"), rootA) + .put(createPkg(rootA, rootB, "pkgB/pkg/pkg"), rootA) + .build(); + createPkg(rootA, rootB, "pkgB/dir"); // create a file in there + + //dumpTree(rootA, System.err); + //dumpTree(rootB, System.err); + + Path linkRoot = fileSystem.getPath("/linkRoot"); + createDirectoryAndParents(linkRoot); + plantLinkForest(packageRootMap, linkRoot); + + //dumpTree(linkRoot, System.err); + + assertLinksTo(linkRoot, rootA, "pkgA"); + assertIsDir(linkRoot, "dir1"); + assertLinksTo(linkRoot, rootA, "dir1/pkgA"); + assertLinksTo(linkRoot, rootB, "dir1/pkgB"); + assertIsDir(linkRoot, "dir2"); + assertIsDir(linkRoot, "dir2/pkg"); + assertLinksTo(linkRoot, rootA, "dir2/pkg/file"); + assertLinksTo(linkRoot, rootB, "dir2/pkg/pkg"); + assertIsDir(linkRoot, "pkgB"); + assertIsDir(linkRoot, "pkgB/dir"); + assertLinksTo(linkRoot, rootB, "pkgB/dir/file"); + assertLinksTo(linkRoot, rootA, "pkgB/dir/pkg"); + assertLinksTo(linkRoot, rootA, "pkgB/pkg"); + } + + @Test + public void testWriteIsoLatin1() throws Exception { + Path file = fileSystem.getPath("/does/not/exist/yet.txt"); + FileSystemUtils.writeIsoLatin1(file, "Line 1", "Line 2", "Line 3"); + String expected = "Line 1\nLine 2\nLine 3\n"; + String actual = new String(FileSystemUtils.readContentAsLatin1(file)); + assertEquals(expected, actual); + } + + @Test + public void testWriteLinesAs() throws Exception { + Path file = fileSystem.getPath("/does/not/exist/yet.txt"); + FileSystemUtils.writeLinesAs(file, UTF_8, "\u00F6"); // an oe umlaut + byte[] expected = new byte[] {(byte) 0xC3, (byte) 0xB6, 0x0A};//"\u00F6\n"; + byte[] actual = FileSystemUtils.readContent(file); + assertArrayEquals(expected, actual); + } + + @Test + public void testGetFileSystem() throws Exception { + Path mountTable = fileSystem.getPath("/proc/mounts"); + FileSystemUtils.writeIsoLatin1(mountTable, + "/dev/sda1 / ext2 blah 0 0", + "/dev/mapper/_dev_sda6 /usr/local/google ext3 blah 0 0", + "devshm /dev/shm tmpfs blah 0 0", + "/dev/fuse /fuse/mnt fuse blah 0 0", + "mtvhome22.nfs:/vol/mtvhome22/johndoe /home/johndoe nfs blah 0 0", + "/dev/foo /foo dummy_foo blah 0 0", + "/dev/foobar /foobar dummy_foobar blah 0 0", + "proc proc proc rw,noexec,nosuid,nodev 0 0"); + Path path = fileSystem.getPath("/usr/local/google/_blaze"); + FileSystemUtils.createDirectoryAndParents(path); + assertEquals("ext3", FileSystemUtils.getFileSystem(path)); + + // Should match the root "/" + path = fileSystem.getPath("/usr/local/tmp"); + FileSystemUtils.createDirectoryAndParents(path); + assertEquals("ext2", FileSystemUtils.getFileSystem(path)); + + // Make sure we don't consider /foobar matches /foo + path = fileSystem.getPath("/foo"); + FileSystemUtils.createDirectoryAndParents(path); + assertEquals("dummy_foo", FileSystemUtils.getFileSystem(path)); + path = fileSystem.getPath("/foobar"); + FileSystemUtils.createDirectoryAndParents(path); + assertEquals("dummy_foobar", FileSystemUtils.getFileSystem(path)); + + path = fileSystem.getPath("/dev/shm/blaze"); + FileSystemUtils.createDirectoryAndParents(path); + assertEquals("tmpfs", FileSystemUtils.getFileSystem(path)); + + Path fusePath = fileSystem.getPath("/fuse/mnt/tmp"); + FileSystemUtils.createDirectoryAndParents(fusePath); + assertEquals("fuse", FileSystemUtils.getFileSystem(fusePath)); + + // Create a symlink and make sure it gives the file system of the symlink target. + path = fileSystem.getPath("/usr/local/google/_blaze/out"); + path.createSymbolicLink(fusePath); + assertEquals("fuse", FileSystemUtils.getFileSystem(path)); + + // Non existent path should return "unknown" + path = fileSystem.getPath("/does/not/exist"); + assertEquals("unknown", FileSystemUtils.getFileSystem(path)); + } + + @Test + public void testStartsWithAnySuccess() throws Exception { + PathFragment a = new PathFragment("a"); + assertTrue(FileSystemUtils.startsWithAny(a, + Arrays.asList(new PathFragment("b"), new PathFragment("a")))); + } + + @Test + public void testStartsWithAnyNotFound() throws Exception { + PathFragment a = new PathFragment("a"); + assertFalse(FileSystemUtils.startsWithAny(a, + Arrays.asList(new PathFragment("b"), new PathFragment("c")))); + } + + @Test + public void testIterateLines() throws Exception { + Path file = fileSystem.getPath("/test.txt"); + FileSystemUtils.writeContent(file, ISO_8859_1, "a\nb"); + assertEquals(Arrays.asList("a", "b"), + Lists.newArrayList(FileSystemUtils.iterateLinesAsLatin1(file))); + + FileSystemUtils.writeContent(file, ISO_8859_1, "a\rb"); + assertEquals(Arrays.asList("a", "b"), + Lists.newArrayList(FileSystemUtils.iterateLinesAsLatin1(file))); + + FileSystemUtils.writeContent(file, ISO_8859_1, "a\r\nb"); + assertEquals(Arrays.asList("a", "b"), + Lists.newArrayList(FileSystemUtils.iterateLinesAsLatin1(file))); + } + + @Test + public void testEnsureSymbolicLinkDoesNotMakeUnnecessaryChanges() throws Exception { + PathFragment target = new PathFragment("/b"); + Path file = fileSystem.getPath("/a"); + file.createSymbolicLink(target); + long prevTimeMillis = clock.currentTimeMillis(); + clock.advanceMillis(1000); + FileSystemUtils.ensureSymbolicLink(file, target); + long timestamp = file.getLastModifiedTime(Symlinks.NOFOLLOW); + assertTrue(timestamp == prevTimeMillis); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemsTest.java new file mode 100644 index 0000000000..88a000fea7 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemsTest.java @@ -0,0 +1,48 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +import com.google.devtools.build.lib.vfs.util.FileSystems; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * This class handles the tests for the FileSystems class. + */ +@RunWith(JUnit4.class) +public class FileSystemsTest { + + @Test + public void testFileSystemsCreatesOnlyOneDefaultNative() { + assertSame(FileSystems.initDefaultAsNative(), + FileSystems.initDefaultAsNative()); + } + + @Test + public void testFileSystemsCreatesOnlyOneDefaultJavaIo() { + assertSame(FileSystems.initDefaultAsJavaIo(), + FileSystems.initDefaultAsJavaIo()); + } + + @Test + public void testFileSystemsCanSwitchDefaults() { + assertNotSame(FileSystems.initDefaultAsNative(), + FileSystems.initDefaultAsJavaIo()); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java b/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java new file mode 100644 index 0000000000..37b7dc4957 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java @@ -0,0 +1,417 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import com.google.devtools.build.lib.testutil.MoreAsserts; +import com.google.devtools.build.lib.testutil.TestUtils; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Tests {@link UnixGlob} + */ +@RunWith(JUnit4.class) +public class GlobTest { + + private Path tmpPath; + private FileSystem fs; + @Before + public void setUp() throws Exception { + fs = new InMemoryFileSystem(); + tmpPath = fs.getPath("/globtmp"); + for (String dir : ImmutableList.of("foo/bar/wiz", + "foo/barnacle/wiz", + "food/barnacle/wiz", + "fool/barnacle/wiz")) { + FileSystemUtils.createDirectoryAndParents(tmpPath.getRelative(dir)); + } + FileSystemUtils.createEmptyFile(tmpPath.getRelative("foo/bar/wiz/file")); + } + + @Test + public void testQuestionMarkMatch() throws Exception { + assertGlobMatches("foo?", /* => */"food", "fool"); + } + + @Test + public void testQuestionMarkNoMatch() throws Exception { + assertGlobMatches("food/bar?" /* => nothing */); + } + + @Test + public void testStartsWithStar() throws Exception { + assertGlobMatches("*oo", /* => */"foo"); + } + + @Test + public void testStartsWithStarWithMiddleStar() throws Exception { + assertGlobMatches("*f*o", /* => */"foo"); + } + + @Test + public void testEndsWithStar() throws Exception { + assertGlobMatches("foo*", /* => */"foo", "food", "fool"); + } + + @Test + public void testEndsWithStarWithMiddleStar() throws Exception { + assertGlobMatches("f*oo*", /* => */"foo", "food", "fool"); + } + + @Test + public void testMiddleStar() throws Exception { + assertGlobMatches("f*o", /* => */"foo"); + } + + @Test + public void testTwoMiddleStars() throws Exception { + assertGlobMatches("f*o*o", /* => */"foo"); + } + + @Test + public void testSingleStarPatternWithNamedChild() throws Exception { + assertGlobMatches("*/bar", /* => */"foo/bar"); + } + + @Test + public void testSingleStarPatternWithChildGlob() throws Exception { + assertGlobMatches("*/bar*", /* => */ + "foo/bar", "foo/barnacle", "food/barnacle", "fool/barnacle"); + } + + @Test + public void testSingleStarAsChildGlob() throws Exception { + assertGlobMatches("foo/*/wiz", /* => */"foo/bar/wiz", "foo/barnacle/wiz"); + } + + @Test + public void testNoAsteriskAndFilesDontExist() throws Exception { + // Note un-UNIX like semantics: + assertGlobMatches("ceci/n'est/pas/une/globbe" /* => nothing */); + } + + @Test + public void testSingleAsteriskUnderNonexistentDirectory() throws Exception { + // Note un-UNIX like semantics: + assertGlobMatches("not-there/*" /* => nothing */); + } + + @Test + public void testGlobWithNonExistentBase() throws Exception { + Collection<Path> globResult = UnixGlob.forPath(fs.getPath("/does/not/exist")) + .addPattern("*.txt") + .globInterruptible(); + assertEquals(0, globResult.size()); + } + + @Test + public void testGlobUnderFile() throws Exception { + assertGlobMatches("foo/bar/wiz/file/*" /* => nothing */); + } + + @Test + public void testSingleFileExclude() throws Exception { + assertGlobWithExcludeMatches("*", "food", "foo", "fool"); + } + + @Test + public void testExcludeAll() throws Exception { + assertGlobWithExcludeMatches("*", "*"); + } + + @Test + public void testExcludeAllButNoMatches() throws Exception { + assertGlobWithExcludeMatches("not-there", "*"); + } + + @Test + public void testSingleFileExcludeDoesntMatch() throws Exception { + assertGlobWithExcludeMatches("food", "foo", "food"); + } + + @Test + public void testSingleFileExcludeForDirectoryWithChildGlob() + throws Exception { + assertGlobWithExcludeMatches("foo/*", "foo", "foo/bar", "foo/barnacle"); + } + + @Test + public void testChildGlobWithChildExclude() + throws Exception { + assertGlobWithExcludeMatches("foo/*", "foo/*"); + assertGlobWithExcludeMatches("foo/bar", "foo/*"); + assertGlobWithExcludeMatches("foo/bar", "foo/bar"); + assertGlobWithExcludeMatches("foo/bar", "*/bar"); + assertGlobWithExcludeMatches("foo/bar", "*/*"); + assertGlobWithExcludeMatches("foo/bar/wiz", "*/*/*"); + assertGlobWithExcludeMatches("foo/bar/wiz", "foo/*/*"); + assertGlobWithExcludeMatches("foo/bar/wiz", "foo/bar/*"); + assertGlobWithExcludeMatches("foo/bar/wiz", "foo/bar/wiz"); + assertGlobWithExcludeMatches("foo/bar/wiz", "*/bar/wiz"); + assertGlobWithExcludeMatches("foo/bar/wiz", "*/*/wiz"); + assertGlobWithExcludeMatches("foo/bar/wiz", "foo/*/wiz"); + } + + private void assertGlobMatches(String pattern, String... expecteds) + throws Exception { + assertGlobWithExcludesMatches( + Collections.singleton(pattern), Collections.<String>emptyList(), + expecteds); + } + + private void assertGlobMatches(Collection<String> pattern, + String... expecteds) + throws Exception { + assertGlobWithExcludesMatches(pattern, Collections.<String>emptyList(), + expecteds); + } + + private void assertGlobWithExcludeMatches(String pattern, String exclude, + String... expecteds) + throws Exception { + assertGlobWithExcludesMatches( + Collections.singleton(pattern), Collections.singleton(exclude), + expecteds); + } + + private void assertGlobWithExcludesMatches(Collection<String> pattern, + Collection<String> excludes, + String... expecteds) + throws Exception { + MoreAsserts.assertSameContents(resolvePaths(expecteds), + new UnixGlob.Builder(tmpPath) + .addPatterns(pattern) + .addExcludes(excludes) + .globInterruptible()); + } + + private Set<Path> resolvePaths(String... relativePaths) { + Set<Path> expectedFiles = new HashSet<>(); + for (String expected : relativePaths) { + Path file = expected.equals(".") + ? tmpPath + : tmpPath.getRelative(expected); + expectedFiles.add(file); + } + return expectedFiles; + } + + @Test + public void testGlobWithoutWildcardsDoesNotCallReaddir() throws Exception { + UnixGlob.FilesystemCalls syscalls = new UnixGlob.FilesystemCalls() { + @Override + public FileStatus statNullable(Path path, Symlinks symlinks) { + return UnixGlob.DEFAULT_SYSCALLS.statNullable(path, symlinks); + } + + @Override + public Collection<Dirent> readdir(Path path, Symlinks symlinks) { + throw new IllegalStateException(); + } + }; + + MoreAsserts.assertSameContents(ImmutableList.of(tmpPath.getRelative("foo/bar/wiz/file")), + new UnixGlob.Builder(tmpPath) + .addPattern("foo/bar/wiz/file") + .setFilesystemCalls(new AtomicReference<>(syscalls)) + .glob()); + } + + @Test + public void testIllegalPatterns() throws Exception { + assertIllegalPattern("(illegal) pattern"); + assertIllegalPattern("[illegal pattern"); + assertIllegalPattern("}illegal pattern"); + assertIllegalPattern("foo**bar"); + assertIllegalPattern(""); + assertIllegalPattern("."); + assertIllegalPattern("/foo"); + assertIllegalPattern("./foo"); + assertIllegalPattern("foo/"); + assertIllegalPattern("foo/./bar"); + assertIllegalPattern("../foo/bar"); + assertIllegalPattern("foo//bar"); + } + + /** + * Tests that globs can contain Java regular expression special characters + */ + @Test + public void testSpecialRegexCharacter() throws Exception { + Path tmpPath2 = fs.getPath("/globtmp2"); + FileSystemUtils.createDirectoryAndParents(tmpPath2); + Path aDotB = tmpPath2.getChild("a.b"); + FileSystemUtils.createEmptyFile(aDotB); + FileSystemUtils.createEmptyFile(tmpPath2.getChild("aab")); + // Note: this contains two asterisks because otherwise a RE is not built, + // as an optimization. + assertThat(UnixGlob.forPath(tmpPath2).addPattern("*a.b*").globInterruptible()).containsExactly( + aDotB); + } + + @Test + public void testMatchesCallWithNoCache() { + assertTrue(UnixGlob.matches("*a*b", "CaCb", null)); + } + + @Test + public void testMultiplePatterns() throws Exception { + assertGlobMatches(Lists.newArrayList("foo", "fool"), "foo", "fool"); + } + + @Test + public void testMultiplePatternsWithExcludes() throws Exception { + assertGlobWithExcludesMatches(Lists.newArrayList("foo", "foo?"), + Lists.newArrayList("fool"), "foo", "food"); + } + + @Test + public void testMultiplePatternsWithOverlap() throws Exception { + assertGlobMatchesAnyOrder(Lists.newArrayList("food", "foo?"), + "food", "fool"); + assertGlobMatchesAnyOrder(Lists.newArrayList("food", "?ood", "f??d"), + "food"); + assertThat(resolvePaths("food", "fool", "foo")).containsExactlyElementsIn( + new UnixGlob.Builder(tmpPath).addPatterns("food", "xxx", "*").glob()); + + } + + private void assertGlobMatchesAnyOrder(ArrayList<String> patterns, + String... paths) throws Exception { + assertThat(resolvePaths(paths)).containsExactlyElementsIn( + new UnixGlob.Builder(tmpPath).addPatterns(patterns).globInterruptible()); + } + + /** + * Tests that a glob returns files in sorted order. + */ + @Test + public void testGlobEntriesAreSorted() throws Exception { + Collection<Path> directoryEntries = tmpPath.getDirectoryEntries(); + List<Path> globResult = new UnixGlob.Builder(tmpPath) + .addPattern("*") + .setExcludeDirectories(false) + .globInterruptible(); + assertThat(Ordering.natural().sortedCopy(directoryEntries)).containsExactlyElementsIn( + globResult).inOrder(); + } + + private void assertIllegalPattern(String pattern) throws Exception { + try { + new UnixGlob.Builder(tmpPath) + .addPattern(pattern) + .globInterruptible(); + fail(); + } catch (IllegalArgumentException e) { + MoreAsserts.assertContainsRegex("in glob pattern", e.getMessage()); + } + } + + @Test + public void testHiddenFiles() throws Exception { + for (String dir : ImmutableList.of(".hidden", "..also.hidden", "not.hidden")) { + FileSystemUtils.createDirectoryAndParents(tmpPath.getRelative(dir)); + } + // Note that these are not in the result: ".", ".." + assertGlobMatches("*", "not.hidden", "foo", "fool", "food", ".hidden", "..also.hidden"); + assertGlobMatches("*.hidden", "not.hidden"); + } + + @Test + public void testCheckCanBeInterrupted() throws Exception { + final Thread mainThread = Thread.currentThread(); + final ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10); + + Predicate<Path> interrupterPredicate = new Predicate<Path>() { + @Override + public boolean apply(Path input) { + mainThread.interrupt(); + return true; + } + }; + + try { + new UnixGlob.Builder(tmpPath) + .addPattern("**") + .setDirectoryFilter(interrupterPredicate) + .setThreadPool(executor) + .globInterruptible(); + fail(); // Should have received InterruptedException + } catch (InterruptedException e) { + // good + } + + assertFalse(executor.isShutdown()); + executor.shutdown(); + assertTrue(executor.awaitTermination(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)); + } + + @Test + public void testCheckCannotBeInterrupted() throws Exception { + final Thread mainThread = Thread.currentThread(); + final ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10); + final AtomicBoolean sentInterrupt = new AtomicBoolean(false); + + Predicate<Path> interrupterPredicate = new Predicate<Path>() { + @Override + public boolean apply(Path input) { + if (!sentInterrupt.getAndSet(true)) { + mainThread.interrupt(); + } + return true; + } + }; + + List<Path> result = new UnixGlob.Builder(tmpPath) + .addPatterns("**", "*") + .setDirectoryFilter(interrupterPredicate).setThreadPool(executor).glob(); + + // In the non-interruptible case, the interrupt bit should be set, but the + // glob should return the correct set of full results. + assertTrue(Thread.interrupted()); + MoreAsserts.assertSameContents(resolvePaths(".", "foo", "foo/bar", "foo/bar/wiz", + "foo/bar/wiz/file", "foo/barnacle", "foo/barnacle/wiz", "food", "food/barnacle", + "food/barnacle/wiz", "fool", "fool/barnacle", "fool/barnacle/wiz"), result); + + assertFalse(executor.isShutdown()); + executor.shutdown(); + assertTrue(executor.awaitTermination(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java new file mode 100644 index 0000000000..fdb6283d20 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java @@ -0,0 +1,40 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for the {@link JavaIoFileSystem}. That file system by itself is not + * capable of creating symlinks; use the unix one to create them, so that the + * test can check that the file system handles their existence correctly. + */ +@RunWith(JUnit4.class) +public class JavaIoFileSystemTest extends SymlinkAwareFileSystemTest { + + @Override + public FileSystem getFreshFileSystem() { + return new JavaIoFileSystem(); + } + + // The tests are just inherited from the FileSystemTest + + // JavaIoFileSystem incorrectly throws a FileNotFoundException for all IO errors. This means that + // statIfFound incorrectly suppresses those errors. + @Override + @Test + public void testBadPermissionsThrowsExceptionOnStatIfFound() {} +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/ModifiedFileSetTest.java b/src/test/java/com/google/devtools/build/lib/vfs/ModifiedFileSetTest.java new file mode 100644 index 0000000000..96001dfa2f --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/ModifiedFileSetTest.java @@ -0,0 +1,54 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import com.google.common.collect.ImmutableList; +import com.google.common.testing.EqualsTester; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for {@link ModifiedFileSet}. + */ +@RunWith(JUnit4.class) +public class ModifiedFileSetTest { + + @Test + public void testHashCodeAndEqualsContract() throws Exception { + PathFragment fragA = new PathFragment("a"); + PathFragment fragB = new PathFragment("b"); + + ModifiedFileSet empty1 = ModifiedFileSet.NOTHING_MODIFIED; + ModifiedFileSet empty2 = ModifiedFileSet.builder().build(); + ModifiedFileSet empty3 = ModifiedFileSet.builder().modifyAll( + ImmutableList.<PathFragment>of()).build(); + + ModifiedFileSet nonEmpty1 = ModifiedFileSet.builder().modifyAll( + ImmutableList.of(fragA, fragB)).build(); + ModifiedFileSet nonEmpty2 = ModifiedFileSet.builder().modifyAll( + ImmutableList.of(fragB, fragA)).build(); + ModifiedFileSet nonEmpty3 = ModifiedFileSet.builder().modify(fragA).modify(fragB).build(); + ModifiedFileSet nonEmpty4 = ModifiedFileSet.builder().modify(fragB).modify(fragA).build(); + + ModifiedFileSet everythingModified = ModifiedFileSet.EVERYTHING_MODIFIED; + + new EqualsTester() + .addEqualityGroup(empty1, empty2, empty3) + .addEqualityGroup(nonEmpty1, nonEmpty2, nonEmpty3, nonEmpty4) + .addEqualityGroup(everythingModified) + .testEquals(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java new file mode 100644 index 0000000000..9ab9bfa9f3 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java @@ -0,0 +1,481 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.testing.EqualsTester; +import com.google.devtools.build.lib.testutil.MoreAsserts; +import com.google.devtools.build.lib.testutil.TestUtils; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +/** + * This class tests the functionality of the PathFragment. + */ +@RunWith(JUnit4.class) +public class PathFragmentTest { + @Test + public void testMergeFourPathsWithAbsolute() { + assertEquals(new PathFragment("x/y/z/a/b/c/d/e"), + new PathFragment(new PathFragment("x/y"), new PathFragment("z/a"), + new PathFragment("/b/c"), // absolute! + new PathFragment("d/e"))); + } + + @Test + public void testEqualsAndHashCode() { + InMemoryFileSystem filesystem = new InMemoryFileSystem(); + + new EqualsTester() + .addEqualityGroup(new PathFragment("../relative/path"), + new PathFragment("../relative/path"), + new PathFragment(new File("../relative/path"))) + .addEqualityGroup(new PathFragment("something/else")) + .addEqualityGroup(new PathFragment("/something/else")) + .addEqualityGroup(new PathFragment("/"), + new PathFragment("//////")) + .addEqualityGroup(new PathFragment("")) + .addEqualityGroup(filesystem.getRootDirectory()) // A Path object. + .testEquals(); + } + + @Test + public void testHashCodeCache() { + PathFragment relativePath = new PathFragment("../relative/path"); + PathFragment rootPath = new PathFragment("/"); + + int oldResult = relativePath.hashCode(); + int rootResult = rootPath.hashCode(); + assertEquals(oldResult, relativePath.hashCode()); + assertEquals(rootResult, rootPath.hashCode()); + } + + private void checkRelativeTo(String path, String base) { + PathFragment relative = new PathFragment(path).relativeTo(base); + assertEquals(new PathFragment(path), new PathFragment(base).getRelative(relative).normalize()); + } + + @Test + public void testRelativeTo() { + assertPath("bar/baz", new PathFragment("foo/bar/baz").relativeTo("foo")); + assertPath("bar/baz", new PathFragment("/foo/bar/baz").relativeTo("/foo")); + assertPath("baz", new PathFragment("foo/bar/baz").relativeTo("foo/bar")); + assertPath("baz", new PathFragment("/foo/bar/baz").relativeTo("/foo/bar")); + assertPath("foo", new PathFragment("/foo").relativeTo("/")); + assertPath("foo", new PathFragment("foo").relativeTo("")); + assertPath("foo/bar", new PathFragment("foo/bar").relativeTo("")); + + checkRelativeTo("foo/bar/baz", "foo"); + checkRelativeTo("/foo/bar/baz", "/foo"); + checkRelativeTo("foo/bar/baz", "foo/bar"); + checkRelativeTo("/foo/bar/baz", "/foo/bar"); + checkRelativeTo("/foo", "/"); + checkRelativeTo("foo", ""); + checkRelativeTo("foo/bar", ""); + } + + @Test + public void testIsAbsolute() { + assertTrue(new PathFragment("/absolute/test").isAbsolute()); + assertFalse(new PathFragment("relative/test").isAbsolute()); + assertTrue(new PathFragment(new File("/absolute/test")).isAbsolute()); + assertFalse(new PathFragment(new File("relative/test")).isAbsolute()); + } + + @Test + public void testIsNormalized() { + assertTrue(new PathFragment("/absolute/path").isNormalized()); + assertTrue(new PathFragment("some//path").isNormalized()); + assertFalse(new PathFragment("some/./path").isNormalized()); + assertFalse(new PathFragment("../some/path").isNormalized()); + assertFalse(new PathFragment("some/other/../path").isNormalized()); + assertTrue(new PathFragment("some/other//tricky..path..").isNormalized()); + assertTrue(new PathFragment("/some/other//tricky..path..").isNormalized()); + } + + @Test + public void testRootNodeReturnsRootString() { + PathFragment rootFragment = new PathFragment("/"); + assertEquals("/", rootFragment.getPathString()); + } + + @Test + public void testGetPathFragmentDoesNotNormalize() { + String nonCanonicalPath = "/a/weird/noncanonical/../path/."; + assertEquals(nonCanonicalPath, + new PathFragment(nonCanonicalPath).getPathString()); + } + + @Test + public void testGetRelative() { + assertEquals("a/b", new PathFragment("a").getRelative("b").getPathString()); + assertEquals("a/b/c/d", new PathFragment("a/b").getRelative("c/d").getPathString()); + assertEquals("/a/b", new PathFragment("c/d").getRelative("/a/b").getPathString()); + assertEquals("a", new PathFragment("a").getRelative("").getPathString()); + assertEquals("/", new PathFragment("/").getRelative("").getPathString()); + } + + @Test + public void testGetChildWorks() { + PathFragment pf = new PathFragment("../some/path"); + assertEquals(new PathFragment("../some/path/hi"), pf.getChild("hi")); + } + + @Test + public void testGetChildRejectsInvalidBaseNames() { + PathFragment pf = new PathFragment("../some/path"); + assertGetChildFails(pf, "."); + assertGetChildFails(pf, ".."); + assertGetChildFails(pf, "x/y"); + assertGetChildFails(pf, "/y"); + assertGetChildFails(pf, "y/"); + assertGetChildFails(pf, ""); + } + + private void assertGetChildFails(PathFragment pf, String baseName) { + try { + pf.getChild(baseName); + fail(); + } catch (Exception e) { /* Expected. */ } + } + + // Tests after here test the canonicalization + private void assertRegular(String expected, String actual) { + assertEquals(expected, new PathFragment(actual).getPathString()); // compare string forms + assertEquals(new PathFragment(expected), new PathFragment(actual)); // compare fragment forms + } + + @Test + public void testEmptyPathToEmptyPath() { + assertRegular("/", "/"); + assertRegular("", ""); + } + + @Test + public void testRedundantSlashes() { + assertRegular("/", "///"); + assertRegular("/foo/bar", "/foo///bar"); + assertRegular("/foo/bar", "////foo//bar"); + } + + @Test + public void testSimpleNameToSimpleName() { + assertRegular("/foo", "/foo"); + assertRegular("foo", "foo"); + } + + @Test + public void testSimplePathToSimplePath() { + assertRegular("/foo/bar", "/foo/bar"); + assertRegular("foo/bar", "foo/bar"); + } + + @Test + public void testStripsTrailingSlash() { + assertRegular("/foo/bar", "/foo/bar/"); + } + + @Test + public void testGetParentDirectory() { + PathFragment fooBarWiz = new PathFragment("foo/bar/wiz"); + PathFragment fooBar = new PathFragment("foo/bar"); + PathFragment foo = new PathFragment("foo"); + PathFragment empty = new PathFragment(""); + assertEquals(fooBar, fooBarWiz.getParentDirectory()); + assertEquals(foo, fooBar.getParentDirectory()); + assertEquals(empty, foo.getParentDirectory()); + assertNull(empty.getParentDirectory()); + + PathFragment fooBarWizAbs = new PathFragment("/foo/bar/wiz"); + PathFragment fooBarAbs = new PathFragment("/foo/bar"); + PathFragment fooAbs = new PathFragment("/foo"); + PathFragment rootAbs = new PathFragment("/"); + assertEquals(fooBarAbs, fooBarWizAbs.getParentDirectory()); + assertEquals(fooAbs, fooBarAbs.getParentDirectory()); + assertEquals(rootAbs, fooAbs.getParentDirectory()); + assertNull(rootAbs.getParentDirectory()); + + // Note, this is surprising but correct behavior: + assertEquals(fooBarAbs, + new PathFragment("/foo/bar/..").getParentDirectory()); + } + + @Test + public void testSegmentsCount() { + assertEquals(2, new PathFragment("foo/bar").segmentCount()); + assertEquals(2, new PathFragment("/foo/bar").segmentCount()); + assertEquals(2, new PathFragment("foo//bar").segmentCount()); + assertEquals(2, new PathFragment("/foo//bar").segmentCount()); + assertEquals(1, new PathFragment("foo/").segmentCount()); + assertEquals(1, new PathFragment("/foo/").segmentCount()); + assertEquals(1, new PathFragment("foo").segmentCount()); + assertEquals(1, new PathFragment("/foo").segmentCount()); + assertEquals(0, new PathFragment("/").segmentCount()); + assertEquals(0, new PathFragment("").segmentCount()); + } + + + @Test + public void testGetSegment() { + assertEquals("foo", new PathFragment("foo/bar").getSegment(0)); + assertEquals("bar", new PathFragment("foo/bar").getSegment(1)); + assertEquals("foo", new PathFragment("/foo/bar").getSegment(0)); + assertEquals("bar", new PathFragment("/foo/bar").getSegment(1)); + assertEquals("foo", new PathFragment("foo/").getSegment(0)); + assertEquals("foo", new PathFragment("/foo/").getSegment(0)); + assertEquals("foo", new PathFragment("foo").getSegment(0)); + assertEquals("foo", new PathFragment("/foo").getSegment(0)); + } + + @Test + public void testBasename() throws Exception { + assertEquals("bar", new PathFragment("foo/bar").getBaseName()); + assertEquals("bar", new PathFragment("/foo/bar").getBaseName()); + assertEquals("foo", new PathFragment("foo/").getBaseName()); + assertEquals("foo", new PathFragment("/foo/").getBaseName()); + assertEquals("foo", new PathFragment("foo").getBaseName()); + assertEquals("foo", new PathFragment("/foo").getBaseName()); + assertEquals("", new PathFragment("/").getBaseName()); + assertEquals("", new PathFragment("").getBaseName()); + } + + private static void assertPath(String expected, PathFragment actual) { + assertEquals(expected, actual.getPathString()); + } + + @Test + public void testReplaceName() throws Exception { + assertPath("foo/baz", new PathFragment("foo/bar").replaceName("baz")); + assertPath("/foo/baz", new PathFragment("/foo/bar").replaceName("baz")); + assertPath("foo", new PathFragment("foo/bar").replaceName("")); + assertPath("baz", new PathFragment("foo/").replaceName("baz")); + assertPath("/baz", new PathFragment("/foo/").replaceName("baz")); + assertPath("baz", new PathFragment("foo").replaceName("baz")); + assertPath("/baz", new PathFragment("/foo").replaceName("baz")); + assertEquals(null, new PathFragment("/").replaceName("baz")); + assertEquals(null, new PathFragment("/").replaceName("")); + assertEquals(null, new PathFragment("").replaceName("baz")); + assertEquals(null, new PathFragment("").replaceName("")); + + assertPath("foo/bar/baz", new PathFragment("foo/bar").replaceName("bar/baz")); + assertPath("foo/bar/baz", new PathFragment("foo/bar").replaceName("bar/baz/")); + + // Absolute path arguments will clobber the original path. + assertPath("/absolute", new PathFragment("foo/bar").replaceName("/absolute")); + assertPath("/", new PathFragment("foo/bar").replaceName("/")); + } + @Test + public void testSubFragment() throws Exception { + assertPath("/foo/bar/baz", + new PathFragment("/foo/bar/baz").subFragment(0, 3)); + assertPath("foo/bar/baz", + new PathFragment("foo/bar/baz").subFragment(0, 3)); + assertPath("/foo/bar", + new PathFragment("/foo/bar/baz").subFragment(0, 2)); + assertPath("bar/baz", + new PathFragment("/foo/bar/baz").subFragment(1, 3)); + assertPath("/foo", + new PathFragment("/foo/bar/baz").subFragment(0, 1)); + assertPath("bar", + new PathFragment("/foo/bar/baz").subFragment(1, 2)); + assertPath("baz", new PathFragment("/foo/bar/baz").subFragment(2, 3)); + assertPath("/", new PathFragment("/foo/bar/baz").subFragment(0, 0)); + assertPath("", new PathFragment("foo/bar/baz").subFragment(0, 0)); + assertPath("", new PathFragment("foo/bar/baz").subFragment(1, 1)); + try { + fail("unexpectedly succeeded: " + new PathFragment("foo/bar/baz").subFragment(3, 2)); + } catch (IndexOutOfBoundsException e) { /* Expected. */ } + try { + fail("unexpectedly succeeded: " + new PathFragment("foo/bar/baz").subFragment(4, 4)); + } catch (IndexOutOfBoundsException e) { /* Expected. */ } + } + + @Test + public void testStartsWith() { + PathFragment foobar = new PathFragment("/foo/bar"); + PathFragment foobarRelative = new PathFragment("foo/bar"); + + // (path, prefix) => true + assertTrue(foobar.startsWith(foobar)); + assertTrue(foobar.startsWith(new PathFragment("/"))); + assertTrue(foobar.startsWith(new PathFragment("/foo"))); + assertTrue(foobar.startsWith(new PathFragment("/foo/"))); + assertTrue(foobar.startsWith(new PathFragment("/foo/bar/"))); // Includes trailing slash. + + // (prefix, path) => false + assertFalse(new PathFragment("/foo").startsWith(foobar)); + assertFalse(new PathFragment("/").startsWith(foobar)); + + // (absolute, relative) => false + assertFalse(foobar.startsWith(foobarRelative)); + assertFalse(foobarRelative.startsWith(foobar)); + + // (relative path, relative prefix) => true + assertTrue(foobarRelative.startsWith(foobarRelative)); + assertTrue(foobarRelative.startsWith(new PathFragment("foo"))); + assertTrue(foobarRelative.startsWith(new PathFragment(""))); + + // (path, sibling) => false + assertFalse(new PathFragment("/foo/wiz").startsWith(foobar)); + assertFalse(foobar.startsWith(new PathFragment("/foo/wiz"))); + + // Does not normalize. + PathFragment foodotbar = new PathFragment("foo/./bar"); + assertTrue(foodotbar.startsWith(foodotbar)); + assertTrue(foodotbar.startsWith(new PathFragment("foo/."))); + assertTrue(foodotbar.startsWith(new PathFragment("foo/./"))); + assertTrue(foodotbar.startsWith(new PathFragment("foo/./bar"))); + assertFalse(foodotbar.startsWith(new PathFragment("foo/bar"))); + } + + @Test + public void testEndsWith() { + PathFragment foobar = new PathFragment("/foo/bar"); + PathFragment foobarRelative = new PathFragment("foo/bar"); + + // (path, suffix) => true + assertTrue(foobar.endsWith(foobar)); + assertTrue(foobar.endsWith(new PathFragment("bar"))); + assertTrue(foobar.endsWith(new PathFragment("foo/bar"))); + assertTrue(foobar.endsWith(new PathFragment("/foo/bar"))); + assertFalse(foobar.endsWith(new PathFragment("/bar"))); + + // (prefix, path) => false + assertFalse(new PathFragment("/foo").endsWith(foobar)); + assertFalse(new PathFragment("/").endsWith(foobar)); + + // (suffix, path) => false + assertFalse(new PathFragment("/bar").endsWith(foobar)); + assertFalse(new PathFragment("bar").endsWith(foobar)); + assertFalse(new PathFragment("").endsWith(foobar)); + + // (absolute, relative) => true + assertTrue(foobar.endsWith(foobarRelative)); + + // (relative, absolute) => false + assertFalse(foobarRelative.endsWith(foobar)); + + // (relative path, relative prefix) => true + assertTrue(foobarRelative.endsWith(foobarRelative)); + assertTrue(foobarRelative.endsWith(new PathFragment("bar"))); + assertTrue(foobarRelative.endsWith(new PathFragment(""))); + + // (path, sibling) => false + assertFalse(new PathFragment("/foo/wiz").endsWith(foobar)); + assertFalse(foobar.endsWith(new PathFragment("/foo/wiz"))); + } + + static List<PathFragment> toPaths(List<String> strs) { + List<PathFragment> paths = Lists.newArrayList(); + for (String s : strs) { + paths.add(new PathFragment(s)); + } + return paths; + } + + @Test + public void testCompareTo() throws Exception { + List<String> pathStrs = ImmutableList.of( + "", "/", "//", ".", "/./", "foo/.//bar", "foo", "/foo", "foo/bar", "foo/Bar", "Foo/bar"); + List<PathFragment> paths = toPaths(pathStrs); + // First test that compareTo is self-consistent. + for (PathFragment x : paths) { + for (PathFragment y : paths) { + for (PathFragment z : paths) { + // Anti-symmetry + assertEquals(Integer.signum(x.compareTo(y)), + -1 * Integer.signum(y.compareTo(x))); + // Transitivity + if (x.compareTo(y) > 0 && y.compareTo(z) > 0) { + MoreAsserts.assertGreaterThan(0, x.compareTo(z)); + } + // "Substitutability" + if (x.compareTo(y) == 0) { + assertEquals(Integer.signum(x.compareTo(z)), Integer.signum(y.compareTo(z))); + } + // Consistency with equals + assertEquals((x.compareTo(y) == 0), x.equals(y)); + } + } + } + // Now test that compareTo does what we expect. The exact ordering here doesn't matter much, + // but there are three things to notice: 1. absolute < relative, 2. comparison is lexicographic + // 3. repeated slashes are ignored. (PathFragment("//") prints as "/"). + Collections.shuffle(paths); + Collections.sort(paths); + List<PathFragment> expectedOrder = toPaths(ImmutableList.of( + "/", "//", "/./", "/foo", "", ".", "Foo/bar", "foo", "foo/.//bar", "foo/Bar", "foo/bar")); + assertEquals(expectedOrder, paths); + } + + @Test + public void testGetSafePathString() { + assertEquals("/", new PathFragment("/").getSafePathString()); + assertEquals("/abc", new PathFragment("/abc").getSafePathString()); + assertEquals(".", new PathFragment("").getSafePathString()); + assertEquals(".", PathFragment.EMPTY_FRAGMENT.getSafePathString()); + assertEquals("abc/def", new PathFragment("abc/def").getSafePathString()); + } + + @Test + public void testNormalize() { + assertEquals(new PathFragment("/a/b"), new PathFragment("/a/b").normalize()); + assertEquals(new PathFragment("/a/b"), new PathFragment("/a/./b").normalize()); + assertEquals(new PathFragment("/b"), new PathFragment("/a/../b").normalize()); + assertEquals(new PathFragment("a/b"), new PathFragment("a/b").normalize()); + assertEquals(new PathFragment("../b"), new PathFragment("a/../../b").normalize()); + assertEquals(new PathFragment(".."), new PathFragment("a/../..").normalize()); + assertEquals(new PathFragment("b"), new PathFragment("a/../b").normalize()); + assertEquals(new PathFragment("a/b"), new PathFragment("a/b/../b").normalize()); + assertEquals(new PathFragment("/.."), new PathFragment("/..").normalize()); + } + + @Test + public void testSerializationSimple() throws Exception { + checkSerialization("a", 91); + } + + @Test + public void testSerializationAbsolute() throws Exception { + checkSerialization("/foo", 94); + } + + @Test + public void testSerializationNested() throws Exception { + checkSerialization("foo/bar/baz", 101); + } + + private void checkSerialization(String pathFragmentString, int expectedSize) throws Exception { + PathFragment a = new PathFragment(pathFragmentString); + byte[] sa = TestUtils.serializeObject(a); + assertEquals(expectedSize, sa.length); + + PathFragment a2 = (PathFragment) TestUtils.deserializeObject(sa); + assertEquals(a, a2); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java new file mode 100644 index 0000000000..43c94d4b8f --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java @@ -0,0 +1,218 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; + +/** + * This class tests the functionality of the PathFragment. + */ +@RunWith(JUnit4.class) +public class PathFragmentWindowsTest { + + @Test + public void testWindowsSeparator() { + assertEquals("bar/baz", new PathFragment("bar\\baz").toString()); + assertEquals("C:/bar/baz", new PathFragment("c:\\bar\\baz").toString()); + } + + @Test + public void testIsAbsoluteWindows() { + assertTrue(new PathFragment("C:/").isAbsolute()); + assertTrue(new PathFragment("C:/").isAbsolute()); + assertTrue(new PathFragment("C:/foo").isAbsolute()); + assertTrue(new PathFragment("d:/foo/bar").isAbsolute()); + + assertFalse(new PathFragment("*:/").isAbsolute()); + + // C: is not an absolute path, it points to the current active directory on drive C:. + assertFalse(new PathFragment("C:").isAbsolute()); + assertFalse(new PathFragment("C:foo").isAbsolute()); + } + + @Test + public void testIsAbsoluteWindowsBackslash() { + assertTrue(new PathFragment(new File("C:\\blah")).isAbsolute()); + assertTrue(new PathFragment(new File("C:\\")).isAbsolute()); + assertTrue(new PathFragment(new File("\\blah")).isAbsolute()); + assertTrue(new PathFragment(new File("\\")).isAbsolute()); + } + + @Test + public void testIsNormalizedWindows() { + assertTrue(new PathFragment("C:/").isNormalized()); + assertTrue(new PathFragment("C:/absolute/path").isNormalized()); + assertFalse(new PathFragment("C:/absolute/./path").isNormalized()); + assertFalse(new PathFragment("C:/absolute/../path").isNormalized()); + } + + @Test + public void testRootNodeReturnsRootStringWindows() { + PathFragment rootFragment = new PathFragment("C:/"); + assertEquals("C:/", rootFragment.getPathString()); + } + + @Test + public void testGetRelativeWindows() { + assertEquals("C:/a/b", new PathFragment("C:/a").getRelative("b").getPathString()); + assertEquals("C:/a/b/c/d", new PathFragment("C:/a/b").getRelative("c/d").getPathString()); + assertEquals("C:/b", new PathFragment("C:/a").getRelative("C:/b").getPathString()); + assertEquals("C:/c/d", new PathFragment("C:/a/b").getRelative("C:/c/d").getPathString()); + assertEquals("C:/b", new PathFragment("a").getRelative("C:/b").getPathString()); + assertEquals("C:/c/d", new PathFragment("a/b").getRelative("C:/c/d").getPathString()); + } + + @Test + public void testGetRelativeMixed() { + assertEquals("/b", new PathFragment("C:/a").getRelative("/b").getPathString()); + assertEquals("C:/b", new PathFragment("/a").getRelative("C:/b").getPathString()); + } + + @Test + public void testGetChildWorks() { + PathFragment pf = new PathFragment("../some/path"); + assertEquals(new PathFragment("../some/path/hi"), pf.getChild("hi")); + } + + // Tests after here test the canonicalization + private void assertRegular(String expected, String actual) { + assertEquals(expected, new PathFragment(actual).getPathString()); // compare string forms + assertEquals(new PathFragment(expected), new PathFragment(actual)); // compare fragment forms + } + + @Test + public void testEmptyPathToEmptyPathWindows() { + assertRegular("C:/", "C:/"); + } + + @Test + public void testEmptyRelativePathToEmptyPathWindows() { + assertRegular("C:", "C:"); + } + + @Test + public void testWindowsVolumeUppercase() { + assertRegular("C:/", "c:/"); + } + + @Test + public void testRedundantSlashesWindows() { + assertRegular("C:/", "C:///"); + assertRegular("C:/foo/bar", "C:/foo///bar"); + assertRegular("C:/foo/bar", "C:////foo//bar"); + } + + @Test + public void testSimpleNameToSimpleNameWindows() { + assertRegular("C:/foo", "C:/foo"); + } + + @Test + public void testStripsTrailingSlashWindows() { + assertRegular("C:/foo/bar", "C:/foo/bar/"); + } + + @Test + public void testGetParentDirectoryWindows() { + PathFragment fooBarWizAbs = new PathFragment("C:/foo/bar/wiz"); + PathFragment fooBarAbs = new PathFragment("C:/foo/bar"); + PathFragment fooAbs = new PathFragment("C:/foo"); + PathFragment rootAbs = new PathFragment("C:/"); + assertEquals(fooBarAbs, fooBarWizAbs.getParentDirectory()); + assertEquals(fooAbs, fooBarAbs.getParentDirectory()); + assertEquals(rootAbs, fooAbs.getParentDirectory()); + assertNull(rootAbs.getParentDirectory()); + + // Note, this is suprising but correct behaviour: + assertEquals(fooBarAbs, + new PathFragment("C:/foo/bar/..").getParentDirectory()); + } + + @Test + public void testSegmentsCountWindows() { + assertEquals(1, new PathFragment("C:/foo").segmentCount()); + assertEquals(0, new PathFragment("C:/").segmentCount()); + } + + @Test + public void testGetSegmentWindows() { + assertEquals("foo", new PathFragment("C:/foo/bar").getSegment(0)); + assertEquals("bar", new PathFragment("C:/foo/bar").getSegment(1)); + assertEquals("foo", new PathFragment("C:/foo/").getSegment(0)); + assertEquals("foo", new PathFragment("C:/foo").getSegment(0)); + } + + @Test + public void testBasenameWindows() throws Exception { + assertEquals("bar", new PathFragment("C:/foo/bar").getBaseName()); + assertEquals("foo", new PathFragment("C:/foo").getBaseName()); + // Never return the drive name as a basename. + assertEquals("", new PathFragment("C:/").getBaseName()); + } + + private static void assertPath(String expected, PathFragment actual) { + assertEquals(expected, actual.getPathString()); + } + + @Test + public void testReplaceNameWindows() throws Exception { + assertPath("C:/foo/baz", new PathFragment("C:/foo/bar").replaceName("baz")); + assertEquals(null, new PathFragment("C:/").replaceName("baz")); + } + + @Test + public void testStartsWithWindows() { + assertTrue(new PathFragment("C:/foo/bar").startsWith(new PathFragment("C:/foo"))); + assertTrue(new PathFragment("C:/foo/bar").startsWith(new PathFragment("C:/"))); + assertTrue(new PathFragment("C:foo/bar").startsWith(new PathFragment("C:"))); + assertTrue(new PathFragment("C:/").startsWith(new PathFragment("C:/"))); + assertTrue(new PathFragment("C:").startsWith(new PathFragment("C:"))); + + // The first path is absolute, the second is not. + assertFalse(new PathFragment("C:/foo/bar").startsWith(new PathFragment("C:"))); + assertFalse(new PathFragment("C:/").startsWith(new PathFragment("C:"))); + } + + @Test + public void testEndsWithWindows() { + assertTrue(new PathFragment("C:/foo/bar").endsWith(new PathFragment("bar"))); + assertTrue(new PathFragment("C:/foo/bar").endsWith(new PathFragment("foo/bar"))); + assertTrue(new PathFragment("C:/foo/bar").endsWith(new PathFragment("C:/foo/bar"))); + assertTrue(new PathFragment("C:/").endsWith(new PathFragment("C:/"))); + } + + @Test + public void testGetSafePathStringWindows() { + assertEquals("C:/", new PathFragment("C:/").getSafePathString()); + assertEquals("C:/abc", new PathFragment("C:/abc").getSafePathString()); + assertEquals("C:/abc/def", new PathFragment("C:/abc/def").getSafePathString()); + } + + @Test + public void testNormalizeWindows() { + assertEquals(new PathFragment("C:/a/b"), new PathFragment("C:/a/b").normalize()); + assertEquals(new PathFragment("C:/a/b"), new PathFragment("C:/a/./b").normalize()); + assertEquals(new PathFragment("C:/b"), new PathFragment("C:/a/../b").normalize()); + assertEquals(new PathFragment("C:/../b"), new PathFragment("C:/../b").normalize()); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java new file mode 100644 index 0000000000..738e454e43 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java @@ -0,0 +1,312 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.Lists; +import com.google.common.testing.EqualsTester; +import com.google.common.testing.GcFinalization; +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.List; + +/** + * A test for {@link Path}. + */ +@RunWith(JUnit4.class) +public class PathTest { + private FileSystem filesystem; + private Path root; + + @Before + public void setUp() throws Exception { + filesystem = new InMemoryFileSystem(BlazeClock.instance()); + root = filesystem.getRootDirectory(); + Path first = root.getChild("first"); + first.createDirectory(); + } + + @Test + public void testStartsWithWorksForSelf() { + assertStartsWithReturns(true, "/first/child", "/first/child"); + } + + @Test + public void testStartsWithWorksForChild() { + assertStartsWithReturns(true, + "/first/child", "/first/child/grandchild"); + } + + @Test + public void testStartsWithWorksForDeepDescendant() { + assertStartsWithReturns(true, + "/first/child", "/first/child/grandchild/x/y/z"); + } + + @Test + public void testStartsWithFailsForParent() { + assertStartsWithReturns(false, "/first/child", "/first"); + } + + @Test + public void testStartsWithFailsForSibling() { + assertStartsWithReturns(false, "/first/child", "/first/child2"); + } + + @Test + public void testStartsWithFailsForLinkToDescendant() + throws Exception { + Path linkTarget = filesystem.getPath("/first/linked_to"); + FileSystemUtils.createEmptyFile(linkTarget); + Path second = filesystem.getPath("/second/"); + second.createDirectory(); + second.getChild("child_link").createSymbolicLink(linkTarget); + assertStartsWithReturns(false, "/first", "/second/child_link"); + } + + @Test + public void testStartsWithFailsForNullPrefix() { + try { + filesystem.getPath("/first").startsWith(null); + fail(); + } catch (Exception e) { + } + } + + private void assertStartsWithReturns(boolean expected, + String ancestor, + String descendant) { + Path parent = filesystem.getPath(ancestor); + Path child = filesystem.getPath(descendant); + assertEquals(expected, child.startsWith(parent)); + } + + @Test + public void testGetChildWorks() { + assertGetChildWorks("second"); + assertGetChildWorks("..."); + assertGetChildWorks("...."); + } + + private void assertGetChildWorks(String childName) { + assertEquals(filesystem.getPath("/first/" + childName), + filesystem.getPath("/first").getChild(childName)); + } + + @Test + public void testGetChildFailsForChildWithSlashes() { + assertGetChildFails("second/third"); + assertGetChildFails("./third"); + assertGetChildFails("../third"); + assertGetChildFails("second/.."); + assertGetChildFails("second/."); + assertGetChildFails("/third"); + assertGetChildFails("third/"); + } + + private void assertGetChildFails(String childName) { + try { + filesystem.getPath("/first").getChild(childName); + fail(); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetChildFailsForDotAndDotDot() { + assertGetChildFails("."); + assertGetChildFails(".."); + } + + @Test + public void testGetChildFailsForEmptyString() { + assertGetChildFails(""); + } + + @Test + public void testRelativeToWorks() { + assertRelativeToWorks("apple", "/fruit/apple", "/fruit"); + assertRelativeToWorks("apple/jonagold", "/fruit/apple/jonagold", "/fruit"); + } + + @Test + public void testGetRelativeWithStringWorks() { + assertGetRelativeWorks("/first/x/y", "y"); + assertGetRelativeWorks("/y", "/y"); + assertGetRelativeWorks("/first/x/x", "./x"); + assertGetRelativeWorks("/first/y", "../y"); + assertGetRelativeWorks("/", "../../../../.."); + } + + @Test + public void testAsFragmentWorks() { + assertAsFragmentWorks("/"); + assertAsFragmentWorks("//"); + assertAsFragmentWorks("/first"); + assertAsFragmentWorks("/first/x/y"); + assertAsFragmentWorks("/first/x/y.foo"); + } + + @Test + public void testGetRelativeWithFragmentWorks() { + Path dir = filesystem.getPath("/first/x"); + assertEquals("/first/x/y", + dir.getRelative(new PathFragment("y")).toString()); + assertEquals("/first/x/x", + dir.getRelative(new PathFragment("./x")).toString()); + assertEquals("/first/y", + dir.getRelative(new PathFragment("../y")).toString()); + + } + + @Test + public void testGetRelativeWithAbsoluteFragmentWorks() { + Path root = filesystem.getPath("/first/x"); + assertEquals("/x/y", + root.getRelative(new PathFragment("/x/y")).toString()); + } + + @Test + public void testGetRelativeWithAbsoluteStringWorks() { + Path root = filesystem.getPath("/first/x"); + assertEquals("/x/y", root.getRelative("/x/y").toString()); + } + + @Test + public void testComparableSortOrder() { + Path zzz = filesystem.getPath("/zzz"); + Path ZZZ = filesystem.getPath("/ZZZ"); + Path abc = filesystem.getPath("/abc"); + Path aBc = filesystem.getPath("/aBc"); + Path AbC = filesystem.getPath("/AbC"); + Path ABC = filesystem.getPath("/ABC"); + List<Path> list = Lists.newArrayList(zzz, ZZZ, ABC, aBc, AbC, abc); + Collections.sort(list); + assertThat(list).containsExactly(ABC, AbC, ZZZ, aBc, abc, zzz).inOrder(); + } + + @Test + public void testParentOfRootIsRoot() { + assertSame(root, root.getRelative("..")); + + assertSame(root.getRelative("dots"), + root.getRelative("broken/../../dots")); + } + + @Test + public void testSingleSegmentEquivalence() { + assertSame( + root.getRelative("aSingleSegment"), + root.getRelative("aSingleSegment")); + } + + @Test + public void testSiblingNonEquivalenceString() { + assertNotSame( + root.getRelative("aSingleSegment"), + root.getRelative("aDifferentSegment")); + } + + @Test + public void testSiblingNonEquivalenceFragment() { + assertNotSame( + root.getRelative(new PathFragment("aSingleSegment")), + root.getRelative(new PathFragment("aDifferentSegment"))); + } + + @Test + public void testHashCodeStableAcrossGarbageCollections() { + Path parent = filesystem.getPath("/a"); + PathFragment childFragment = new PathFragment("b"); + Path child = parent.getRelative(childFragment); + WeakReference<Path> childRef = new WeakReference<>(child); + int childHashCode1 = childRef.get().hashCode(); + assertEquals(childHashCode1, parent.getRelative(childFragment).hashCode()); + child = null; + GcFinalization.awaitClear(childRef); + int childHashCode2 = parent.getRelative(childFragment).hashCode(); + assertEquals(childHashCode1, childHashCode2); + } + + @Test + public void testSerialization() throws Exception { + FileSystem oldFileSystem = Path.getFileSystemForSerialization(); + try { + Path.setFileSystemForSerialization(filesystem); + Path root = filesystem.getPath("/"); + Path p1 = filesystem.getPath("/foo"); + Path p2 = filesystem.getPath("/foo/bar"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + + oos.writeObject(root); + oos.writeObject(p1); + oos.writeObject(p2); + + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bis); + + Path dsRoot = (Path) ois.readObject(); + Path dsP1 = (Path) ois.readObject(); + Path dsP2 = (Path) ois.readObject(); + + new EqualsTester() + .addEqualityGroup(root, dsRoot) + .addEqualityGroup(p1, dsP1) + .addEqualityGroup(p2, dsP2) + .testEquals(); + + assertTrue(p2.startsWith(p1)); + assertTrue(p2.startsWith(dsP1)); + assertTrue(dsP2.startsWith(p1)); + assertTrue(dsP2.startsWith(dsP1)); + } finally { + Path.setFileSystemForSerialization(oldFileSystem); + } + } + + private void assertAsFragmentWorks(String expected) { + assertEquals(new PathFragment(expected), filesystem.getPath(expected).asFragment()); + } + + private void assertGetRelativeWorks(String expected, String relative) { + assertEquals(filesystem.getPath(expected), + filesystem.getPath("/first/x").getRelative(relative)); + } + + private void assertRelativeToWorks(String expected, String relative, String original) { + assertEquals(new PathFragment(expected), + filesystem.getPath(relative).relativeTo(filesystem.getPath(original))); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java new file mode 100644 index 0000000000..c92fc2b634 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java @@ -0,0 +1,98 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * A test for windows aspects of {@link Path}. + */ +@RunWith(JUnit4.class) +public class PathWindowsTest { + private FileSystem filesystem; + private Path root; + + @Before + public void setUp() throws Exception { + filesystem = new InMemoryFileSystem(BlazeClock.instance()); + root = filesystem.getRootDirectory(); + Path first = root.getChild("first"); + first.createDirectory(); + } + + private void assertAsFragmentWorks(String expected) { + assertEquals(new PathFragment(expected), filesystem.getPath(expected).asFragment()); + } + + @Test + public void testWindowsPath() { + Path p = filesystem.getPath("C:/foo/bar"); + assertEquals("C:/foo/bar", p.getPathString()); + assertEquals("C:/foo/bar", p.toString()); + } + + @Test + public void testAsFragmentWindows() { + assertAsFragmentWorks("C:/"); + assertAsFragmentWorks("C://"); + assertAsFragmentWorks("C:/first"); + assertAsFragmentWorks("C:/first/x/y"); + assertAsFragmentWorks("C:/first/x/y.foo"); + } + + @Test + public void testGetRelativeWithFragmentWindows() { + Path dir = filesystem.getPath("C:/first/x"); + assertEquals("C:/first/x/y", + dir.getRelative(new PathFragment("y")).toString()); + assertEquals("C:/first/x/x", + dir.getRelative(new PathFragment("./x")).toString()); + assertEquals("C:/first/y", + dir.getRelative(new PathFragment("../y")).toString()); + assertEquals("C:/first/y", + dir.getRelative(new PathFragment("../y")).toString()); + assertEquals("C:/y", + dir.getRelative(new PathFragment("../../../y")).toString()); + } + + @Test + public void testGetRelativeWithAbsoluteFragmentWindows() { + Path root = filesystem.getPath("C:/first/x"); + assertEquals("C:/x/y", + root.getRelative(new PathFragment("C:/x/y")).toString()); + } + + @Test + public void testGetRelativeWithAbsoluteStringWorksWindows() { + Path root = filesystem.getPath("C:/first/x"); + assertEquals("C:/x/y", root.getRelative("C:/x/y").toString()); + } + + @Test + public void testParentOfRootIsRootWindows() { + assertSame(root, root.getRelative("..")); + + assertSame(root.getRelative("dots"), + root.getRelative("broken/../../dots")); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java b/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java new file mode 100644 index 0000000000..5e0012ac08 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java @@ -0,0 +1,227 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.testutil.MoreAsserts.assertSameContents; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import com.google.devtools.build.lib.testutil.MoreAsserts; +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Tests {@link UnixGlob} recursive globs. + */ +@RunWith(JUnit4.class) +public class RecursiveGlobTest { + + private Path tmpPath; + private FileSystem fileSystem; + + @Before + public void setUp() throws Exception { + fileSystem = new InMemoryFileSystem(BlazeClock.instance()); + tmpPath = fileSystem.getPath("/rglobtmp"); + for (String dir : ImmutableList.of("foo/bar/wiz", + "foo/baz/wiz", + "foo/baz/quip/wiz", + "food/baz/wiz", + "fool/baz/wiz")) { + FileSystemUtils.createDirectoryAndParents(tmpPath.getRelative(dir)); + } + FileSystemUtils.createEmptyFile(tmpPath.getRelative("foo/bar/wiz/file")); + } + + @Test + public void testDoubleStar() throws Exception { + assertGlobMatches("**", ".", "foo", "foo/bar", "foo/bar/wiz", "foo/baz", "foo/baz/quip", + "foo/baz/quip/wiz", "foo/baz/wiz", "foo/bar/wiz/file", "food", "food/baz", + "food/baz/wiz", "fool", "fool/baz", "fool/baz/wiz"); + } + + @Test + public void testDoubleDoubleStar() throws Exception { + assertGlobMatches("**/**", ".", "foo", "foo/bar", "foo/bar/wiz", "foo/baz", "foo/baz/quip", + "foo/baz/quip/wiz", "foo/baz/wiz", "foo/bar/wiz/file", "food", "food/baz", + "food/baz/wiz", "fool", "fool/baz", "fool/baz/wiz"); + } + + @Test + public void testDirectoryWithDoubleStar() throws Exception { + assertGlobMatches("foo/**", "foo", "foo/bar", "foo/bar/wiz", "foo/baz", "foo/baz/quip", + "foo/baz/quip/wiz", "foo/baz/wiz", "foo/bar/wiz/file"); + } + + @Test + public void testIllegalPatterns() throws Exception { + for (String prefix : Lists.newArrayList("", "*/", "**/", "ba/")) { + String suffix = ("/" + prefix).substring(0, prefix.length()); + for (String pattern : Lists.newArrayList("**fo", "fo**", "**fo**", "fo**fo", "fo**fo**fo")) { + assertIllegalWildcard(prefix + pattern); + assertIllegalWildcard(pattern + suffix); + assertIllegalWildcard("foo", pattern + suffix); + } + } + } + + @Test + public void testDoubleStarPatternWithNamedChild() throws Exception { + assertGlobMatches("**/bar", "foo/bar"); + } + + @Test + public void testDoubleStarPatternWithChildGlob() throws Exception { + assertGlobMatches("**/ba*", + "foo/bar", "foo/baz", "food/baz", "fool/baz"); + } + + @Test + public void testDoubleStarAsChildGlob() throws Exception { + assertGlobMatches("foo/**/wiz", "foo/bar/wiz", "foo/baz/quip/wiz", "foo/baz/wiz"); + } + + @Test + public void testDoubleStarUnderNonexistentDirectory() throws Exception { + assertGlobMatches("not-there/**" /* => nothing */); + } + + @Test + public void testDoubleStarGlobWithNonExistentBase() throws Exception { + Collection<Path> globResult = UnixGlob.forPath(fileSystem.getPath("/does/not/exist")) + .addPattern("**") + .globInterruptible(); + assertEquals(0, globResult.size()); + } + + @Test + public void testDoubleStarUnderFile() throws Exception { + assertGlobMatches("foo/bar/wiz/file/**" /* => nothing */); + } + + @Test + public void testSingleFileExclude() throws Exception { + assertGlobWithExcludeMatches("**", "food", ".", "foo", "foo/bar", "foo/bar/wiz", "foo/baz", + "foo/baz/quip", "foo/baz/quip/wiz", "foo/baz/wiz", + "foo/bar/wiz/file", "food/baz", "food/baz/wiz", "fool", "fool/baz", + "fool/baz/wiz"); + } + + @Test + public void testSingleFileExcludeForDirectoryWithChildGlob() + throws Exception { + assertGlobWithExcludeMatches("foo/**", "foo", "foo/bar", "foo/bar/wiz", "foo/baz", + "foo/baz/quip", "foo/baz/quip/wiz", "foo/baz/wiz", + "foo/bar/wiz/file"); + } + + @Test + public void testGlobExcludeForDirectoryWithChildGlob() + throws Exception { + assertGlobWithExcludeMatches("foo/**", "foo/*", "foo", "foo/bar/wiz", "foo/baz/quip", + "foo/baz/quip/wiz", "foo/baz/wiz", "foo/bar/wiz/file"); + } + + @Test + public void testExcludeAll() throws Exception { + assertGlobWithExcludesMatches(Lists.newArrayList("**"), + Lists.newArrayList("*", "*/*", "*/*/*", "*/*/*/*"), "."); + } + + @Test + public void testManualGlobExcludeForDirectoryWithChildGlob() + throws Exception { + assertGlobWithExcludesMatches(Lists.newArrayList("foo/**"), + Lists.newArrayList("foo", "foo/*", "foo/*/*", "foo/*/*/*")); + } + + private void assertGlobMatches(String pattern, String... expecteds) + throws Exception { + assertGlobWithExcludesMatches( + Collections.singleton(pattern), Collections.<String>emptyList(), + expecteds); + } + + private void assertGlobWithExcludeMatches(String pattern, String exclude, + String... expecteds) + throws Exception { + assertGlobWithExcludesMatches( + Collections.singleton(pattern), Collections.singleton(exclude), + expecteds); + } + + private void assertGlobWithExcludesMatches(Collection<String> pattern, + Collection<String> excludes, + String... expecteds) throws Exception { + assertSameContents(resolvePaths(expecteds), + new UnixGlob.Builder(tmpPath) + .addPatterns(pattern) + .addExcludes(excludes) + .globInterruptible()); + } + + private Set<Path> resolvePaths(String... relativePaths) { + Set<Path> expectedFiles = new HashSet<>(); + for (String expected : relativePaths) { + Path file = expected.equals(".") + ? tmpPath + : tmpPath.getRelative(expected); + expectedFiles.add(file); + } + return expectedFiles; + } + + /** + * Tests that a recursive glob returns files in sorted order. + */ + @Test + public void testGlobEntriesAreSorted() throws Exception { + List<Path> globResult = new UnixGlob.Builder(tmpPath) + .addPattern("**") + .setExcludeDirectories(false) + .globInterruptible(); + + assertThat(Ordering.natural().sortedCopy(globResult)).containsExactlyElementsIn(globResult) + .inOrder(); + } + + private void assertIllegalWildcard(String pattern, String... excludePatterns) + throws Exception { + try { + new UnixGlob.Builder(tmpPath) + .addPattern(pattern) + .addExcludes(excludePatterns) + .globInterruptible(); + fail(); + } catch (IllegalArgumentException e) { + MoreAsserts.assertContainsRegex("recursive wildcard must be its own segment", e.getMessage()); + } + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/RootedPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/RootedPathTest.java new file mode 100644 index 0000000000..46d286dcdc --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/RootedPathTest.java @@ -0,0 +1,56 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import com.google.common.testing.EqualsTester; +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for {@link RootedPath}. + */ +@RunWith(JUnit4.class) +public class RootedPathTest { + private FileSystem filesystem; + private Path root; + + @Before + public void setUp() throws Exception { + filesystem = new InMemoryFileSystem(BlazeClock.instance()); + root = filesystem.getRootDirectory(); + } + + @Test + public void testEqualsAndHashCodeContract() throws Exception { + Path pkgRoot1 = root.getRelative("pkgroot1"); + Path pkgRoot2 = root.getRelative("pkgroot2"); + RootedPath rootedPathA1 = RootedPath.toRootedPath(pkgRoot1, new PathFragment("foo/bar")); + RootedPath rootedPathA2 = RootedPath.toRootedPath(pkgRoot1, new PathFragment("foo/bar")); + RootedPath absolutePath1 = RootedPath.toRootedPath(root, new PathFragment("pkgroot1/foo/bar")); + RootedPath rootedPathB1 = RootedPath.toRootedPath(pkgRoot2, new PathFragment("foo/bar")); + RootedPath rootedPathB2 = RootedPath.toRootedPath(pkgRoot2, new PathFragment("foo/bar")); + RootedPath absolutePath2 = RootedPath.toRootedPath(root, new PathFragment("pkgroot2/foo/bar")); + new EqualsTester() + .addEqualityGroup(rootedPathA1, rootedPathA2) + .addEqualityGroup(rootedPathB1, rootedPathB2) + .addEqualityGroup(absolutePath1) + .addEqualityGroup(absolutePath2) + .testEquals(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java new file mode 100644 index 0000000000..6c8071f075 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java @@ -0,0 +1,806 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; + +/** + * Generic tests for any file system that implements {@link ScopeEscapableFileSystem}, + * i.e. any file system that supports symlinks that escape its scope. + * + * Each suitable file system test should inherit from this class, thereby obtaining + * all the tests. + */ +public abstract class ScopeEscapableFileSystemTest extends SymlinkAwareFileSystemTest { + + /** + * Trivial FileSystem implementation that can record the last path passed to each method + * and read/write to a unified "state" variable (which can then be checked by tests) for + * each data type this class manipulates. + * + * The default implementation of each method throws an exception. Each test case should + * selectively override the methods it expects to be invoked. + */ + private static class TestDelegator extends FileSystem { + protected Path lastPath; + protected boolean booleanState; + protected long longState; + protected Object objectState; + + public void setState(boolean state) { booleanState = state; } + public void setState(long state) { longState = state; } + public void setState(Object state) { objectState = state; } + + public boolean booleanState() { return booleanState; } + public long longState() { return longState; } + public Object objectState() { return objectState; } + + public PathFragment lastPath() { + Path ans = lastPath; + // Clear this out to protect against accidental matches when testing the same path multiple + // consecutive times. + lastPath = null; + return ans != null ? ans.asFragment() : null; + } + + @Override public boolean supportsModifications() { return true; } + @Override public boolean supportsSymbolicLinks() { return true; } + + private static RuntimeException re() { + return new RuntimeException("This method should not be called in this context"); + } + + @Override protected boolean isReadable(Path path) { throw re(); } + @Override protected boolean isWritable(Path path) { throw re(); } + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { throw re(); } + @Override protected boolean isFile(Path path, boolean followSymlinks) { throw re(); } + @Override protected boolean isExecutable(Path path) { throw re(); } + @Override protected boolean exists(Path path, boolean followSymlinks) {throw re(); } + @Override protected boolean isSymbolicLink(Path path) { throw re(); } + @Override protected boolean createDirectory(Path path) { throw re(); } + @Override protected boolean delete(Path path) { throw re(); } + + @Override protected long getFileSize(Path path, boolean followSymlinks) { throw re(); } + @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { throw re(); } + + @Override protected void setWritable(Path path, boolean writable) { throw re(); } + @Override protected void setExecutable(Path path, boolean executable) { throw re(); } + @Override protected void setReadable(Path path, boolean readable) { throw re(); } + @Override protected void setLastModifiedTime(Path path, long newTime) { throw re(); } + @Override protected void renameTo(Path sourcePath, Path targetPath) { throw re(); } + @Override protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) { + throw re(); + } + + @Override protected PathFragment readSymbolicLink(Path path) { throw re(); } + @Override protected InputStream getInputStream(Path path) { throw re(); } + @Override protected Collection<Path> getDirectoryEntries(Path path) { throw re(); } + @Override protected OutputStream getOutputStream(Path path, boolean append) { throw re(); } + @Override + protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException { + throw re(); + } + } + + protected static final PathFragment SCOPE_ROOT = new PathFragment("/fs/root"); + + private Path fileLink; + private PathFragment fileLinkTarget; + private Path dirLink; + private PathFragment dirLinkTarget; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + Preconditions.checkState(testFS instanceof ScopeEscapableFileSystem, + "Not ScopeEscapable: " + testFS); + ((ScopeEscapableFileSystem) testFS).enableScopeChecking(false); + for (int i = 1; i <= SCOPE_ROOT.segmentCount(); i++) { + testFS.getPath(SCOPE_ROOT.subFragment(0, i)).createDirectory(); + } + + fileLink = testFS.getPath(SCOPE_ROOT.getRelative("link")); + fileLinkTarget = new PathFragment("/should/be/delegated/fileLinkTarget"); + testFS.createSymbolicLink(fileLink, fileLinkTarget); + + dirLink = testFS.getPath(SCOPE_ROOT.getRelative("dirlink")); + dirLinkTarget = new PathFragment("/should/be/delegated/dirLinkTarget"); + testFS.createSymbolicLink(dirLink, dirLinkTarget); + } + + /** + * Returns the file system supplied by {@link #getFreshFileSystem}, cast to + * a {@link ScopeEscapableFileSystem}. Also enables scope checking within + * the file system (which we keep disabled for inherited tests that aren't + * intended to test scope boundaries). + */ + private ScopeEscapableFileSystem scopedFS() { + ScopeEscapableFileSystem fs = (ScopeEscapableFileSystem) testFS; + fs.enableScopeChecking(true); + return fs; + } + + // Checks that the semi-resolved path passed to the delegator matches the expected value. + private void checkPath(TestDelegator delegator, PathFragment expectedDelegatedPath) { + assertTrue(expectedDelegatedPath.equals(delegator.lastPath())); + } + + // Asserts that the condition is false and checks that the expected path was delegated. + private void assertFalseWithPathCheck(boolean result, TestDelegator delegator, + PathFragment expectedDelegatedPath) { + assertFalse(result); + checkPath(delegator, expectedDelegatedPath); + } + + // Asserts that the condition is true and checks that the expected path was delegated. + private void assertTrueWithPathCheck(boolean result, TestDelegator delegator, + PathFragment expectedDelegatedPath) { + assertTrue(result); + checkPath(delegator, expectedDelegatedPath); + } + + ///////////////////////////////////////////////////////////////////////////// + // Tests: + ///////////////////////////////////////////////////////////////////////////// + + @Test + public void testIsReadableCallOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected boolean isReadable(Path path) { + lastPath = path; + return booleanState(); + } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + assertFalseWithPathCheck(fileLink.isReadable(), delegator, fileLinkTarget); + assertFalseWithPathCheck(dirLink.getRelative("a").isReadable(), delegator, + dirLinkTarget.getRelative("a")); + + delegator.setState(true); + assertTrueWithPathCheck(fileLink.isReadable(), delegator, fileLinkTarget); + assertTrueWithPathCheck(dirLink.getRelative("a").isReadable(), delegator, + dirLinkTarget.getRelative("a")); + } + + @Test + public void testIsWritableCallOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected boolean isWritable(Path path) { + lastPath = path; + return booleanState(); + } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + assertFalseWithPathCheck(fileLink.isWritable(), delegator, fileLinkTarget); + assertFalseWithPathCheck(dirLink.getRelative("a").isWritable(), delegator, + dirLinkTarget.getRelative("a")); + + delegator.setState(true); + assertTrueWithPathCheck(fileLink.isWritable(), delegator, fileLinkTarget); + assertTrueWithPathCheck(dirLink.getRelative("a").isWritable(), delegator, + dirLinkTarget.getRelative("a")); + } + + @Test + public void testisExecutableCallOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected boolean isExecutable(Path path) { + lastPath = path; + return booleanState(); + } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + assertFalseWithPathCheck(fileLink.isExecutable(), delegator, fileLinkTarget); + assertFalseWithPathCheck(dirLink.getRelative("a").isExecutable(), delegator, + dirLinkTarget.getRelative("a")); + + delegator.setState(true); + assertTrueWithPathCheck(fileLink.isExecutable(), delegator, fileLinkTarget); + assertTrueWithPathCheck(dirLink.getRelative("a").isExecutable(), delegator, + dirLinkTarget.getRelative("a")); + } + + @Test + public void testIsDirectoryCallOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { + lastPath = path; + return booleanState(); + } + @Override protected boolean exists(Path path, boolean followSymlinks) { return true; } + @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + assertFalseWithPathCheck(fileLink.isDirectory(), delegator, fileLinkTarget); + assertFalseWithPathCheck(dirLink.getRelative("a").isDirectory(), delegator, + dirLinkTarget.getRelative("a")); + + delegator.setState(true); + assertTrueWithPathCheck(fileLink.isDirectory(), delegator, fileLinkTarget); + assertTrueWithPathCheck(dirLink.getRelative("a").isDirectory(), delegator, + dirLinkTarget.getRelative("a")); + } + + @Test + public void testIsFileCallOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected boolean isFile(Path path, boolean followSymlinks) { + lastPath = path; + return booleanState(); + } + @Override protected boolean exists(Path path, boolean followSymlinks) { return true; } + @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + assertFalseWithPathCheck(fileLink.isFile(), delegator, fileLinkTarget); + assertFalseWithPathCheck(dirLink.getRelative("a").isFile(), delegator, + dirLinkTarget.getRelative("a")); + + delegator.setState(true); + assertTrueWithPathCheck(fileLink.isFile(), delegator, fileLinkTarget); + assertTrueWithPathCheck(dirLink.getRelative("a").isFile(), delegator, + dirLinkTarget.getRelative("a")); + } + + @Test + public void testIsSymbolicLinkCallOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected boolean isSymbolicLink(Path path) { + lastPath = path; + return booleanState(); + } + @Override protected boolean exists(Path path, boolean followSymlinks) { return true; } + @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; } + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; } + }; + scopedFS().setDelegator(delegator); + + // We shouldn't follow final-segment links, so they should never invoke the delegator. + delegator.setState(false); + assertTrue(fileLink.isSymbolicLink()); + assertTrue(delegator.lastPath() == null); + + assertFalseWithPathCheck(dirLink.getRelative("a").isSymbolicLink(), delegator, + dirLinkTarget.getRelative("a")); + + delegator.setState(true); + assertTrueWithPathCheck(dirLink.getRelative("a").isSymbolicLink(), delegator, + dirLinkTarget.getRelative("a")); + } + + /** + * Returns a test delegator that reflects info passed to Path.exists() calls. + */ + private TestDelegator newExistsDelegator() { + return new TestDelegator() { + @Override protected boolean exists(Path path, boolean followSymlinks) { + lastPath = path; + return booleanState(); + } + @Override protected FileStatus stat(Path path, boolean followSymlinks) throws IOException { + if (!exists(path, followSymlinks)) { + throw new IOException("Expected exception on stat of non-existent file"); + } + return super.stat(path, followSymlinks); + } + @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; } + }; + } + + @Test + public void testExistsCallOnEscapingSymlink() throws Exception { + TestDelegator delegator = newExistsDelegator(); + scopedFS().setDelegator(delegator); + + delegator.setState(false); + assertFalseWithPathCheck(fileLink.exists(), delegator, fileLinkTarget); + assertFalseWithPathCheck(dirLink.getRelative("a").exists(), delegator, + dirLinkTarget.getRelative("a")); + + delegator.setState(true); + assertTrueWithPathCheck(fileLink.exists(), delegator, fileLinkTarget); + assertTrueWithPathCheck(dirLink.getRelative("a").exists(), delegator, + dirLinkTarget.getRelative("a")); + } + + @Test + public void testCreateDirectoryCallOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected boolean createDirectory(Path path) { + lastPath = path; + return booleanState(); + } + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + assertFalseWithPathCheck(dirLink.getRelative("a").createDirectory(), delegator, + dirLinkTarget.getRelative("a")); + + delegator.setState(true); + assertTrueWithPathCheck(dirLink.getRelative("a").createDirectory(), delegator, + dirLinkTarget.getRelative("a")); + } + + @Test + public void testDeleteCallOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected boolean delete(Path path) { + lastPath = path; + return booleanState(); + } + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; } + @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + assertTrue(fileLink.delete()); + assertTrue(delegator.lastPath() == null); // Deleting a link shouldn't require delegation. + assertFalseWithPathCheck(dirLink.getRelative("a").delete(), delegator, + dirLinkTarget.getRelative("a")); + + delegator.setState(true); + assertTrueWithPathCheck(dirLink.getRelative("a").delete(), delegator, + dirLinkTarget.getRelative("a")); + } + + @Test + public void testCallGetFileSizeOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected long getFileSize(Path path, boolean followSymlinks) { + lastPath = path; + return longState(); + } + @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; } + }; + scopedFS().setDelegator(delegator); + + final int state1 = 10; + delegator.setState(state1); + assertEquals(state1, fileLink.getFileSize()); + checkPath(delegator, fileLinkTarget); + assertEquals(state1, dirLink.getRelative("a").getFileSize()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + + final int state2 = 10; + delegator.setState(state2); + assertEquals(state2, fileLink.getFileSize()); + checkPath(delegator, fileLinkTarget); + assertEquals(state2, dirLink.getRelative("a").getFileSize()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + } + + @Test + public void testCallGetLastModifiedTimeOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { + lastPath = path; + return longState(); + } + }; + scopedFS().setDelegator(delegator); + + final int state1 = 10; + delegator.setState(state1); + assertEquals(state1, fileLink.getLastModifiedTime()); + checkPath(delegator, fileLinkTarget); + assertEquals(state1, dirLink.getRelative("a").getLastModifiedTime()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + + final int state2 = 10; + delegator.setState(state2); + assertEquals(state2, fileLink.getLastModifiedTime()); + checkPath(delegator, fileLinkTarget); + assertEquals(state2, dirLink.getRelative("a").getLastModifiedTime()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + } + + @Test + public void testCallSetReadableOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected void setReadable(Path path, boolean readable) { + lastPath = path; + setState(readable); + } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + fileLink.setReadable(true); + assertTrue(delegator.booleanState()); + checkPath(delegator, fileLinkTarget); + fileLink.setReadable(false); + assertFalse(delegator.booleanState()); + checkPath(delegator, fileLinkTarget); + + delegator.setState(false); + dirLink.getRelative("a").setReadable(true); + assertTrue(delegator.booleanState()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + dirLink.getRelative("a").setReadable(false); + assertFalse(delegator.booleanState()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + } + + @Test + public void testCallSetWritableOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected void setWritable(Path path, boolean writable) { + lastPath = path; + setState(writable); + } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + fileLink.setWritable(true); + assertTrue(delegator.booleanState()); + checkPath(delegator, fileLinkTarget); + fileLink.setWritable(false); + assertFalse(delegator.booleanState()); + checkPath(delegator, fileLinkTarget); + + delegator.setState(false); + dirLink.getRelative("a").setWritable(true); + assertTrue(delegator.booleanState()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + dirLink.getRelative("a").setWritable(false); + assertFalse(delegator.booleanState()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + } + + @Test + public void testCallSetExecutableOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected void setReadable(Path path, boolean readable) { + lastPath = path; + setState(readable); + } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(false); + fileLink.setReadable(true); + assertTrue(delegator.booleanState()); + checkPath(delegator, fileLinkTarget); + fileLink.setReadable(false); + assertFalse(delegator.booleanState()); + checkPath(delegator, fileLinkTarget); + + delegator.setState(false); + dirLink.getRelative("a").setReadable(true); + assertTrue(delegator.booleanState()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + dirLink.getRelative("a").setReadable(false); + assertFalse(delegator.booleanState()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + } + + @Test + public void testCallSetLastModifiedTimeOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected void setLastModifiedTime(Path path, long newTime) { + lastPath = path; + setState(newTime); + } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(0); + fileLink.setLastModifiedTime(10); + assertEquals(10, delegator.longState()); + checkPath(delegator, fileLinkTarget); + fileLink.setLastModifiedTime(15); + assertEquals(15, delegator.longState()); + checkPath(delegator, fileLinkTarget); + + dirLink.getRelative("a").setLastModifiedTime(20); + assertEquals(20, delegator.longState()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + dirLink.getRelative("a").setLastModifiedTime(25); + assertEquals(25, delegator.longState()); + checkPath(delegator, dirLinkTarget.getRelative("a")); + } + + @Test + public void testCallRenameToOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected void renameTo(Path sourcePath, Path targetPath) { + lastPath = sourcePath; + setState(targetPath); + } + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; } + }; + scopedFS().setDelegator(delegator); + + // Renaming a link should work fine. + delegator.setState(null); + fileLink.renameTo(testFS.getPath(SCOPE_ROOT).getRelative("newname")); + assertEquals(null, delegator.lastPath()); // Renaming a link shouldn't require delegation. + assertEquals(null, delegator.objectState()); + + // Renaming an out-of-scope path to an in-scope path should fail due to filesystem mismatch + // errors. + Path newPath = testFS.getPath(SCOPE_ROOT.getRelative("blah")); + try { + dirLink.getRelative("a").renameTo(newPath); + fail("This is an attempt at a cross-filesystem renaming, which should fail"); + } catch (IOException e) { + // Expected. + } + + // Renaming an out-of-scope path to another out-of-scope path can be valid. + newPath = dirLink.getRelative("b"); + dirLink.getRelative("a").renameTo(newPath); + assertEquals(dirLinkTarget.getRelative("a"), delegator.lastPath()); + assertEquals(dirLinkTarget.getRelative("b"), ((Path) delegator.objectState()).asFragment()); + } + + @Test + public void testCallCreateSymbolicLinkOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) { + lastPath = linkPath; + setState(targetFragment); + } + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; } + }; + scopedFS().setDelegator(delegator); + + PathFragment newLinkTarget = new PathFragment("/something/else"); + dirLink.getRelative("a").createSymbolicLink(newLinkTarget); + assertEquals(dirLinkTarget.getRelative("a"), delegator.lastPath()); + assertSame(newLinkTarget, delegator.objectState()); + } + + @Test + public void testCallReadSymbolicLinkOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected PathFragment readSymbolicLink(Path path) { + lastPath = path; + return (PathFragment) objectState; + } + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; } + }; + scopedFS().setDelegator(delegator); + + // Since we're not following the link, this shouldn't invoke delegation. + delegator.setState(new PathFragment("whatever")); + PathFragment p = fileLink.readSymbolicLink(); + assertEquals(null, delegator.lastPath()); + assertNotSame(delegator.objectState(), p); + + // This should. + p = dirLink.getRelative("a").readSymbolicLink(); + assertEquals(dirLinkTarget.getRelative("a"), delegator.lastPath()); + assertSame(delegator.objectState(), p); + } + + @Test + public void testCallGetInputStreamOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected InputStream getInputStream(Path path) { + lastPath = path; + return (InputStream) objectState; + } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(new ByteArrayInputStream("blah".getBytes())); + InputStream is = fileLink.getInputStream(); + assertEquals(fileLinkTarget, delegator.lastPath()); + assertSame(delegator.objectState(), is); + + delegator.setState(new ByteArrayInputStream("blah2".getBytes())); + is = dirLink.getInputStream(); + assertEquals(dirLinkTarget, delegator.lastPath()); + assertSame(delegator.objectState(), is); + } + + @Test + public void testCallGetOutputStreamOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected OutputStream getOutputStream(Path path, boolean append) { + lastPath = path; + return (OutputStream) objectState; + } + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(new ByteArrayOutputStream()); + OutputStream os = fileLink.getOutputStream(); + assertEquals(fileLinkTarget, delegator.lastPath()); + assertSame(delegator.objectState(), os); + + delegator.setState(new ByteArrayOutputStream()); + os = dirLink.getOutputStream(); + assertEquals(dirLinkTarget, delegator.lastPath()); + assertSame(delegator.objectState(), os); + } + + @Test + public void testCallGetDirectoryEntriesOnEscapingSymlink() throws Exception { + TestDelegator delegator = new TestDelegator() { + @Override protected Collection<Path> getDirectoryEntries(Path path) { + lastPath = path; + return ImmutableList.of((Path) objectState); + } + @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; } + }; + scopedFS().setDelegator(delegator); + + delegator.setState(testFS.getPath("/anything")); + Collection<Path> entries = dirLink.getDirectoryEntries(); + assertEquals(dirLinkTarget, delegator.lastPath()); + assertEquals(1, entries.size()); + assertSame(delegator.objectState(), entries.iterator().next()); + } + + /** + * Asserts that "link" is an in-scope link that doesn't result in an out-of-FS + * delegation. If link is relative, its path is relative to SCOPE_ROOT. + * + * Note that we don't actually check that the canonicalized target path matches + * the link's target value. Such testing should be covered by + * SymlinkAwareFileSystemTest. + */ + private void assertInScopeLink(String link, String target, TestDelegator d) throws IOException { + Path l = testFS.getPath(SCOPE_ROOT.getRelative(link)); + testFS.createSymbolicLink(l, new PathFragment(target)); + l.exists(); + assertNull(d.lastPath()); + } + + /** + * Asserts that "link" is an out-of-scope link and that the re-delegated path + * matches expectedPath. If link is relative, its path is relative to SCOPE_ROOT. + */ + private void assertOutOfScopeLink(String link, String target, String expectedPath, + TestDelegator d) throws IOException { + Path l = testFS.getPath(SCOPE_ROOT.getRelative(link)); + testFS.createSymbolicLink(l, new PathFragment(target)); + l.exists(); + assertEquals(expectedPath, d.lastPath().getPathString()); + } + + /** + * Returns the scope root with the final n segments chopped off (or a 0-segment path + * if n > SCOPE_ROOT.segmentCount()). + */ + private String chopScopeRoot(int n) { + return SCOPE_ROOT + .subFragment(0, n > SCOPE_ROOT.segmentCount() ? 0 : SCOPE_ROOT.segmentCount() - n) + .getPathString(); + } + + /** + * Tests that absolute symlinks with ".." and "." segments are delegated to + * the expected paths. + */ + @Test + public void testAbsoluteSymlinksWithParentReferences() throws Exception { + TestDelegator d = newExistsDelegator(); + scopedFS().setDelegator(d); + testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir"))); + String scopeRoot = SCOPE_ROOT.getPathString(); + String scopeBase = SCOPE_ROOT.getBaseName(); + + // Symlinks that should never escape our scope. + assertInScopeLink("ilink1", scopeRoot, d); + assertInScopeLink("ilink2", scopeRoot + "/target", d); + assertInScopeLink("ilink3", scopeRoot + "/dir/../target", d); + assertInScopeLink("ilink4", scopeRoot + "/dir/../dir/dir2/../target", d); + assertInScopeLink("ilink5", scopeRoot + "/./dir/.././target", d); + assertInScopeLink("ilink6", scopeRoot + "/../" + scopeBase + "/target", d); + assertInScopeLink("ilink7", "/some/path/../.." + scopeRoot + "/target", d); + + // Symlinks that should escape our scope. + assertOutOfScopeLink("olink1", scopeRoot + "/../target", chopScopeRoot(1) + "/target", d); + assertOutOfScopeLink("olink2", "/some/other/path", "/some/other/path", d); + assertOutOfScopeLink("olink3", scopeRoot + "/../target", chopScopeRoot(1) + "/target", d); + assertOutOfScopeLink("olink4", chopScopeRoot(1) + "/target", chopScopeRoot(1) + "/target", d); + assertOutOfScopeLink("olink5", scopeRoot + "/../../../../target", "/target", d); + + // In-scope symlink that's not the final segment in a query. + Path iDirLink = testFS.getPath(SCOPE_ROOT.getRelative("ilinkdir")); + testFS.createSymbolicLink(iDirLink, SCOPE_ROOT.getRelative("dir")); + iDirLink.getRelative("file").exists(); + assertNull(d.lastPath()); + + // Out-of-scope symlink that's not the final segment in a query. + Path oDirLink = testFS.getPath(SCOPE_ROOT.getRelative("olinkdir")); + testFS.createSymbolicLink(oDirLink, new PathFragment("/some/other/dir")); + oDirLink.getRelative("file").exists(); + assertEquals("/some/other/dir/file", d.lastPath().getPathString()); + } + + /** + * Tests that relative symlinks with ".." and "." segments are delegated to + * the expected paths. + */ + @Test + public void testRelativeSymlinksWithParentReferences() throws Exception { + TestDelegator d = newExistsDelegator(); + scopedFS().setDelegator(d); + testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir"))); + testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2"))); + testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2/dir3"))); + String scopeRoot = SCOPE_ROOT.getPathString(); + String scopeBase = SCOPE_ROOT.getBaseName(); + + // Symlinks that should never escape our scope. + assertInScopeLink("ilink1", "target", d); + assertInScopeLink("ilink2", "dir/../otherdir/target", d); + assertInScopeLink("dir/ilink3", "../target", d); + assertInScopeLink("dir/dir2/ilink4", "../../target", d); + assertInScopeLink("dir/dir2/ilink5", ".././../dir/./target", d); + assertInScopeLink("dir/dir2/ilink6", "../dir2/../../dir/dir2/dir3/../../../target", d); + + // Symlinks that should escape our scope. + assertOutOfScopeLink("olink1", "../target", chopScopeRoot(1) + "/target", d); + assertOutOfScopeLink("dir/olink2", "../../target", chopScopeRoot(1) + "/target", d); + assertOutOfScopeLink("olink3", "../" + scopeBase + "/target", scopeRoot + "/target", d); + assertOutOfScopeLink("dir/dir2/olink5", "../../../target", chopScopeRoot(1) + "/target", d); + assertOutOfScopeLink("dir/dir2/olink6", "../dir2/../../dir/dir2/../../../target", + chopScopeRoot(1) + "/target", d); + assertOutOfScopeLink("dir/olink7", "../../../target", chopScopeRoot(2) + "target", d); + assertOutOfScopeLink("olink8", "../../../../../target", "/target", d); + + // In-scope symlink that's not the final segment in a query. + Path iDirLink = testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2/ilinkdir")); + testFS.createSymbolicLink(iDirLink, new PathFragment("../../dir")); + iDirLink.getRelative("file").exists(); + assertNull(d.lastPath()); + + // Out-of-scope symlink that's not the final segment in a query. + Path oDirLink = testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2/olinkdir")); + testFS.createSymbolicLink(oDirLink, new PathFragment("../../../other/dir")); + oDirLink.getRelative("file").exists(); + assertEquals(chopScopeRoot(1) + "/other/dir/file", d.lastPath().getPathString()); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java new file mode 100644 index 0000000000..a728c88796 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java @@ -0,0 +1,717 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.devtools.build.lib.testutil.MoreAsserts; +import com.google.devtools.build.lib.vfs.FileSystem.NotASymlinkException; + +import org.junit.Before; +import org.junit.Test; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collection; + +/** + * This class handles the generic tests that any filesystem must pass. + * + * <p>Each filesystem-test should inherit from this class, thereby obtaining + * all the tests. + */ +public abstract class SymlinkAwareFileSystemTest extends FileSystemTest { + + protected Path xLinkToFile; + protected Path xLinkToLinkToFile; + protected Path xLinkToDirectory; + protected Path xDanglingLink; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + // % ls -lR + // -rw-rw-r-- xFile + // drwxrwxr-x xNonEmptyDirectory + // -rw-rw-r-- xNonEmptyDirectory/foo + // drwxrwxr-x xEmptyDirectory + // lrwxrwxr-x xLinkToFile -> xFile + // lrwxrwxr-x xLinkToDirectory -> xEmptyDirectory + // lrwxrwxr-x xLinkToLinkToFile -> xLinkToFile + // lrwxrwxr-x xDanglingLink -> xNothing + + xLinkToFile = absolutize("xLinkToFile"); + xLinkToLinkToFile = absolutize("xLinkToLinkToFile"); + xLinkToDirectory = absolutize("xLinkToDirectory"); + xDanglingLink = absolutize("xDanglingLink"); + + createSymbolicLink(xLinkToFile, xFile); + createSymbolicLink(xLinkToLinkToFile, xLinkToFile); + createSymbolicLink(xLinkToDirectory, xEmptyDirectory); + createSymbolicLink(xDanglingLink, xNothing); + } + + @Test + public void testCreateLinkToFile() throws IOException { + Path newPath = xEmptyDirectory.getChild("new-file"); + FileSystemUtils.createEmptyFile(newPath); + + Path linkPath = xEmptyDirectory.getChild("some-link"); + + createSymbolicLink(linkPath, newPath); + + assertTrue(linkPath.isSymbolicLink()); + + assertTrue(linkPath.isFile()); + assertFalse(linkPath.isFile(Symlinks.NOFOLLOW)); + assertTrue(linkPath.isFile(Symlinks.FOLLOW)); + + assertFalse(linkPath.isDirectory()); + assertFalse(linkPath.isDirectory(Symlinks.NOFOLLOW)); + assertFalse(linkPath.isDirectory(Symlinks.FOLLOW)); + + if (supportsSymlinks) { + assertEquals(newPath.toString().length(), linkPath.getFileSize(Symlinks.NOFOLLOW)); + assertEquals(newPath.getFileSize(Symlinks.NOFOLLOW), linkPath.getFileSize()); + } + assertEquals(2, + linkPath.getParentDirectory().getDirectoryEntries().size()); + assertThat(linkPath.getParentDirectory().getDirectoryEntries()).containsExactly(newPath, + linkPath); + } + + @Test + public void testCreateLinkToDirectory() throws IOException { + Path newPath = xEmptyDirectory.getChild("new-file"); + newPath.createDirectory(); + + Path linkPath = xEmptyDirectory.getChild("some-link"); + + createSymbolicLink(linkPath, newPath); + + assertTrue(linkPath.isSymbolicLink()); + assertFalse(linkPath.isFile()); + assertTrue(linkPath.isDirectory()); + assertEquals(2, + linkPath.getParentDirectory().getDirectoryEntries().size()); + assertThat(linkPath.getParentDirectory(). + getDirectoryEntries()).containsExactly(newPath, linkPath); + } + + @Test + public void testFileCanonicalPath() throws IOException { + Path newPath = absolutize("new-file"); + FileSystemUtils.createEmptyFile(newPath); + newPath = newPath.resolveSymbolicLinks(); + + Path link1 = absolutize("some-link"); + Path link2 = absolutize("some-link2"); + + createSymbolicLink(link1, newPath); + createSymbolicLink(link2, link1); + + assertCanonicalPathsMatch(newPath, link1, link2); + } + + @Test + public void testDirectoryCanonicalPath() throws IOException { + Path newPath = absolutize("new-folder"); + newPath.createDirectory(); + newPath = newPath.resolveSymbolicLinks(); + + Path newFile = newPath.getChild("file"); + FileSystemUtils.createEmptyFile(newFile); + + Path link1 = absolutize("some-link"); + Path link2 = absolutize("some-link2"); + + createSymbolicLink(link1, newPath); + createSymbolicLink(link2, link1); + + Path linkFile1 = link1.getChild("file"); + Path linkFile2 = link2.getChild("file"); + + assertCanonicalPathsMatch(newFile, linkFile1, linkFile2); + } + + private void assertCanonicalPathsMatch(Path newPath, Path link1, Path link2) + throws IOException { + assertEquals(newPath, link1.resolveSymbolicLinks()); + assertEquals(newPath, link2.resolveSymbolicLinks()); + } + + // + // createDirectory + // + + @Test + public void testCreateDirectoryWhereDanglingSymlinkAlreadyExists() { + try { + xDanglingLink.createDirectory(); + fail(); + } catch (IOException e) { + assertEquals(xDanglingLink + " (File exists)", e.getMessage()); + } + assertTrue(xDanglingLink.isSymbolicLink()); // still a symbolic link + assertFalse(xDanglingLink.isDirectory(Symlinks.FOLLOW)); // link still dangles + } + + @Test + public void testCreateDirectoryWhereSymlinkAlreadyExists() { + try { + xLinkToDirectory.createDirectory(); + fail(); + } catch (IOException e) { + assertEquals(xLinkToDirectory + " (File exists)", e.getMessage()); + } + assertTrue(xLinkToDirectory.isSymbolicLink()); // still a symbolic link + assertTrue(xLinkToDirectory.isDirectory(Symlinks.FOLLOW)); // link still points to dir + } + + // createSymbolicLink(PathFragment) + + @Test + public void testCreateSymbolicLinkFromFragment() throws IOException { + String[] linkTargets = { + "foo", + "foo/bar", + ".", + "..", + "../foo", + "../../foo", + "../../../../../../../../../../../../../../../../../../../../../foo", + "/foo", + "/foo/bar", + "/..", + "/foo/../bar", + }; + Path linkPath = absolutize("link"); + for (String linkTarget : linkTargets) { + PathFragment relative = new PathFragment(linkTarget); + linkPath.delete(); + createSymbolicLink(linkPath, relative); + if (supportsSymlinks) { + assertEquals(linkTarget.length(), linkPath.getFileSize(Symlinks.NOFOLLOW)); + assertEquals(relative, linkPath.readSymbolicLink()); + } + } + } + + @Test + public void testLinkToRootResolvesCorrectly() throws IOException { + Path rootPath = testFS.getPath("/"); + Path linkPath = absolutize("link"); + createSymbolicLink(linkPath, rootPath); + + // resolveSymbolicLinks requires an existing path: + try { + linkPath.getRelative("test").resolveSymbolicLinks(); + fail(); + } catch (FileNotFoundException e) { /* ok */ } + + // The path may not be a symlink, neither on Darwin nor on Linux. + Path rootChild = testFS.getPath("/sbin"); + if (!rootChild.isDirectory()) { + rootChild.createDirectory(); + } + assertEquals(rootChild, linkPath.getRelative("sbin").resolveSymbolicLinks()); + } + + @Test + public void testLinkToFragmentContainingLinkResolvesCorrectly() throws IOException { + Path link1 = absolutize("link1"); + PathFragment link1target = new PathFragment("link2/foo"); + Path link2 = absolutize("link2"); + Path link2target = xNonEmptyDirectory; + + createSymbolicLink(link1, link1target); // ln -s link2/foo link1 + createSymbolicLink(link2, link2target); // ln -s xNonEmptyDirectory link2 + // link1 --> xNonEmptyDirectory/foo + assertEquals(link1.resolveSymbolicLinks(), link2target.getRelative("foo")); + } + + // + // readSymbolicLink / resolveSymbolicLinks + // + + @Test + public void testRecursiveSymbolicLink() throws IOException { + Path link = absolutize("recursive-link"); + createSymbolicLink(link, link); + + if (supportsSymlinks) { + try { + link.resolveSymbolicLinks(); + fail(); + } catch (IOException e) { + assertEquals(link + " (Too many levels of symbolic links)", + e.getMessage()); + } + } + } + + @Test + public void testMutuallyRecursiveSymbolicLinks() throws IOException { + Path link1 = absolutize("link1"); + Path link2 = absolutize("link2"); + createSymbolicLink(link2, link1); + createSymbolicLink(link1, link2); + + if (supportsSymlinks) { + try { + link1.resolveSymbolicLinks(); + fail(); + } catch (IOException e) { + assertEquals(link1 + " (Too many levels of symbolic links)", e.getMessage()); + } + } + } + + @Test + public void testResolveSymbolicLinksENOENT() { + if (supportsSymlinks) { + try { + xDanglingLink.resolveSymbolicLinks(); + fail(); + } catch (IOException e) { + assertEquals(xNothing + " (No such file or directory)", e.getMessage()); + } + } + } + + @Test + public void testResolveSymbolicLinksENOTDIR() throws IOException { + if (supportsSymlinks) { + Path badLinkTarget = xFile.getChild("bad"); // parent is not a directory! + Path badLink = absolutize("badLink"); + createSymbolicLink(badLink, badLinkTarget); + try { + badLink.resolveSymbolicLinks(); + fail(); + } catch (IOException e) { + // ok. Ideally we would assert "(Not a directory)" in the error + // message, but that would require yet another stat in the + // implementation. + } + } + } + + @Test + public void testResolveSymbolicLinksWithUplevelRefs() throws IOException { + if (supportsSymlinks) { + // Create a series of links that refer to xFile as ./xFile, + // ./../foo/xFile, ./../../bar/foo/xFile, etc. They should all resolve + // to xFile. + Path ancestor = xFile; + String prefix = "./"; + while ((ancestor = ancestor.getParentDirectory()) != null) { + xLinkToFile.delete(); + createSymbolicLink(xLinkToFile, new PathFragment(prefix + xFile.relativeTo(ancestor))); + assertEquals(xFile, xLinkToFile.resolveSymbolicLinks()); + + prefix += "../"; + } + } + } + + @Test + public void testReadSymbolicLink() throws IOException { + if (supportsSymlinks) { + assertEquals(xNothing.toString(), + xDanglingLink.readSymbolicLink().toString()); + } + + assertEquals(xFile.toString(), + xLinkToFile.readSymbolicLink().toString()); + + assertEquals(xEmptyDirectory.toString(), + xLinkToDirectory.readSymbolicLink().toString()); + + try { + xFile.readSymbolicLink(); // not a link + fail(); + } catch (NotASymlinkException e) { + assertEquals(xFile.toString(), e.getMessage()); + } + + try { + xNothing.readSymbolicLink(); // nothing there + fail(); + } catch (IOException e) { + assertEquals(xNothing + " (No such file or directory)", e.getMessage()); + } + } + + @Test + public void testCannotCreateSymbolicLinkWithReadOnlyParent() + throws IOException { + xEmptyDirectory.setWritable(false); + Path xChildOfReadonlyDir = xEmptyDirectory.getChild("x"); + if (supportsSymlinks) { + try { + xChildOfReadonlyDir.createSymbolicLink(xNothing); + fail(); + } catch (IOException e) { + assertEquals(xChildOfReadonlyDir + " (Permission denied)", e.getMessage()); + } + } + } + + // + // createSymbolicLink + // + + @Test + public void testCanCreateDanglingLink() throws IOException { + Path newPath = absolutize("non-existing-dir/new-file"); + Path someLink = absolutize("dangling-link"); + createSymbolicLink(someLink, newPath); + assertTrue(someLink.isSymbolicLink()); + assertTrue(someLink.exists(Symlinks.NOFOLLOW)); // the link itself exists + assertFalse(someLink.exists()); // ...but the referent doesn't + if (supportsSymlinks) { + try { + someLink.resolveSymbolicLinks(); + } catch (FileNotFoundException e) { + assertEquals(newPath.getParentDirectory() + + " (No such file or directory)", e.getMessage()); + } + } + } + + @Test + public void testCannotCreateSymbolicLinkWithoutParent() throws IOException { + Path xChildOfMissingDir = xNothing.getChild("x"); + if (supportsSymlinks) { + try { + xChildOfMissingDir.createSymbolicLink(xFile); + fail(); + } catch (FileNotFoundException e) { + MoreAsserts.assertEndsWith(" (No such file or directory)", e.getMessage()); + } + } + } + + @Test + public void testCreateSymbolicLinkWhereNothingExists() throws IOException { + createSymbolicLink(xNothing, xFile); + assertTrue(xNothing.isSymbolicLink()); + } + + @Test + public void testCreateSymbolicLinkWhereDirectoryAlreadyExists() { + try { + createSymbolicLink(xEmptyDirectory, xFile); + fail(); + } catch (IOException e) { // => couldn't be created + assertEquals(xEmptyDirectory + " (File exists)", e.getMessage()); + } + assertTrue(xEmptyDirectory.isDirectory(Symlinks.NOFOLLOW)); + } + + @Test + public void testCreateSymbolicLinkWhereFileAlreadyExists() { + try { + createSymbolicLink(xFile, xEmptyDirectory); + fail(); + } catch (IOException e) { // => couldn't be created + assertEquals(xFile + " (File exists)", e.getMessage()); + } + assertTrue(xFile.isFile(Symlinks.NOFOLLOW)); + } + + @Test + public void testCreateSymbolicLinkWhereDanglingSymlinkAlreadyExists() { + try { + createSymbolicLink(xDanglingLink, xFile); + fail(); + } catch (IOException e) { + assertEquals(xDanglingLink + " (File exists)", e.getMessage()); + } + assertTrue(xDanglingLink.isSymbolicLink()); // still a symbolic link + assertFalse(xDanglingLink.isDirectory()); // link still dangles + } + + @Test + public void testCreateSymbolicLinkWhereSymlinkAlreadyExists() { + try { + createSymbolicLink(xLinkToDirectory, xNothing); + fail(); + } catch (IOException e) { + assertEquals(xLinkToDirectory + " (File exists)", e.getMessage()); + } + assertTrue(xLinkToDirectory.isSymbolicLink()); // still a symbolic link + assertTrue(xLinkToDirectory.isDirectory()); // link still points to dir + } + + @Test + public void testDeleteLink() throws IOException { + Path newPath = xEmptyDirectory.getChild("new-file"); + Path someLink = xEmptyDirectory.getChild("a-link"); + FileSystemUtils.createEmptyFile(newPath); + createSymbolicLink(someLink, newPath); + + assertEquals(xEmptyDirectory.getDirectoryEntries().size(), 2); + + assertTrue(someLink.delete()); + assertEquals(xEmptyDirectory.getDirectoryEntries().size(), 1); + + assertThat(xEmptyDirectory.getDirectoryEntries()).containsExactly(newPath); + } + + // Testing the links + @Test + public void testLinkFollowedToDirectory() throws IOException { + Path theDirectory = absolutize("foo/"); + assertTrue(theDirectory.createDirectory()); + Path newPath1 = absolutize("foo/new-file-1"); + Path newPath2 = absolutize("foo/new-file-2"); + Path newPath3 = absolutize("foo/new-file-3"); + + FileSystemUtils.createEmptyFile(newPath1); + FileSystemUtils.createEmptyFile(newPath2); + FileSystemUtils.createEmptyFile(newPath3); + + Path linkPath = absolutize("link"); + createSymbolicLink(linkPath, theDirectory); + + Path resultPath1 = absolutize("link/new-file-1"); + Path resultPath2 = absolutize("link/new-file-2"); + Path resultPath3 = absolutize("link/new-file-3"); + assertThat(linkPath.getDirectoryEntries()).containsExactly(resultPath1, resultPath2, + resultPath3); + } + + @Test + public void testDanglingLinkIsNoFile() throws IOException { + Path newPath1 = absolutize("new-file-1"); + Path newPath2 = absolutize("new-file-2"); + FileSystemUtils.createEmptyFile(newPath1); + assertTrue(newPath2.createDirectory()); + + Path linkPath1 = absolutize("link1"); + Path linkPath2 = absolutize("link2"); + createSymbolicLink(linkPath1, newPath1); + createSymbolicLink(linkPath2, newPath2); + + newPath1.delete(); + newPath2.delete(); + + assertFalse(linkPath1.isFile()); + assertFalse(linkPath2.isDirectory()); + } + + @Test + public void testWriteOnLinkChangesFile() throws IOException { + Path testFile = absolutize("test-file"); + FileSystemUtils.createEmptyFile(testFile); + String testData = "abc19"; + + Path testLink = absolutize("a-link"); + createSymbolicLink(testLink, testFile); + + FileSystemUtils.writeContentAsLatin1(testLink, testData); + String resultData = + new String(FileSystemUtils.readContentAsLatin1(testFile)); + + assertEquals(testData,resultData); + } + + // + // Symlink tests: + // + + @Test + public void testExistsWithSymlinks() throws IOException { + Path a = absolutize("a"); + Path b = absolutize("b"); + FileSystemUtils.createEmptyFile(b); + createSymbolicLink(a, b); // ln -sf "b" "a" + assertTrue(a.exists()); // = exists(FOLLOW) + assertTrue(b.exists()); // = exists(FOLLOW) + assertTrue(a.exists(Symlinks.FOLLOW)); + assertTrue(b.exists(Symlinks.FOLLOW)); + assertTrue(a.exists(Symlinks.NOFOLLOW)); + assertTrue(b.exists(Symlinks.NOFOLLOW)); + b.delete(); // "a" is now a dangling link + assertFalse(a.exists()); // = exists(FOLLOW) + assertFalse(b.exists()); // = exists(FOLLOW) + assertFalse(a.exists(Symlinks.FOLLOW)); + assertFalse(b.exists(Symlinks.FOLLOW)); + + assertTrue(a.exists(Symlinks.NOFOLLOW)); // symlink still exists + assertFalse(b.exists(Symlinks.NOFOLLOW)); + } + + @Test + public void testIsDirectoryWithSymlinks() throws IOException { + Path a = absolutize("a"); + Path b = absolutize("b"); + b.createDirectory(); + createSymbolicLink(a, b); // ln -sf "b" "a" + assertTrue(a.isDirectory()); // = isDirectory(FOLLOW) + assertTrue(b.isDirectory()); // = isDirectory(FOLLOW) + assertTrue(a.isDirectory(Symlinks.FOLLOW)); + assertTrue(b.isDirectory(Symlinks.FOLLOW)); + assertFalse(a.isDirectory(Symlinks.NOFOLLOW)); // it's a link! + assertTrue(b.isDirectory(Symlinks.NOFOLLOW)); + b.delete(); // "a" is now a dangling link + assertFalse(a.isDirectory()); // = isDirectory(FOLLOW) + assertFalse(b.isDirectory()); // = isDirectory(FOLLOW) + assertFalse(a.isDirectory(Symlinks.FOLLOW)); + assertFalse(b.isDirectory(Symlinks.FOLLOW)); + assertFalse(a.isDirectory(Symlinks.NOFOLLOW)); + assertFalse(b.isDirectory(Symlinks.NOFOLLOW)); + } + + @Test + public void testIsFileWithSymlinks() throws IOException { + Path a = absolutize("a"); + Path b = absolutize("b"); + FileSystemUtils.createEmptyFile(b); + createSymbolicLink(a, b); // ln -sf "b" "a" + assertTrue(a.isFile()); // = isFile(FOLLOW) + assertTrue(b.isFile()); // = isFile(FOLLOW) + assertTrue(a.isFile(Symlinks.FOLLOW)); + assertTrue(b.isFile(Symlinks.FOLLOW)); + assertFalse(a.isFile(Symlinks.NOFOLLOW)); // it's a link! + assertTrue(b.isFile(Symlinks.NOFOLLOW)); + b.delete(); // "a" is now a dangling link + assertFalse(a.isFile()); // = isFile() + assertFalse(b.isFile()); // = isFile() + assertFalse(a.isFile()); + assertFalse(b.isFile()); + assertFalse(a.isFile(Symlinks.NOFOLLOW)); + assertFalse(b.isFile(Symlinks.NOFOLLOW)); + } + + @Test + public void testGetDirectoryEntriesOnLinkToDirectory() throws Exception { + Path fooAlias = xNothing.getChild("foo"); + createSymbolicLink(xNothing, xNonEmptyDirectory); + Collection<Path> dirents = xNothing.getDirectoryEntries(); + assertThat(dirents).containsExactly(fooAlias); + } + + @Test + public void testFilesOfLinkedDirectories() throws Exception { + Path child = xEmptyDirectory.getChild("child"); + Path aliasToChild = xLinkToDirectory.getChild("child"); + + assertFalse(aliasToChild.exists()); + FileSystemUtils.createEmptyFile(child); + assertTrue(aliasToChild.exists()); + assertTrue(aliasToChild.isFile()); + assertFalse(aliasToChild.isDirectory()); + + validateLinkedReferenceObeysReadOnly(child, aliasToChild); + validateLinkedReferenceObeysExecutable(child, aliasToChild); + } + + @Test + public void testDirectoriesOfLinkedDirectories() throws Exception { + Path childDir = xEmptyDirectory.getChild("childDir"); + Path linkToChildDir = xLinkToDirectory.getChild("childDir"); + + assertFalse(linkToChildDir.exists()); + childDir.createDirectory(); + assertTrue(linkToChildDir.exists()); + assertTrue(linkToChildDir.isDirectory()); + assertFalse(linkToChildDir.isFile()); + + validateLinkedReferenceObeysReadOnly(childDir, linkToChildDir); + validateLinkedReferenceObeysExecutable(childDir, linkToChildDir); + } + + @Test + public void testDirectoriesOfLinkedDirectoriesOfLinkedDirectories() throws Exception { + Path childDir = xEmptyDirectory.getChild("childDir"); + Path linkToLinkToDirectory = absolutize("xLinkToLinkToDirectory"); + createSymbolicLink(linkToLinkToDirectory, xLinkToDirectory); + Path linkToChildDir = linkToLinkToDirectory.getChild("childDir"); + + assertFalse(linkToChildDir.exists()); + childDir.createDirectory(); + assertTrue(linkToChildDir.exists()); + assertTrue(linkToChildDir.isDirectory()); + assertFalse(linkToChildDir.isFile()); + + validateLinkedReferenceObeysReadOnly(childDir, linkToChildDir); + validateLinkedReferenceObeysExecutable(childDir, linkToChildDir); + } + + private void validateLinkedReferenceObeysReadOnly(Path path, Path link) throws IOException { + path.setWritable(false); + assertFalse(path.isWritable()); + assertFalse(link.isWritable()); + path.setWritable(true); + assertTrue(path.isWritable()); + assertTrue(link.isWritable()); + path.setWritable(false); + assertFalse(path.isWritable()); + assertFalse(link.isWritable()); + } + + private void validateLinkedReferenceObeysExecutable(Path path, Path link) throws IOException { + path.setExecutable(true); + assertTrue(path.isExecutable()); + assertTrue(link.isExecutable()); + path.setExecutable(false); + assertFalse(path.isExecutable()); + assertFalse(link.isExecutable()); + path.setExecutable(true); + assertTrue(path.isExecutable()); + assertTrue(link.isExecutable()); + } + + @Test + public void testReadingFileFromLinkedDirectory() throws Exception { + Path linkedTo = absolutize("linkedTo"); + linkedTo.createDirectory(); + Path child = linkedTo.getChild("child"); + FileSystemUtils.createEmptyFile(child); + + byte[] outputData = "This is a test".getBytes(); + FileSystemUtils.writeContent(child, outputData); + + Path link = absolutize("link"); + createSymbolicLink(link, linkedTo); + Path linkedChild = link.getChild("child"); + byte[] inputData = FileSystemUtils.readContent(linkedChild); + assertArrayEquals(outputData, inputData); + } + + @Test + public void testCreatingFileInLinkedDirectory() throws Exception { + Path linkedTo = absolutize("linkedTo"); + linkedTo.createDirectory(); + Path child = linkedTo.getChild("child"); + + Path link = absolutize("link"); + createSymbolicLink(link, linkedTo); + Path linkedChild = link.getChild("child"); + byte[] outputData = "This is a test".getBytes(); + FileSystemUtils.writeContent(linkedChild, outputData); + + byte[] inputData = FileSystemUtils.readContent(child); + assertArrayEquals(outputData, inputData); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java new file mode 100644 index 0000000000..396a9f8441 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java @@ -0,0 +1,330 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.util.Clock; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Tests for the UnionFileSystem, both of generic FileSystem functionality + * (inherited) and tests of UnionFileSystem-specific behavior. + */ +@RunWith(JUnit4.class) +public class UnionFileSystemTest extends SymlinkAwareFileSystemTest { + private XAttrInMemoryFs inDelegate; + private XAttrInMemoryFs outDelegate; + private XAttrInMemoryFs defaultDelegate; + private UnionFileSystem unionfs; + + private static final String XATTR_VAL = "SOME_XATTR_VAL"; + private static final String XATTR_KEY = "SOME_XATTR_KEY"; + + private void setupDelegateFileSystems() { + inDelegate = new XAttrInMemoryFs(BlazeClock.instance()); + outDelegate = new XAttrInMemoryFs(BlazeClock.instance()); + defaultDelegate = new XAttrInMemoryFs(BlazeClock.instance()); + + unionfs = createDefaultUnionFileSystem(); + } + + private UnionFileSystem createDefaultUnionFileSystem() { + return createDefaultUnionFileSystem(false); + } + + private UnionFileSystem createDefaultUnionFileSystem(boolean readOnly) { + return new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of( + new PathFragment("/in"), inDelegate, + new PathFragment("/out"), outDelegate), + defaultDelegate, readOnly); + } + + @Override + protected FileSystem getFreshFileSystem() { + // Executed with each new test because it is called by super.setUp(). + setupDelegateFileSystems(); + return unionfs; + } + + @Override + public void destroyFileSystem(FileSystem fileSystem) { + // Nothing. + } + + // Tests of UnionFileSystem-specific behavior below. + + @Test + public void testBasicDelegation() throws Exception { + unionfs = createDefaultUnionFileSystem(); + Path fooPath = unionfs.getPath("/foo"); + Path inPath = unionfs.getPath("/in"); + Path outPath = unionfs.getPath("/out/in.txt"); + assertSame(inDelegate, unionfs.getDelegate(inPath)); + assertSame(outDelegate, unionfs.getDelegate(outPath)); + assertSame(defaultDelegate, unionfs.getDelegate(fooPath)); + } + + @Test + public void testBasicXattr() throws Exception { + Path fooPath = unionfs.getPath("/foo"); + Path inPath = unionfs.getPath("/in"); + Path outPath = unionfs.getPath("/out/in.txt"); + + assertArrayEquals(XATTR_VAL.getBytes(UTF_8), inPath.getxattr(XATTR_KEY)); + assertArrayEquals(XATTR_VAL.getBytes(UTF_8), outPath.getxattr(XATTR_KEY)); + assertArrayEquals(XATTR_VAL.getBytes(UTF_8), fooPath.getxattr(XATTR_KEY)); + assertNull(inPath.getxattr("not_key")); + assertNull(outPath.getxattr("not_key")); + assertNull(fooPath.getxattr("not_key")); + } + + @Test + public void testDefaultFileSystemRequired() throws Exception { + try { + new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of(), null); + fail("Able to create a UnionFileSystem with no default!"); + } catch (NullPointerException expected) { + // OK - should fail in this case. + } + } + + // Check for appropriate registration and lookup of delegate filesystems based + // on path prefixes, including non-canonical paths. + @Test + public void testPrefixDelegation() throws Exception { + unionfs = new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of( + new PathFragment("/foo"), inDelegate, + new PathFragment("/foo/bar"), outDelegate), defaultDelegate); + + assertSame(inDelegate, unionfs.getDelegate(unionfs.getPath("/foo/foo.txt"))); + assertSame(outDelegate, unionfs.getDelegate(unionfs.getPath("/foo/bar/foo.txt"))); + assertSame(inDelegate, unionfs.getDelegate(unionfs.getPath("/foo/bar/../foo.txt"))); + assertSame(defaultDelegate, unionfs.getDelegate(unionfs.getPath("/bar/foo.txt"))); + assertSame(defaultDelegate, unionfs.getDelegate(unionfs.getPath("/foo/bar/../.."))); + } + + // Checks that files cannot be modified when the filesystem is created + // read-only, even if the delegate filesystems are read/write. + @Test + public void testModificationFlag() throws Exception { + assertTrue(unionfs.supportsModifications()); + Path outPath = unionfs.getPath("/out/foo.txt"); + assertTrue(unionfs.createDirectory(outPath.getParentDirectory())); + OutputStream outFile = unionfs.getOutputStream(outPath); + outFile.write('b'); + outFile.close(); + + unionfs.setExecutable(outPath, true); + + // Note that this does not destroy the underlying filesystems; + // UnionFileSystem is just a view. + unionfs = createDefaultUnionFileSystem(true); + assertFalse(unionfs.supportsModifications()); + + InputStream outFileInput = unionfs.getInputStream(outPath); + int outFileByte = outFileInput.read(); + outFileInput.close(); + assertEquals('b', outFileByte); + + assertTrue(unionfs.isExecutable(outPath)); + + // Modifying files through the unionfs isn't permitted, even if the + // delegates are read/write. + try { + unionfs.setExecutable(outPath, false); + fail("Modification to a read-only UnionFileSystem succeeded."); + } catch (UnsupportedOperationException expected) { + // OK - should fail. + } + } + + // Checks that roots of delegate filesystems are created outside of the + // delegate filesystems; i.e. they can be seen from the filesystem of the parent. + @Test + public void testDelegateRootDirectoryCreation() throws Exception { + Path foo = unionfs.getPath("/foo"); + Path bar = unionfs.getPath("/bar"); + Path out = unionfs.getPath("/out"); + assertTrue(unionfs.createDirectory(foo)); + assertTrue(unionfs.createDirectory(bar)); + assertTrue(unionfs.createDirectory(out)); + Path outFile = unionfs.getPath("/out/in"); + FileSystemUtils.writeContentAsLatin1(outFile, "Out"); + + // FileSystemTest.setUp() silently creates the test root on the filesystem... + Path testDirUnderRoot = unionfs.getPath(workingDir.asFragment().subFragment(0, 1)); + assertThat(unionfs.getDirectoryEntries(unionfs.getRootDirectory())).containsExactly(foo, bar, + out, testDirUnderRoot); + assertThat(unionfs.getDirectoryEntries(out)).containsExactly(outFile); + + assertSame(unionfs.getDelegate(foo), defaultDelegate); + assertEquals(foo.asFragment(), unionfs.adjustPath(foo, defaultDelegate).asFragment()); + assertSame(unionfs.getDelegate(bar), defaultDelegate); + assertSame(unionfs.getDelegate(outFile), outDelegate); + assertSame(unionfs.getDelegate(out), outDelegate); + + // As a fragment (i.e. without filesystem or root info), the path name should be preserved. + assertEquals(outFile.asFragment(), unionfs.adjustPath(outFile, outDelegate).asFragment()); + } + + // Ensure that the right filesystem is still chosen when paths contain "..". + @Test + public void testDelegationOfUpLevelReferences() throws Exception { + assertSame(defaultDelegate, unionfs.getDelegate(unionfs.getPath("/in/../foo.txt"))); + assertSame(inDelegate, unionfs.getDelegate(unionfs.getPath("/out/../in"))); + assertSame(outDelegate, unionfs.getDelegate(unionfs.getPath("/out/../in/../out/foo.txt"))); + assertSame(inDelegate, unionfs.getDelegate(unionfs.getPath("/in/./foo.txt"))); + } + + // Basic *explicit* cross-filesystem symlink check. + // Note: This does not work implicitly yet, as the next test illustrates. + @Test + public void testCrossDeviceSymlinks() throws Exception { + assertTrue(unionfs.createDirectory(unionfs.getPath("/out"))); + + // Create an "/in" directory directly on the output delegate to bypass the + // UnionFileSystem's mapping. + assertTrue(inDelegate.getPath("/in").createDirectory()); + OutputStream outStream = inDelegate.getPath("/in/bar.txt").getOutputStream(); + outStream.write('i'); + outStream.close(); + + Path outFoo = unionfs.getPath("/out/foo"); + unionfs.createSymbolicLink(outFoo, new PathFragment("../in/bar.txt")); + assertTrue(unionfs.stat(outFoo, false).isSymbolicLink()); + + try { + unionfs.stat(outFoo, true).isFile(); + fail("Stat on cross-device symlink succeeded!"); + } catch (FileNotFoundException expected) { + // OK + } + + Path resolved = unionfs.resolveSymbolicLinks(outFoo); + assertSame(unionfs, resolved.getFileSystem()); + InputStream barInput = resolved.getInputStream(); + int barChar = barInput.read(); + barInput.close(); + assertEquals('i', barChar); + } + + @Test + public void testNoDelegateLeakage() throws Exception { + assertSame(unionfs, unionfs.getPath("/in/foo.txt").getFileSystem()); + assertSame(unionfs, unionfs.getPath("/in/foo/bar").getParentDirectory().getFileSystem()); + unionfs.createDirectory(unionfs.getPath("/out")); + unionfs.createDirectory(unionfs.getPath("/out/foo")); + unionfs.createDirectory(unionfs.getPath("/out/foo/bar")); + assertSame(unionfs, Iterables.getOnlyElement(unionfs.getDirectoryEntries( + unionfs.getPath("/out/foo"))).getParentDirectory().getFileSystem()); + } + + // Prefix mappings can apply to files starting with a prefix within a directory. + @Test + public void testWithinDirectoryMapping() throws Exception { + unionfs = new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of( + new PathFragment("/fruit/a"), inDelegate, + new PathFragment("/fruit/b"), outDelegate), defaultDelegate); + assertTrue(unionfs.createDirectory(unionfs.getPath("/fruit"))); + assertTrue(defaultDelegate.getPath("/fruit").isDirectory()); + assertTrue(inDelegate.getPath("/fruit").createDirectory()); + assertTrue(outDelegate.getPath("/fruit").createDirectory()); + + Path apple = unionfs.getPath("/fruit/apple"); + Path banana = unionfs.getPath("/fruit/banana"); + Path cherry = unionfs.getPath("/fruit/cherry"); + unionfs.createDirectory(apple); + unionfs.createDirectory(banana); + assertSame(inDelegate, unionfs.getDelegate(apple)); + assertSame(outDelegate, unionfs.getDelegate(banana)); + assertSame(defaultDelegate, unionfs.getDelegate(cherry)); + + FileSystemUtils.writeContentAsLatin1(apple.getRelative("table"), "penny"); + FileSystemUtils.writeContentAsLatin1(banana.getRelative("nana"), "nanana"); + FileSystemUtils.writeContentAsLatin1(cherry, "garcia"); + + assertEquals("penny", new String( + FileSystemUtils.readContentAsLatin1(inDelegate.getPath("/fruit/apple/table")))); + assertEquals("nanana", new String( + FileSystemUtils.readContentAsLatin1(outDelegate.getPath("/fruit/banana/nana")))); + assertEquals("garcia", new String( + FileSystemUtils.readContentAsLatin1(defaultDelegate.getPath("/fruit/cherry")))); + } + + // Write using the VFS through a UnionFileSystem and check that the file can + // be read back in the same location using standard Java IO. + // There is a similar test in UnixFileSystem, but this is essential to ensure + // that paths aren't being remapped in some nasty way on the underlying FS. + @Test + public void testDelegateOperationsReflectOnLocalFilesystem() throws Exception { + unionfs = new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of( + workingDir.getParentDirectory().asFragment(), new UnixFileSystem()), + defaultDelegate, false); + // This is a child of the current tmpdir, and doesn't exist on its own. + // It would be created in setup(), but of course, that didn't use a UnixFileSystem. + unionfs.createDirectory(workingDir); + Path testFile = unionfs.getPath(workingDir.getRelative("test_file").asFragment()); + assertTrue(testFile.asFragment().startsWith(workingDir.asFragment())); + String testString = "This is a test file"; + FileSystemUtils.writeContentAsLatin1(testFile, testString); + try { + assertEquals(testString, new String(FileSystemUtils.readContentAsLatin1(testFile))); + } finally { + testFile.delete(); + assertTrue(unionfs.delete(workingDir)); + } + } + + // Regression test for [UnionFS: Directory creation across mapping fails.] + @Test + public void testCreateParentsAcrossMapping() throws Exception { + unionfs = new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of( + new PathFragment("/out/dir"), outDelegate), defaultDelegate, false); + Path outDir = unionfs.getPath("/out/dir/biz/bang"); + FileSystemUtils.createDirectoryAndParents(outDir); + assertTrue(outDir.isDirectory()); + } + + private static class XAttrInMemoryFs extends InMemoryFileSystem { + public XAttrInMemoryFs(Clock clock) { + super(clock); + } + + @Override + protected byte[] getxattr(Path path, String name, boolean followSymlinks) { + assertSame(this, path.getFileSystem()); + return (name.equals(XATTR_KEY)) ? XATTR_VAL.getBytes(UTF_8) : null; + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixFileSystemTest.java new file mode 100644 index 0000000000..0ded4048a6 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixFileSystemTest.java @@ -0,0 +1,63 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; + +/** + * Tests for the {@link UnixFileSystem} class. + */ +@RunWith(JUnit4.class) +public class UnixFileSystemTest extends SymlinkAwareFileSystemTest { + + @Override + protected FileSystem getFreshFileSystem() { + return new UnixFileSystem(); + } + + @Override + public void destroyFileSystem(FileSystem fileSystem) { + // Nothing. + } + + @Override + protected void expectNotFound(Path path) throws IOException { + assertNull(path.statIfFound()); + } + + // Most tests are just inherited from FileSystemTest. + + @Test + public void testCircularSymlinkFound() throws Exception { + Path linkA = absolutize("link-a"); + Path linkB = absolutize("link-b"); + linkA.createSymbolicLink(linkB); + linkB.createSymbolicLink(linkA); + assertFalse(linkA.exists(Symlinks.FOLLOW)); + try { + linkA.statIfFound(Symlinks.FOLLOW); + fail(); + } catch (IOException expected) { + // Expected. + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixPathEqualityTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathEqualityTest.java new file mode 100644 index 0000000000..f5f58e2c7e --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathEqualityTest.java @@ -0,0 +1,118 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * This tests how canonical paths and non-canonical paths are equal with each + * other, and also how paths from different filesystems behave with each other. + */ +@RunWith(JUnit4.class) +public class UnixPathEqualityTest { + + private FileSystem otherUnixFs; + private FileSystem unixFs; + + @Before + public void setUp() throws Exception { + unixFs = new UnixFileSystem(); + otherUnixFs = new UnixFileSystem(); + assertTrue(unixFs != otherUnixFs); + } + + private void assertTwoWayEquals(Object obj1, Object obj2) { + assertTrue(obj1.equals(obj2)); + assertTrue(obj2.equals(obj1)); + assertEquals(obj1.hashCode(), obj2.hashCode()); + } + + private void assertTwoWayNotEquals(Object obj1, Object obj2) { + assertFalse(obj1.equals(obj2)); + assertFalse(obj2.equals(obj1)); + } + + @Test + public void testPathsAreEqualEvenIfNotCanonical() { + // This path is already canonical, so there's no difference between + // the canonical / nonCanonical path, as far as equals is concerned + Path nonCanonical = unixFs.getPath("/a/canonical/unix/path"); + Path canonical = unixFs.getPath("/a/canonical/unix/path"); + assertTwoWayEquals(nonCanonical, canonical); + } + + @Test + public void testPathsAreNeverEqualWithStrings() { + // Make sure that paths aren't equal to plain old strings + Path nonCanonical = unixFs.getPath("/a/non/../canonical/unix/path"); + Path canonical = unixFs.getPath("/a/non/../canonical/unix/path"); + assertTwoWayNotEquals(nonCanonical, "/a/non/../canonical/unix/path"); + assertTwoWayNotEquals(canonical, "/a/non/../canonical/unix/path"); + } + + @Test + public void testCanonicalPathsFromDifferentFileSystemsAreNeverEqual() { + Path canonical = unixFs.getPath("/canonical/path"); + Path otherCanonical = otherUnixFs.getPath("/canonical/path"); + assertTwoWayNotEquals(canonical, otherCanonical); + } + + @Test + public void testNonCanonicalPathsFromDifferentFileSystemsAreNeverEqual() { + Path nonCanonical = unixFs.getPath("/non/canonical/path"); + Path otherNonCanonical = otherUnixFs.getPath("/non/canonical/path"); + assertTwoWayNotEquals(nonCanonical, otherNonCanonical); + } + + @Test + public void testCrossFilesystemStartsWithReturnsFalse() { + assertFalse(unixFs.getPath("/a").startsWith(otherUnixFs.getPath("/b"))); + } + + @Test + public void testCrossFilesystemOperationsForbidden() throws Exception { + Path a = unixFs.getPath("/a"); + Path b = otherUnixFs.getPath("/b"); + + try { + a.renameTo(b); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("different filesystems"); + } + + try { + a.relativeTo(b); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("different filesystems"); + } + + try { + a.createSymbolicLink(b); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("different filesystems"); + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixPathGetParentTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathGetParentTest.java new file mode 100644 index 0000000000..0f679c3e78 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathGetParentTest.java @@ -0,0 +1,87 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static org.junit.Assert.assertEquals; + +import com.google.devtools.build.lib.testutil.TestUtils; +import com.google.devtools.build.lib.vfs.util.FileSystems; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; + +/** + * A test for {@link Path} in the context of {@link UnixFileSystem}. + */ +@RunWith(JUnit4.class) +public class UnixPathGetParentTest { + + private FileSystem unixFs; + private Path testRoot; + + @Before + public void setUp() throws Exception { + unixFs = FileSystems.initDefaultAsNative(); + testRoot = unixFs.getPath(TestUtils.tmpDir()).getRelative("UnixPathGetParentTest"); + FileSystemUtils.createDirectoryAndParents(testRoot); + } + + @After + public void tearDown() throws Exception { + FileSystemUtils.deleteTree(testRoot); // (comment out during debugging) + } + + private Path getParent(String path) { + return unixFs.getPath(path).getParentDirectory(); + } + + @Test + public void testAbsoluteRootHasNoParent() { + assertEquals(null, getParent("/")); + } + + @Test + public void testParentOfSimpleDirectory() { + assertEquals("/foo", getParent("/foo/bar").getPathString()); + } + + @Test + public void testParentOfDotDotInMiddleOfPathname() { + assertEquals("/", getParent("/foo/../bar").getPathString()); + } + + @Test + public void testGetPathDoesNormalizationWithoutIO() throws IOException { + Path tmp = testRoot.getChild("tmp"); + Path tmpWiz = tmp.getChild("wiz"); + + tmp.createDirectory(); + + // ln -sf /tmp /tmp/wiz + tmpWiz.createSymbolicLink(tmp); + + assertEquals(testRoot, tmp.getParentDirectory()); + + assertEquals(tmp, tmpWiz.getParentDirectory()); + + // Under UNIX, inode(/tmp/wiz/..) == inode(/). However getPath() does not + // perform I/O, only string operations, so it disagrees: + assertEquals(tmp, tmp.getRelative(new PathFragment("wiz/.."))); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java new file mode 100644 index 0000000000..b59336725a --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java @@ -0,0 +1,279 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.Lists; +import com.google.devtools.build.lib.testutil.TestUtils; +import com.google.devtools.build.lib.vfs.util.FileSystems; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +/** + * Tests for {@link Path}. + */ +@RunWith(JUnit4.class) +public class UnixPathTest { + + private FileSystem unixFs; + private File aDirectory; + private File aFile; + private File anotherFile; + private File tmpDir; + + protected FileSystem getUnixFileSystem() { + return FileSystems.initDefaultAsNative(); + } + + @Before + public void setUp() throws Exception { + unixFs = getUnixFileSystem(); + tmpDir = new File(TestUtils.tmpDir(), "tmpDir"); + tmpDir.mkdirs(); + aDirectory = new File(tmpDir, "a_directory"); + aDirectory.mkdirs(); + aFile = new File(tmpDir, "a_file"); + new FileWriter(aFile).close(); + anotherFile = new File(aDirectory, "another_file.txt"); + new FileWriter(anotherFile).close(); + } + + @Test + public void testExists() { + assertTrue(unixFs.getPath(aDirectory.getPath()).exists()); + assertTrue(unixFs.getPath(aFile.getPath()).exists()); + assertFalse(unixFs.getPath("/does/not/exist").exists()); + } + + @Test + public void testDirectoryEntriesForDirectory() throws IOException { + Collection<Path> entries = + unixFs.getPath(tmpDir.getPath()).getDirectoryEntries(); + List<Path> expectedEntries = Arrays.asList( + unixFs.getPath(tmpDir.getPath() + "/a_file"), + unixFs.getPath(tmpDir.getPath() + "/a_directory")); + + assertEquals(new HashSet<Object>(expectedEntries), + new HashSet<Object>(entries)); + } + + @Test + public void testDirectoryEntriesForFileThrowsException() { + try { + unixFs.getPath(aFile.getPath()).getDirectoryEntries(); + fail("No exception thrown."); + } catch (IOException x) { + // The expected result. + } + } + + @Test + public void testIsFileIsTrueForFile() { + assertTrue(unixFs.getPath(aFile.getPath()).isFile()); + } + + @Test + public void testIsFileIsFalseForDirectory() { + assertFalse(unixFs.getPath(aDirectory.getPath()).isFile()); + } + + @Test + public void testBaseName() { + assertEquals("base", unixFs.getPath("/foo/base").getBaseName()); + } + + @Test + public void testBaseNameRunsAfterDotDotInterpretation() { + assertEquals("base", unixFs.getPath("/base/foo/..").getBaseName()); + } + + @Test + public void testParentOfRootIsRoot() { + assertEquals(unixFs.getPath("/"), unixFs.getPath("/..")); + assertEquals(unixFs.getPath("/"), unixFs.getPath("/../../../../../..")); + assertEquals(unixFs.getPath("/foo"), unixFs.getPath("/../../../foo")); + } + + @Test + public void testIsDirectory() { + assertTrue(unixFs.getPath(aDirectory.getPath()).isDirectory()); + assertFalse(unixFs.getPath(aFile.getPath()).isDirectory()); + assertFalse(unixFs.getPath("/does/not/exist").isDirectory()); + } + + @Test + public void testListNonExistingDirectoryThrowsException() { + try { + unixFs.getPath("/does/not/exist").getDirectoryEntries(); + fail("No exception thrown."); + } catch (IOException ex) { + // success! + } + } + + private void assertPathSet(Collection<Path> actual, String... expected) { + List<String> actualStrings = Lists.newArrayListWithCapacity(actual.size()); + + for (Path path : actual) { + actualStrings.add(path.getPathString()); + } + + assertThat(actualStrings).containsExactlyElementsIn(Arrays.asList(expected)); + } + + @Test + public void testGlob() throws Exception { + Collection<Path> textFiles = UnixGlob.forPath(unixFs.getPath(tmpDir.getPath())) + .addPattern("*/*.txt") + .globInterruptible(); + assertEquals(1, textFiles.size()); + Path onlyFile = textFiles.iterator().next(); + assertEquals(unixFs.getPath(anotherFile.getPath()), onlyFile); + + Collection<Path> onlyFiles = + UnixGlob.forPath(unixFs.getPath(tmpDir.getPath())) + .addPattern("*") + .setExcludeDirectories(true) + .globInterruptible(); + assertPathSet(onlyFiles, aFile.getPath()); + + Collection<Path> directoriesToo = + UnixGlob.forPath(unixFs.getPath(tmpDir.getPath())) + .addPattern("*") + .setExcludeDirectories(false) + .globInterruptible(); + assertPathSet(directoriesToo, aFile.getPath(), aDirectory.getPath()); + } + + @Test + public void testGetRelative() { + Path relative = unixFs.getPath("/foo").getChild("bar"); + Path expected = unixFs.getPath("/foo/bar"); + assertEquals(expected, relative); + } + + @Test + public void testEqualsAndHash() { + Path path = unixFs.getPath("/foo/bar"); + Path equalPath = unixFs.getPath("/foo/bar"); + Path differentPath = unixFs.getPath("/foo/bar/baz"); + Object differentType = new Object(); + + assertEquals(path.hashCode(), equalPath.hashCode()); + assertEquals(path, equalPath); + assertFalse(path.equals(differentPath)); + assertFalse(path.equals(differentType)); + } + + @Test + public void testLatin1ReadAndWrite() throws IOException { + char[] allLatin1Chars = new char[256]; + for (int i = 0; i < 256; i++) { + allLatin1Chars[i] = (char) i; + } + Path path = unixFs.getPath(aFile.getPath()); + String latin1String = new String(allLatin1Chars); + FileSystemUtils.writeContentAsLatin1(path, latin1String); + String fileContent = new String(FileSystemUtils.readContentAsLatin1(path)); + assertEquals(fileContent, latin1String); + } + + /** + * Verify that the encoding implemented by + * {@link FileSystemUtils#writeContentAsLatin1(Path, String)} + * really is 8859-1 (latin1). + */ + @Test + public void testVerifyLatin1() throws IOException { + char[] allLatin1Chars = new char[256]; + for( int i = 0; i < 256; i++) { + allLatin1Chars[i] = (char)i; + } + Path path = unixFs.getPath(aFile.getPath()); + String latin1String = new String(allLatin1Chars); + FileSystemUtils.writeContentAsLatin1(path, latin1String); + byte[] bytes = FileSystemUtils.readContent(path); + assertEquals(new String(bytes, "ISO-8859-1"), latin1String); + } + + @Test + public void testBytesReadAndWrite() throws IOException { + byte[] bytes = new byte[] { (byte) 0xdeadbeef, (byte) 0xdeadbeef>>8, + (byte) 0xdeadbeef>>16, (byte) 0xdeadbeef>>24 }; + Path path = unixFs.getPath(aFile.getPath()); + FileSystemUtils.writeContent(path, bytes); + byte[] content = FileSystemUtils.readContent(path); + assertEquals(bytes.length, content.length); + for (int i = 0; i < bytes.length; i++) { + assertEquals(bytes[i], content[i]); + } + } + + @Test + public void testInputOutputStreams() throws IOException { + Path path = unixFs.getPath(aFile.getPath()); + OutputStream out = path.getOutputStream(); + for (int i = 0; i < 256; i++) { + out.write(i); + } + out.close(); + InputStream in = path.getInputStream(); + for (int i = 0; i < 256; i++) { + assertEquals(i, in.read()); + } + assertEquals(-1, in.read()); + in.close(); + } + + @Test + public void testAbsolutePathRoot() { + assertEquals("/", new Path(null).toString()); + } + + @Test + public void testAbsolutePath() { + Path segment = new Path(null, "bar.txt", + new Path(null, "foo", new Path(null))); + assertEquals("/foo/bar.txt", segment.toString()); + } + + @Test + public void testDerivedSegmentEquality() { + Path absoluteSegment = new Path(null); + + Path derivedNode = absoluteSegment.getChild("derivedSegment"); + Path otherDerivedNode = absoluteSegment.getChild("derivedSegment"); + + assertSame(derivedNode, otherDerivedNode); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/ZipFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/ZipFileSystemTest.java new file mode 100644 index 0000000000..9dc12764ae --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/ZipFileSystemTest.java @@ -0,0 +1,233 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.Lists; +import com.google.common.io.CharStreams; +import com.google.devtools.build.lib.testutil.BlazeTestUtils; +import com.google.devtools.build.lib.testutil.TestConstants; +import com.google.devtools.build.lib.vfs.util.FileSystems; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@RunWith(JUnit4.class) +public class ZipFileSystemTest { + + /** + * Expected listing of sample zip files, in alpha sorted order + */ + private static final String[] LISTING = { + "/dir1", + "/dir1/file1a", + "/dir1/file1b", + "/dir2", + "/dir2/dir3", + "/dir2/dir3/dir4", + "/dir2/dir3/dir4/file4", + "/dir2/file2", + "/file0", + }; + + private FileSystem zipFS1; + private FileSystem zipFS2; + + @Before + public void setUp() throws Exception { + FileSystem unixFs = FileSystems.initDefaultAsNative(); + Path testdataDir = unixFs.getPath(BlazeTestUtils.runfilesDir()).getRelative( + TestConstants.JAVATESTS_ROOT + "/com/google/devtools/build/lib/vfs"); + Path zPath1 = testdataDir.getChild("sample_with_dirs.zip"); + Path zPath2 = testdataDir.getChild("sample_without_dirs.zip"); + zipFS1 = new ZipFileSystem(zPath1); + zipFS2 = new ZipFileSystem(zPath2); + } + + private void checkExists(FileSystem fs) { + assertTrue(fs.getPath("/dir2/dir3/dir4").exists()); + assertTrue(fs.getPath("/dir2/dir3/dir4/file4").exists()); + assertFalse(fs.getPath("/dir2/dir3/dir4/bogus").exists()); + } + + @Test + public void testExists() { + checkExists(zipFS1); + checkExists(zipFS2); + } + + private void checkIsFile(FileSystem fs) { + assertFalse(fs.getPath("/dir2/dir3/dir4").isFile()); + assertTrue(fs.getPath("/dir2/dir3/dir4/file4").isFile()); + assertFalse(fs.getPath("/dir2/dir3/dir4/bogus").isFile()); + } + + @Test + public void testIsFile() { + checkIsFile(zipFS1); + checkIsFile(zipFS2); + } + + private void checkIsDir(FileSystem fs) { + assertTrue(fs.getPath("/dir2/dir3/dir4").isDirectory()); + assertFalse(fs.getPath("/dir2/dir3/dir4/file4").isDirectory()); + assertFalse(fs.getPath("/bogus/mobogus").isDirectory()); + assertFalse(fs.getPath("/bogus").isDirectory()); + } + + @Test + public void testIsDir() { + checkIsDir(zipFS1); + checkIsDir(zipFS2); + } + + /** + * Recursively add the contents of a given path, rendered as strings, into a + * given list. + */ + private static void listChildren(Path p, List<String> list) + throws IOException { + for (Path c : p.getDirectoryEntries()) { + list.add(c.getPathString()); + if (c.isDirectory()) { + listChildren(c, list); + } + } + } + + private void checkListing(FileSystem fs) throws Exception { + List<String> list = new ArrayList<>(); + listChildren(fs.getRootDirectory(), list); + Collections.sort(list); + assertEquals(Lists.newArrayList(LISTING), list); + } + + @Test + public void testListing() throws Exception { + checkListing(zipFS1); + checkListing(zipFS2); + + // Regression test for: creation of a path (i.e. a file *name*) + // must not affect the result of getDirectoryEntries(). + zipFS1.getPath("/dir1/notthere"); + checkListing(zipFS1); + } + + private void checkFileSize(FileSystem fs, String name, long expectedSize) + throws IOException { + assertEquals(expectedSize, fs.getPath(name).getFileSize()); + } + + @Test + public void testCanReadRoot() { + Path rootDirectory = zipFS1.getRootDirectory(); + assertTrue(rootDirectory.isDirectory()); + } + + @Test + public void testFileSize() throws IOException { + checkFileSize(zipFS1, "/dir1/file1a", 5); + checkFileSize(zipFS2, "/dir1/file1a", 5); + checkFileSize(zipFS1, "/dir2/dir3/dir4/file4", 5000); + checkFileSize(zipFS2, "/dir2/dir3/dir4/file4", 5000); + } + + private void checkCantGetFileSize(FileSystem fs, String name) { + try { + fs.getPath(name).getFileSize(); + fail(); + } catch (IOException expected) { + // expected + } + } + + @Test + public void testCantGetFileSize() { + checkCantGetFileSize(zipFS1, "/dir2/dir3/dir4/bogus"); + checkCantGetFileSize(zipFS2, "/dir2/dir3/dir4/bogus"); + } + + private void checkOpenFile(FileSystem fs, String name, int expectedSize) + throws Exception { + InputStream is = fs.getPath(name).getInputStream(); + List<String> lines = CharStreams.readLines(new InputStreamReader(is, "ISO-8859-1")); + assertEquals(expectedSize, lines.size()); + for (int i = 0; i < expectedSize; i++) { + assertEquals("body", lines.get(i)); + } + } + + @Test + public void testOpenSmallFile() throws Exception { + checkOpenFile(zipFS1, "/dir1/file1a", 1); + checkOpenFile(zipFS2, "/dir1/file1a", 1); + } + + @Test + public void testOpenBigFile() throws Exception { + checkOpenFile(zipFS1, "/dir2/dir3/dir4/file4", 1000); + checkOpenFile(zipFS2, "/dir2/dir3/dir4/file4", 1000); + } + + private void checkCantOpenFile(FileSystem fs, String name) { + try { + fs.getPath(name).getInputStream(); + fail(); + } catch (IOException expected) { + // expected + } + } + + @Test + public void testCantOpenFile() throws Exception { + checkCantOpenFile(zipFS1, "/dir2/dir3/dir4/bogus"); + checkCantOpenFile(zipFS2, "/dir2/dir3/dir4/bogus"); + } + + private void checkCantCreateAnything(FileSystem fs, String name) { + Path p = fs.getPath(name); + try { + p.createDirectory(); + fail(); + } catch (Exception expected) {} + try { + FileSystemUtils.createEmptyFile(p); + fail(); + } catch (Exception expected) {} + try { + p.createSymbolicLink(p); + fail(); + } catch (Exception expected) {} + } + + @Test + public void testCantCreateAnything() throws Exception { + checkCantCreateAnything(zipFS1, "/dir2/dir3/dir4/new"); + checkCantCreateAnything(zipFS2, "/dir2/dir3/dir4/new"); + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfoTest.java b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfoTest.java new file mode 100644 index 0000000000..dbdd64a328 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfoTest.java @@ -0,0 +1,73 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs.inmemoryfs; + +import static org.junit.Assert.fail; + +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.util.Clock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class InMemoryContentInfoTest { + + private Clock clock; + + @Before + public void setUp() throws Exception { + clock = BlazeClock.instance(); + } + + @Test + public void testDirectoryCannotAddNullChild() { + InMemoryDirectoryInfo directory = new InMemoryDirectoryInfo(clock); + + try { + directory.addChild("bar", null); + fail("NullPointerException not thrown."); + } catch (NullPointerException e) { + // success. + } + } + + @Test + public void testDirectoryCannotAddChildTwice() { + InMemoryDirectoryInfo directory = new InMemoryDirectoryInfo(clock); + InMemoryFileInfo otherFile = new InMemoryFileInfo(clock); + directory.addChild("bar", otherFile); + + try { + directory.addChild("bar", otherFile); + fail("IllegalArgumentException not thrown."); + } catch (IllegalArgumentException e) { + // success. + } + } + + @Test + public void testDirectoryRemoveNonExistingChild() { + InMemoryDirectoryInfo directory = new InMemoryDirectoryInfo(clock); + try { + directory.removeChild("bar"); + fail(); + } catch (IllegalArgumentException e) { + // success + } + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java new file mode 100644 index 0000000000..65ea6f6557 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java @@ -0,0 +1,414 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs.inmemoryfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.Lists; +import com.google.devtools.build.lib.testutil.TestThread; +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.ScopeEscapableFileSystemTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Tests for {@link InMemoryFileSystem}. Note that most tests are inherited + * from {@link ScopeEscapableFileSystemTest} and ancestors. This specific + * file focuses only on concurrency tests. + * + */ +@RunWith(JUnit4.class) +public class InMemoryFileSystemTest extends ScopeEscapableFileSystemTest { + + @Override + public FileSystem getFreshFileSystem() { + return new InMemoryFileSystem(BlazeClock.instance(), SCOPE_ROOT); + } + + @Override + public void destroyFileSystem(FileSystem fileSystem) { + // Nothing. + } + + private static final int NUM_THREADS_FOR_CONCURRENCY_TESTS = 10; + private static final String TEST_FILE_DATA = "data"; + + /** + * Writes the given data to the given file. + */ + private static void writeToFile(Path path, String data) throws IOException { + OutputStream out = path.getOutputStream(); + out.write(data.getBytes(Charset.defaultCharset())); + out.close(); + } + + /** + * Tests concurrent creation of a substantial tree hierarchy including + * files, directories, symlinks, file contents, and permissions. + */ + @Test + public void testConcurrentTreeConstruction() throws Exception { + final int NUM_TO_WRITE = 10000; + final AtomicInteger baseSelector = new AtomicInteger(); + + // 1) Define the intended path structure. + class PathCreator extends TestThread { + @Override + public void runTest() throws Exception { + Path base = testFS.getPath("/base" + baseSelector.getAndIncrement()); + base.createDirectory(); + + for (int i = 0; i < NUM_TO_WRITE; i++) { + Path subdir1 = base.getRelative("subdir1_" + i); + subdir1.createDirectory(); + Path subdir2 = base.getRelative("subdir2_" + i); + subdir2.createDirectory(); + + Path file = base.getRelative("somefile" + i); + writeToFile(file, TEST_FILE_DATA); + + subdir1.setReadable(true); + subdir2.setReadable(false); + file.setReadable(true); + + subdir1.setWritable(false); + subdir2.setWritable(true); + file.setWritable(false); + + subdir1.setExecutable(false); + subdir2.setExecutable(true); + file.setExecutable(false); + + subdir1.setLastModifiedTime(100); + subdir2.setLastModifiedTime(200); + file.setLastModifiedTime(300); + + Path symlink = base.getRelative("symlink" + i); + symlink.createSymbolicLink(file); + } + } + } + + // 2) Construct the tree. + Collection<TestThread> threads = + Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS); + for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) { + TestThread thread = new PathCreator(); + thread.start(); + threads.add(thread); + } + for (TestThread thread : threads) { + thread.joinAndAssertState(0); + } + + // 3) Define the validation logic. + class PathValidator extends TestThread { + @Override + public void runTest() throws Exception { + Path base = testFS.getPath("/base" + baseSelector.getAndIncrement()); + assertTrue(base.exists()); + assertFalse(base.getRelative("notreal").exists()); + + for (int i = 0; i < NUM_TO_WRITE; i++) { + Path subdir1 = base.getRelative("subdir1_" + i); + assertTrue(subdir1.exists()); + assertTrue(subdir1.isDirectory()); + assertTrue(subdir1.isReadable()); + assertFalse(subdir1.isWritable()); + assertFalse(subdir1.isExecutable()); + assertEquals(100, subdir1.getLastModifiedTime()); + + Path subdir2 = base.getRelative("subdir2_" + i); + assertTrue(subdir2.exists()); + assertTrue(subdir2.isDirectory()); + assertFalse(subdir2.isReadable()); + assertTrue(subdir2.isWritable()); + assertTrue(subdir2.isExecutable()); + assertEquals(200, subdir2.getLastModifiedTime()); + + Path file = base.getRelative("somefile" + i); + assertTrue(file.exists()); + assertTrue(file.isFile()); + assertTrue(file.isReadable()); + assertFalse(file.isWritable()); + assertFalse(file.isExecutable()); + assertEquals(300, file.getLastModifiedTime()); + BufferedReader reader = new BufferedReader( + new InputStreamReader(file.getInputStream(), Charset.defaultCharset())); + assertEquals(TEST_FILE_DATA, reader.readLine()); + assertEquals(null, reader.readLine()); + + Path symlink = base.getRelative("symlink" + i); + assertTrue(symlink.exists()); + assertTrue(symlink.isSymbolicLink()); + assertEquals(file.asFragment(), symlink.readSymbolicLink()); + } + } + } + + // 4) Validate the results. + baseSelector.set(0); + threads = Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS); + for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) { + TestThread thread = new PathValidator(); + thread.start(); + threads.add(thread); + } + for (TestThread thread : threads) { + thread.joinAndAssertState(0); + } + } + + /** + * Tests concurrent creation of many files, all within the same directory. + */ + @Test + public void testConcurrentDirectoryConstruction() throws Exception { + final int NUM_TO_WRITE = 10000; + final AtomicInteger baseSelector = new AtomicInteger(); + + // 1) Define the intended path structure. + class PathCreator extends TestThread { + @Override + public void runTest() throws Exception { + final int threadId = baseSelector.getAndIncrement(); + Path base = testFS.getPath("/common_dir"); + base.createDirectory(); + + for (int i = 0; i < NUM_TO_WRITE; i++) { + Path file = base.getRelative("somefile_" + threadId + "_" + i); + writeToFile(file, TEST_FILE_DATA); + file.setReadable(i % 2 == 0); + file.setWritable(i % 3 == 0); + file.setExecutable(i % 4 == 0); + file.setLastModifiedTime(i); + Path symlink = base.getRelative("symlink_" + threadId + "_" + i); + symlink.createSymbolicLink(file); + } + } + } + + // 2) Create the files. + Collection<TestThread> threads = + Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS); + for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) { + TestThread thread = new PathCreator(); + thread.start(); + threads.add(thread); + } + for (TestThread thread : threads) { + thread.joinAndAssertState(0); + } + + // 3) Define the validation logic. + class PathValidator extends TestThread { + @Override + public void runTest() throws Exception { + final int threadId = baseSelector.getAndIncrement(); + Path base = testFS.getPath("/common_dir"); + assertTrue(base.exists()); + + for (int i = 0; i < NUM_TO_WRITE; i++) { + Path file = base.getRelative("somefile_" + threadId + "_" + i); + assertTrue(file.exists()); + assertTrue(file.isFile()); + assertEquals(i % 2 == 0, file.isReadable()); + assertEquals(i % 3 == 0, file.isWritable()); + assertEquals(i % 4 == 0, file.isExecutable()); + assertEquals(i, file.getLastModifiedTime()); + if (file.isReadable()) { + BufferedReader reader = new BufferedReader( + new InputStreamReader(file.getInputStream(), Charset.defaultCharset())); + assertEquals(TEST_FILE_DATA, reader.readLine()); + assertEquals(null, reader.readLine()); + } + + Path symlink = base.getRelative("symlink_" + threadId + "_" + i); + assertTrue(symlink.exists()); + assertTrue(symlink.isSymbolicLink()); + assertEquals(file.asFragment(), symlink.readSymbolicLink()); + } + } + } + + // 4) Validate the results. + baseSelector.set(0); + threads = Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS); + for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) { + TestThread thread = new PathValidator(); + thread.start(); + threads.add(thread); + } + for (TestThread thread : threads) { + thread.joinAndAssertState(0); + } + } + + /** + * Tests concurrent file deletion. + */ + @Test + public void testConcurrentDeletion() throws Exception { + final int NUM_TO_WRITE = 10000; + final AtomicInteger baseSelector = new AtomicInteger(); + + final Path base = testFS.getPath("/base"); + base.createDirectory(); + + // 1) Create a bunch of files. + for (int i = 0; i < NUM_TO_WRITE; i++) { + writeToFile(base.getRelative("file" + i), TEST_FILE_DATA); + } + + // 2) Define our deletion strategy. + class FileDeleter extends TestThread { + @Override + public void runTest() throws Exception { + for (int i = 0; i < NUM_TO_WRITE / NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) { + int whichFile = baseSelector.getAndIncrement(); + Path file = base.getRelative("file" + whichFile); + if (whichFile % 25 != 0) { + assertTrue(file.delete()); + } else { + // Throw another concurrent access point into the mix. + file.setExecutable(whichFile % 2 == 0); + } + assertFalse(base.getRelative("doesnotexist" + whichFile).delete()); + } + } + } + + // 3) Delete some files. + Collection<TestThread> threads = + Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS); + for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) { + TestThread thread = new FileDeleter(); + thread.start(); + threads.add(thread); + } + for (TestThread thread : threads) { + thread.joinAndAssertState(0); + } + + // 4) Check the results. + for (int i = 0; i < NUM_TO_WRITE; i++) { + Path file = base.getRelative("file" + i); + if (i % 25 != 0) { + assertFalse(file.exists()); + } else { + assertTrue(file.exists()); + assertEquals(i % 2 == 0, file.isExecutable()); + } + } + } + + /** + * Tests concurrent file renaming. + */ + @Test + public void testConcurrentRenaming() throws Exception { + final int NUM_TO_WRITE = 10000; + final AtomicInteger baseSelector = new AtomicInteger(); + + final Path base = testFS.getPath("/base"); + base.createDirectory(); + + // 1) Create a bunch of files. + for (int i = 0; i < NUM_TO_WRITE; i++) { + writeToFile(base.getRelative("file" + i), TEST_FILE_DATA); + } + + // 2) Define our renaming strategy. + class FileDeleter extends TestThread { + @Override + public void runTest() throws Exception { + for (int i = 0; i < NUM_TO_WRITE / NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) { + int whichFile = baseSelector.getAndIncrement(); + Path file = base.getRelative("file" + whichFile); + if (whichFile % 25 != 0) { + Path newName = base.getRelative("newname" + whichFile); + file.renameTo(newName); + } else { + // Throw another concurrent access point into the mix. + file.setExecutable(whichFile % 2 == 0); + } + assertFalse(base.getRelative("doesnotexist" + whichFile).delete()); + } + } + } + + // 3) Rename some files. + Collection<TestThread> threads = + Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS); + for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) { + TestThread thread = new FileDeleter(); + thread.start(); + threads.add(thread); + } + for (TestThread thread : threads) { + thread.joinAndAssertState(0); + } + + // 4) Check the results. + for (int i = 0; i < NUM_TO_WRITE; i++) { + Path file = base.getRelative("file" + i); + if (i % 25 != 0) { + assertFalse(file.exists()); + assertTrue(base.getRelative("newname" + i).exists()); + } else { + assertTrue(file.exists()); + assertEquals(i % 2 == 0, file.isExecutable()); + } + } + } + + @Test + public void testEloop() throws Exception { + Path a = testFS.getPath("/a"); + Path b = testFS.getPath("/b"); + a.createSymbolicLink(new PathFragment("b")); + b.createSymbolicLink(new PathFragment("a")); + try { + a.stat(); + } catch (IOException e) { + assertEquals("/a (Too many levels of symbolic links)", e.getMessage()); + } + } + + @Test + public void testEloopSelf() throws Exception { + Path a = testFS.getPath("/a"); + a.createSymbolicLink(new PathFragment("a")); + try { + a.stat(); + } catch (IOException e) { + assertEquals("/a (Too many levels of symbolic links)", e.getMessage()); + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/sample_with_dirs.zip b/src/test/java/com/google/devtools/build/lib/vfs/sample_with_dirs.zip Binary files differnew file mode 100644 index 0000000000..22ff63cd32 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/sample_with_dirs.zip diff --git a/src/test/java/com/google/devtools/build/lib/vfs/sample_without_dirs.zip b/src/test/java/com/google/devtools/build/lib/vfs/sample_without_dirs.zip Binary files differnew file mode 100644 index 0000000000..f3ec5ab792 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/sample_without_dirs.zip diff --git a/src/test/java/com/google/devtools/build/lib/vfs/util/FileSystems.java b/src/test/java/com/google/devtools/build/lib/vfs/util/FileSystems.java new file mode 100644 index 0000000000..6a79a2b948 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/util/FileSystems.java @@ -0,0 +1,93 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs.util; + +import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; +import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.JavaIoFileSystem; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.UnionFileSystem; +import com.google.devtools.build.lib.vfs.UnixFileSystem; +import com.google.devtools.build.lib.vfs.ZipFileSystem; + +import java.io.IOException; +import java.util.Map; + +/** + * This static file system singleton manages access to a single default + * {@link FileSystem} instance created within the methods of this class. + */ +@ThreadSafe +public final class FileSystems { + + private FileSystems() {} + + private static FileSystem defaultFileSystem; + + /** + * Initializes the default {@link FileSystem} instance as a platform native + * (Unix) file system, creating one iff needed, and returns the instance. + * + * <p>This method is idempotent as long as the initialization is of the same + * type (Native/JavaIo/Union). + */ + public static synchronized FileSystem initDefaultAsNative() { + if (!(defaultFileSystem instanceof UnixFileSystem)) { + defaultFileSystem = new UnixFileSystem(); + } + return defaultFileSystem; + } + + /** + * Initializes the default {@link FileSystem} instance as a java.io.File + * file system, creating one iff needed, and returns the instance. + * + * <p>This method is idempotent as long as the initialization is of the same + * type (Native/JavaIo/Union). + */ + public static synchronized FileSystem initDefaultAsJavaIo() { + if (!(defaultFileSystem instanceof JavaIoFileSystem)) { + defaultFileSystem = new JavaIoFileSystem(); + } + return defaultFileSystem; + } + + /** + * Initializes the default {@link FileSystem} instance as a + * {@link UnionFileSystem}, creating one iff needed, + * and returns the instance. + * + * <p>This method is idempotent as long as the initialization is of the same + * type (Native/JavaIo/Union). + * + * @param prefixMapping the desired mapping of path prefixes to delegate file systems + * @param rootFileSystem the default file system for paths that don't match any prefix map + */ + public static synchronized FileSystem initDefaultAsUnion( + Map<PathFragment, FileSystem> prefixMapping, FileSystem rootFileSystem) { + if (!(defaultFileSystem instanceof UnionFileSystem)) { + defaultFileSystem = new UnionFileSystem(prefixMapping, rootFileSystem); + } + return defaultFileSystem; + } + + /** + * Returns a new instance of a simple {@link FileSystem} implementation that + * presents the contents of a zip file as a read-only file system view. + */ + public static FileSystem newZipFileSystem(Path zipFile) throws IOException { + return new ZipFileSystem(zipFile); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java b/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java new file mode 100644 index 0000000000..5d93351a8d --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java @@ -0,0 +1,158 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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.google.devtools.build.lib.vfs.util; + +import com.google.devtools.build.lib.testutil.TestUtils; +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.util.StringUtilities; +import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; + +import junit.framework.AssertionFailedError; + +import java.io.File; +import java.io.IOException; + +/** + * Base class for a testing apparatus for a scratch filesystem. + */ +public class FsApparatus { + + /* ---------- State that the apparatus initializes / operates on --------- */ + protected FileSystem fileSystem = null; + protected Path workingDir = null; + + public static FsApparatus newInMemory() { + return new FsApparatus(); + } + + // TestUtil.getTmpDir is slow, so cache the result here + private static final String TMP_DIR = + new File(TestUtils.tmpDir(), "bs").toString(); + + + /** + * When using a Native file system, absolute paths will be treated as absolute paths on the unix + * file system, as opposed to paths relative to the backing temp directory. So for simplicity, + * you ought to only use relative paths for FsApparatus#file, FsApparatus#dir, and + * FsApparatus#path. Otherwise, be aware of the following issue + * + * Path p1 = scratch.path(...); + * Path p2 = scratch.path(p1.getPathString()); + * + * We'd like the invariant that p1.equals(p2) regardless if scratch is in-memory or not, but this + * does not hold with our usage of Unix filesystems. + */ + public static FsApparatus newNative() { + FileSystem fs = FileSystems.initDefaultAsNative(); + Path wd = fs.getPath(TMP_DIR); + + try { + FileSystemUtils.deleteTree(wd); + } catch (IOException e) { + throw new AssertionFailedError(e.getMessage()); + } + + return new FsApparatus(fs, wd); + } + + private FsApparatus() { + fileSystem = new InMemoryFileSystem(BlazeClock.instance()); + workingDir = fileSystem.getPath("/"); + } + + public FsApparatus(FileSystem fs, Path cwd) { + fileSystem = fs; + workingDir = cwd; + } + + public FsApparatus(FileSystem fs) { + fileSystem = fs; + workingDir = fs.getPath("/"); + } + + public FileSystem fs() { + return fileSystem; + } + + /** + * Initializes this apparatus (if it hasn't been initialized yet), and creates + * a scratch file in the scratch filesystem with the given {@code pathName} + * with {@code lines} being its content. The method returns a Path instance + * for the scratch file. + */ + public Path file(String pathName, String... lines) throws IOException { + Path file = path(pathName); + Path parentDir = file.getParentDirectory(); + if (!parentDir.exists()) { + FileSystemUtils.createDirectoryAndParents(parentDir); + } + if (file.exists()) { + throw new IOException("Could not create scratch file (file exists) " + + file); + } + String fileContent = StringUtilities.joinLines(lines); + FileSystemUtils.writeContentAsLatin1(file, fileContent); + return file; + } + + /** + * Initializes this apparatus (if it hasn't been initialized yet), and creates + * a directory in the scratch filesystem, with the given {@code pathName}. + * Creates parent directories as necessary. + */ + public Path dir(String pathName) throws IOException { + Path dir = path(pathName); + if (!dir.exists()) { + FileSystemUtils.createDirectoryAndParents(dir); + } + if (!dir.isDirectory()) { + throw new IOException("Exists, but is not a directory: " + dir); + } + return dir; + } + + /** + * Initializes this apparatus (if it hasn't been initialized yet), and returns + * a path object describing a file, directory, or symlink pointed at by + * {@code pathName}. Note that this will not create any entity in the + * filesystem; i.e., the file that the object is describing may not exist in + * the filesystem. + */ + public Path path(String pathName) { + return workingDir.getRelative(pathName); + } + + /** + * Create a fresh directory in the system temporary directory, instead of the + * testing directory provided by the testing framework. This path is usually + * shorter than a path starting with TestUtil.getTmpDir(). We care about the + * length because of the path length restriction for Unix local socket files. + * + * Clients are responsible for deleting the directory after tests. + */ + public Path createUnixTempDir() throws IOException { + if (fileSystem instanceof InMemoryFileSystem) { + throw new IOException("Can not create Unix temporary directories in " + + "an in-memory file system"); + } + File file = File.createTempFile("scratch", "tmp"); + final Path path = fileSystem.getPath(file.getAbsolutePath()); + path.delete(); + path.createDirectory(); + return path; + } +} |