blob: d816ba7bdf786d209d4f58ec361a6641e9234189 [file] [log] [blame]
// 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 java.nio.charset.StandardCharsets.ISO_8859_1;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeTrue;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.testutil.TestSpec;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileSystem.NotASymlinkException;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.SymlinkTargetType;
import com.google.devtools.build.lib.vfs.Symlinks;
import com.google.devtools.build.lib.windows.util.WindowsTestUtil;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link WindowsFileSystem}. */
@RunWith(TestParameterInjector.class)
@TestSpec(supportedOs = OS.WINDOWS)
public class WindowsFileSystemTest {
@TestParameter boolean createSymbolicLinks;
private WindowsFileSystem fs;
private Path scratchRoot;
private WindowsTestUtil testUtil;
@Before
public void createScratchDir() throws Exception {
fs = new WindowsFileSystem(DigestHashFunction.SHA256, createSymbolicLinks);
scratchRoot = TestUtils.createUniqueTmpDir(fs);
testUtil = new WindowsTestUtil(scratchRoot.getPathString());
}
@After
public void destroyScratchDir() throws Exception {
scratchRoot.deleteTree();
}
@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.asFragment(), /* followSymlinks */ true)).isFalse();
assertThat(fs.isDirectory(juncBadPath.asFragment(), /* 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.isSymlinkOrJunction(new File(root, "shrtpath/a"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "shrtpath/b"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "shrtpath/c"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "longlinkpath/a"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "longlinkpath/b"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "longlinkpath/c"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "longli~1/a"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "longli~1/b"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "longli~1/c"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "abbreviated/a"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "abbreviated/b"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "abbreviated/c"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "abbrev~1/a"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "abbrev~1/b"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "abbrev~1/c"))).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "control/a"))).isFalse();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "control/b"))).isFalse();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "control/c"))).isFalse();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "shrttrgt/file1.txt")))
.isFalse();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "longtargetpath/file2.txt")))
.isFalse();
assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(root, "longta~1/file2.txt")))
.isFalse();
assertThrows(
FileNotFoundException.class,
() -> WindowsFileSystem.isSymlinkOrJunction(new File(root, "non-existent")));
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.isSymlinkOrJunction(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.isSymlinkOrJunction(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.isSymlinkOrJunction(longPath)).isFalse();
assertThat(WindowsFileSystem.isSymlinkOrJunction(shortPath)).isFalse();
assertThat(longPath.delete()).isTrue();
testUtil.createJunctions(ImmutableMap.of("target\\helloworld.txt", "target"));
assertThat(WindowsFileSystem.isSymlinkOrJunction(longPath)).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(shortPath)).isTrue();
assertThat(longPath.delete()).isTrue();
assertThat(longPath.mkdir()).isTrue();
assertThat(WindowsFileSystem.isSymlinkOrJunction(longPath)).isFalse();
assertThat(WindowsFileSystem.isSymlinkOrJunction(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 = scratchRoot.getRelative(shortPath);
assertThat(p.getPathString()).endsWith(longPath);
assertThat(p).isEqualTo(scratchRoot.getRelative(shortPath));
assertThat(p).isEqualTo(scratchRoot.getRelative(longPath));
assertThat(scratchRoot.getRelative(shortPath)).isEqualTo(p);
assertThat(scratchRoot.getRelative(longPath)).isEqualTo(p);
}
@Test
public void testUnresolvableShortPathWhichIsThenCreated() throws Exception {
String shortPath = "unreso~1.sho/foo/will~1.exi/bar/hello.txt";
String longPath = "unresolvable.shortpath/foo/will.exist/bar/hello.txt";
// Assert that we can create an unresolvable path.
Path p = scratchRoot.getRelative(shortPath);
assertThat(p.getPathString()).endsWith(shortPath);
// Assert that we can then create the whole path, and can now resolve the short form.
testUtil.scratchFile(longPath, "hello");
Path q = scratchRoot.getRelative(shortPath);
assertThat(q.getPathString()).endsWith(longPath);
assertThat(p).isNotEqualTo(q);
}
/**
* 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 = scratchRoot.getRelative("longpa~1");
Path p2 = scratchRoot.getRelative("longpa~1");
assertThat(p1.exists()).isFalse();
assertThat(p1).isEqualTo(p2);
testUtil.scratchDir("longpathnow");
Path q1 = scratchRoot.getRelative("longpa~1");
assertThat(q1.exists()).isTrue();
assertThat(q1).isEqualTo(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 = scratchRoot.getRelative("longpa~1");
assertThat(r1.exists()).isTrue();
assertThat(r1).isEqualTo(scratchRoot.getRelative("longpaththen"));
}
@Test
public void testSymbolicLinkToExistingFile(@TestParameter SymlinkTargetType targetType)
throws Exception {
Path linkPath = scratchRoot.getRelative("link");
Path targetPath = scratchRoot.getRelative("target");
assertThat(targetPath.getParentDirectory().exists()).isTrue();
assertThat(targetPath.getParentDirectory().isDirectory()).isTrue();
FileSystemUtils.writeContentAsLatin1(targetPath, "hello");
linkPath.createSymbolicLink(targetPath, targetType);
if (createSymbolicLinks) {
assertThat(linkPath.isSymbolicLink()).isTrue();
assertThat(linkPath.readSymbolicLink()).isEqualTo(targetPath.asFragment());
} else {
assertThat(linkPath.isSymbolicLink()).isFalse();
assertThrows(NotASymlinkException.class, () -> linkPath.readSymbolicLink());
}
assertThat(linkPath.exists()).isTrue();
assertThat(linkPath.isFile()).isTrue();
assertThat(FileSystemUtils.readContent(linkPath, ISO_8859_1)).isEqualTo("hello");
linkPath.delete();
assertThat(linkPath.exists()).isFalse();
assertThat(targetPath.exists()).isTrue();
}
@Test
public void testSymbolicLinkToExistingDirectory(@TestParameter SymlinkTargetType targetType)
throws Exception {
Path linkPath = scratchRoot.getRelative("link");
Path linkChildPath = linkPath.getRelative("hello.txt");
Path targetPath = scratchRoot.getRelative("target");
Path targetChildPath = targetPath.getRelative("hello.txt");
targetPath.createDirectory();
FileSystemUtils.writeContentAsLatin1(targetChildPath, "hello");
linkPath.createSymbolicLink(targetPath, targetType);
assertThat(linkPath.isSymbolicLink()).isTrue();
assertThat(linkPath.readSymbolicLink()).isEqualTo(targetPath.asFragment());
assertThat(linkPath.exists()).isTrue();
assertThat(linkPath.isDirectory()).isTrue();
assertThat(linkChildPath.exists()).isTrue();
assertThat(linkChildPath.isFile()).isTrue();
assertThat(FileSystemUtils.readContent(linkChildPath, ISO_8859_1)).isEqualTo("hello");
linkPath.delete();
assertThat(linkPath.exists()).isFalse();
assertThat(targetPath.exists()).isTrue();
}
@Test
public void testCreateSymbolicLinkToNonExistingTargetOfUnspecifiedType() throws Exception {
Path linkPath = scratchRoot.getRelative("link");
Path targetPath = scratchRoot.getRelative("target");
linkPath.createSymbolicLink(targetPath, SymlinkTargetType.UNSPECIFIED);
assertThat(linkPath.isSymbolicLink()).isTrue();
assertThat(linkPath.readSymbolicLink()).isEqualTo(targetPath.asFragment());
assertThat(linkPath.exists()).isFalse();
// Check that a dangling symlink is preferred over a dangling junction when supported.
// Do this by creating a target of the corresponding type and verifying that it can be accessed.
if (createSymbolicLinks) {
FileSystemUtils.writeContentAsLatin1(targetPath, "hello");
assertThat(linkPath.exists()).isTrue();
assertThat(linkPath.isFile()).isTrue();
assertThat(FileSystemUtils.readContent(linkPath, ISO_8859_1)).isEqualTo("hello");
} else {
targetPath.createDirectory();
assertThat(linkPath.exists()).isTrue();
assertThat(linkPath.isDirectory()).isTrue();
}
}
@Test
public void testCreateSymbolicLinkToNonExistingTargetOfFileType() throws Exception {
// This is only expected to work if symlinks are enabled.
// Otherwise, our only recourse is to create a dangling junction, which does not work for files.
assumeTrue(createSymbolicLinks);
Path linkPath = scratchRoot.getRelative("link");
Path targetPath = scratchRoot.getRelative("target");
linkPath.createSymbolicLink(targetPath, SymlinkTargetType.FILE);
assertThat(linkPath.isSymbolicLink()).isTrue();
assertThat(linkPath.readSymbolicLink()).isEqualTo(targetPath.asFragment());
FileSystemUtils.writeContentAsLatin1(targetPath, "hello");
assertThat(linkPath.exists()).isTrue();
assertThat(linkPath.isFile()).isTrue();
assertThat(FileSystemUtils.readContent(linkPath, ISO_8859_1)).isEqualTo("hello");
}
@Test
public void testCreateSymbolicLinkToNonExistingTargetOfDirectoryType() throws Exception {
Path linkPath = scratchRoot.getRelative("link");
Path linkChildPath = linkPath.getRelative("hello.txt");
Path targetPath = scratchRoot.getRelative("target");
Path targetChildPath = targetPath.getRelative("hello.txt");
linkPath.createSymbolicLink(targetPath, SymlinkTargetType.DIRECTORY);
assertThat(linkPath.isSymbolicLink()).isTrue();
assertThat(linkPath.readSymbolicLink()).isEqualTo(targetPath.asFragment());
targetPath.createDirectory();
FileSystemUtils.writeContentAsLatin1(targetChildPath, "hello");
assertThat(linkPath.exists()).isTrue();
assertThat(linkPath.isDirectory()).isTrue();
assertThat(linkChildPath.exists()).isTrue();
assertThat(linkChildPath.isFile()).isTrue();
assertThat(FileSystemUtils.readContent(linkChildPath, ISO_8859_1)).isEqualTo("hello");
}
@Test
public void testReadSymbolicLinkForFile() throws Exception {
Path filePath = scratchRoot.getRelative("file");
FileSystemUtils.writeContentAsLatin1(filePath, "hello");
assertThrows(NotASymlinkException.class, filePath::readSymbolicLink);
}
@Test
public void testReadSymbolicLinkForDirectory() throws Exception {
Path dirPath = scratchRoot.getRelative("dir");
dirPath.createDirectory();
assertThrows(NotASymlinkException.class, dirPath::readSymbolicLink);
}
@Test
public void testReadSymbolicLinkForNonexistentPath() throws Exception {
Path nonexistentPath = scratchRoot.getRelative("nonexistent");
assertThrows(FileNotFoundException.class, nonexistentPath::readSymbolicLink);
}
@Test
public void testReadOnlyAttribute() throws Exception {
testUtil.scratchFile("dir\\hello.txt", "hello");
testUtil.createJunctions(ImmutableMap.of("junc", "dir"));
Path dir = testUtil.createVfsPath(fs, "dir");
Path file = testUtil.createVfsPath(fs, "dir\\hello.txt");
Path dirViaJunction = testUtil.createVfsPath(fs, "junc");
Path fileViaJunction = testUtil.createVfsPath(fs, "junc\\hello.txt");
assertWritable(dir);
dir.setWritable(false); // no-op
assertWritable(dir);
dir.setWritable(true); // no-op
assertWritable(dir);
assertWritable(dirViaJunction);
dirViaJunction.setWritable(false); // no-op
assertWritable(dirViaJunction);
dirViaJunction.setWritable(true); // no-op
assertWritable(dirViaJunction);
assertWritable(file);
file.setWritable(false);
assertNotWritable(file);
file.setWritable(true);
assertWritable(file);
assertThat(fileViaJunction.isWritable()).isTrue();
fileViaJunction.setWritable(false);
assertNotWritable(fileViaJunction);
fileViaJunction.setWritable(true);
assertWritable(fileViaJunction);
}
private static void assertWritable(Path path) throws Exception {
assertThat(path.isWritable()).isTrue();
assertThat(path.stat().getPermissions()).isEqualTo(0755);
}
private static void assertNotWritable(Path path) throws Exception {
assertThat(path.isWritable()).isFalse();
assertThat(path.stat().getPermissions()).isEqualTo(0555);
}
}