Part 1 of the Implementation for new 'subpackages()` built-in helper function.
Design proposal: https://docs.google.com/document/d/13UOT0GoQofxDW40ILzH2sWpUOmuYy6QZ7CUmhej9vgk/edit#
This CL modifies the globber infrastructure to support an additional mode of listing sub-directories.
* Add new Globber Operation enum allowing, Globber implementations to
discriminate between glob, glob w/directories and the future sub-packages
use-case.
* Modify UnixGlob to replace Predicate and bools with UnixGlobPathDiscriminator interface for:
a) Determining whether to traverse a sub-directory (previously was lambda)
b) function for determing what entries to include in the List<Path> produced by UnixGlob.globAsync.
These allow relatively simple re-use of the same logic for both subpackages and glob
4) Add a few tests for UnixGlob to ensure both cases continue to work as expected.
PiperOrigin-RevId: 421125424
diff --git a/src/main/java/com/google/devtools/build/lib/includescanning/BUILD b/src/main/java/com/google/devtools/build/lib/includescanning/BUILD
index 7da1f11..d156e18 100644
--- a/src/main/java/com/google/devtools/build/lib/includescanning/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/includescanning/BUILD
@@ -33,6 +33,7 @@
"//src/main/java/com/google/devtools/build/lib/exec:module_action_context_registry",
"//src/main/java/com/google/devtools/build/lib/exec:spawn_strategy_resolver",
"//src/main/java/com/google/devtools/build/lib/packages",
+ "//src/main/java/com/google/devtools/build/lib/packages:globber",
"//src/main/java/com/google/devtools/build/lib/profiler",
"//src/main/java/com/google/devtools/build/lib/rules/cpp",
"//src/main/java/com/google/devtools/build/lib/skyframe:containing_package_lookup_value",
diff --git a/src/main/java/com/google/devtools/build/lib/includescanning/IncludeParser.java b/src/main/java/com/google/devtools/build/lib/includescanning/IncludeParser.java
index 4c27684..ab119a4 100644
--- a/src/main/java/com/google/devtools/build/lib/includescanning/IncludeParser.java
+++ b/src/main/java/com/google/devtools/build/lib/includescanning/IncludeParser.java
@@ -38,6 +38,7 @@
import com.google.devtools.build.lib.concurrent.BlazeInterners;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.includescanning.IncludeParser.Inclusion.Kind;
+import com.google.devtools.build.lib.packages.Globber;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
@@ -348,7 +349,7 @@
containingPackageLookupValue.getContainingPackageName(),
containingPackageLookupValue.getContainingPackageRoot(),
pattern,
- /*excludeDirs=*/ true,
+ Globber.Operation.FILES,
relativePath.relativeTo(packageFragment)));
} catch (InvalidGlobPatternException e) {
env.getListener()
diff --git a/src/main/java/com/google/devtools/build/lib/packages/BUILD b/src/main/java/com/google/devtools/build/lib/packages/BUILD
index 42d4cb4..146d5cf 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/packages/BUILD
@@ -25,17 +25,35 @@
)
java_library(
+ name = "globber",
+ srcs = ["Globber.java"],
+)
+
+java_library(
+ name = "globber_utils",
+ srcs = ["GlobberUtils.java"],
+ deps = [
+ ":globber",
+ "//third_party:error_prone_annotations",
+ ],
+)
+
+java_library(
name = "packages",
srcs = glob(
["*.java"],
exclude = [
"BuilderFactoryForTesting.java", # see builder_factory_for_testing
+ "Globber.java",
+ "GlobberUtils.java",
"ExecGroup.java",
"ConfiguredAttributeMapper.java",
],
),
deps = [
":exec_group",
+ ":globber",
+ ":globber_utils",
"//src/main/java/com/google/devtools/build/docgen/annot",
"//src/main/java/com/google/devtools/build/lib/actions:execution_requirements",
"//src/main/java/com/google/devtools/build/lib/actions:thread_state_receiver",
diff --git a/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java b/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
index 33c882d..11caecc 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
@@ -15,7 +15,6 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
@@ -29,6 +28,7 @@
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.UnixGlob;
+import com.google.devtools.build.lib.vfs.UnixGlobPathDiscriminator;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -44,36 +44,26 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
-/**
- * Caches the results of glob expansion for a package.
- */
+/** Caches the results of glob expansion for a package. */
@ThreadSafety.ThreadCompatible
public class GlobCache {
/**
- * A mapping from glob expressions (e.g. "*.java") to the list of files it
- * matched (in the order returned by VFS) at the time the package was
- * constructed. Required for sound dependency analysis.
+ * A mapping from glob expressions (e.g. "*.java") to the list of files it matched (in the order
+ * returned by VFS) at the time the package was constructed. Required for sound dependency
+ * analysis.
*
- * We don't use a Multimap because it provides no way to distinguish "key not
- * present" from (key -> {}).
+ * <p>We don't use a Multimap because it provides no way to distinguish "key not present" from
+ * (key -> {}).
*/
- private final Map<Pair<String, Boolean>, Future<List<Path>>> globCache = new HashMap<>();
+ private final Map<Pair<String, Globber.Operation>, Future<List<Path>>> globCache =
+ new HashMap<>();
- /**
- * The directory in which our package's BUILD file resides.
- */
+ /** The directory in which our package's BUILD file resides. */
private final Path packageDirectory;
- /**
- * The name of the package we belong to.
- */
+ /** The name of the package we belong to. */
private final PackageIdentifier packageId;
- /**
- * The package locator-based directory traversal predicate.
- */
- private final Predicate<Path> childDirectoryPredicate;
-
/** System call caching layer. */
private final AtomicReference<? extends UnixGlob.FilesystemCalls> syscalls;
@@ -84,6 +74,10 @@
private final AtomicBoolean globalStarted = new AtomicBoolean(false);
+ private final CachingPackageLocator packageLocator;
+
+ private final ImmutableSet<PathFragment> ignoredGlobPrefixes;
+
/**
* Create a glob expansion cache.
*
@@ -120,47 +114,55 @@
this.maxDirectoriesToEagerlyVisit = maxDirectoriesToEagerlyVisit;
Preconditions.checkNotNull(locator);
- childDirectoryPredicate =
- directory -> {
- if (directory.equals(packageDirectory)) {
- return true;
- }
+ this.packageLocator = locator;
+ this.ignoredGlobPrefixes = ignoredGlobPrefixes;
+ }
- PathFragment subPackagePath =
- packageId.getPackageFragment().getRelative(directory.relativeTo(packageDirectory));
+ private boolean globCacheShouldTraverseDirectory(Path directory) {
+ if (directory.equals(packageDirectory)) {
+ return true;
+ }
- for (PathFragment ignoredPrefix : ignoredGlobPrefixes) {
- if (subPackagePath.startsWith(ignoredPrefix)) {
- return false;
- }
- }
+ PathFragment subPackagePath =
+ packageId.getPackageFragment().getRelative(directory.relativeTo(packageDirectory));
- PackageIdentifier subPackageId =
- PackageIdentifier.create(packageId.getRepository(), subPackagePath);
- return locator.getBuildFileForPackage(subPackageId) == null;
- };
+ for (PathFragment ignoredPrefix : ignoredGlobPrefixes) {
+ if (subPackagePath.startsWith(ignoredPrefix)) {
+ return false;
+ }
+ }
+
+ return !isSubPackage(PackageIdentifier.create(packageId.getRepository(), subPackagePath));
+ }
+
+ private boolean isSubPackage(Path directory) {
+ return isSubPackage(
+ PackageIdentifier.create(
+ packageId.getRepository(),
+ packageId.getPackageFragment().getRelative(directory.relativeTo(packageDirectory))));
+ }
+
+ private boolean isSubPackage(PackageIdentifier subPackageId) {
+ return packageLocator.getBuildFileForPackage(subPackageId) != null;
}
/**
- * Returns the future result of evaluating glob "pattern" against this
- * package's directory, using the package's cache of previously-started
- * globs if possible.
+ * Returns the future result of evaluating glob "pattern" against this package's directory, using
+ * the package's cache of previously-started globs if possible.
*
- * @return the list of paths matching the pattern, relative to the package's
- * directory.
- * @throws BadGlobException if the glob was syntactically invalid, or
- * contained uplevel references.
+ * @return the list of paths matching the pattern, relative to the package's directory.
+ * @throws BadGlobException if the glob was syntactically invalid, or contained uplevel
+ * references.
*/
- Future<List<Path>> getGlobUnsortedAsync(String pattern, boolean excludeDirs)
+ Future<List<Path>> getGlobUnsortedAsync(String pattern, Globber.Operation globberOperation)
throws BadGlobException {
- Future<List<Path>> cached = globCache.get(Pair.of(pattern, excludeDirs));
+ Future<List<Path>> cached = globCache.get(Pair.of(pattern, globberOperation));
if (cached == null) {
- if (maxDirectoriesToEagerlyVisit > -1
- && !globalStarted.getAndSet(true)) {
+ if (maxDirectoriesToEagerlyVisit > -1 && !globalStarted.getAndSet(true)) {
packageDirectory.prefetchPackageAsync(maxDirectoriesToEagerlyVisit);
}
- cached = safeGlobUnsorted(pattern, excludeDirs);
- setGlobPaths(pattern, excludeDirs, cached);
+ cached = safeGlobUnsorted(pattern, globberOperation);
+ setGlobPaths(pattern, globberOperation, cached);
}
return cached;
}
@@ -168,20 +170,20 @@
@VisibleForTesting
List<String> getGlobUnsorted(String pattern)
throws IOException, BadGlobException, InterruptedException {
- return getGlobUnsorted(pattern, false);
+ return getGlobUnsorted(pattern, Globber.Operation.FILES_AND_DIRS);
}
@VisibleForTesting
- protected List<String> getGlobUnsorted(String pattern, boolean excludeDirs)
+ protected List<String> getGlobUnsorted(String pattern, Globber.Operation globberOperation)
throws IOException, BadGlobException, InterruptedException {
- Future<List<Path>> futureResult = getGlobUnsortedAsync(pattern, excludeDirs);
+ Future<List<Path>> futureResult = getGlobUnsortedAsync(pattern, globberOperation);
List<Path> globPaths = fromFuture(futureResult);
// Replace the UnixGlob.GlobFuture with a completed future object, to allow
// garbage collection of the GlobFuture and GlobVisitor objects.
if (!(futureResult instanceof SettableFuture<?>)) {
SettableFuture<List<Path>> completedFuture = SettableFuture.create();
completedFuture.set(globPaths);
- globCache.put(Pair.of(pattern, excludeDirs), completedFuture);
+ globCache.put(Pair.of(pattern, globberOperation), completedFuture);
}
List<String> result = Lists.newArrayListWithCapacity(globPaths.size());
@@ -198,16 +200,15 @@
}
/** Adds glob entries to the cache. */
- private void setGlobPaths(String pattern, boolean excludeDirectories, Future<List<Path>> result) {
- globCache.put(Pair.of(pattern, excludeDirectories), result);
+ private void setGlobPaths(
+ String pattern, Globber.Operation globberOperation, Future<List<Path>> result) {
+ globCache.put(Pair.of(pattern, globberOperation), result);
}
- /**
- * Actually execute a glob against the filesystem. Otherwise similar to
- * getGlob().
- */
+ /** Actually execute a glob against the filesystem. Otherwise similar to getGlob(). */
@VisibleForTesting
- Future<List<Path>> safeGlobUnsorted(String pattern, boolean excludeDirs) throws BadGlobException {
+ Future<List<Path>> safeGlobUnsorted(String pattern, Globber.Operation globberOperation)
+ throws BadGlobException {
// Forbidden patterns:
if (pattern.indexOf('?') != -1) {
throw new BadGlobException("glob pattern '" + pattern + "' contains forbidden '?' wildcard");
@@ -220,8 +221,7 @@
try {
return UnixGlob.forPath(packageDirectory)
.addPattern(pattern)
- .setExcludeDirectories(excludeDirs)
- .setDirectoryFilter(childDirectoryPredicate)
+ .setPathDiscriminator(new GlobUnixPathDiscriminator(globberOperation))
.setExecutor(globExecutor)
.setFilesystemCalls(syscalls)
.globAsync();
@@ -230,18 +230,14 @@
}
}
- /**
- * Sanitize the future exceptions - the only expected checked exception
- * is IOException.
- */
+ /** Sanitize the future exceptions - the only expected checked exception is IOException. */
private static List<Path> fromFuture(Future<List<Path>> future)
throws IOException, InterruptedException {
try {
return future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
- Throwables.propagateIfPossible(cause,
- IOException.class, InterruptedException.class);
+ Throwables.propagateIfPossible(cause, IOException.class, InterruptedException.class);
throw new RuntimeException(e);
}
}
@@ -253,26 +249,24 @@
* <p>Called by PackageFactory via Package.
*/
public List<String> globUnsorted(
- List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+ List<String> includes,
+ List<String> excludes,
+ Globber.Operation globberOperation,
+ boolean allowEmpty)
throws IOException, BadGlobException, InterruptedException {
// Start globbing all patterns in parallel. The getGlob() calls below will
// block on an individual pattern's results, but the other globs can
// continue in the background.
for (String pattern : includes) {
@SuppressWarnings("unused")
- Future<?> possiblyIgnoredError = getGlobUnsortedAsync(pattern, excludeDirs);
+ Future<?> possiblyIgnoredError = getGlobUnsortedAsync(pattern, globberOperation);
}
HashSet<String> results = new HashSet<>();
for (String pattern : includes) {
- List<String> items = getGlobUnsorted(pattern, excludeDirs);
+ List<String> items = getGlobUnsorted(pattern, globberOperation);
if (!allowEmpty && items.isEmpty()) {
- throw new BadGlobException(
- "glob pattern '"
- + pattern
- + "' didn't match anything, but allow_empty is set to False "
- + "(the default value of allow_empty can be set with "
- + "--incompatible_disallow_empty_glob).");
+ GlobberUtils.throwBadGlobExceptionEmptyResult(pattern, globberOperation);
}
results.addAll(items);
}
@@ -282,21 +276,16 @@
throw new BadGlobException(ex.getMessage());
}
if (!allowEmpty && results.isEmpty()) {
- throw new BadGlobException(
- "all files in the glob have been excluded, but allow_empty is set to False "
- + "(the default value of allow_empty can be set with "
- + "--incompatible_disallow_empty_glob).");
+ GlobberUtils.throwBadGlobExceptionAllExcluded(globberOperation);
}
return new ArrayList<>(results);
}
- public Set<Pair<String, Boolean>> getKeySet() {
+ public Set<Pair<String, Globber.Operation>> getKeySet() {
return globCache.keySet();
}
- /**
- * Block on the completion of all potentially-abandoned background tasks.
- */
+ /** Block on the completion of all potentially-abandoned background tasks. */
public void finishBackgroundTasks() {
finishBackgroundTasks(globCache.values());
}
@@ -334,4 +323,45 @@
public String toString() {
return "GlobCache for " + packageId + " in " + packageDirectory;
}
+
+ /**
+ * Used by 'glob()' and 'subpackages()' with UnixGlob to determine if a directory should be
+ * traversed when recursing through a filesystem directory structure or include a Path in the
+ * result. This essentially filters out a set of ignored prefixes and then checks to see if a
+ * given sub-dir actually represents a sub-package or not when traversing.
+ *
+ * <p>The logic of including inspects the Globber.Operation to determine if it will include all
+ * files, include directories or subpackages in the output.
+ */
+ private class GlobUnixPathDiscriminator implements UnixGlobPathDiscriminator {
+ private final Globber.Operation globberOperation;
+
+ GlobUnixPathDiscriminator(Globber.Operation globberOperation) {
+ this.globberOperation = globberOperation;
+ }
+
+ @Override
+ public boolean shouldTraverseDirectory(Path directory) {
+ return globCacheShouldTraverseDirectory(directory);
+ }
+
+ @Override
+ public boolean shouldIncludePathInResult(Path path, boolean isDirectory) {
+ switch (globberOperation) {
+ case FILES_AND_DIRS:
+ return !isDirectory || !isSubPackage(path);
+ case SUBPACKAGES:
+ // no files, or root pkg
+ if (!isDirectory || path.equals(packageDirectory)) {
+ return false;
+ }
+ return isSubPackage(path);
+
+ case FILES:
+ return !isDirectory;
+ }
+ throw new IllegalStateException(
+ "Unexpected unhandled Globber.Operation enum value: " + globberOperation);
+ }
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Globber.java b/src/main/java/com/google/devtools/build/lib/packages/Globber.java
index a580a47..4bc2184 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Globber.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Globber.java
@@ -21,6 +21,19 @@
/** An opaque token for fetching the result of a glob computation. */
abstract class Token {}
+ /**
+ * Indiciates they type of globbing operations we're doing, whether looking for Files+Dirs or
+ * sub-packages.
+ */
+ enum Operation {
+ // Return only files,
+ FILES,
+ // Return files and directories, but not sub-packages
+ FILES_AND_DIRS,
+ // Return only sub-packages
+ SUBPACKAGES,
+ }
+
/** Used to indicate an invalid glob pattern. */
class BadGlobException extends Exception {
public BadGlobException(String message) {
@@ -35,7 +48,7 @@
* invalid.
*/
Token runAsync(
- List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+ List<String> includes, List<String> excludes, Operation operation, boolean allowEmpty)
throws BadGlobException, InterruptedException;
/**
diff --git a/src/main/java/com/google/devtools/build/lib/packages/GlobberUtils.java b/src/main/java/com/google/devtools/build/lib/packages/GlobberUtils.java
new file mode 100644
index 0000000..766cb83
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/GlobberUtils.java
@@ -0,0 +1,56 @@
+// Copyright 2022 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.packages;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+
+/** Static functionality shared by different implementations of the Globber interface. */
+@CheckReturnValue
+public final class GlobberUtils {
+
+ private GlobberUtils() {}
+
+ public static void throwBadGlobExceptionEmptyResult(
+ String pattern, Globber.Operation globberOperation) throws Globber.BadGlobException {
+ switch (globberOperation) {
+ case SUBPACKAGES:
+ throw new Globber.BadGlobException(
+ "subpackages pattern '"
+ + pattern
+ + "' didn't match anything, but allow_empty is set to False (the default value)");
+ default:
+ throw new Globber.BadGlobException(
+ "glob pattern '"
+ + pattern
+ + "' didn't match anything, but allow_empty is set to False "
+ + "(the default value of allow_empty can be set with "
+ + "--incompatible_disallow_empty_glob).");
+ }
+ }
+
+ public static void throwBadGlobExceptionAllExcluded(Globber.Operation globberOperation)
+ throws Globber.BadGlobException {
+ switch (globberOperation) {
+ case SUBPACKAGES:
+ throw new Globber.BadGlobException(
+ "all subpackages in subpackages() have been excluded, but allow_empty is"
+ + " set to False ");
+ default:
+ throw new Globber.BadGlobException(
+ "all files in the glob have been excluded, but allow_empty is set to False "
+ + "(the default value of allow_empty can be set with "
+ + "--incompatible_disallow_empty_glob).");
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/NonSkyframeGlobber.java b/src/main/java/com/google/devtools/build/lib/packages/NonSkyframeGlobber.java
index 01246fc..1a85563 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/NonSkyframeGlobber.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/NonSkyframeGlobber.java
@@ -30,27 +30,34 @@
public static class Token extends Globber.Token {
private final List<String> includes;
private final List<String> excludes;
- private final boolean excludeDirs;
+ private final Globber.Operation globberOperation;
private final boolean allowEmpty;
private Token(
- List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty) {
+ List<String> includes,
+ List<String> excludes,
+ Globber.Operation globberOperation,
+ boolean allowEmpty) {
this.includes = includes;
this.excludes = excludes;
- this.excludeDirs = excludeDirs;
+ this.globberOperation = globberOperation;
this.allowEmpty = allowEmpty;
}
}
@Override
public Token runAsync(
- List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+ List<String> includes,
+ List<String> excludes,
+ Globber.Operation globberOperation,
+ boolean allowEmpty)
throws BadGlobException {
+
for (String pattern : includes) {
@SuppressWarnings("unused")
- Future<?> possiblyIgnoredError = globCache.getGlobUnsortedAsync(pattern, excludeDirs);
+ Future<?> possiblyIgnoredError = globCache.getGlobUnsortedAsync(pattern, globberOperation);
}
- return new Token(includes, excludes, excludeDirs, allowEmpty);
+ return new Token(includes, excludes, globberOperation, allowEmpty);
}
@Override
@@ -58,10 +65,7 @@
throws BadGlobException, IOException, InterruptedException {
Token ourToken = (Token) token;
return globCache.globUnsorted(
- ourToken.includes,
- ourToken.excludes,
- ourToken.excludeDirs,
- ourToken.allowEmpty);
+ ourToken.includes, ourToken.excludes, ourToken.globberOperation, ourToken.allowEmpty);
}
@Override
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
index dd57fe7..c1e86cc 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
@@ -610,8 +610,9 @@
if (maxDirectoriesToEagerlyVisitInGlobbing == -2) {
try {
boolean allowEmpty = true;
- globber.runAsync(globs, ImmutableList.of(), /*excludeDirs=*/ true, allowEmpty);
- globber.runAsync(globsWithDirs, ImmutableList.of(), /*excludeDirs=*/ false, allowEmpty);
+ globber.runAsync(globs, ImmutableList.of(), Globber.Operation.FILES, allowEmpty);
+ globber.runAsync(
+ globsWithDirs, ImmutableList.of(), Globber.Operation.FILES_AND_DIRS, allowEmpty);
} catch (BadGlobException ex) {
logger.atWarning().withCause(ex).log(
"Suppressing exception for globs=%s, globsWithDirs=%s", globs, globsWithDirs);
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java b/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java
index b18598b..9209f9d 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java
@@ -99,6 +99,8 @@
List<String> includes = Type.STRING_LIST.convert(include, "'glob' argument");
List<String> excludes = Type.STRING_LIST.convert(exclude, "'glob' argument");
+ Globber.Operation op =
+ excludeDirs.signum() != 0 ? Globber.Operation.FILES : Globber.Operation.FILES_AND_DIRS;
List<String> matches;
boolean allowEmpty;
@@ -113,8 +115,7 @@
}
try {
- Globber.Token globToken =
- context.globber.runAsync(includes, excludes, excludeDirs.signum() != 0, allowEmpty);
+ Globber.Token globToken = context.globber.runAsync(includes, excludes, op, allowEmpty);
matches = context.globber.fetchUnsorted(globToken);
} catch (IOException e) {
logger.atWarning().withCause(e).log(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
index 8576a85..52b6c43 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -299,6 +299,8 @@
"//src/main/java/com/google/devtools/build/lib/io:process_package_directory_exception",
"//src/main/java/com/google/devtools/build/lib/packages",
"//src/main/java/com/google/devtools/build/lib/packages:exec_group",
+ "//src/main/java/com/google/devtools/build/lib/packages:globber",
+ "//src/main/java/com/google/devtools/build/lib/packages:globber_utils",
"//src/main/java/com/google/devtools/build/lib/packages/semantics",
"//src/main/java/com/google/devtools/build/lib/pkgcache",
"//src/main/java/com/google/devtools/build/lib/pkgcache:QueryTransitivePackagePreloader",
@@ -1426,6 +1428,7 @@
":sky_functions",
"//src/main/java/com/google/devtools/build/lib/cmdline",
"//src/main/java/com/google/devtools/build/lib/concurrent",
+ "//src/main/java/com/google/devtools/build/lib/packages:globber",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
"//src/main/java/com/google/devtools/build/lib/util:string",
"//src/main/java/com/google/devtools/build/lib/vfs",
@@ -1450,6 +1453,7 @@
"//src/main/java/com/google/devtools/build/lib/io:file_symlink_infinite_expansion_exception",
"//src/main/java/com/google/devtools/build/lib/io:file_symlink_infinite_expansion_uniqueness_function",
"//src/main/java/com/google/devtools/build/lib/io:inconsistent_filesystem_exception",
+ "//src/main/java/com/google/devtools/build/lib/packages:globber",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
"//src/main/java/com/google/devtools/build/skyframe",
@@ -1467,6 +1471,7 @@
"//src/main/java/com/google/devtools/build/lib/cmdline",
"//src/main/java/com/google/devtools/build/lib/collect/nestedset",
"//src/main/java/com/google/devtools/build/lib/concurrent",
+ "//src/main/java/com/google/devtools/build/lib/packages:globber",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
"//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
index f64d451c..a436090 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
@@ -18,6 +18,7 @@
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.concurrent.BlazeInterners;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.Globber;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.util.StringCanonicalizer;
import com.google.devtools.build.lib.vfs.PathFragment;
@@ -46,7 +47,7 @@
* @param subdir the subdirectory being looked at (must exist and must be a directory. It's
* assumed that there are no other packages between {@code packageName} and {@code subdir}.
* @param pattern a valid glob pattern
- * @param excludeDirs true if directories should be excluded from results
+ * @param globberOperation type of Globber operation being tracked.
*/
@AutoCodec.Instantiator
public static GlobDescriptor create(
@@ -54,36 +55,35 @@
Root packageRoot,
PathFragment subdir,
String pattern,
- boolean excludeDirs) {
+ Globber.Operation globberOperation) {
return interner.intern(
- new GlobDescriptor(packageId, packageRoot, subdir, pattern, excludeDirs));
-
+ new GlobDescriptor(packageId, packageRoot, subdir, pattern, globberOperation));
}
private final PackageIdentifier packageId;
private final Root packageRoot;
private final PathFragment subdir;
private final String pattern;
- private final boolean excludeDirs;
+ private final Globber.Operation globberOperation;
private GlobDescriptor(
PackageIdentifier packageId,
Root packageRoot,
PathFragment subdir,
String pattern,
- boolean excludeDirs) {
+ Globber.Operation globberOperation) {
this.packageId = Preconditions.checkNotNull(packageId);
this.packageRoot = Preconditions.checkNotNull(packageRoot);
this.subdir = Preconditions.checkNotNull(subdir);
this.pattern = Preconditions.checkNotNull(StringCanonicalizer.intern(pattern));
- this.excludeDirs = excludeDirs;
+ this.globberOperation = globberOperation;
}
@Override
public String toString() {
return String.format(
- "<GlobDescriptor packageName=%s packageRoot=%s subdir=%s pattern=%s excludeDirs=%s>",
- packageId, packageRoot, subdir, pattern, excludeDirs);
+ "<GlobDescriptor packageName=%s packageRoot=%s subdir=%s pattern=%s globberOperation=%s>",
+ packageId, packageRoot, subdir, pattern, globberOperation.name());
}
/**
@@ -117,11 +117,9 @@
return pattern;
}
- /**
- * Returns true if directories should be excluded from results.
- */
- public boolean excludeDirs() {
- return excludeDirs;
+ /** Returns the type of Globber operation that produced the results. */
+ public Globber.Operation globberOperation() {
+ return globberOperation;
}
@Override
@@ -133,9 +131,11 @@
return false;
}
GlobDescriptor other = (GlobDescriptor) obj;
- return packageId.equals(other.packageId) && packageRoot.equals(other.packageRoot)
- && subdir.equals(other.subdir) && pattern.equals(other.pattern)
- && excludeDirs == other.excludeDirs;
+ return packageId.equals(other.packageId)
+ && packageRoot.equals(other.packageRoot)
+ && subdir.equals(other.subdir)
+ && pattern.equals(other.pattern)
+ && globberOperation == other.globberOperation;
}
@Override
@@ -143,7 +143,7 @@
// Generated instead of Objects.hashCode to avoid intermediate array required for latter.
final int prime = 31;
int result = 1;
- result = prime * result + (excludeDirs ? 1231 : 1237);
+ result = prime * result + globberOperation.hashCode();
result = prime * result + packageId.hashCode();
result = prime * result + packageRoot.hashCode();
result = prime * result + pattern.hashCode();
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
index 5170a8a..5aef741 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
@@ -25,6 +25,7 @@
import com.google.devtools.build.lib.io.FileSymlinkInfiniteExpansionException;
import com.google.devtools.build.lib.io.FileSymlinkInfiniteExpansionUniquenessFunction;
import com.google.devtools.build.lib.io.InconsistentFilesystemException;
+import com.google.devtools.build.lib.packages.Globber;
import com.google.devtools.build.lib.vfs.Dirent;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
@@ -62,6 +63,7 @@
public SkyValue compute(SkyKey skyKey, Environment env)
throws GlobFunctionException, InterruptedException {
GlobDescriptor glob = (GlobDescriptor) skyKey.argument();
+ Globber.Operation globberOperation = glob.globberOperation();
RepositoryName repositoryName = glob.getPackageId().getRepository();
IgnoredPackagePrefixesValue ignoredPackagePrefixes =
@@ -121,7 +123,6 @@
boolean globMatchesBareFile = patternTail == null;
-
RootedPath dirRootedPath = RootedPath.toRootedPath(glob.getPackageRoot(), dirPathFragment);
if (alwaysUseDirListing || containsGlobs(patternHead)) {
// Pattern contains globs, so a directory listing is required.
@@ -139,7 +140,8 @@
// "**" also matches an empty segment, so try the case where it is not present.
if (globMatchesBareFile) {
// Recursive globs aren't supposed to match the package's directory.
- if (!glob.excludeDirs() && !globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
+ if (globberOperation == Globber.Operation.FILES_AND_DIRS
+ && !globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
matches.add(globSubdir);
}
} else {
@@ -152,7 +154,7 @@
glob.getPackageRoot(),
globSubdir,
patternTail,
- glob.excludeDirs());
+ globberOperation);
Map<SkyKey, SkyValue> listingAndRecursiveGlobMap =
env.getValues(
ImmutableList.of(keyForRecursiveGlobInCurrentDirectory, directoryListingKey));
@@ -369,7 +371,7 @@
private static SkyKey getSkyKeyForSubdir(
String fileName, GlobDescriptor glob, String subdirPattern) {
if (subdirPattern == null) {
- if (glob.excludeDirs()) {
+ if (glob.globberOperation() == Globber.Operation.FILES) {
return null;
} else {
return PackageLookupValue.key(
@@ -389,7 +391,7 @@
glob.getPackageRoot(),
glob.getSubdir().getRelative(fileName),
subdirPattern,
- glob.excludeDirs());
+ glob.globberOperation());
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
index bcc89fc..bc7900a 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
@@ -20,6 +20,7 @@
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.Globber;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.UnixGlob;
@@ -84,7 +85,7 @@
PackageIdentifier packageId,
Root packageRoot,
String pattern,
- boolean excludeDirs,
+ Globber.Operation globOperation,
PathFragment subdir)
throws InvalidGlobPatternException {
if (pattern.indexOf('?') != -1) {
@@ -96,7 +97,7 @@
throw new InvalidGlobPatternException(pattern, error);
}
- return internalKey(packageId, packageRoot, subdir, pattern, excludeDirs);
+ return internalKey(packageId, packageRoot, subdir, pattern, globOperation);
}
/**
@@ -110,8 +111,8 @@
Root packageRoot,
PathFragment subdir,
String pattern,
- boolean excludeDirs) {
- return GlobDescriptor.create(packageId, packageRoot, subdir, pattern, excludeDirs);
+ Globber.Operation globOperation) {
+ return GlobDescriptor.create(packageId, packageRoot, subdir, pattern, globOperation);
}
/**
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
index fc19b2d..74bf760 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
@@ -41,6 +41,7 @@
import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
import com.google.devtools.build.lib.packages.CachingPackageLocator;
import com.google.devtools.build.lib.packages.Globber;
+import com.google.devtools.build.lib.packages.GlobberUtils;
import com.google.devtools.build.lib.packages.InvalidPackageNameException;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.NonSkyframeGlobber;
@@ -854,9 +855,12 @@
@Override
public Token runAsync(
- List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+ List<String> includes,
+ List<String> excludes,
+ Globber.Operation globberOperation,
+ boolean allowEmpty)
throws BadGlobException, InterruptedException {
- return delegate.runAsync(includes, excludes, excludeDirs, allowEmpty);
+ return delegate.runAsync(includes, excludes, globberOperation, allowEmpty);
}
@Override
@@ -956,10 +960,11 @@
return ImmutableSet.copyOf(globDepsRequested);
}
- private SkyKey getGlobKey(String pattern, boolean excludeDirs) throws BadGlobException {
+ private SkyKey getGlobKey(String pattern, Globber.Operation globberOperation)
+ throws BadGlobException {
try {
return GlobValue.key(
- packageId, packageRoot, pattern, excludeDirs, PathFragment.EMPTY_FRAGMENT);
+ packageId, packageRoot, pattern, globberOperation, PathFragment.EMPTY_FRAGMENT);
} catch (InvalidGlobPatternException e) {
throw new BadGlobException(e.getMessage());
}
@@ -967,13 +972,16 @@
@Override
public Token runAsync(
- List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+ List<String> includes,
+ List<String> excludes,
+ Globber.Operation globberOperation,
+ boolean allowEmpty)
throws BadGlobException, InterruptedException {
LinkedHashSet<SkyKey> globKeys = Sets.newLinkedHashSetWithExpectedSize(includes.size());
Map<SkyKey, String> globKeyToPatternMap = Maps.newHashMapWithExpectedSize(includes.size());
for (String pattern : includes) {
- SkyKey globKey = getGlobKey(pattern, excludeDirs);
+ SkyKey globKey = getGlobKey(pattern, globberOperation);
globKeys.add(globKey);
globKeyToPatternMap.put(globKey, pattern);
}
@@ -997,9 +1005,9 @@
globsToDelegate.isEmpty()
? null
: nonSkyframeGlobber.runAsync(
- globsToDelegate, ImmutableList.of(), excludeDirs, allowEmpty);
+ globsToDelegate, ImmutableList.of(), globberOperation, allowEmpty);
return new HybridToken(
- globValueMap, globKeys, nonSkyframeIncludesToken, excludes, allowEmpty);
+ globValueMap, globKeys, nonSkyframeIncludesToken, excludes, globberOperation, allowEmpty);
}
private static Collection<SkyKey> getMissingKeys(
@@ -1057,6 +1065,8 @@
private final List<String> excludes;
+ private final Globber.Operation globberOperation;
+
private final boolean allowEmpty;
private HybridToken(
@@ -1064,11 +1074,13 @@
Iterable<SkyKey> includesGlobKeys,
@Nullable NonSkyframeGlobber.Token nonSkyframeGlobberIncludesToken,
List<String> excludes,
+ Globber.Operation globberOperation,
boolean allowEmpty) {
this.globValueMap = globValueMap;
this.includesGlobKeys = includesGlobKeys;
this.nonSkyframeGlobberIncludesToken = nonSkyframeGlobberIncludesToken;
this.excludes = excludes;
+ this.globberOperation = globberOperation;
this.allowEmpty = allowEmpty;
}
@@ -1083,12 +1095,8 @@
foundMatch = true;
}
if (!allowEmpty && !foundMatch) {
- throw new BadGlobException(
- "glob pattern '"
- + ((GlobDescriptor) includeGlobKey.argument()).getPattern()
- + "' didn't match anything, but allow_empty is set to False "
- + "(the default value of allow_empty can be set with "
- + "--incompatible_disallow_empty_glob).");
+ GlobberUtils.throwBadGlobExceptionEmptyResult(
+ ((GlobDescriptor) includeGlobKey.argument()).getPattern(), globberOperation);
}
}
if (nonSkyframeGlobberIncludesToken != null) {
@@ -1102,10 +1110,7 @@
List<String> result = new ArrayList<>(matches);
if (!allowEmpty && result.isEmpty()) {
- throw new BadGlobException(
- "all files in the glob have been excluded, but allow_empty is set to False "
- + "(the default value of allow_empty can be set with "
- + "--incompatible_disallow_empty_glob).");
+ GlobberUtils.throwBadGlobExceptionAllExcluded(globberOperation);
}
return result;
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
index 23ab5ab..92f1c33 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
@@ -17,8 +17,6 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
@@ -60,6 +58,9 @@
* <p>Importantly, note that the glob matches are in an unspecified order.
*/
public final class UnixGlob {
+ private static final UnixGlobPathDiscriminator DEFAULT_DISCRIMINATOR =
+ new UnixGlobPathDiscriminator() {};
+
private UnixGlob() {}
/** Indicates an invalid glob pattern. */
@@ -72,51 +73,46 @@
private static List<Path> globInternal(
Path base,
Collection<String> patterns,
- boolean excludeDirectories,
- Predicate<Path> dirPred,
+ UnixGlobPathDiscriminator pathDiscriminator,
FilesystemCalls syscalls,
Executor executor)
throws IOException, InterruptedException, BadPattern {
GlobVisitor visitor = new GlobVisitor(executor);
- return visitor.glob(base, patterns, excludeDirectories, dirPred, syscalls);
+ return visitor.glob(base, patterns, pathDiscriminator, syscalls);
}
private static List<Path> globInternalUninterruptible(
Path base,
Collection<String> patterns,
- boolean excludeDirectories,
- Predicate<Path> dirPred,
+ UnixGlobPathDiscriminator pathDiscriminator,
FilesystemCalls syscalls,
Executor executor)
throws IOException, BadPattern {
GlobVisitor visitor = new GlobVisitor(executor);
- return visitor.globUninterruptible(base, patterns, excludeDirectories, dirPred, syscalls);
+ return visitor.globUninterruptible(base, patterns, pathDiscriminator, syscalls);
}
private static long globInternalAndReturnNumGlobTasksForTesting(
Path base,
Collection<String> patterns,
- boolean excludeDirectories,
- Predicate<Path> dirPred,
+ UnixGlobPathDiscriminator pathDiscriminator,
FilesystemCalls syscalls,
Executor executor)
throws IOException, InterruptedException, BadPattern {
GlobVisitor visitor = new GlobVisitor(executor);
- visitor.glob(base, patterns, excludeDirectories, dirPred, syscalls);
+ visitor.glob(base, patterns, pathDiscriminator, syscalls);
return visitor.getNumGlobTasksForTesting();
}
private static Future<List<Path>> globAsyncInternal(
Path base,
Collection<String> patterns,
- boolean excludeDirectories,
- Predicate<Path> dirPred,
+ UnixGlobPathDiscriminator pathDiscriminator,
FilesystemCalls syscalls,
Executor executor)
throws BadPattern {
Preconditions.checkNotNull(executor, "%s %s", base, patterns);
- return new GlobVisitor(executor)
- .globAsync(base, patterns, excludeDirectories, dirPred, syscalls);
+ return new GlobVisitor(executor).globAsync(base, patterns, pathDiscriminator, syscalls);
}
/**
@@ -345,8 +341,7 @@
public static class Builder {
private Path base;
private List<String> patterns;
- private boolean excludeDirectories;
- private Predicate<Path> pathFilter;
+ private UnixGlobPathDiscriminator pathDiscriminator = DEFAULT_DISCRIMINATOR;
private Executor executor;
private AtomicReference<? extends FilesystemCalls> syscalls =
new AtomicReference<>(DEFAULT_SYSCALLS);
@@ -357,8 +352,6 @@
public Builder(Path base) {
this.base = base;
this.patterns = Lists.newArrayList();
- this.excludeDirectories = false;
- this.pathFilter = Predicates.alwaysTrue();
}
/**
@@ -402,14 +395,6 @@
}
/**
- * If set to true, directories are not returned in the glob result.
- */
- public Builder setExcludeDirectories(boolean excludeDirectories) {
- this.excludeDirectories = excludeDirectories;
- return this;
- }
-
- /**
* Sets the executor to use for parallel glob evaluation. If unset, evaluation is done
* in-thread.
*/
@@ -418,21 +403,27 @@
return this;
}
-
/**
- * If set, the given predicate is called for every directory
- * encountered. If it returns false, the corresponding item is not
- * returned in the output and directories are not traversed either.
+ * Sets the UnixGlobPathDiscriminator which determines how to handle Path entries encountered
+ * during glob traversal. The interface determines if Paths should be added to the {@code
+ * List<Path>} results and whether to traverse a given directory during recursion.
+ *
+ * <p>The UnixGlobPathDiscriminator should only be called with Paths that have been resolved to
+ * a regular file or regular directory, it will not properly handle symlinks or special files.
+ *
+ * <p>This is used for handling the previous use case of 'excludeDirectories' where we wish to
+ * exclude files from the glob and decide which directories to traverse, like skipping sub-dirs
+ * containing BUILD files.
*/
- public Builder setDirectoryFilter(Predicate<Path> pathFilter) {
- this.pathFilter = pathFilter;
+ public Builder setPathDiscriminator(UnixGlobPathDiscriminator pathDiscriminator) {
+ this.pathDiscriminator = pathDiscriminator;
return this;
}
/** Executes the glob. */
public List<Path> glob() throws IOException, BadPattern {
return globInternalUninterruptible(
- base, patterns, excludeDirectories, pathFilter, syscalls.get(), executor);
+ base, patterns, pathDiscriminator, syscalls.get(), executor);
}
/**
@@ -441,14 +432,14 @@
* @throws InterruptedException if the thread is interrupted.
*/
public List<Path> globInterruptible() throws IOException, InterruptedException, BadPattern {
- return globInternal(base, patterns, excludeDirectories, pathFilter, syscalls.get(), executor);
+ return globInternal(base, patterns, pathDiscriminator, syscalls.get(), executor);
}
@VisibleForTesting
public long globInterruptibleAndReturnNumGlobTasksForTesting()
throws IOException, InterruptedException, BadPattern {
return globInternalAndReturnNumGlobTasksForTesting(
- base, patterns, excludeDirectories, pathFilter, syscalls.get(), executor);
+ base, patterns, pathDiscriminator, syscalls.get(), executor);
}
/**
@@ -456,8 +447,7 @@
* non-null argument.
*/
public Future<List<Path>> globAsync() throws BadPattern {
- return globAsyncInternal(
- base, patterns, excludeDirectories, pathFilter, syscalls.get(), executor);
+ return globAsyncInternal(base, patterns, pathDiscriminator, syscalls.get(), executor);
}
}
@@ -522,9 +512,11 @@
/**
* Performs wildcard globbing: returns the list of filenames that match any of {@code patterns}
- * relative to {@code base}. Directories are traversed if and only if they match {@code
- * dirPred}. The predicate is also called for the root of the traversal. The order of the
- * returned list is unspecified.
+ * relative to {@code base}. Directories are traversed if and only if they return true from
+ * {@code pathDiscriminator.shouldTraverseDirectory}. The predicate is also called for the root
+ * of the traversal. {@code pathDiscriminator.shouldIncludePathInResult} is called to determine
+ * if a directory result should be included in the output. The The order of the returned list is
+ * unspecified.
*
* <p>Patterns may include "*" and "?", but not "[a-z]".
*
@@ -538,12 +530,11 @@
List<Path> glob(
Path base,
Collection<String> patterns,
- boolean excludeDirectories,
- Predicate<Path> dirPred,
+ UnixGlobPathDiscriminator pathDiscriminator,
FilesystemCalls syscalls)
throws IOException, InterruptedException, BadPattern {
try {
- return globAsync(base, patterns, excludeDirectories, dirPred, syscalls).get();
+ return globAsync(base, patterns, pathDiscriminator, syscalls).get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
Throwables.propagateIfPossible(cause, IOException.class);
@@ -554,13 +545,12 @@
List<Path> globUninterruptible(
Path base,
Collection<String> patterns,
- boolean excludeDirectories,
- Predicate<Path> dirPred,
+ UnixGlobPathDiscriminator pathDiscriminator,
FilesystemCalls syscalls)
throws IOException, BadPattern {
try {
return Uninterruptibles.getUninterruptibly(
- globAsync(base, patterns, excludeDirectories, dirPred, syscalls));
+ globAsync(base, patterns, pathDiscriminator, syscalls));
} catch (ExecutionException e) {
Throwable cause = e.getCause();
Throwables.propagateIfPossible(cause, IOException.class);
@@ -580,8 +570,7 @@
Future<List<Path>> globAsync(
Path base,
Collection<String> patterns,
- boolean excludeDirectories,
- Predicate<Path> dirPred,
+ UnixGlobPathDiscriminator pathDiscriminator,
FilesystemCalls syscalls)
throws BadPattern {
FileStatus baseStat;
@@ -610,9 +599,10 @@
++numRecursivePatterns;
}
}
- GlobTaskContext context = numRecursivePatterns > 1
- ? new RecursiveGlobTaskContext(splitPattern, excludeDirectories, dirPred, syscalls)
- : new GlobTaskContext(splitPattern, excludeDirectories, dirPred, syscalls);
+ GlobTaskContext context =
+ numRecursivePatterns > 1
+ ? new RecursiveGlobTaskContext(splitPattern, pathDiscriminator, syscalls)
+ : new GlobTaskContext(splitPattern, pathDiscriminator, syscalls);
context.queueGlob(base, baseStat.isDirectory(), 0);
}
} finally {
@@ -657,10 +647,9 @@
@Override
public String toString() {
return String.format(
- "%s glob(include=[%s], exclude_directories=%s)",
+ "%s glob(include=[%s])",
base.getPathString(),
- "\"" + Joiner.on("\", \"").join(context.patternParts) + "\"",
- context.excludeDirectories);
+ "\"" + Joiner.on("\", \"").join(context.patternParts) + "\"");
}
});
}
@@ -720,18 +709,15 @@
/** A context for evaluating all the subtasks of a single top-level glob task. */
private class GlobTaskContext {
private final String[] patternParts;
- private final boolean excludeDirectories;
- private final Predicate<Path> dirPred;
+ private final UnixGlobPathDiscriminator pathDiscriminator;
private final FilesystemCalls syscalls;
GlobTaskContext(
String[] patternParts,
- boolean excludeDirectories,
- Predicate<Path> dirPred,
+ UnixGlobPathDiscriminator pathDiscriminator,
FilesystemCalls syscalls) {
this.patternParts = patternParts;
- this.excludeDirectories = excludeDirectories;
- this.dirPred = dirPred;
+ this.pathDiscriminator = pathDiscriminator;
this.syscalls = syscalls;
}
@@ -779,10 +765,9 @@
private RecursiveGlobTaskContext(
String[] patternParts,
- boolean excludeDirectories,
- Predicate<Path> dirPred,
+ UnixGlobPathDiscriminator pathDiscriminator,
FilesystemCalls syscalls) {
- super(patternParts, excludeDirectories, dirPred, syscalls);
+ super(patternParts, pathDiscriminator, syscalls);
}
@Override
@@ -811,14 +796,14 @@
*/
private void reallyGlob(Path base, boolean baseIsDir, int idx, GlobTaskContext context)
throws IOException {
- if (baseIsDir && !context.dirPred.apply(base)) {
+
+ if (baseIsDir && !context.pathDiscriminator.shouldTraverseDirectory(base)) {
+ maybeAddResult(context, base, baseIsDir);
return;
}
if (idx == context.patternParts.length) { // Base case.
- if (!(context.excludeDirectories && baseIsDir)) {
- results.add(base);
- }
+ maybeAddResult(context, base, baseIsDir);
return;
}
@@ -868,6 +853,12 @@
}
}
+ private void maybeAddResult(GlobTaskContext context, Path base, boolean isDirectory) {
+ if (context.pathDiscriminator.shouldIncludePathInResult(base, isDirectory)) {
+ results.add(base);
+ }
+ }
+
/**
* Process symlinks asynchronously. If we should used readdir(..., Symlinks.FOLLOW), that would
* result in a sequential symlink resolution with many file system implementations. If the
@@ -894,7 +885,7 @@
if (isDir) {
context.queueGlob(path, /* baseIsDir= */ true, idx + (isRecursivePattern ? 0 : 1));
} else if (idx + 1 == context.patternParts.length) {
- results.add(path);
+ maybeAddResult(context, path, /* isDirectory= */ false);
}
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixGlobPathDiscriminator.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlobPathDiscriminator.java
new file mode 100644
index 0000000..04e49b0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlobPathDiscriminator.java
@@ -0,0 +1,41 @@
+// Copyright 2022 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 com.google.errorprone.annotations.CheckReturnValue;
+
+/**
+ * Interface that provides details on how UnixGlob should discriminate between different Paths.
+ * Instances of this interface will be handed either real directories or real files after symlink
+ * resolution and excluding special files.
+ */
+@CheckReturnValue
+public interface UnixGlobPathDiscriminator {
+
+ /**
+ * Determine if UnixGlob should enumerate entries in this directory and traverse it during
+ * recursive globbing. Defaults to true.
+ */
+ default boolean shouldTraverseDirectory(Path path) {
+ return true;
+ }
+
+ /**
+ * Determine if UnixGlob should include the given path in a {@code List<Path>} result. Defaults to
+ * true;
+ */
+ default boolean shouldIncludePathInResult(Path path, boolean isDirectory) {
+ return true;
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/BUILD b/src/test/java/com/google/devtools/build/lib/packages/BUILD
index 61ef029..12a0a08 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/packages/BUILD
@@ -70,6 +70,7 @@
"//src/main/java/com/google/devtools/build/lib/exec:test_policy",
"//src/main/java/com/google/devtools/build/lib/packages",
"//src/main/java/com/google/devtools/build/lib/packages:exec_group",
+ "//src/main/java/com/google/devtools/build/lib/packages:globber",
"//src/main/java/com/google/devtools/build/lib/pkgcache",
"//src/main/java/com/google/devtools/build/lib/runtime/commands",
"//src/main/java/com/google/devtools/build/lib/skyframe:tests_for_target_pattern_value",
diff --git a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
index b830b661..786942f 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
@@ -142,7 +142,7 @@
@Test
public void testIgnoredDirectory() throws Exception {
createCache(PathFragment.create("isolated/foo"));
- List<Path> paths = cache.safeGlobUnsorted("**/*.js", true).get();
+ List<Path> paths = cache.safeGlobUnsorted("**/*.js", Globber.Operation.FILES).get();
assertPathsAre(
paths,
"/workspace/isolated/first.js",
@@ -153,7 +153,7 @@
@Test
public void testSafeGlob() throws Exception {
- List<Path> paths = cache.safeGlobUnsorted("*.js", false).get();
+ List<Path> paths = cache.safeGlobUnsorted("*.js", Globber.Operation.FILES_AND_DIRS).get();
assertPathsAre(paths,
"/workspace/isolated/first.js", "/workspace/isolated/second.js");
}
@@ -161,7 +161,9 @@
@Test
public void testSafeGlobInvalidPattern() throws Exception {
String invalidPattern = "Foo?.txt";
- assertThrows(BadGlobException.class, () -> cache.safeGlobUnsorted(invalidPattern, false).get());
+ assertThrows(
+ BadGlobException.class,
+ () -> cache.safeGlobUnsorted(invalidPattern, Globber.Operation.FILES_AND_DIRS).get());
}
@Test
@@ -181,38 +183,53 @@
assertThat(cache.getKeySet()).isEmpty();
cache.getGlobUnsorted("*.java");
- assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false));
+ assertThat(cache.getKeySet())
+ .containsExactly(Pair.of("*.java", Globber.Operation.FILES_AND_DIRS));
cache.getGlobUnsorted("*.java");
- assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false));
+ assertThat(cache.getKeySet())
+ .containsExactly(Pair.of("*.java", Globber.Operation.FILES_AND_DIRS));
cache.getGlobUnsorted("*.js");
- assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.js", false));
+ assertThat(cache.getKeySet())
+ .containsExactly(
+ Pair.of("*.java", Globber.Operation.FILES_AND_DIRS),
+ Pair.of("*.js", Globber.Operation.FILES_AND_DIRS));
- cache.getGlobUnsorted("*.java", true);
- assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.js", false),
- Pair.of("*.java", true));
+ cache.getGlobUnsorted("*.java", Globber.Operation.FILES);
+ assertThat(cache.getKeySet())
+ .containsExactly(
+ Pair.of("*.java", Globber.Operation.FILES_AND_DIRS),
+ Pair.of("*.js", Globber.Operation.FILES_AND_DIRS),
+ Pair.of("*.java", Globber.Operation.FILES));
assertThrows(BadGlobException.class, () -> cache.getGlobUnsorted("invalid?"));
- assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.js", false),
- Pair.of("*.java", true));
+ assertThat(cache.getKeySet())
+ .containsExactly(
+ Pair.of("*.java", Globber.Operation.FILES_AND_DIRS),
+ Pair.of("*.js", Globber.Operation.FILES_AND_DIRS),
+ Pair.of("*.java", Globber.Operation.FILES));
cache.getGlobUnsorted("foo/first.*");
- assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.java", true),
- Pair.of("*.js", false), Pair.of("foo/first.*", false));
+ assertThat(cache.getKeySet())
+ .containsExactly(
+ Pair.of("*.java", Globber.Operation.FILES_AND_DIRS),
+ Pair.of("*.java", Globber.Operation.FILES),
+ Pair.of("*.js", Globber.Operation.FILES_AND_DIRS),
+ Pair.of("foo/first.*", Globber.Operation.FILES_AND_DIRS));
}
@Test
public void testGlob() throws Exception {
- assertEmpty(cache.globUnsorted(list("*.java"), NONE, false, true));
+ assertEmpty(cache.globUnsorted(list("*.java"), NONE, Globber.Operation.FILES, true));
- assertThat(cache.globUnsorted(list("*.*"), NONE, false, true))
+ assertThat(cache.globUnsorted(list("*.*"), NONE, Globber.Operation.FILES, true))
.containsExactly("first.js", "first.txt", "second.js", "second.txt");
- assertThat(cache.globUnsorted(list("*.*"), list("first.js"), false, true))
+ assertThat(cache.globUnsorted(list("*.*"), list("first.js"), Globber.Operation.FILES, true))
.containsExactly("first.txt", "second.js", "second.txt");
- assertThat(cache.globUnsorted(list("*.txt", "first.*"), NONE, false, true))
+ assertThat(cache.globUnsorted(list("*.txt", "first.*"), NONE, Globber.Operation.FILES, true))
.containsExactly("first.txt", "second.txt", "first.js");
}
@@ -225,13 +242,17 @@
@Test
public void testSingleFileExclude_star() throws Exception {
- assertThat(cache.globUnsorted(list("*"), list("first.txt"), false, true))
+ assertThat(
+ cache.globUnsorted(
+ list("*"), list("first.txt"), Globber.Operation.FILES_AND_DIRS, true))
.containsExactly("BUILD", "bar", "first.js", "foo", "second.js", "second.txt");
}
@Test
public void testSingleFileExclude_starStar() throws Exception {
- assertThat(cache.globUnsorted(list("**"), list("first.txt"), false, true))
+ assertThat(
+ cache.globUnsorted(
+ list("**"), list("first.txt"), Globber.Operation.FILES_AND_DIRS, true))
.containsExactly(
"BUILD",
"bar",
@@ -247,48 +268,78 @@
@Test
public void testExcludeAll_star() throws Exception {
- assertThat(cache.globUnsorted(list("*"), list("*"), false, true)).isEmpty();
+ assertThat(cache.globUnsorted(list("*"), list("*"), Globber.Operation.FILES_AND_DIRS, true))
+ .isEmpty();
}
@Test
public void testExcludeAll_star_noMatchesAnyway() throws Exception {
- assertThat(cache.globUnsorted(list("nope"), list("*"), false, true)).isEmpty();
+ assertThat(cache.globUnsorted(list("nope"), list("*"), Globber.Operation.FILES_AND_DIRS, true))
+ .isEmpty();
}
@Test
public void testExcludeAll_starStar() throws Exception {
- assertThat(cache.globUnsorted(list("**"), list("**"), false, true)).isEmpty();
+ assertThat(cache.globUnsorted(list("**"), list("**"), Globber.Operation.FILES_AND_DIRS, true))
+ .isEmpty();
}
@Test
public void testExcludeAll_manual() throws Exception {
- assertThat(cache.globUnsorted(list("**"), list("*", "*/*", "*/*/*"), false, true)).isEmpty();
+ assertThat(
+ cache.globUnsorted(
+ list("**"), list("*", "*/*", "*/*/*"), Globber.Operation.FILES_AND_DIRS, true))
+ .isEmpty();
}
@Test
public void testSingleFileExcludeDoesntMatch() throws Exception {
- assertThat(cache.globUnsorted(list("first.txt"), list("nope.txt"), false, true))
+ assertThat(
+ cache.globUnsorted(
+ list("first.txt"), list("nope.txt"), Globber.Operation.FILES_AND_DIRS, true))
.containsExactly("first.txt");
}
@Test
public void testExcludeDirectory() throws Exception {
- assertThat(cache.globUnsorted(list("foo/*"), NONE, true, true))
+ assertThat(cache.globUnsorted(list("foo/*"), NONE, Globber.Operation.FILES, true))
.containsExactly("foo/first.js", "foo/second.js");
- assertThat(cache.globUnsorted(list("foo/*"), list("foo"), false, true))
+ assertThat(
+ cache.globUnsorted(list("foo/*"), list("foo"), Globber.Operation.FILES_AND_DIRS, true))
.containsExactly("foo/first.js", "foo/second.js");
}
@Test
public void testChildGlobWithChildExclude() throws Exception {
- assertThat(cache.globUnsorted(list("foo/*"), list("foo/*"), false, true)).isEmpty();
assertThat(
- cache.globUnsorted(list("foo/first.js", "foo/second.js"), list("foo/*"), false, true))
+ cache.globUnsorted(
+ list("foo/*"), list("foo/*"), Globber.Operation.FILES_AND_DIRS, true))
.isEmpty();
- assertThat(cache.globUnsorted(list("foo/first.js"), list("foo/first.js"), false, true))
+ assertThat(
+ cache.globUnsorted(
+ list("foo/first.js", "foo/second.js"),
+ list("foo/*"),
+ Globber.Operation.FILES_AND_DIRS,
+ true))
.isEmpty();
- assertThat(cache.globUnsorted(list("foo/first.js"), list("*/first.js"), false, true)).isEmpty();
- assertThat(cache.globUnsorted(list("foo/first.js"), list("*/*"), false, true)).isEmpty();
+ assertThat(
+ cache.globUnsorted(
+ list("foo/first.js"), list("foo/first.js"), Globber.Operation.FILES_AND_DIRS, true))
+ .isEmpty();
+ assertThat(
+ cache.globUnsorted(
+ list("foo/first.js"), list("*/first.js"), Globber.Operation.FILES_AND_DIRS, true))
+ .isEmpty();
+ assertThat(
+ cache.globUnsorted(
+ list("foo/first.js"), list("*/*"), Globber.Operation.FILES_AND_DIRS, true))
+ .isEmpty();
+ }
+
+ @Test
+ public void testSubpackages() throws Exception {
+ assertThat(cache.globUnsorted(list("**"), list(), Globber.Operation.SUBPACKAGES, true))
+ .containsExactly("sub");
}
private void assertEmpty(Collection<?> glob) {
diff --git a/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java b/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
index d3df2e4..ee56bb8 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
@@ -1264,7 +1264,7 @@
executorService,
-1,
ThreadStateReceiver.NULL_INSTANCE);
- assertThat(globCache.globUnsorted(include, exclude, false, true))
+ assertThat(globCache.globUnsorted(include, exclude, Globber.Operation.FILES_AND_DIRS, true))
.containsExactlyElementsIn(expected);
} finally {
executorService.shutdownNow();
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
index 9a591cf..969be1e 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -170,6 +170,7 @@
"//src/main/java/com/google/devtools/build/lib/exec:single_build_file_cache",
"//src/main/java/com/google/devtools/build/lib/io:inconsistent_filesystem_exception",
"//src/main/java/com/google/devtools/build/lib/packages",
+ "//src/main/java/com/google/devtools/build/lib/packages:globber",
"//src/main/java/com/google/devtools/build/lib/packages/semantics",
"//src/main/java/com/google/devtools/build/lib/pkgcache",
"//src/main/java/com/google/devtools/build/lib/pkgcache:QueryTransitivePackagePreloader",
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/GlobDescriptorTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/GlobDescriptorTest.java
index 7eac47f..d2e7753 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/GlobDescriptorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/GlobDescriptorTest.java
@@ -17,6 +17,7 @@
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Globber;
import com.google.devtools.build.lib.skyframe.serialization.testutils.FsUtils;
import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
import com.google.devtools.build.lib.vfs.PathFragment;
@@ -38,13 +39,13 @@
Root.fromPath(FsUtils.TEST_FILESYSTEM.getPath("/packageRoot")),
PathFragment.create("subdir"),
"pattern",
- /*excludeDirs=*/ false),
+ Globber.Operation.FILES_AND_DIRS),
GlobDescriptor.create(
PackageIdentifier.create("@bar", PathFragment.create("//foo")),
Root.fromPath(FsUtils.TEST_FILESYSTEM.getPath("/anotherPackageRoot")),
PathFragment.create("anotherSubdir"),
"pattern",
- /*excludeDirs=*/ true))
+ Globber.Operation.FILES))
.setVerificationFunction(GlobDescriptorTest::verifyEquivalent);
FsUtils.addDependencies(serializationTester);
serializationTester.runTests();
@@ -62,22 +63,24 @@
Root.fromPath(FsUtils.TEST_FILESYSTEM.getPath("/packageRoot")),
PathFragment.create("subdir"),
"pattern",
- /*excludeDirs=*/ false);
+ Globber.Operation.FILES_AND_DIRS);
- GlobDescriptor sameCopy = GlobDescriptor.create(
- original.getPackageId(),
- original.getPackageRoot(),
- original.getSubdir(),
- original.getPattern(),
- original.excludeDirs());
+ GlobDescriptor sameCopy =
+ GlobDescriptor.create(
+ original.getPackageId(),
+ original.getPackageRoot(),
+ original.getSubdir(),
+ original.getPattern(),
+ original.globberOperation());
assertThat(sameCopy).isSameInstanceAs(original);
- GlobDescriptor diffCopy = GlobDescriptor.create(
- original.getPackageId(),
- original.getPackageRoot(),
- original.getSubdir(),
- original.getPattern(),
- !original.excludeDirs());
+ GlobDescriptor diffCopy =
+ GlobDescriptor.create(
+ original.getPackageId(),
+ original.getPackageRoot(),
+ original.getSubdir(),
+ original.getPattern(),
+ Globber.Operation.FILES);
assertThat(diffCopy).isNotEqualTo(original);
}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/GlobFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/GlobFunctionTest.java
index e451622..c66ec8a 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/GlobFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/GlobFunctionTest.java
@@ -32,6 +32,7 @@
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.events.NullEventHandler;
import com.google.devtools.build.lib.io.InconsistentFilesystemException;
+import com.google.devtools.build.lib.packages.Globber;
import com.google.devtools.build.lib.packages.RuleClassProvider;
import com.google.devtools.build.lib.packages.WorkspaceFileValue;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
@@ -332,12 +333,17 @@
// Each "equality group" forms a set of elements that are all equals() to one another,
// and also produce the same hashCode.
new EqualsTester()
- .addEqualityGroup(runGlob(false, "no-such-file")) // Matches nothing.
- .addEqualityGroup(runGlob(false, "BUILD"), runGlob(true, "BUILD")) // Matches BUILD.
- .addEqualityGroup(runGlob(false, "**")) // Matches lots of things.
.addEqualityGroup(
- runGlob(false, "f*o/bar*"),
- runGlob(false, "foo/bar*")) // Matches foo/bar and foo/barnacle.
+ runGlob("no-such-file", Globber.Operation.FILES_AND_DIRS)) // Matches nothing.
+ .addEqualityGroup(
+ runGlob("BUILD", Globber.Operation.FILES_AND_DIRS),
+ runGlob("BUILD", Globber.Operation.FILES)) // Matches BUILD.
+ .addEqualityGroup(
+ runGlob("**", Globber.Operation.FILES_AND_DIRS)) // Matches lots of things.
+ .addEqualityGroup(
+ runGlob("f*o/bar*", Globber.Operation.FILES_AND_DIRS),
+ runGlob(
+ "foo/bar*", Globber.Operation.FILES_AND_DIRS)) // Matches foo/bar and foo/barnacle.
.testEquals();
}
@@ -417,11 +423,11 @@
}
private void assertGlobMatches(String pattern, String... expecteds) throws Exception {
- assertGlobMatches(false, pattern, expecteds);
+ assertGlobMatches(pattern, Globber.Operation.FILES_AND_DIRS, expecteds);
}
- private void assertGlobMatches(boolean excludeDirs, String pattern, String... expecteds)
- throws Exception {
+ private void assertGlobMatches(
+ String pattern, Globber.Operation globberOperation, String... expecteds) throws Exception {
// The order requirement is not strictly necessary -- a change to GlobFunction semantics that
// changes the output order is fine, but we require that the order be the same here to detect
// potential non-determinism in output order, which would be bad.
@@ -430,27 +436,28 @@
// directories.
assertThat(
Iterables.transform(
- runGlob(excludeDirs, pattern).getMatches().toList(), Functions.toStringFunction()))
+ runGlob(pattern, globberOperation).getMatches().toList(),
+ Functions.toStringFunction()))
.containsExactlyElementsIn(ImmutableList.copyOf(expecteds))
.inOrder();
}
private void assertGlobWithoutDirsMatches(String pattern, String... expecteds) throws Exception {
- assertGlobMatches(true, pattern, expecteds);
+ assertGlobMatches(pattern, Globber.Operation.FILES, expecteds);
}
private void assertGlobsEqual(String pattern1, String pattern2) throws Exception {
- GlobValue value1 = runGlob(false, pattern1);
- GlobValue value2 = runGlob(false, pattern2);
+ GlobValue value1 = runGlob(pattern1, Globber.Operation.FILES_AND_DIRS);
+ GlobValue value2 = runGlob(pattern2, Globber.Operation.FILES_AND_DIRS);
new EqualsTester()
.addEqualityGroup(value1, value2)
.testEquals();
}
- private GlobValue runGlob(boolean excludeDirs, String pattern) throws Exception {
+ private GlobValue runGlob(String pattern, Globber.Operation globberOperation) throws Exception {
SkyKey skyKey =
GlobValue.key(
- PKG_ID, Root.fromPath(root), pattern, excludeDirs, PathFragment.EMPTY_FRAGMENT);
+ PKG_ID, Root.fromPath(root), pattern, globberOperation, PathFragment.EMPTY_FRAGMENT);
EvaluationResult<SkyValue> result =
evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS);
if (result.hasError()) {
@@ -533,7 +540,11 @@
InvalidGlobPatternException.class,
() ->
GlobValue.key(
- PKG_ID, Root.fromPath(root), pattern, false, PathFragment.EMPTY_FRAGMENT));
+ PKG_ID,
+ Root.fromPath(root),
+ pattern,
+ Globber.Operation.FILES_AND_DIRS,
+ PathFragment.EMPTY_FRAGMENT));
}
/**
@@ -681,7 +692,12 @@
differencer.inject(ImmutableMap.of(FileValue.key(pkgRootedPath), pkgDirValue));
String expectedMessage = "/root/workspace/pkg is no longer an existing directory";
SkyKey skyKey =
- GlobValue.key(PKG_ID, Root.fromPath(root), "*/foo", false, PathFragment.EMPTY_FRAGMENT);
+ GlobValue.key(
+ PKG_ID,
+ Root.fromPath(root),
+ "*/foo",
+ Globber.Operation.FILES_AND_DIRS,
+ PathFragment.EMPTY_FRAGMENT);
EvaluationResult<GlobValue> result =
evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS);
assertThat(result.hasError()).isTrue();
@@ -705,7 +721,12 @@
DirectoryListingStateValue.key(fooBarDirRootedPath), fooBarDirListingValue));
String expectedMessage = "/root/workspace/pkg/foo/bar/wiz is no longer an existing directory.";
SkyKey skyKey =
- GlobValue.key(PKG_ID, Root.fromPath(root), "**/wiz", false, PathFragment.EMPTY_FRAGMENT);
+ GlobValue.key(
+ PKG_ID,
+ Root.fromPath(root),
+ "**/wiz",
+ Globber.Operation.FILES_AND_DIRS,
+ PathFragment.EMPTY_FRAGMENT);
EvaluationResult<GlobValue> result =
evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS);
assertThat(result.hasError()).isTrue();
@@ -776,7 +797,11 @@
"readdir and stat disagree about whether " + fileRootedPath.asPath() + " is a symlink";
SkyKey skyKey =
GlobValue.key(
- PKG_ID, Root.fromPath(root), "foo/bar/wiz/*", false, PathFragment.EMPTY_FRAGMENT);
+ PKG_ID,
+ Root.fromPath(root),
+ "foo/bar/wiz/*",
+ Globber.Operation.FILES_AND_DIRS,
+ PathFragment.EMPTY_FRAGMENT);
EvaluationResult<GlobValue> result =
evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS);
assertThat(result.hasError()).isTrue();
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/BUILD b/src/test/java/com/google/devtools/build/lib/vfs/BUILD
index 76fdd0d..f764760 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/vfs/BUILD
@@ -67,6 +67,7 @@
"//src/test/java/com/google/devtools/build/lib/testutil:TestThread",
"//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
"//src/test/java/com/google/devtools/build/lib/vfs/util",
+ "//src/test/java/com/google/devtools/build/lib/vfs/util:test_glob_path_discriminator",
"//third_party:guava",
"//third_party:guava-testlib",
"//third_party:junit4",
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
index 28f6ab5..0463f57 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java
@@ -16,12 +16,12 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
-import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import com.google.devtools.build.lib.vfs.util.TestUnixGlobPathDiscriminator;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
@@ -37,15 +37,14 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-/**
- * Tests {@link UnixGlob}
- */
+/** Tests {@link UnixGlob} */
@RunWith(JUnit4.class)
public class GlobTest {
@@ -55,7 +54,7 @@
private Path throwOnStat = null;
@Before
- public final void initializeFileSystem() throws Exception {
+ public final void initializeFileSystem() throws Exception {
fs =
new InMemoryFileSystem(DigestHashFunction.SHA256) {
@Override
@@ -77,10 +76,12 @@
}
};
tmpPath = fs.getPath("/globtmp");
- for (String dir : ImmutableList.of("foo/bar/wiz",
- "foo/barnacle/wiz",
- "food/barnacle/wiz",
- "fool/barnacle/wiz")) {
+
+ final ImmutableList<String> directories =
+ ImmutableList.of(
+ "foo/bar/wiz", "foo/barnacle/wiz", "food/barnacle/wiz", "fool/barnacle/wiz");
+
+ for (String dir : directories) {
FileSystemUtils.createDirectoryAndParents(tmpPath.getRelative(dir));
}
FileSystemUtils.createEmptyFile(tmpPath.getRelative("foo/bar/wiz/file"));
@@ -93,7 +94,7 @@
@Test
public void testQuestionMarkMatch() throws Exception {
- assertGlobMatches("foo?", /* => */"food", "fool");
+ assertGlobMatches("foo?", /* => */ "food", "fool");
}
@Test
@@ -103,48 +104,48 @@
@Test
public void testStartsWithStar() throws Exception {
- assertGlobMatches("*oo", /* => */"foo");
+ assertGlobMatches("*oo", /* => */ "foo");
}
@Test
public void testStartsWithStarWithMiddleStar() throws Exception {
- assertGlobMatches("*f*o", /* => */"foo");
+ assertGlobMatches("*f*o", /* => */ "foo");
}
@Test
public void testEndsWithStar() throws Exception {
- assertGlobMatches("foo*", /* => */"foo", "food", "fool");
+ assertGlobMatches("foo*", /* => */ "foo", "food", "fool");
}
@Test
public void testEndsWithStarWithMiddleStar() throws Exception {
- assertGlobMatches("f*oo*", /* => */"foo", "food", "fool");
+ assertGlobMatches("f*oo*", /* => */ "foo", "food", "fool");
}
@Test
public void testMiddleStar() throws Exception {
- assertGlobMatches("f*o", /* => */"foo");
+ assertGlobMatches("f*o", /* => */ "foo");
}
@Test
public void testTwoMiddleStars() throws Exception {
- assertGlobMatches("f*o*o", /* => */"foo");
+ assertGlobMatches("f*o*o", /* => */ "foo");
}
@Test
public void testSingleStarPatternWithNamedChild() throws Exception {
- assertGlobMatches("*/bar", /* => */"foo/bar");
+ assertGlobMatches("*/bar", /* => */ "foo/bar");
}
@Test
public void testSingleStarPatternWithChildGlob() throws Exception {
- assertGlobMatches("*/bar*", /* => */
- "foo/bar", "foo/barnacle", "food/barnacle", "fool/barnacle");
+ 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");
+ assertGlobMatches("foo/*/wiz", /* => */ "foo/bar/wiz", "foo/barnacle/wiz");
}
@Test
@@ -160,10 +161,93 @@
}
@Test
+ public void testFilteredResults_noDirs() throws Exception {
+
+ assertThat(
+ new UnixGlob.Builder(tmpPath)
+ .addPatterns("**")
+ .setPathDiscriminator(
+ new TestUnixGlobPathDiscriminator(
+ p -> /*traversalPredicate=*/ true,
+ /*resultPredicate=*/ (p, isDir) -> !isDir))
+ .globInterruptible())
+ .containsExactlyElementsIn(resolvePaths("foo/bar/wiz/file"));
+ }
+
+ @Test
+ public void testFilteredResults_noFiles() throws Exception {
+ assertThat(
+ new UnixGlob.Builder(tmpPath)
+ .addPatterns("**")
+ .setPathDiscriminator(
+ new TestUnixGlobPathDiscriminator(
+ /*traversalPredicate=*/ p -> true,
+ /*resultPredicate=*/ (p, isDir) -> isDir))
+ .globInterruptible())
+ .containsExactlyElementsIn(
+ resolvePaths(
+ "",
+ "foo",
+ "foo/bar",
+ "foo/bar/wiz",
+ "foo/barnacle",
+ "foo/barnacle/wiz",
+ "food",
+ "food/barnacle",
+ "food/barnacle/wiz",
+ "fool",
+ "fool/barnacle",
+ "fool/barnacle/wiz"));
+ }
+
+ @Test
+ public void testFilteredResults_pathMatch() throws Exception {
+
+ Path wanted = tmpPath.getRelative("food/barnacle/wiz");
+
+ assertThat(
+ new UnixGlob.Builder(tmpPath)
+ .addPatterns("**")
+ .setPathDiscriminator(
+ new TestUnixGlobPathDiscriminator(
+ /*traversalPredicate=*/ p -> true,
+ /*resultPredicate=*/ (path, isDir) -> path.equals(wanted)))
+ .globInterruptible())
+ .containsExactly(wanted);
+ }
+
+ @Test
+ public void testTraversal_onlyFoo() throws Exception {
+ // Use a directory traversal filter to only walk the root dir and "foo", but not "fool or "food"
+ // So we'll end up the directories, "fool" and "food", but not sub-dirs.
+ assertThat(
+ new UnixGlob.Builder(tmpPath)
+ .addPatterns("**")
+ .setPathDiscriminator(
+ new TestUnixGlobPathDiscriminator(
+ /*traversalPredicate=*/ path ->
+ path.equals(tmpPath)
+ || path.getPathString().contains("foo/")
+ || path.getPathString().endsWith("foo"),
+ /*resultPredicate=*/ (x, isDir) -> true))
+ .globInterruptible())
+ .containsExactlyElementsIn(
+ resolvePaths(
+ "",
+ "foo",
+ "foo/bar",
+ "foo/bar/wiz",
+ "foo/bar/wiz/file",
+ "foo/barnacle",
+ "foo/barnacle/wiz",
+ "fool",
+ "food"));
+ }
+
+ @Test
public void testGlobWithNonExistentBase() throws Exception {
- Collection<Path> globResult = UnixGlob.forPath(fs.getPath("/does/not/exist"))
- .addPattern("*.txt")
- .globInterruptible();
+ Collection<Path> globResult =
+ UnixGlob.forPath(fs.getPath("/does/not/exist")).addPattern("*.txt").globInterruptible();
assertThat(globResult).isEmpty();
}
@@ -172,27 +256,19 @@
assertGlobMatches("foo/bar/wiz/file/*" /* => nothing */);
}
- private void assertGlobMatches(String pattern, String... expecteds)
- throws Exception {
+ private void assertGlobMatches(String pattern, String... expecteds) throws Exception {
assertGlobMatches(Collections.singleton(pattern), expecteds);
}
- private void assertGlobMatches(Collection<String> pattern,
- String... expecteds)
- throws Exception {
- assertThat(
- new UnixGlob.Builder(tmpPath)
- .addPatterns(pattern)
- .globInterruptible())
- .containsExactlyElementsIn(resolvePaths(expecteds));
+ private void assertGlobMatches(Collection<String> pattern, String... expecteds) throws Exception {
+ assertThat(new UnixGlob.Builder(tmpPath).addPatterns(pattern).globInterruptible())
+ .containsExactlyElementsIn(resolvePaths(expecteds));
}
private Set<Path> resolvePaths(String... relativePaths) {
Set<Path> expectedFiles = new HashSet<>();
for (String expected : relativePaths) {
- Path file = expected.equals(".")
- ? tmpPath
- : tmpPath.getRelative(expected);
+ Path file = expected.equals(".") ? tmpPath : tmpPath.getRelative(expected);
expectedFiles.add(file);
}
return expectedFiles;
@@ -270,9 +346,7 @@
assertIllegalPattern("foo//bar");
}
- /**
- * Tests that globs can contain Java regular expression special characters
- */
+ /** Tests that globs can contain Java regular expression special characters */
@Test
public void testSpecialRegexCharacter() throws Exception {
Path tmpPath2 = fs.getPath("/globtmp2");
@@ -357,19 +431,18 @@
@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());
-
+ 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());
+ private void assertGlobMatchesAnyOrder(ArrayList<String> patterns, String... paths)
+ throws Exception {
+ assertThat(resolvePaths(paths))
+ .containsExactlyElementsIn(
+ new UnixGlob.Builder(tmpPath).addPatterns(patterns).globInterruptible());
}
private void assertIllegalPattern(String pattern) throws Exception {
@@ -419,16 +492,20 @@
Predicate<Path> interrupterPredicate =
new Predicate<Path>() {
@Override
- public boolean apply(Path input) {
+ public boolean test(Path input) {
mainThread.interrupt();
return true;
}
};
+ UnixGlobPathDiscriminator interrupterDiscriminator =
+ new TestUnixGlobPathDiscriminator(
+ /*traversalPredicate=*/ interrupterPredicate, /*resultPredicate=*/ (x, isDir) -> true);
+
Future<?> globResult =
new UnixGlob.Builder(tmpPath)
.addPattern("**")
- .setDirectoryFilter(interrupterPredicate)
+ .setPathDiscriminator(interrupterDiscriminator)
.setExecutor(executor)
.globAsync();
assertThrows(InterruptedException.class, () -> globResult.get());
@@ -450,20 +527,25 @@
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;
- }
- };
+ Predicate<Path> interrupterPredicate =
+ new Predicate<Path>() {
+ @Override
+ public boolean test(Path input) {
+ if (!sentInterrupt.getAndSet(true)) {
+ mainThread.interrupt();
+ }
+ return true;
+ }
+ };
+
+ UnixGlobPathDiscriminator interrupterDiscriminator =
+ new TestUnixGlobPathDiscriminator(
+ /*traversalPredicate=*/ interrupterPredicate, /*resultPredicate=*/ (x, isDir) -> true);
List<Path> result =
new UnixGlob.Builder(tmpPath)
.addPatterns("**", "*")
- .setDirectoryFilter(interrupterPredicate)
+ .setPathDiscriminator(interrupterDiscriminator)
.setExecutor(executor)
.glob();
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java
index ab515c0..d7e0bd3 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java
@@ -20,6 +20,7 @@
import com.google.common.testing.EqualsTester;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.lib.vfs.util.FileSystems;
+import com.google.devtools.build.lib.vfs.util.TestUnixGlobPathDiscriminator;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -133,16 +134,17 @@
Collection<Path> onlyFiles =
UnixGlob.forPath(fs.getPath(tmpDir.getPath()))
- .addPattern("*")
- .setExcludeDirectories(true)
- .globInterruptible();
+ .addPattern("*")
+ .setPathDiscriminator(
+ new TestUnixGlobPathDiscriminator(p -> true, (p, isDir) -> !isDir))
+ .globInterruptible();
assertPathSet(onlyFiles, aFile.getPath());
Collection<Path> directoriesToo =
UnixGlob.forPath(fs.getPath(tmpDir.getPath()))
- .addPattern("*")
- .setExcludeDirectories(false)
- .globInterruptible();
+ .addPattern("*")
+ .setPathDiscriminator(new TestUnixGlobPathDiscriminator(p -> true, (p, isDir) -> true))
+ .globInterruptible();
assertPathSet(directoriesToo, aFile.getPath(), aDirectory.getPath());
}
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
index 1c2ac8a..07abb57 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java
@@ -20,6 +20,7 @@
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.clock.BlazeClock;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import com.google.devtools.build.lib.vfs.util.TestUnixGlobPathDiscriminator;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -136,10 +137,12 @@
@Test
public void testRecursiveGlobsAreOptimized() throws Exception {
- long numGlobTasks = new UnixGlob.Builder(tmpPath)
- .addPattern("**")
- .setExcludeDirectories(false)
- .globInterruptibleAndReturnNumGlobTasksForTesting();
+ long numGlobTasks =
+ new UnixGlob.Builder(tmpPath)
+ .addPattern("**")
+ .setPathDiscriminator(
+ new TestUnixGlobPathDiscriminator(p -> true, (p, isDir) -> !isDir))
+ .globInterruptibleAndReturnNumGlobTasksForTesting();
// The old glob implementation used to use 41 total glob tasks.
// Yes, checking for an exact value here is super brittle, but it lets us catch performance
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/util/BUILD b/src/test/java/com/google/devtools/build/lib/vfs/util/BUILD
index 379d2ea..30ef7d8 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/util/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/vfs/util/BUILD
@@ -26,7 +26,10 @@
java_library(
name = "util_internal",
testonly = 1,
- srcs = glob(["*.java"]),
+ srcs = [
+ "FileSystems.java",
+ "FsApparatus.java",
+ ],
deps = [
"//src/main/java/com/google/devtools/build/lib/clock",
"//src/main/java/com/google/devtools/build/lib/util:os",
@@ -38,3 +41,13 @@
"//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
],
)
+
+java_library(
+ name = "test_glob_path_discriminator",
+ testonly = 1,
+ srcs = ["TestUnixGlobPathDiscriminator.java"],
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib/vfs",
+ "//third_party:error_prone_annotations",
+ ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/util/TestUnixGlobPathDiscriminator.java b/src/test/java/com/google/devtools/build/lib/vfs/util/TestUnixGlobPathDiscriminator.java
new file mode 100644
index 0000000..50d81d6
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/util/TestUnixGlobPathDiscriminator.java
@@ -0,0 +1,47 @@
+// Copyright 2022 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.util;
+
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.UnixGlobPathDiscriminator;
+import com.google.errorprone.annotations.CheckReturnValue;
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+
+/**
+ * Test version of UnixGlobPathDiscriminator that accepts predicate/bipredicate for handling
+ * specific use-cases without creating a new class.
+ */
+@CheckReturnValue
+public final class TestUnixGlobPathDiscriminator implements UnixGlobPathDiscriminator {
+
+ private final Predicate<Path> traversalPredicate;
+ private final BiPredicate<Path, Boolean> resultPredicate;
+
+ public TestUnixGlobPathDiscriminator(
+ Predicate<Path> traversalPredicate, BiPredicate<Path, Boolean> resultPredicate) {
+ this.traversalPredicate = traversalPredicate;
+ this.resultPredicate = resultPredicate;
+ }
+
+ @Override
+ public boolean shouldTraverseDirectory(Path path) {
+ return traversalPredicate.test(path);
+ }
+
+ @Override
+ public boolean shouldIncludePathInResult(Path path, boolean isDirectory) {
+ return resultPredicate.test(path, isDirectory);
+ }
+}