// Copyright 2016 The Bazel Authors. 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.windows; import static com.google.common.truth.Truth.assertThat; import static com.google.devtools.build.lib.windows.WindowsFileSystem.SHORT_NAME_MATCHER; import static org.junit.Assert.assertSame; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.testutil.TestSpec; import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.lib.vfs.Symlinks; import com.google.devtools.build.lib.windows.WindowsFileSystem.WindowsPath; import com.google.devtools.build.lib.windows.util.WindowsTestUtil; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link WindowsFileSystem}. */ @RunWith(JUnit4.class) @TestSpec(localOnly = true, supportedOs = OS.WINDOWS) public class WindowsFileSystemTest { private String scratchRoot; private WindowsTestUtil testUtil; private WindowsFileSystem fs; @Before public void loadJni() throws Exception { WindowsTestUtil.loadJni(); scratchRoot = new File(System.getenv("TEST_TMPDIR")).getAbsolutePath() + "/x"; testUtil = new WindowsTestUtil(scratchRoot); fs = new WindowsFileSystem(); cleanupScratchDir(); } @After public void cleanupScratchDir() throws Exception { testUtil.deleteAllUnder(""); } @Test public void testShortNameMatcher() { assertThat(SHORT_NAME_MATCHER.apply("abc")).isFalse(); // no ~ in the name assertThat(SHORT_NAME_MATCHER.apply("abc~")).isFalse(); // no number after the ~ assertThat(SHORT_NAME_MATCHER.apply("~abc")).isFalse(); // no ~ followed by number assertThat(SHORT_NAME_MATCHER.apply("too_long_path")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("too_long_path~1")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("abcd~1234")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("h~1")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("h~12")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("h~12.")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("h~12.a")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("h~12.abc")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("h~123456")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("hellow~1")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("hellow~1.")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("hellow~1.a")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("hellow~1.abc")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("hello~1.abcd")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("hellow~1.abcd")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("hello~12")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("hello~12.")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("hello~12.a")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("hello~12.abc")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("hello~12.abcd")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("hellow~12")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("hellow~12.")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("hellow~12.a")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("hellow~12.ab")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("~h~1")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("~h~1.")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("~h~1.a")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("~h~1.abc")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("~h~1.abcd")).isFalse(); // too long for 8dot3 assertThat(SHORT_NAME_MATCHER.apply("~h~12")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("~h~12~1")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("~h~12~1.")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("~h~12~1.a")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("~h~12~1.abc")).isTrue(); assertThat(SHORT_NAME_MATCHER.apply("~h~12~1.abcd")).isFalse(); // too long for 8dot3 } @Test public void testCanWorkWithJunctionSymlinks() throws Exception { testUtil.scratchFile("dir\\hello.txt", "hello"); testUtil.scratchDir("non_existent"); testUtil.createJunctions(ImmutableMap.of("junc", "dir", "junc_bad", "non_existent")); Path juncPath = testUtil.createVfsPath(fs, "junc"); Path dirPath = testUtil.createVfsPath(fs, "dir"); Path juncBadPath = testUtil.createVfsPath(fs, "junc_bad"); Path nonExistentPath = testUtil.createVfsPath(fs, "non_existent"); // Test junction creation. assertThat(juncPath.exists(Symlinks.NOFOLLOW)).isTrue(); assertThat(dirPath.exists(Symlinks.NOFOLLOW)).isTrue(); assertThat(juncBadPath.exists(Symlinks.NOFOLLOW)).isTrue(); assertThat(nonExistentPath.exists(Symlinks.NOFOLLOW)).isTrue(); // Test recognizing and dereferencing a directory junction. assertThat(juncPath.isSymbolicLink()).isTrue(); assertThat(juncPath.isDirectory(Symlinks.FOLLOW)).isTrue(); assertThat(juncPath.isDirectory(Symlinks.NOFOLLOW)).isFalse(); assertThat(juncPath.getDirectoryEntries()) .containsExactly(testUtil.createVfsPath(fs, "junc\\hello.txt")); // Test deleting a directory junction. assertThat(juncPath.delete()).isTrue(); assertThat(juncPath.exists(Symlinks.NOFOLLOW)).isFalse(); // Test recognizing a dangling directory junction. assertThat(nonExistentPath.delete()).isTrue(); assertThat(nonExistentPath.exists(Symlinks.NOFOLLOW)).isFalse(); assertThat(juncBadPath.exists(Symlinks.NOFOLLOW)).isTrue(); // TODO(bazel-team): fix https://github.com/bazelbuild/bazel/issues/1690 and uncomment the // assertion below. //assertThat(fs.isSymbolicLink(juncBadPath)).isTrue(); assertThat(fs.isDirectory(juncBadPath, /* followSymlinks */ true)).isFalse(); assertThat(fs.isDirectory(juncBadPath, /* followSymlinks */ false)).isFalse(); // Test deleting a dangling junction. assertThat(juncBadPath.delete()).isTrue(); assertThat(juncBadPath.exists(Symlinks.NOFOLLOW)).isFalse(); } @Test public void testMockJunctionCreation() throws Exception { String root = testUtil.scratchDir("dir").getParent().toString(); testUtil.scratchFile("dir/file.txt", "hello"); testUtil.createJunctions(ImmutableMap.of("junc", "dir")); String[] children = new File(root + "/junc").list(); assertThat(children).isNotNull(); assertThat(children).hasLength(1); assertThat(Arrays.asList(children)).containsExactly("file.txt"); } @Test public void testIsJunction() throws Exception { final Map<String, String> junctions = new HashMap<>(); junctions.put("shrtpath/a", "shrttrgt"); junctions.put("shrtpath/b", "longtargetpath"); junctions.put("shrtpath/c", "longta~1"); junctions.put("longlinkpath/a", "shrttrgt"); junctions.put("longlinkpath/b", "longtargetpath"); junctions.put("longlinkpath/c", "longta~1"); junctions.put("abbrev~1/a", "shrttrgt"); junctions.put("abbrev~1/b", "longtargetpath"); junctions.put("abbrev~1/c", "longta~1"); String root = testUtil.scratchDir("shrtpath").getParent().toAbsolutePath().toString(); testUtil.scratchDir("longlinkpath"); testUtil.scratchDir("abbreviated"); testUtil.scratchDir("control/a"); testUtil.scratchDir("control/b"); testUtil.scratchDir("control/c"); testUtil.scratchFile("shrttrgt/file1.txt", "hello"); testUtil.scratchFile("longtargetpath/file2.txt", "hello"); testUtil.createJunctions(junctions); assertThat(WindowsFileSystem.isJunction(new File(root, "shrtpath/a"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "shrtpath/b"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "shrtpath/c"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "longlinkpath/a"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "longlinkpath/b"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "longlinkpath/c"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "longli~1/a"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "longli~1/b"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "longli~1/c"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "abbreviated/a"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "abbreviated/b"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "abbreviated/c"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "abbrev~1/a"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "abbrev~1/b"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "abbrev~1/c"))).isTrue(); assertThat(WindowsFileSystem.isJunction(new File(root, "control/a"))).isFalse(); assertThat(WindowsFileSystem.isJunction(new File(root, "control/b"))).isFalse(); assertThat(WindowsFileSystem.isJunction(new File(root, "control/c"))).isFalse(); assertThat(WindowsFileSystem.isJunction(new File(root, "shrttrgt/file1.txt"))) .isFalse(); assertThat(WindowsFileSystem.isJunction(new File(root, "longtargetpath/file2.txt"))) .isFalse(); assertThat(WindowsFileSystem.isJunction(new File(root, "longta~1/file2.txt"))) .isFalse(); try { WindowsFileSystem.isJunction(new File(root, "non-existent")); Assert.fail("expected failure"); } catch (IOException e) { assertThat(e.getMessage()).contains("cannot find"); } assertThat(Arrays.asList(new File(root + "/shrtpath/a").list())).containsExactly("file1.txt"); assertThat(Arrays.asList(new File(root + "/shrtpath/b").list())).containsExactly("file2.txt"); assertThat(Arrays.asList(new File(root + "/shrtpath/c").list())).containsExactly("file2.txt"); assertThat(Arrays.asList(new File(root + "/longlinkpath/a").list())) .containsExactly("file1.txt"); assertThat(Arrays.asList(new File(root + "/longlinkpath/b").list())) .containsExactly("file2.txt"); assertThat(Arrays.asList(new File(root + "/longlinkpath/c").list())) .containsExactly("file2.txt"); assertThat(Arrays.asList(new File(root + "/abbreviated/a").list())) .containsExactly("file1.txt"); assertThat(Arrays.asList(new File(root + "/abbreviated/b").list())) .containsExactly("file2.txt"); assertThat(Arrays.asList(new File(root + "/abbreviated/c").list())) .containsExactly("file2.txt"); } @Test public void testIsJunctionIsTrueForDanglingJunction() throws Exception { java.nio.file.Path helloPath = testUtil.scratchFile("target\\hello.txt", "hello"); testUtil.createJunctions(ImmutableMap.of("link", "target")); File linkPath = new File(helloPath.getParent().getParent().toFile(), "link"); assertThat(Arrays.asList(linkPath.list())).containsExactly("hello.txt"); assertThat(WindowsFileSystem.isJunction(linkPath)).isTrue(); assertThat(helloPath.toFile().delete()).isTrue(); assertThat(helloPath.getParent().toFile().delete()).isTrue(); assertThat(helloPath.getParent().toFile().exists()).isFalse(); assertThat(Arrays.asList(linkPath.getParentFile().list())).containsExactly("link"); assertThat(WindowsFileSystem.isJunction(linkPath)).isTrue(); assertThat( Files.exists( linkPath.toPath(), WindowsFileSystem.symlinkOpts(/* followSymlinks */ false))) .isTrue(); assertThat( Files.exists( linkPath.toPath(), WindowsFileSystem.symlinkOpts(/* followSymlinks */ true))) .isFalse(); } @Test public void testIsJunctionHandlesFilesystemChangesCorrectly() throws Exception { File longPath = testUtil.scratchFile("target\\helloworld.txt", "hello").toAbsolutePath().toFile(); File shortPath = new File(longPath.getParentFile(), "hellow~1.txt"); assertThat(WindowsFileSystem.isJunction(longPath)).isFalse(); assertThat(WindowsFileSystem.isJunction(shortPath)).isFalse(); assertThat(longPath.delete()).isTrue(); testUtil.createJunctions(ImmutableMap.of("target\\helloworld.txt", "target")); assertThat(WindowsFileSystem.isJunction(longPath)).isTrue(); assertThat(WindowsFileSystem.isJunction(shortPath)).isTrue(); assertThat(longPath.delete()).isTrue(); assertThat(longPath.mkdir()).isTrue(); assertThat(WindowsFileSystem.isJunction(longPath)).isFalse(); assertThat(WindowsFileSystem.isJunction(shortPath)).isFalse(); } @Test public void testShortPathResolution() throws Exception { String shortPath = "shortp~1.res/foo/withsp~1/bar/~witht~1/hello.txt"; String longPath = "shortpath.resolution/foo/with spaces/bar/~with tilde/hello.txt"; testUtil.scratchFile(longPath, "hello"); Path p = fs.getPath(scratchRoot).getRelative(shortPath); assertThat(p.getPathString()).endsWith(longPath); assertThat(p).isEqualTo(fs.getPath(scratchRoot).getRelative(shortPath)); assertThat(p).isEqualTo(fs.getPath(scratchRoot).getRelative(longPath)); assertSame(p, fs.getPath(scratchRoot).getRelative(shortPath)); assertSame(p, fs.getPath(scratchRoot).getRelative(longPath)); } @Test public void testUnresolvableShortPathWhichIsThenCreated() throws Exception { String shortPath = "unreso~1.sho/foo/will~1.exi/bar/hello.txt"; String longPrefix = "unresolvable.shortpath/foo/"; String longPath = longPrefix + "will.exist/bar/hello.txt"; testUtil.scratchDir(longPrefix); final WindowsPath foo = (WindowsPath) fs.getPath(scratchRoot).getRelative(longPrefix); // Assert that we can create an unresolvable path. Path p = fs.getPath(scratchRoot).getRelative(shortPath); assertThat(p.getPathString()).endsWith(longPrefix + "will~1.exi/bar/hello.txt"); // Assert that said path is not cached in its parent's `children` cache. final List<String> children = new ArrayList<>(); Predicate<Path> collector = new Predicate<Path>() { @Override public boolean apply(Path child) { children.add(child.relativeTo(foo).getPathString()); return true; } }; foo.applyToChildren(collector); assertThat(children).isEmpty(); // Assert that we can then create the whole path, and can now resolve the short form. testUtil.scratchFile(longPath, "hello"); Path q = fs.getPath(scratchRoot).getRelative(shortPath); assertThat(q.getPathString()).endsWith(longPath); // Assert however that the unresolved and resolved Path objects are different, and only the // resolved one is cached. assertThat(p).isNotEqualTo(q); foo.applyToChildren(collector); assertThat(children).containsExactly("will.exist"); } /** * Test the scenario when a short path resolves to different long ones over time. * * <p>This can happen if the user deletes a directory during the bazel server's lifetime, then * recreates it with the same name prefix such that the resulting directory's 8dot3 name is the * same as the old one's. */ @Test public void testShortPathResolvesToDifferentPathsOverTime() throws Exception { Path p1 = fs.getPath(scratchRoot).getRelative("longpa~1"); Path p2 = fs.getPath(scratchRoot).getRelative("longpa~1"); assertThat(p1.exists()).isFalse(); assertThat(p1).isEqualTo(p2); assertThat(p1).isNotSameAs(p2); testUtil.scratchDir("longpathnow"); Path q1 = fs.getPath(scratchRoot).getRelative("longpa~1"); Path q2 = fs.getPath(scratchRoot).getRelative("longpa~1"); assertThat(q1.exists()).isTrue(); assertThat(q1).isEqualTo(q2); // Assert q1 == q2, because we could successfully resolve the short path to a long name and we // cache them by the long name, so it's irrelevant they were created from a 8dot3 name, or what // that name resolves to later in time. assertThat(q1).isSameAs(q2); assertThat(q1).isSameAs(fs.getPath(scratchRoot).getRelative("longpathnow")); // Delete the original resolution of "longpa~1" ("longpathnow"). assertThat(q1.delete()).isTrue(); assertThat(q1.exists()).isFalse(); // Create a directory whose 8dot3 name is also "longpa~1" but its long name is different. testUtil.scratchDir("longpaththen"); Path r1 = fs.getPath(scratchRoot).getRelative("longpa~1"); Path r2 = fs.getPath(scratchRoot).getRelative("longpa~1"); assertThat(r1.exists()).isTrue(); assertThat(r1).isEqualTo(r2); assertThat(r1).isSameAs(r2); assertThat(r1).isSameAs(fs.getPath(scratchRoot).getRelative("longpaththen")); // r1 == r2 and q1 == q2, but r1 != q1, because the resolution of "longpa~1" changed over time. assertThat(r1).isNotEqualTo(q1); assertThat(r1).isNotSameAs(q1); } @Test public void testCreateSymbolicLink() throws Exception { // Create the `scratchRoot` directory. assertThat(fs.getPath(scratchRoot).createDirectory()).isTrue(); // Create symlink with directory target, relative path. Path link1 = fs.getPath(scratchRoot).getRelative("link1"); fs.createSymbolicLink(link1, PathFragment.create("..")); // Create symlink with directory target, absolute path. Path link2 = fs.getPath(scratchRoot).getRelative("link2"); fs.createSymbolicLink(link2, fs.getPath(scratchRoot).getRelative("link1").asFragment()); // Create scratch files that'll be symlink targets. testUtil.scratchFile("foo.txt", "hello"); testUtil.scratchFile("bar.txt", "hello"); // Create symlink with file target, relative path. Path link3 = fs.getPath(scratchRoot).getRelative("link3"); fs.createSymbolicLink(link3, PathFragment.create("foo.txt")); // Create symlink with file target, absolute path. Path link4 = fs.getPath(scratchRoot).getRelative("link4"); fs.createSymbolicLink(link4, fs.getPath(scratchRoot).getRelative("bar.txt").asFragment()); // Assert that link1 and link2 are true junctions and have the right contents. for (Path p : ImmutableList.of(link1, link2)) { assertThat(WindowsFileOperations.isJunction(p.getPathString())).isTrue(); assertThat(p.isSymbolicLink()).isTrue(); assertThat( Iterables.transform( Arrays.asList(new File(p.getPathString()).listFiles()), new Function<File, String>() { @Override public String apply(File input) { return input.getName(); } })) .containsExactly("x"); } // Assert that link3 and link4 are copies of files. for (Path p : ImmutableList.of(link3, link4)) { assertThat(WindowsFileOperations.isJunction(p.getPathString())).isFalse(); assertThat(p.isSymbolicLink()).isFalse(); assertThat(p.isFile()).isTrue(); } } }