From d08b27fa9701fecfdb69e1b0d1ac2459efc2129b Mon Sep 17 00:00:00 2001 From: Han-Wen Nienhuys Date: Wed, 25 Feb 2015 16:45:20 +0100 Subject: Update from Google. -- MOE_MIGRATED_REVID=85702957 --- .../build/lib/vfs/UnionFileSystemTest.java | 330 +++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java (limited to 'src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java') 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.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.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.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.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.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.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; + } + } +} -- cgit v1.2.3