blob: 1303b4b883ab88918d1e4fd6d4393b33073de28b [file] [log] [blame]
// Copyright 2019 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.vfs;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.clock.BlazeClock;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests glob() on Windows (when glob is case-insensitive). */
@RunWith(JUnit4.class)
public class WindowsGlobTest {
private static void assertMatches(
UnixGlob.FilesystemCalls fsCalls, Path root, String pattern, String... expecteds)
throws IOException {
AtomicReference<UnixGlob.FilesystemCalls> sysCalls = new AtomicReference<>(fsCalls);
assertThat(
Iterables.transform(
new UnixGlob.Builder(root)
.addPattern(pattern)
.setFilesystemCalls(sysCalls)
.setExcludeDirectories(true)
.glob(),
// Convert Paths to Strings, otherwise they'd be compared with the host system's
// Path
// comparison semantics.
a -> a.relativeTo(root).toString()))
.containsExactlyElementsIn(expecteds);
}
private static void assertExcludes(
Collection<String> unfiltered,
String exclusionPattern,
boolean caseSensitive,
Collection<String> expected) {
Set<String> matched = new HashSet<>(unfiltered);
UnixGlob.removeExcludes(matched, ImmutableList.of(exclusionPattern), caseSensitive);
assertThat(matched).containsExactlyElementsIn(expected);
}
@Test
public void testMatches() throws Exception {
assertThat(UnixGlob.matches("Foo/**", "Foo/Bar/a.txt", null, true)).isTrue();
assertThat(UnixGlob.matches("Foo/**", "Foo/Bar/a.txt", null, false)).isTrue();
assertThat(UnixGlob.matches("foo/**", "Foo/Bar/a.txt", null, true)).isFalse();
assertThat(UnixGlob.matches("foo/**", "Foo/Bar/a.txt", null, false)).isTrue();
assertThat(UnixGlob.matches("F*o*o/**", "Foo/Bar/a.txt", null, true)).isTrue();
assertThat(UnixGlob.matches("F*o*o/**", "Foo/Bar/a.txt", null, false)).isTrue();
assertThat(UnixGlob.matches("f*o*o/**", "Foo/Bar/a.txt", null, true)).isFalse();
assertThat(UnixGlob.matches("f*o*o/**", "Foo/Bar/a.txt", null, false)).isTrue();
assertThat(UnixGlob.matches("Foo/**", "Foo/Bar/a.txt", null, true)).isTrue();
assertThat(UnixGlob.matches("Foo/**", "Foo/Bar/a.txt", null, false)).isTrue();
}
@Test
public void testExcludes() throws Exception {
assertExcludes(
Arrays.asList("Foo/Bar/a.txt", "Foo/Bar/b.dat"),
"Foo/**/*.dat",
true,
Arrays.asList("Foo/Bar/a.txt"));
assertExcludes(
Arrays.asList("Foo/Bar/a.txt", "Foo/Bar/b.dat"),
"Foo/**/*.dat",
false,
Arrays.asList("Foo/Bar/a.txt"));
assertExcludes(
Arrays.asList("Foo/Bar/a.txt", "Foo/Bar/b.dat"),
"foo/**/*.dat",
true,
Arrays.asList("Foo/Bar/a.txt", "Foo/Bar/b.dat"));
assertExcludes(
Arrays.asList("Foo/Bar/a.txt", "Foo/Bar/b.dat"),
"foo/**/*.dat",
false,
Arrays.asList("Foo/Bar/a.txt"));
}
private enum MockStat implements FileStatus {
FILE(true, false),
DIR(false, true),
UNKNOWN(false, false);
private final boolean isFile;
private final boolean isDir;
private MockStat(boolean isFile, boolean isDir) {
this.isFile = isFile;
this.isDir = isDir;
}
@Override
public boolean isFile() {
return isFile;
}
@Override
public boolean isDirectory() {
return isDir;
}
@Override
public boolean isSymbolicLink() {
return false;
}
@Override
public boolean isSpecialFile() {
return false;
}
@Override
public long getSize() throws IOException {
return 0;
}
@Override
public long getLastModifiedTime() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public long getLastChangeTime() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public long getNodeId() throws IOException {
throw new UnsupportedOperationException();
}
}
private static FileSystem mockFs(boolean caseSensitive) throws IOException {
FileSystem fs =
new InMemoryFileSystem(BlazeClock.instance()) {
@Override
public boolean isFilePathCaseSensitive() {
return caseSensitive;
}
@Override
public boolean isGlobCaseSensitive() {
return isFilePathCaseSensitive();
}
};
fs.getPath("/globtmp/Foo/Bar").createDirectoryAndParents();
FileSystemUtils.writeContentAsLatin1(fs.getPath("/globtmp/Foo/Bar/a.txt"), "foo");
FileSystemUtils.writeContentAsLatin1(fs.getPath("/globtmp/Foo/Bar/b.dat"), "bar");
return fs;
}
private static Map<String, Collection<Dirent>> mockDirents(Path root) {
// Map keys must be Strings not Paths, lest they follow the host OS' case-sensitivity policy.
Map<String, Collection<Dirent>> d =
root.getFileSystem().isGlobCaseSensitive()
? new HashMap<>()
: new TreeMap<>((String x, String y) -> x.compareToIgnoreCase(y));
d.put(
root.getParentDirectory().toString(),
ImmutableList.of(new Dirent(root.getBaseName(), Dirent.Type.DIRECTORY)));
d.put(root.toString(), ImmutableList.of(new Dirent("Foo", Dirent.Type.DIRECTORY)));
d.put(
root.getRelative("Foo").toString(),
ImmutableList.of(new Dirent("Bar", Dirent.Type.DIRECTORY)));
d.put(
root.getRelative("Foo/Bar").toString(),
ImmutableList.of(
new Dirent("a.txt", Dirent.Type.FILE), new Dirent("b.dat", Dirent.Type.FILE)));
return d;
}
private static Map<String, FileStatus> mockStats(Path root) {
// Map keys must be Strings not Paths, lest they follow the host OS' case-sensitivity policy.
Map<String, FileStatus> d =
root.getFileSystem().isGlobCaseSensitive()
? new HashMap<>()
: new TreeMap<>((String x, String y) -> x.compareToIgnoreCase(y));
d.put(root.getParentDirectory().toString(), MockStat.DIR);
d.put(root.toString(), MockStat.DIR);
d.put(root.getRelative("Foo").toString(), MockStat.DIR);
d.put(root.getRelative("Foo/Bar").toString(), MockStat.DIR);
d.put(root.getRelative("Foo/Bar/a.txt").toString(), MockStat.FILE);
d.put(root.getRelative("Foo/Bar/b.dat").toString(), MockStat.FILE);
return d;
}
private static UnixGlob.FilesystemCalls mockFsCalls(Path root) {
return new UnixGlob.FilesystemCalls() {
// These maps use case-sensitive or case-insensitive key comparison depending on
// root.getFileSystem().isGlobCaseSensitive()
private final Map<String, Collection<Dirent>> dirents = mockDirents(root);
private final Map<String, FileStatus> stats = mockStats(root);
@Override
public Collection<Dirent> readdir(Path path) throws IOException {
String p = path.toString();
if (dirents.containsKey(p)) {
return dirents.get(p);
}
throw new IOException(p);
}
@Override
public FileStatus statIfFound(Path path, Symlinks symlinks) throws IOException {
String p = path.toString();
if (stats.containsKey(p)) {
return stats.get(p);
}
return MockStat.UNKNOWN;
}
@Override
public Dirent.Type getType(Path path, Symlinks symlinks) throws IOException {
String p = path.toString();
if (dirents.containsKey(p)) {
for (Dirent d : dirents.get(p)) {
if (d.getName().equals(path.getBaseName())) {
return d.getType();
}
}
}
throw new IOException(p);
}
};
}
@Test
public void testFoo() throws Exception {
FileSystem unixFs = mockFs(/* caseSensitive */ true);
FileSystem winFs = mockFs(/* caseSensitive */ false);
Path unixRoot = unixFs.getPath("/globtmp");
Path winRoot = winFs.getPath("/globtmp");
Path unixRoot2 = unixFs.getPath("/globTMP");
Path winRoot2 = winFs.getPath("/globTMP");
UnixGlob.FilesystemCalls unixFsCalls = mockFsCalls(unixRoot);
UnixGlob.FilesystemCalls winFsCalls = mockFsCalls(winRoot);
// Try a simple, non-recursive glob that matches no files. (Directories are exluded.)
assertMatches(unixFsCalls, unixRoot, "Foo/*");
assertMatches(winFsCalls, winRoot, "Foo/*");
// Try a simple, non-recursive glob that should match some files.
assertMatches(unixFsCalls, unixRoot, "Foo/*/*", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
assertMatches(winFsCalls, winRoot, "Foo/*/*", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
// Try a recursive glob.
assertMatches(unixFsCalls, unixRoot, "Foo/**", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
assertMatches(winFsCalls, winRoot, "Foo/**", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
// Try a recursive glob, but use incorrect casing in the pattern.
// The case-insensitive glob should match, but the results retain the casing of the pattern
// ("foO") because the glob logic checks its existence with 'statIfFound', and uses the name
// from the pattern and not from the filesystem.
// The **-matched parts use the actual casing ("Bar") because the glob does a 'readdir' to get
// these.
assertMatches(unixFsCalls, unixRoot, "foO/**");
assertMatches(winFsCalls, winRoot, "foO/**", "foO/Bar/a.txt", "foO/Bar/b.dat");
// Try the same with another path component in the glob pattern. The casing of that pattern
// should be retained in the results.
assertMatches(unixFsCalls, unixRoot, "foO/baR/*");
assertMatches(winFsCalls, winRoot, "foO/baR/*", "foO/baR/a.txt", "foO/baR/b.dat");
// Even if the root's casing is incorrect, the case-insensitive glob should match it.
assertMatches(unixFsCalls, unixRoot2, "**");
assertMatches(winFsCalls, winRoot2, "**", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
// Try again the "wrong" root with a "wrong" first component. The result should retain the
// casing.
assertMatches(unixFsCalls, unixRoot2, "foO/**");
assertMatches(winFsCalls, winRoot2, "foO/**", "foO/Bar/a.txt", "foO/Bar/b.dat");
// Try the same with more "wrong" path components in the pattern. The results should retain all
// the casing.
assertMatches(unixFsCalls, unixRoot2, "foO/baR/A.TXT");
assertMatches(winFsCalls, winRoot2, "foO/baR/A.TXT", "foO/baR/A.TXT");
// Try a so-called "complex" pattern in the directory name. The glob logic creates a regex and
// matches it against the result of a 'readdir', so the result retains the filesystem casing.
assertMatches(unixFsCalls, unixRoot, "Foo/*R/*");
assertMatches(winFsCalls, winRoot, "Foo/*R/*", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
// Try the same for a file name pattern. Again, the filesystem casing is used because the
// matching is done with a regex.
assertMatches(unixFsCalls, unixRoot, "Foo/Bar/*.TXT");
assertMatches(winFsCalls, winRoot, "Foo/Bar/*.TXT", "Foo/Bar/a.txt");
// Try the same with a recursive pattern.
assertMatches(unixFsCalls, unixRoot, "F*o*o/**", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
assertMatches(winFsCalls, winRoot, "F*o*o/**", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
// Try the same with wrong casing.
assertMatches(unixFsCalls, unixRoot, "f*o*O/**");
assertMatches(winFsCalls, winRoot, "f*o*O/**", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
// A "complex" first pattern with the right casing, but wrongly-cased root.
assertMatches(unixFsCalls, unixRoot2, "F*o*o/**");
assertMatches(winFsCalls, winRoot2, "F*o*o/**", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
// A "complex" first pattern with the wrong casing and wrongly-cased root.
assertMatches(unixFsCalls, unixRoot2, "f*o*O/**");
assertMatches(winFsCalls, winRoot2, "f*o*O/**", "Foo/Bar/a.txt", "Foo/Bar/b.dat");
}
@Test
public void testStartsWithCase() {
assertThat(UnixGlob.startsWithCase("", "", true)).isTrue();
assertThat(UnixGlob.startsWithCase("", "", false)).isTrue();
assertThat(UnixGlob.startsWithCase("Foo", "", true)).isTrue();
assertThat(UnixGlob.startsWithCase("Foo", "", false)).isTrue();
assertThat(UnixGlob.startsWithCase("", "Foo", true)).isFalse();
assertThat(UnixGlob.startsWithCase("", "Foo", false)).isFalse();
assertThat(UnixGlob.startsWithCase("Fo", "Foo", true)).isFalse();
assertThat(UnixGlob.startsWithCase("Fo", "Foo", false)).isFalse();
assertThat(UnixGlob.startsWithCase("Foo", "Foo", true)).isTrue();
assertThat(UnixGlob.startsWithCase("Foo", "Foo", false)).isTrue();
assertThat(UnixGlob.startsWithCase("Foox", "Foo", true)).isTrue();
assertThat(UnixGlob.startsWithCase("Foox", "Foo", false)).isTrue();
assertThat(UnixGlob.startsWithCase("xFoo", "Foo", true)).isFalse();
assertThat(UnixGlob.startsWithCase("xFoo", "Foo", false)).isFalse();
assertThat(UnixGlob.startsWithCase("Foox", "foO", true)).isFalse();
assertThat(UnixGlob.startsWithCase("Foox", "foO", false)).isTrue();
}
@Test
public void testEndsWithCase() {
assertThat(UnixGlob.endsWithCase("", "", true)).isTrue();
assertThat(UnixGlob.endsWithCase("", "", false)).isTrue();
assertThat(UnixGlob.endsWithCase("Foo", "", true)).isTrue();
assertThat(UnixGlob.endsWithCase("Foo", "", false)).isTrue();
assertThat(UnixGlob.endsWithCase("", "Foo", true)).isFalse();
assertThat(UnixGlob.endsWithCase("", "Foo", false)).isFalse();
assertThat(UnixGlob.endsWithCase("Fo", "Foo", true)).isFalse();
assertThat(UnixGlob.endsWithCase("Fo", "Foo", false)).isFalse();
assertThat(UnixGlob.endsWithCase("Foo", "Foo", true)).isTrue();
assertThat(UnixGlob.endsWithCase("Foo", "Foo", false)).isTrue();
assertThat(UnixGlob.endsWithCase("Foox", "Foo", true)).isFalse();
assertThat(UnixGlob.endsWithCase("Foox", "Foo", false)).isFalse();
assertThat(UnixGlob.endsWithCase("xFoo", "Foo", true)).isTrue();
assertThat(UnixGlob.endsWithCase("xFoo", "Foo", false)).isTrue();
assertThat(UnixGlob.endsWithCase("xFoo", "foO", true)).isFalse();
assertThat(UnixGlob.endsWithCase("xFoo", "foO", false)).isTrue();
}
}