Part 2 Implementation for new 'subpackages()` built-in helper function.
Design proposal: https://docs.google.com/document/d/13UOT0GoQofxDW40ILzH2sWpUOmuYy6QZ7CUmhej9vgk/edit#
Overview:
Add StarlarkNativeModule 'subpackages' function with parameters that mirror glob()
PiperOrigin-RevId: 422652954
diff --git a/src/main/java/com/google/devtools/build/docgen/templates/be/functions.vm b/src/main/java/com/google/devtools/build/docgen/templates/be/functions.vm
index 4824b14..08634cf 100644
--- a/src/main/java/com/google/devtools/build/docgen/templates/be/functions.vm
+++ b/src/main/java/com/google/devtools/build/docgen/templates/be/functions.vm
@@ -19,6 +19,7 @@
<li><a href="#exports_files">exports_files</a></li>
<li><a href="#glob">glob</a></li>
<li><a href="#select">select</a></li>
+ <li><a href="#subpackages">subpackages</a></li>
</ul>
</div>
#end
@@ -636,48 +637,53 @@
<li><code>select</code> works with most, but not all, attributes. Incompatible
attributes are marked <code>nonconfigurable</code> in their documentation.
- </li>
-</ul>
+<!-- =================================================================
+ subpackages()
+ =================================================================
+ -->
-By default, Bazel produces the following error when no conditions match:
-<pre class="code">
-Configurable attribute "foo" doesn't match this configuration (would a default
-condition help?).
-Conditions checked:
- //pkg:conditionA.
- //pkg:conditionB.
-</pre>
+<h2 id="subpackages">subpackages</h2>
-You can signal more precise errors with <code>no_match_error</code>.
-
-<h3 id="select_example">Examples</h3>
-
-<pre class="code">
-config_setting(
- name = "windows",
- values = {
- "crosstool_top": "//crosstools/windows",
- },
-)
-
-cc_binary(
- name = "multiplatform_app",
- ...
- linkopts = select({
- ":windows": [
- "-Wl,windows_support1.lib",
- "-Wl,windows_support2.lib",
- ],
- "//conditions:default": [],
- ...
-)
-</pre>
-
-<p>In the above example, <code>multiplatform_app</code> links with additional
- options when invoked with <code>bazel build //pkg:multiplatform_app
- --crosstool_top=//crosstools/windows </code>.
+<pre>subpackages(include, exclude=[], allow_empty=True)</pre>
<p>
+ <code>subpackages()</code> is a helper function, similar to <code>glob()</code>
+ that lists subpackages instead of files and directories. It uses the same
+ path patterns as <code>glob()</code> and can match any subpackage that is a
+ direct decendant of the currently loading BUILD file. See <a
+ href="#glob">glob</a> for detailed explinations and example of include and
+ exclude patterns.
+</p>
+
+<p>
+ The resulting list of subpackages returned is in sorted order and contains a
+ paths relative to the current loading package that match the given patterns in
+ <code>include</code> and not those in <code>exclude</code>.
+
+<h3 id=subpackages_example">Example</h3>
+
+<p>
+ The following example lists all the direct subpackages for the package <code>foo/BUILD</code>
+
+<pre class="code">
+# The following BUILD files exist:
+# foo/BUILD
+# foo/bar/baz/BUILD
+# foo/sub/BUILD
+# foo/sub/deeper/BUILD
+#
+# In foo/BUILD a call to
+subs = subpackages(include = ["**"])
+
+# results in subs == ["sub", "foo/bar/baz"]`
+#
+# foo/sub/deeper is not included because it is a subpackage of 'foo/sub' not of
+# 'foo'
+</pre>
+
+ <p>
+ In general it is preferred that instead of calling this function directly
+ that users use the 'subpackages' module of
#if (!$singlePage)
#parse("com/google/devtools/build/docgen/templates/be/footer.vm")
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 c1e86cc..b0e9567 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
@@ -601,6 +601,7 @@
Program buildFileProgram,
ImmutableList<String> globs,
ImmutableList<String> globsWithDirs,
+ ImmutableList<String> subpackages,
ImmutableMap<String, Object> predeclared,
ImmutableMap<String, Module> loadedModules,
StarlarkSemantics starlarkSemantics,
@@ -613,6 +614,8 @@
globber.runAsync(globs, ImmutableList.of(), Globber.Operation.FILES, allowEmpty);
globber.runAsync(
globsWithDirs, ImmutableList.of(), Globber.Operation.FILES_AND_DIRS, allowEmpty);
+ globber.runAsync(
+ subpackages, ImmutableList.of(), Globber.Operation.SUBPACKAGES, allowEmpty);
} catch (BadGlobException ex) {
logger.atWarning().withCause(ex).log(
"Suppressing exception for globs=%s, globsWithDirs=%s", globs, globsWithDirs);
@@ -734,6 +737,7 @@
StarlarkFile file,
Collection<String> globs,
Collection<String> globsWithDirs,
+ Collection<String> subpackages,
Map<Location, String> generatorNameByLocation,
Consumer<SyntaxError> errors) {
final boolean[] success = {true};
@@ -747,11 +751,16 @@
// Extract literal glob patterns from calls of the form:
// glob(include = ["pattern"])
// glob(["pattern"])
- // This may spuriously match user-defined functions named glob;
- // that's ok, it's only a heuristic.
+ // subpackages(include = ["pattern"])
+ // This may spuriously match user-defined functions named glob or
+ // subpackages; that's ok, it's only a heuristic.
void extractGlobPatterns(CallExpression call) {
- if (call.getFunction() instanceof Identifier
- && ((Identifier) call.getFunction()).getName().equals("glob")) {
+ if (call.getFunction() instanceof Identifier) {
+ String functionName = ((Identifier) call.getFunction()).getName();
+ if (!functionName.equals("glob") && !functionName.equals("subpackages")) {
+ return;
+ }
+
Expression excludeDirectories = null, include = null;
List<Argument> arguments = call.getArguments();
for (int i = 0; i < arguments.size(); i++) {
@@ -779,7 +788,11 @@
exclude = false;
}
}
- (exclude ? globs : globsWithDirs).add(pattern);
+ if (functionName.equals("glob")) {
+ (exclude ? globs : globsWithDirs).add(pattern);
+ } else {
+ subpackages.add(pattern);
+ }
}
}
}
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 9209f9d..916f312 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
@@ -102,7 +102,6 @@
Globber.Operation op =
excludeDirs.signum() != 0 ? Globber.Operation.FILES : Globber.Operation.FILES_AND_DIRS;
- List<String> matches;
boolean allowEmpty;
if (allowEmptyArgument == Starlark.UNBOUND) {
allowEmpty =
@@ -114,35 +113,7 @@
"expected boolean for argument `allow_empty`, got `%s`", allowEmptyArgument);
}
- try {
- Globber.Token globToken = context.globber.runAsync(includes, excludes, op, allowEmpty);
- matches = context.globber.fetchUnsorted(globToken);
- } catch (IOException e) {
- logger.atWarning().withCause(e).log(
- "Exception processing includes=%s, excludes=%s)", includes, excludes);
- String errorMessage =
- String.format(
- "error globbing [%s]%s: %s",
- Joiner.on(", ").join(includes),
- excludes.isEmpty() ? "" : " - [" + Joiner.on(", ").join(excludes) + "]",
- e.getMessage());
- Location loc = thread.getCallerLocation();
- Event error =
- Package.error(
- loc,
- errorMessage,
- // If there are other IOExceptions that can result from user error, they should be
- // tested for here. Currently FileNotFoundException is not one of those, because globs
- // only encounter that error in the presence of an inconsistent filesystem.
- e instanceof FileSymlinkException
- ? Code.EVAL_GLOBS_SYMLINK_ERROR
- : Code.GLOB_IO_EXCEPTION);
- context.eventHandler.handle(error);
- context.pkgBuilder.setIOException(e, errorMessage, error.getProperty(DetailedExitCode.class));
- matches = ImmutableList.of();
- } catch (BadGlobException e) {
- throw new EvalException(e);
- }
+ List<String> matches = runGlobOperation(context, thread, includes, excludes, op, allowEmpty);
ArrayList<String> result = new ArrayList<>(matches.size());
for (String match : matches) {
@@ -751,4 +722,62 @@
super(msg);
}
}
+
+ @Override
+ public Sequence<?> subpackages(
+ Sequence<?> include, Sequence<?> exclude, boolean allowEmpty, StarlarkThread thread)
+ throws EvalException, InterruptedException {
+ BazelStarlarkContext.from(thread).checkLoadingPhase("native.subpackages");
+ PackageContext context = getContext(thread);
+
+ List<String> includes = Type.STRING_LIST.convert(include, "'subpackages' argument");
+ List<String> excludes = Type.STRING_LIST.convert(exclude, "'subpackages' argument");
+
+ List<String> matches =
+ runGlobOperation(
+ context, thread, includes, excludes, Globber.Operation.SUBPACKAGES, allowEmpty);
+ matches.sort(naturalOrder());
+
+ return StarlarkList.copyOf(thread.mutability(), matches);
+ }
+
+ private List<String> runGlobOperation(
+ PackageContext context,
+ StarlarkThread thread,
+ List<String> includes,
+ List<String> excludes,
+ Globber.Operation operation,
+ boolean allowEmpty)
+ throws EvalException, InterruptedException {
+ try {
+ Globber.Token globToken = context.globber.runAsync(includes, excludes, operation, allowEmpty);
+ return context.globber.fetchUnsorted(globToken);
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log(
+ "Exception processing includes=%s, excludes=%s)", includes, excludes);
+ String errorMessage =
+ String.format(
+ "error globbing [%s]%s op=%s: %s",
+ Joiner.on(", ").join(includes),
+ excludes.isEmpty() ? "" : " - [" + Joiner.on(", ").join(excludes) + "]",
+ operation,
+ e.getMessage());
+ Location loc = thread.getCallerLocation();
+ Event error =
+ Package.error(
+ loc,
+ errorMessage,
+ // If there are other IOExceptions that can result from user error, they should be
+ // tested for here. Currently FileNotFoundException is not one of those, because globs
+ // only encounter that error in the presence of an inconsistent filesystem.
+ e instanceof FileSymlinkException
+ ? Code.EVAL_GLOBS_SYMLINK_ERROR
+ : Code.GLOB_IO_EXCEPTION);
+ context.eventHandler.handle(error);
+ context.pkgBuilder.setIOException(e, errorMessage, error.getProperty(DetailedExitCode.class));
+ return ImmutableList.of();
+ } catch (BadGlobException e) {
+ throw new EvalException(e);
+ }
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java
index 7e7c547..963956d 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java
@@ -146,8 +146,9 @@
List<String> globs = new ArrayList<>(); // unused
if (PackageFactory.checkBuildSyntax(
file,
- globs,
- globs,
+ /*globs=*/ globs,
+ /*globsWithDirs=*/ globs,
+ /*subpackages=*/ globs,
new HashMap<>(),
error ->
localReporter.handle(
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 5aef741..2885689 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
@@ -84,19 +84,32 @@
// Note that the glob's package is assumed to exist which implies that the package's BUILD file
// exists which implies that the package's directory exists.
if (!globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
+ PathFragment subDirFragment =
+ glob.getPackageId().getPackageFragment().getRelative(globSubdir);
+
PackageLookupValue globSubdirPkgLookupValue =
(PackageLookupValue)
env.getValue(
- PackageLookupValue.key(
- PackageIdentifier.create(
- repositoryName,
- glob.getPackageId().getPackageFragment().getRelative(globSubdir))));
+ PackageLookupValue.key(PackageIdentifier.create(repositoryName, subDirFragment)));
if (globSubdirPkgLookupValue == null) {
return null;
}
+
if (globSubdirPkgLookupValue.packageExists()) {
// We crossed the package boundary, that is, pkg/subdir contains a BUILD file and thus
- // defines another package, so glob expansion should not descend into that subdir.
+ // defines another package, so glob expansion should not descend into
+ // that subdir.
+ //
+ // For SUBPACKAGES, we encounter this when the pattern is a recursive ** and we are a
+ // terminal package for that pattern. In that case we should include the subDirFragment
+ // PathFragment (relative to the glob's package) in the GlobValue.getMatches,
+ // otherwise for file/dir matching return EMPTY;
+ if (globberOperation == Globber.Operation.SUBPACKAGES) {
+ return new GlobValue(
+ NestedSetBuilder.<PathFragment>stableOrder()
+ .add(subDirFragment.relativeTo(glob.getPackageId().getPackageFragment()))
+ .build());
+ }
return GlobValue.EMPTY;
} else if (globSubdirPkgLookupValue
instanceof PackageLookupValue.IncorrectRepositoryReferencePackageLookupValue) {
@@ -215,7 +228,7 @@
if (keyToRequest != null) {
subdirMap.put(keyToRequest, dirent);
}
- } else if (globMatchesBareFile) {
+ } else if (globMatchesBareFile && globberOperation != Globber.Operation.SUBPACKAGES) {
sortedResultMap.put(dirent, glob.getSubdir().getRelative(fileName));
}
}
@@ -272,7 +285,7 @@
if (keyToRequest != null) {
symlinkSubdirMap.put(keyToRequest, dirent);
}
- } else if (globMatchesBareFile) {
+ } else if (globMatchesBareFile && globberOperation != Globber.Operation.SUBPACKAGES) {
sortedResultMap.put(dirent, glob.getSubdir().getRelative(fileName));
}
} else {
@@ -314,7 +327,7 @@
addToMatches(fileMatches, matches);
}
}
- } else if (globMatchesBareFile) {
+ } else if (globMatchesBareFile && globberOperation != Globber.Operation.SUBPACKAGES) {
matches.add(glob.getSubdir().getRelative(fileName));
}
}
@@ -352,9 +365,10 @@
private static void addToMatches(Object toAdd, NestedSetBuilder<PathFragment> matches) {
if (toAdd instanceof PathFragment) {
matches.add((PathFragment) toAdd);
- } else {
+ } else if (toAdd instanceof NestedSet) {
matches.addTransitive((NestedSet<PathFragment>) toAdd);
}
+ // else Not actually a valid type and ignore.
}
/**
@@ -373,15 +387,17 @@
if (subdirPattern == null) {
if (glob.globberOperation() == Globber.Operation.FILES) {
return null;
- } else {
- return PackageLookupValue.key(
- PackageIdentifier.create(
- glob.getPackageId().getRepository(),
- glob.getPackageId()
- .getPackageFragment()
- .getRelative(glob.getSubdir())
- .getRelative(fileName)));
}
+
+ // For FILES_AND_DIRS and SUBPACKAGES we want to maybe inspect a
+ // PackageLookupValue for it.
+ return PackageLookupValue.key(
+ PackageIdentifier.create(
+ glob.getPackageId().getRepository(),
+ glob.getPackageId()
+ .getPackageFragment()
+ .getRelative(glob.getSubdir())
+ .getRelative(fileName)));
} else {
// There is some more pattern to match. Get the glob for the subdirectory. Note that this
// directory may also match directly in the case of a pattern that starts with "**", but that
@@ -396,38 +412,55 @@
}
/**
- * Returns matches coming from the directory {@code fileName} if appropriate, either an individual
- * file or a nested set of files.
+ * Returns an Object indicating a match was found for the given fileName in the given
+ * valueRequested. The Object will be one of:
*
- * <p>{@code valueRequested} must be the SkyValue whose key was returned by
- * {@link #getSkyKeyForSubdir} for these parameters.
+ * <ul>
+ * <li>{@code null} if no matches for the given parameters exists
+ * <li>{@code NestedSet<PathFragment>} if a match exists, either because we are looking for
+ * files/directories or the SkyValue is a package and we're globbing for {@link
+ * Globber.Operation.SUBPACKAGES}
+ * </ul>
+ *
+ * <p>{@code valueRequested} must be the SkyValue whose key was returned by {@link
+ * #getSkyKeyForSubdir} for these parameters.
*/
@Nullable
private static Object getSubdirMatchesFromSkyValue(
- String fileName,
- GlobDescriptor glob,
- SkyValue valueRequested) {
+ String fileName, GlobDescriptor glob, SkyValue valueRequested) {
if (valueRequested instanceof GlobValue) {
return ((GlobValue) valueRequested).getMatches();
- } else {
- Preconditions.checkState(
- valueRequested instanceof PackageLookupValue,
- "%s is not a GlobValue or PackageLookupValue (%s %s)",
- valueRequested,
- fileName,
- glob);
- PackageLookupValue packageLookupValue = (PackageLookupValue) valueRequested;
- if (packageLookupValue.packageExists()) {
- // This is a separate package, so ignore it.
- return null;
- } else if (packageLookupValue
- instanceof PackageLookupValue.IncorrectRepositoryReferencePackageLookupValue) {
- // This is a separate repository, so ignore it.
- return null;
- } else {
- return glob.getSubdir().getRelative(fileName);
- }
}
+
+ Preconditions.checkState(
+ valueRequested instanceof PackageLookupValue,
+ "%s is not a GlobValue or PackageLookupValue (%s %s)",
+ valueRequested,
+ fileName,
+ glob);
+
+ PackageLookupValue packageLookupValue = (PackageLookupValue) valueRequested;
+ if (packageLookupValue
+ instanceof PackageLookupValue.IncorrectRepositoryReferencePackageLookupValue) {
+ // This is a separate repository, so ignore it.
+ return null;
+ }
+
+ boolean isSubpackagesOp = glob.globberOperation() == Globber.Operation.SUBPACKAGES;
+ boolean pkgExists = packageLookupValue.packageExists();
+
+ if (!isSubpackagesOp && pkgExists) {
+ // We're in our repo and fileName is a package. Since we're not doing SUBPACKAGES listing, we
+ // do not want to add it to the results.
+ return null;
+ } else if (isSubpackagesOp && !pkgExists) {
+ // We're in our repo and the package exists. Since we're doing SUBPACKAGES listing, we do
+ // want to add fileName to the results.
+ return null;
+ }
+
+ // The fileName should be added to the results of the glob.
+ return glob.getSubdir().getRelative(fileName);
}
/**
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 74bf760..68bb140 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
@@ -135,6 +135,7 @@
@Nullable private final Program prog;
@Nullable private final ImmutableList<String> globs;
@Nullable private final ImmutableList<String> globsWithDirs;
+ @Nullable private final ImmutableList<String> subpackages;
@Nullable private final ImmutableMap<Location, String> generatorMap;
@Nullable private final ImmutableMap<String, Object> predeclared;
@@ -147,11 +148,13 @@
Program prog,
ImmutableList<String> globs,
ImmutableList<String> globsWithDirs,
+ ImmutableList<String> subpackages,
ImmutableMap<Location, String> generatorMap,
ImmutableMap<String, Object> predeclared) {
this.errors = null;
this.prog = prog;
this.globs = globs;
+ this.subpackages = subpackages;
this.globsWithDirs = globsWithDirs;
this.generatorMap = generatorMap;
this.predeclared = predeclared;
@@ -163,6 +166,7 @@
this.prog = null;
this.globs = null;
this.globsWithDirs = null;
+ this.subpackages = null;
this.generatorMap = null;
this.predeclared = null;
}
@@ -1335,6 +1339,7 @@
compiled.prog,
compiled.globs,
compiled.globsWithDirs,
+ compiled.subpackages,
compiled.predeclared,
loadedModules,
starlarkBuiltinsValue.starlarkSemantics,
@@ -1435,9 +1440,11 @@
// - record the generator_name of each top-level macro call
Set<String> globs = new HashSet<>();
Set<String> globsWithDirs = new HashSet<>();
+ Set<String> subpackages = new HashSet<>();
Map<Location, String> generatorMap = new HashMap<>();
ImmutableList.Builder<SyntaxError> errors = ImmutableList.builder();
- if (!PackageFactory.checkBuildSyntax(file, globs, globsWithDirs, generatorMap, errors::add)) {
+ if (!PackageFactory.checkBuildSyntax(
+ file, globs, globsWithDirs, subpackages, generatorMap, errors::add)) {
return new CompiledBuildFile(errors.build());
}
@@ -1485,6 +1492,7 @@
prog,
ImmutableList.copyOf(globs),
ImmutableList.copyOf(globsWithDirs),
+ ImmutableList.copyOf(subpackages),
ImmutableMap.copyOf(generatorMap),
ImmutableMap.copyOf(predeclared));
}
diff --git a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkNativeModuleApi.java b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkNativeModuleApi.java
index 4776b8d..efdb8e5 100644
--- a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkNativeModuleApi.java
+++ b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkNativeModuleApi.java
@@ -237,4 +237,41 @@
+ "<code>REPOSITORY_NAME</code>.",
useStarlarkThread = true)
String repositoryName(StarlarkThread thread) throws EvalException;
+
+ @StarlarkMethod(
+ name = "subpackages",
+ doc =
+ "Returns a new mutable list of every direct subpackage of the current package,"
+ + " regardless of file-system directory depth. List returned is sorted and contains"
+ + " the names of subpackages relative to the current package. It is advised to"
+ + " prefer using the methods in bazel_skylib.subpackages module rather than calling"
+ + " this function directly.",
+ parameters = {
+ @Param(
+ name = "include",
+ allowedTypes = {@ParamType(type = Sequence.class, generic1 = String.class)},
+ positional = false,
+ named = true,
+ doc = "The list of glob patterns to include in subpackages scan."),
+ @Param(
+ name = "exclude",
+ allowedTypes = {@ParamType(type = Sequence.class, generic1 = String.class)},
+ defaultValue = "[]",
+ positional = false,
+ named = true,
+ doc = "The list of glob patterns to exclude from subpackages scan."),
+ @Param(
+ name = "allow_empty",
+ defaultValue = "False",
+ positional = false,
+ named = true,
+ doc =
+ "Whether we fail if the call returns an empty list. By default empty list indicates"
+ + " potential error in BUILD file where the call to subpackages() is"
+ + " superflous. Setting to true allows this function to succeed in that case.")
+ },
+ useStarlarkThread = true)
+ Sequence<?> subpackages(
+ Sequence<?> include, Sequence<?> exclude, boolean allowEmpty, StarlarkThread thread)
+ throws EvalException, InterruptedException;
}
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStarlarkNativeModuleApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStarlarkNativeModuleApi.java
index c22d07c2de..b4fd592 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStarlarkNativeModuleApi.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStarlarkNativeModuleApi.java
@@ -77,6 +77,13 @@
return "";
}
+ @Override
+ public Sequence<?> subpackages(
+ Sequence<?> include, Sequence<?> exclude, boolean allowEmpty, StarlarkThread thread)
+ throws EvalException, InterruptedException {
+ return StarlarkList.of(thread.mutability());
+ }
+
@Nullable
@Override
public Object getValue(String name) throws EvalException {
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 12a0a08..b19ea22 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/packages/BUILD
@@ -69,10 +69,12 @@
"//src/main/java/com/google/devtools/build/lib/events",
"//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:configured_attribute_mapper",
"//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:configured_target_and_data",
"//src/main/java/com/google/devtools/build/lib/skyframe:tests_for_target_pattern_value",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
"//src/main/java/com/google/devtools/build/lib/util",
diff --git a/src/test/java/com/google/devtools/build/lib/packages/NativeGlobTest.java b/src/test/java/com/google/devtools/build/lib/packages/NativeGlobTest.java
new file mode 100644
index 0000000..19e40a9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/NativeGlobTest.java
@@ -0,0 +1,161 @@
+// Copyright 2021 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 static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Root;
+import java.io.IOException;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@code native.glob} function. */
+@RunWith(JUnit4.class)
+public class NativeGlobTest extends BuildViewTestCase {
+
+ @Test
+ public void glob_simple() throws Exception {
+ makeFile("test/starlark/file1.txt");
+ makeFile("test/starlark/file2.txt");
+ makeFile("test/starlark/file3.txt");
+
+ makeGlobFilegroup("test/starlark/BUILD", "glob(['*'])");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of(
+ "//test/starlark:BUILD",
+ "//test/starlark:file1.txt",
+ "//test/starlark:file2.txt",
+ "//test/starlark:file3.txt"));
+ }
+
+ @Test
+ public void glob_not_empty() throws Exception {
+
+ makeGlobFilegroup("test/starlark/BUILD", "glob(['foo*'], allow_empty=False)");
+
+ AssertionError e =
+ assertThrows(
+ AssertionError.class,
+ () -> assertAttrLabelList("//test/starlark:files", "srcs", ImmutableList.of()));
+ assertThat(e).hasMessageThat().contains("allow_empty");
+ }
+
+ @Test
+ public void glob_simple_subdirs() throws Exception {
+ makeFile("test/starlark/sub/file1.txt");
+ makeFile("test/starlark/sub2/file2.txt");
+ makeFile("test/starlark/sub3/file3.txt");
+
+ makeGlobFilegroup("test/starlark/BUILD", "glob(['**'])");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of(
+ "//test/starlark:BUILD",
+ "//test/starlark:sub/file1.txt",
+ "//test/starlark:sub2/file2.txt",
+ "//test/starlark:sub3/file3.txt"));
+ }
+
+ @Test
+ public void glob_incremental() throws Exception {
+ makeFile("test/starlark/file1.txt");
+ makeGlobFilegroup("test/starlark/BUILD", "glob(['**'])");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of("//test/starlark:BUILD", "//test/starlark:file1.txt"));
+
+ scratch.file("test/starlark/file2.txt");
+ scratch.file("test/starlark/sub/subfile3.txt");
+
+ // Poke SkyFrame to tell it what changed.
+ invalidateSkyFrameFiles(
+ "test/starlark", "test/starlark/file2.txt", "test/starlark/sub/subfile3.txt");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of(
+ "//test/starlark:BUILD",
+ "//test/starlark:file1.txt",
+ "//test/starlark:file2.txt",
+ "//test/starlark:sub/subfile3.txt"));
+ }
+
+ /**
+ * Constructs a BUILD file containing a single rule with uses glob() to list files look for a rule
+ * called :files in it.
+ */
+ private void makeGlobFilegroup(String buildPath, String glob) throws IOException {
+ scratch.file(buildPath, "filegroup(", " name = 'files',", " srcs = " + glob, ")");
+ }
+
+ private void assertAttrLabelList(String target, String attrName, List<String> expectedLabels)
+ throws Exception {
+ ConfiguredTargetAndData cfgTarget = getConfiguredTargetAndData(target);
+ assertThat(cfgTarget).isNotNull();
+
+ ImmutableList<Label> labels =
+ expectedLabels.stream().map(this::makeLabel).collect(toImmutableList());
+
+ ConfiguredAttributeMapper configuredAttributeMapper =
+ getMapperFromConfiguredTargetAndTarget(cfgTarget);
+ assertThat(configuredAttributeMapper.get(attrName, BuildType.LABEL_LIST))
+ .containsExactlyElementsIn(labels);
+ }
+
+ private Label makeLabel(String label) {
+ try {
+ return Label.parseAbsolute(label, ImmutableMap.of());
+ } catch (Exception e) {
+ // Always fails the test.
+ assertThat(e).isNull();
+ return null;
+ }
+ }
+
+ private void invalidateSkyFrameFiles(String... files) throws Exception {
+ ModifiedFileSet.Builder builder = ModifiedFileSet.builder();
+
+ for (String f : files) {
+ builder.modify(PathFragment.create(f));
+ }
+
+ getSkyframeExecutor()
+ .invalidateFilesUnderPathForTesting(
+ reporter, builder.build(), Root.fromPath(rootDirectory));
+ }
+
+ private void makeFile(String fileName) throws IOException {
+ scratch.file(fileName, "Content: " + fileName);
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/NativeSubpackagesTest.java b/src/test/java/com/google/devtools/build/lib/packages/NativeSubpackagesTest.java
new file mode 100644
index 0000000..15a648f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/NativeSubpackagesTest.java
@@ -0,0 +1,341 @@
+// Copyright 2021 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 static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Root;
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@code native.subpackages} function. */
+@RunWith(JUnit4.class)
+public class NativeSubpackagesTest extends BuildViewTestCase {
+
+ private static final String ALL_SUBDIRS = "**";
+
+ @Test
+ public void subpackages_simple_subDir() throws Exception {
+ makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);
+ makeFilesSubPackage("test/starlark/sub");
+
+ assertAttrLabelList(
+ "//test/starlark:files", "srcs", ImmutableList.of("//test/starlark/sub:files"));
+ }
+
+ @Test
+ public void subpackages_simple_include() throws Exception {
+ makeSubpackageFileGroup("test/starlark/BUILD", "sub1/**", null, null);
+
+ makeFilesSubPackage("test/starlark/sub");
+ makeFilesSubPackage("test/starlark/sub1");
+ makeFilesSubPackage("test/starlark/sub2");
+
+ assertAttrLabelList(
+ "//test/starlark:files", "srcs", ImmutableList.of("//test/starlark/sub1:files"));
+ }
+
+ @Test
+ public void subpackages_simple_exclude() throws Exception {
+ makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, "['sub2/**']", null);
+
+ makeFilesSubPackage("test/starlark/sub");
+ makeFilesSubPackage("test/starlark/sub1");
+ makeFilesSubPackage("test/starlark/sub2");
+ makeFilesSubPackage("test/starlark/sub3");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of(
+ "//test/starlark/sub:files",
+ "//test/starlark/sub1:files",
+ "//test/starlark/sub3:files"));
+ }
+
+ @Test
+ public void subpackages_simple_empty_allow() throws Exception {
+ makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, true);
+ assertAttrLabelList("//test/starlark:files", "srcs", ImmutableList.of());
+ }
+
+ @Test
+ public void subpackages_simple_empty_disallow() throws Exception {
+ makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);
+
+ // force evaluation
+ AssertionError e =
+ assertThrows(AssertionError.class, () -> getConfiguredTarget("//test/starlark:files"));
+ assertThat(e).hasMessageThat().contains("subpackages pattern '**' didn't match anything");
+ }
+
+ @Test
+ public void subpackages_deeplyNested_withSubdirs() throws Exception {
+ makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, true);
+
+ // Setup a dir with 2 subdirs, 1 a package one not
+ makeFilesSubPackage("test/starlark/sub");
+ // Should be blocked by 'sub'
+ makeFilesSubPackage("test/starlark/sub/sub2");
+
+ makeFilesSubPackage("test/starlark/sub3");
+ makeFilesSubPackage("test/starlark/not_sub/sub_is_pkg/eventually");
+
+ scratch.file("test/starlark/not_sub/file1.txt");
+ scratch.file("test/starlark/not_sub/double_not_sub/file.txt");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of(
+ "//test/starlark/sub:files",
+ "//test/starlark/sub3:files",
+ "//test/starlark/not_sub/sub_is_pkg/eventually:files"));
+ }
+
+ @Test
+ public void subpackages_incremental_addSubPkg() throws Exception {
+ makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);
+
+ // Setup a two subdirs one shallow and one deep
+ makeFilesSubPackage("test/starlark/sub");
+ makeFilesSubPackage("test/starlark/deep/1/2/3");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of("//test/starlark/sub:files", "//test/starlark/deep/1/2/3:files"));
+
+ // Add a 2nd shallow and 2nd deep mid
+ makeFilesSubPackage("test/starlark/sub2");
+
+ // Poke Skyframe by invalidating the dirent and files that changed.
+ invalidateSkyFrameFiles(
+ "test/starlark/sub2", "test/starlark/sub2/BUILD", "test/starlark/sub2/file.txt");
+
+ // We should now be aware of the new one via Skyframe invalidation.
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of(
+ "//test/starlark/sub:files",
+ "//test/starlark/sub2:files",
+ "//test/starlark/deep/1/2/3:files"));
+ }
+
+ @Test
+ public void subpackages_incremental_delSubPkg() throws Exception {
+ makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);
+
+ // Setup a single subdir
+ makeFilesSubPackage("test/starlark/sub");
+ makeFilesSubPackage("test/starlark/sub2");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of("//test/starlark/sub:files", "//test/starlark/sub2:files"));
+
+ scratch.deleteFile("test/starlark/sub2/BUILD");
+ scratch.deleteFile("test/starlark/sub2/file.txt");
+
+ invalidateSkyFrameFiles("test/starlark/sub2/BUILD", "test/starlark/sub2/file.txt");
+
+ // We should now be aware of the new one.
+ assertAttrLabelList(
+ "//test/starlark:files", "srcs", ImmutableList.of("//test/starlark/sub:files"));
+ }
+
+ @Test
+ public void subpackages_incremental_convertSubDirToPkg() throws Exception {
+ makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);
+
+ // Setup both immediate and deeply nested sub-dirs with BUILD files.
+ makeFilesSubPackage("test/starlark/sub");
+ scratch.file("test/starlark/sub2/file2.txt");
+
+ // Initially we have a subdir with 'sub/BUILD' and sub2/file2.txt"
+ assertAttrLabelList(
+ "//test/starlark:files", "srcs", ImmutableList.of("//test/starlark/sub:files"));
+
+ // Then we add a BUILD file to sub2 making it a package Skyframe should pick
+ // that up once invalidated.
+ makeFilesSubPackage("test/starlark/sub2");
+
+ // Poke Skyframe by invalidating the dirent and files that changed.
+ invalidateSkyFrameFiles("test/starlark/sub2/BUILD", "test/starlark/sub2/file.txt");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of("//test/starlark/sub:files", "//test/starlark/sub2:files"));
+ }
+
+ @Test
+ public void invalidPositionalParams() throws Exception {
+ scratch.file("foo/subdir/BUILD");
+ scratch.file("foo/BUILD", "[sh_library(name = p) for p in subpackages(['subdir'])]");
+
+ AssertionError e =
+ assertThrows(AssertionError.class, () -> getConfiguredTargetAndData("//foo:subdir"));
+ assertThat(e).hasMessageThat().contains("got unexpected positional argument");
+ }
+
+ @Test
+ public void invalidMissingInclude() throws Exception {
+ scratch.file("foo/subdir/BUILD");
+ scratch.file("foo/BUILD", "[sh_library(name = p) for p in subpackages()]");
+
+ AssertionError e =
+ assertThrows(AssertionError.class, () -> getConfiguredTargetAndData("//foo:subdir"));
+ assertThat(e).hasMessageThat().contains("missing 1 required named argument: include");
+ }
+
+ @Test
+ public void validNoWildCardInclude() throws Exception {
+ makeSubpackageFileGroup(
+ "test/starlark/BUILD", /*include=*/ ImmutableList.of("sub", "sub2/deep"), null, null);
+ makeFilesSubPackage("test/starlark/sub");
+ makeFilesSubPackage("test/starlark/sub2/deep");
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of("//test/starlark/sub:files", "//test/starlark/sub2/deep:files"));
+ }
+
+ @Test
+ public void includeValidMatchSubdir() throws Exception {
+ scratch.file("foo/subdir/BUILD");
+ scratch.file(
+ "foo/BUILD", "[sh_library(name = p) for p in subpackages(include = ['subdir/*'])]");
+
+ getConfiguredTargetAndData("//foo:subdir");
+ }
+
+ @Test
+ public void includeValidSubMatchSubdir() throws Exception {
+ makeFilesSubPackage("test/starlark/subdir/sub/deeper");
+ makeFilesSubPackage("test/starlark/subdir/sub2/deeper");
+ makeFilesSubPackage("test/starlark/subdir/sub3/deeper");
+
+ makeSubpackageFileGroup("test/starlark/BUILD", "subdir/*/deeper", null, null);
+
+ assertAttrLabelList(
+ "//test/starlark:files",
+ "srcs",
+ ImmutableList.of(
+ "//test/starlark/subdir/sub/deeper:files",
+ "//test/starlark/subdir/sub2/deeper:files",
+ "//test/starlark/subdir/sub3/deeper:files"));
+ }
+
+ /**
+ * Constructs a BUILD file with a single filegroup target whose srcs attribute is the list of all
+ * //p:files, where //p is a subpackage returned by a call to native.subpackages.
+ */
+ private void makeSubpackageFileGroup(
+ String buildPath, ImmutableList<String> include, String exclude, Boolean allowEmpty)
+ throws IOException {
+ StringBuilder subpackages = new StringBuilder();
+ subpackages.append("subpackages(include = [");
+ subpackages.append(include.stream().map(i -> "'" + i + "'").collect(Collectors.joining(", ")));
+ subpackages.append("]");
+
+ if (exclude != null) {
+ subpackages.append(", exclude = ");
+ subpackages.append(exclude);
+ }
+
+ if (allowEmpty != null) {
+ subpackages.append(", allow_empty = ");
+ subpackages.append(allowEmpty ? "True" : "False");
+ }
+ subpackages.append(")");
+
+ scratch.file(
+ buildPath,
+ "filegroup(",
+ " name = 'files',",
+ " srcs = [",
+ " '//%s/%s:files' % (package_name(), s) for s in " + subpackages,
+ " ],",
+ ")");
+ }
+
+ private void makeSubpackageFileGroup(
+ String buildPath, String include, String exclude, Boolean allowEmpty) throws IOException {
+ makeSubpackageFileGroup(buildPath, ImmutableList.of(include), exclude, allowEmpty);
+ }
+
+ /**
+ * Creates a BUILD file and single file at the given packagePath, the BUILD file will contain a
+ * single filegroup called 'files' which contains the created file.
+ */
+ private void makeFilesSubPackage(String packagePath) throws IOException {
+ scratch.file(packagePath + "/file.txt");
+ scratch.file(
+ packagePath + "/BUILD", "filegroup(", " name = 'files',", " srcs = glob(['*']),", ")");
+ }
+
+ private void assertAttrLabelList(String target, String attrName, List<String> expectedLabels)
+ throws Exception {
+ ConfiguredTargetAndData cfgTarget = getConfiguredTargetAndData(target);
+ assertThat(cfgTarget).isNotNull();
+
+ ImmutableList<Label> labels =
+ expectedLabels.stream().map(this::makeLabel).collect(toImmutableList());
+
+ ConfiguredAttributeMapper configuredAttributeMapper =
+ getMapperFromConfiguredTargetAndTarget(cfgTarget);
+ assertThat(configuredAttributeMapper.get(attrName, BuildType.LABEL_LIST))
+ .containsExactlyElementsIn(labels);
+ }
+
+ private Label makeLabel(String label) {
+ try {
+ return Label.parseAbsolute(label, ImmutableMap.of());
+ } catch (Exception e) {
+ fail("Unable to construct Label from " + label);
+ return null;
+ }
+ }
+
+ private void invalidateSkyFrameFiles(String... files) throws Exception {
+ ModifiedFileSet.Builder builder = ModifiedFileSet.builder();
+
+ for (String f : files) {
+ builder.modify(PathFragment.create(f));
+ }
+
+ getSkyframeExecutor()
+ .invalidateFilesUnderPathForTesting(
+ reporter, builder.build(), Root.fromPath(rootDirectory));
+ }
+}
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 ee56bb8..e801662 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
@@ -752,7 +752,7 @@
assertThrows(NoSuchPackageException.class, () -> loadPackage("pkg"));
assertThat(ex)
.hasMessageThat()
- .contains("error globbing [globs/**]: " + dir + " (Permission denied)");
+ .contains("error globbing [globs/**] op=FILES: " + dir + " (Permission denied)");
}
@Test
@@ -1147,10 +1147,12 @@
"third_variable = glob(['c'], exclude_directories = 0)"));
List<String> globs = new ArrayList<>();
List<String> globsWithDirs = new ArrayList<>();
+ List<String> subpackages = new ArrayList<>();
PackageFactory.checkBuildSyntax(
- file, globs, globsWithDirs, new HashMap<>(), /*eventHandler=*/ null);
+ file, globs, globsWithDirs, subpackages, new HashMap<>(), /*eventHandler=*/ null);
assertThat(globs).containsExactly("ab", "a", "**/*");
assertThat(globsWithDirs).containsExactly("c");
+ assertThat(subpackages).isEmpty();
}
// Tests of BUILD file dialect checks:
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 c66ec8a..97eb1de 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
@@ -838,4 +838,148 @@
return super.statIfFound(path, followSymlinks);
}
}
+
+ private void assertSubpackageMatches(String pattern, String... expecteds) throws Exception {
+ assertThat(
+ Iterables.transform(
+ runGlob(pattern, Globber.Operation.SUBPACKAGES).getMatches().toList(),
+ Functions.toStringFunction()))
+ .containsExactlyElementsIn(ImmutableList.copyOf(expecteds));
+ }
+
+ private void makeEmptyPackage(Path newPackagePath) throws Exception {
+ newPackagePath.createDirectoryAndParents();
+ FileSystemUtils.createEmptyFile(newPackagePath.getRelative("BUILD"));
+ }
+
+ private void makeEmptyPackage(String path) throws Exception {
+ makeEmptyPackage(pkgPath.getRelative(path));
+ }
+
+ @Test
+ public void subpackages_simple() throws Exception {
+ makeEmptyPackage("horse");
+ makeEmptyPackage("monkey");
+ makeEmptyPackage("horse/saddle");
+
+ // "horse/saddle" should not be in the results because horse/saddle is too deep. a2/b2 added by
+ // setup().
+ assertSubpackageMatches("**", /* => */ "a2/b2", "horse", "monkey");
+ }
+
+ @Test
+ public void subpackages_empty() throws Exception {
+ assertSubpackageMatches("foo/*");
+ assertSubpackageMatches("foo/**");
+ }
+
+ @Test
+ public void subpackages_oneLevelDeep() throws Exception {
+ makeEmptyPackage("base/sub");
+ makeEmptyPackage("base/sub2");
+ makeEmptyPackage("base/sub3");
+
+ assertSubpackageMatches("base/*", /* => */ "base/sub", "base/sub2", "base/sub3");
+ assertSubpackageMatches("base/**", /* => */ "base/sub", "base/sub2", "base/sub3");
+ }
+
+ @Test
+ public void subpackages_oneLevel_notDeepEnough() throws Exception {
+ makeEmptyPackage("base/sub/pkg");
+ makeEmptyPackage("base/sub2/pkg");
+ makeEmptyPackage("base/sub3/pkg");
+
+ // * doesn't go deep enough
+ assertSubpackageMatches("base/*");
+ // But if we go with ** it works fine.
+ assertSubpackageMatches("base/**", /* => */ "base/sub/pkg", "base/sub2/pkg", "base/sub3/pkg");
+ }
+
+ @Test
+ public void subpackages_deepRecurse() throws Exception {
+ makeEmptyPackage("base/sub/1");
+ makeEmptyPackage("base/sub/2");
+ makeEmptyPackage("base/sub2/3");
+ makeEmptyPackage("base/sub2/4");
+ makeEmptyPackage("base/sub3/5");
+ makeEmptyPackage("base/sub3/6");
+
+ FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/bar/BUILD"));
+ // "foo/bar" should not be in the results because foo/bar is a separate package.
+ assertSubpackageMatches(
+ "base/*/*",
+ "base/sub/1",
+ "base/sub/2",
+ "base/sub2/3",
+ "base/sub2/4",
+ "base/sub3/5",
+ "base/sub3/6");
+
+ assertSubpackageMatches(
+ "base/**",
+ "base/sub/1",
+ "base/sub/2",
+ "base/sub2/3",
+ "base/sub2/4",
+ "base/sub3/5",
+ "base/sub3/6");
+ }
+
+ @Test
+ public void subpackages_middleWidlcard() throws Exception {
+ makeEmptyPackage("base/sub1/same");
+ makeEmptyPackage("base/sub2/same");
+ makeEmptyPackage("base/sub3/same");
+ makeEmptyPackage("base/sub4/same");
+ makeEmptyPackage("base/sub5/same");
+ makeEmptyPackage("base/sub6/same");
+
+ assertSubpackageMatches(
+ "base/*/same",
+ "base/sub1/same",
+ "base/sub2/same",
+ "base/sub3/same",
+ "base/sub4/same",
+ "base/sub5/same",
+ "base/sub6/same");
+
+ assertSubpackageMatches(
+ "base/**/same",
+ "base/sub1/same",
+ "base/sub2/same",
+ "base/sub3/same",
+ "base/sub4/same",
+ "base/sub5/same",
+ "base/sub6/same");
+ }
+
+ @Test
+ public void subpackages_noWildcard() throws Exception {
+ makeEmptyPackage("sub1");
+ makeEmptyPackage("sub2");
+ makeEmptyPackage("sub3/deep");
+ makeEmptyPackage("sub4/deeper/deeper");
+
+ assertSubpackageMatches("sub");
+ assertSubpackageMatches("sub1", "sub1");
+ assertSubpackageMatches("sub2", "sub2");
+ assertSubpackageMatches("sub3/deep", "sub3/deep");
+ assertSubpackageMatches("sub4/deeper/deeper", "sub4/deeper/deeper");
+ }
+
+ @Test
+ public void subpackages_testSymlinks() throws Exception {
+ Path newPackagePath = pkgPath.getRelative("path/to/pkg");
+ makeEmptyPackage(newPackagePath);
+
+ pkgPath.getRelative("symlinks").createDirectoryAndParents();
+ FileSystemUtils.ensureSymbolicLink(pkgPath.getRelative("symlinks/deeplink"), newPackagePath);
+ FileSystemUtils.ensureSymbolicLink(pkgPath.getRelative("shallowlink"), newPackagePath);
+
+ assertSubpackageMatches("**", "a2/b2", "symlinks/deeplink", "path/to/pkg", "shallowlink");
+ assertSubpackageMatches("*", "shallowlink");
+
+ assertSubpackageMatches("symlinks/**", "symlinks/deeplink");
+ assertSubpackageMatches("symlinks/*", "symlinks/deeplink");
+ }
}