Automated rollback of commit 07eb2d77f94f9e11e64082dab9581ad7acf44dc7. *** Reason for rollback *** Re-fix the `subpackages()` bug when considering a corner case In the initial change, we have introduced another bug: Consider computing `subpackages(["sub/**"])` while `sub/BUILD` exists, we should return "sub", which is the behavior of file/directory glob matching. However, the initial fix returns EMPTY in this case. This change also adds more test coverage for calling `subpackages(["sub/**"])` when `sub` is a subpackage. PiperOrigin-RevId: 591917000 Change-Id: Iab41cc59b0da7f7794a90cd10aaac8900ee0d79b
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 80c354a..c5280ba 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
@@ -394,7 +394,8 @@ The <code>include</code> and <code>exclude</code> lists contain path patterns that are relative to the current package. Every pattern may consist of one or more path segments. As usual with Unix paths, these segments are separated by -<code>/</code>. Segments may contain the <code>*</code> wildcard: this matches +<code>/</code>. The segments in the pattern are matched against the segments of +the path. Segments may contain the <code>*</code> wildcard: this matches any substring in the path segment (even the empty substring), excluding the directory separator <code>/</code>. This wildcard can be used multiple times within one path segment. Additionally, the <code>**</code> wildcard can match @@ -405,26 +406,37 @@ Examples: <ul> <li><code>foo/bar.txt</code> matches exactly the <code>foo/bar.txt</code> file -in this package</li> +in this package (unless <code>foo/</code> is a subpackage)</li> <li><code>foo/*.txt</code> matches every file in the <code>foo/</code> directory -if the file ends with -<code>.txt</code> (unless <code>foo/</code> is a subpackage)</li> +if the file ends with <code>.txt</code> (unless <code>foo/</code> is a +subpackage)</li> <li><code>foo/a*.htm*</code> matches every file in the <code>foo/</code> directory that starts with <code>a</code>, then has an arbitrary string (could -be empty), then has <code>.htm</code>, and ends with another arbitrary string; -such as <code>foo/axx.htm</code> and <code>foo/a.html</code> or -<code>foo/axxx.html</code></li> +be empty), then has <code>.htm</code>, and ends with another arbitrary string +(unless <code>foo/</code> is a subpackage); such as <code>foo/axx.htm</code> +and <code>foo/a.html</code> or <code>foo/axxx.html</code></li> +<li><code>foo/*</code> matches every file in the <code>foo/</code> directory, +(unless <code>foo/</code> is a subpackage); it does not match <code>foo</code> +directory itself even if <code>exclude_directories</code> is set to +</code>0</code></li> +<li><code>foo/**</code> matches every file in every non-subpackage subdirectory +under package's first level subdirectory <code>foo/</code>; if +<code>exclude_directories</code> is set to </code>0</code>, <code>foo</code> +directory itself also matches the pattern; in this case, <code>**</code> is +considered to match zero path segments</li> <li><code>**/a.txt</code> matches every <code>a.txt</code> file in every -subdirectory of this package</li> +non-subpackage subdirectory of this package</li> <li><code>**/bar/**/*.txt</code> matches every <code>.txt</code> file in every -subdirectory of this package, if at least one directory on the resulting path is -called <code>bar</code>, such as <code>xxx/bar/yyy/zzz/a.txt</code> or -<code>bar/a.txt</code> (remember that <code>**</code> also matches zero -segments) or <code>bar/zzz/a.txt</code></li> -<li><code>**</code> matches every file in every subdirectory of this -package</li> +non-subpackage subdirectory of this package, if at least one directory on the +resulting path is called <code>bar</code>, such as +<code>xxx/bar/yyy/zzz/a.txt</code> or <code>bar/a.txt</code> (remember that +<code>**</code> also matches zero segments) or <code>bar/zzz/a.txt</code></li> +<li><code>**</code> matches every file in every non-subpackage subdirectory of +this package</li> <li><code>foo**/a.txt</code> is an invalid pattern, because <code>**</code> must stand on its own as a segment</li> +<li><code>foo/</code> is an invalid pattern, because the second segment defined +after <code>/</code> is an empty string</li> </ul> <p> @@ -741,16 +753,45 @@ # The following BUILD files exist: # foo/BUILD # foo/bar/baz/BUILD +# foo/bar/but/bad/BUILD # foo/sub/BUILD # foo/sub/deeper/BUILD # # In foo/BUILD a call to -subs = subpackages(include = ["**"]) +subs1 = subpackages(include = ["**"]) -# results in subs == ["sub", "bar/baz"] +# results in subs1 == ["sub", "bar/baz", "bar/but/bad"] # # 'sub/deeper' is not included because it is a subpackage of 'foo/sub' not of # 'foo' + +subs2 = subpackages(include = ["bar/*"]) +# results in subs2 = ["bar/baz"] +# +# Since 'bar' is not a subpackage itself, this looks for any subpackages under +# all first level subdirectories of 'bar'. + +subs3 = subpackages(include = ["bar/**"]) +# results in subs3 = ["bar/baz", "bar/but/bad"] +# +# Since bar is not a subpackage itself, this looks for any subpackages which are +# (1) under all subdirectories of 'bar' which can be at any level, (2) not a +# subpackage of another subpackages. + +subs4 = subpackages(include = ["sub"]) +subs5 = subpackages(include = ["sub/*"]) +subs6 = subpackages(include = ["sub/**"]) +# results in subs4 and subs6 being ["sub"] +# results in subs5 = []. +# +# In subs4, expression "sub" checks whether 'foo/sub' is a package (i.e. is a +# subpackage of 'foo'). +# In subs5, "sub/*" looks for subpackages under directory 'foo/sub'. Since +# 'foo/sub' is already a subpackage itself, the subdirectories will not be +# traversed anymore. +# In subs6, 'foo/sub' is a subpackage itself and matches pattern "sub/**", so it +# is returned. But the subdirectories of 'foo/sub' will not be traversed +# anymore. </pre> <p>
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 bb3346c..302d8dc 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
@@ -13,6 +13,8 @@ // limitations under the License. package com.google.devtools.build.lib.skyframe; +import static java.util.Arrays.stream; + import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; @@ -78,6 +80,20 @@ } } + String pattern = glob.getPattern(); + // Split off the first path component of the pattern. + int slashPos = pattern.indexOf('/'); + String patternHead; + String patternTail; + if (slashPos == -1) { + patternHead = pattern; + patternTail = null; + } else { + // Substrings will share the backing array of the original glob string. That should be fine. + patternHead = pattern.substring(0, slashPos); + patternTail = pattern.substring(slashPos + 1); + } + // 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)) { @@ -97,11 +113,11 @@ // 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) { + // For SUBPACKAGES, we encounter this when all remaining patterns in the glob expression + // are `**`s. In that case we should include the subpackage's PathFragment (relative to the + // package fragment) in the GlobValue.getMatches. Otherwise, return EMPTY. + if (globberOperation == Globber.Operation.SUBPACKAGES + && areAllRemainingPatternsDoubleStar(patternHead, patternTail)) { return new GlobValue( NestedSetBuilder.<PathFragment>stableOrder() .add(subDirFragment.relativeTo(glob.getPackageId().getPackageFragment())) @@ -115,20 +131,6 @@ } } - String pattern = glob.getPattern(); - // Split off the first path component of the pattern. - int slashPos = pattern.indexOf('/'); - String patternHead; - String patternTail; - if (slashPos == -1) { - patternHead = pattern; - patternTail = null; - } else { - // Substrings will share the backing array of the original glob string. That should be fine. - patternHead = pattern.substring(0, slashPos); - patternTail = pattern.substring(slashPos + 1); - } - NestedSetBuilder<PathFragment> matches = NestedSetBuilder.stableOrder(); boolean globMatchesBareFile = patternTail == null; @@ -362,6 +364,17 @@ return new GlobValue(matchesBuilt); } + private static boolean areAllRemainingPatternsDoubleStar( + String patternHead, @Nullable String patternTail) { + if (!patternHead.equals("**")) { + return false; + } + if (patternTail == null) { + return true; + } + return stream(patternTail.split("/")).allMatch("**"::equals); + } + private static void processSubdir( Map.Entry<SkyKey, SkyValue> keyAndValue, Map<SkyKey, Dirent> subdirMap, @@ -484,8 +497,8 @@ } /** - * Used to declare all the exception types that can be wrapped in the exception thrown by - * {@link GlobFunction#compute}. + * Used to declare all the exception types that can be wrapped in the exception thrown by {@link + * GlobFunction#compute}. */ private static final class GlobFunctionException extends SkyFunctionException { GlobFunctionException(InconsistentFilesystemException e, Transience transience) {
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 4272102..03de45e 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
@@ -34,6 +34,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -227,7 +228,7 @@ StringBuilder regexp = new StringBuilder(); for (int i = 0, len = pattern.length(); i < len; i++) { char c = pattern.charAt(i); - switch(c) { + switch (c) { case '*': int toIncrement = 0; if (len > i + 1 && pattern.charAt(i + 1) == '*') { @@ -755,7 +756,12 @@ } if (baseIsDir && !context.pathDiscriminator.shouldTraverseDirectory(base)) { - maybeAddResult(context, base, baseIsDir); + if (areAllRemainingPatternsDoubleStar(context, idx)) { + // For SUBPACKAGES, we encounter this when all remaining patterns in the glob expression + // are `**`s. In that case we should include the subpackage's PathFragment (relative to + // the package fragment) in the matching results. + maybeAddResult(context, base, baseIsDir); + } return; } @@ -807,6 +813,12 @@ } } + private static boolean areAllRemainingPatternsDoubleStar( + GlobTaskContext context, int startIdx) { + return Arrays.stream(context.patternParts, startIdx, context.patternParts.length) + .allMatch("**"::equals); + } + /** * 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
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 db5b460..0469085 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
@@ -338,11 +338,54 @@ } @Test - public void testSubpackages() throws Exception { + public void testSubpackages_noWildcard() throws Exception { + assertThat(cache.globUnsorted(list("sub/sub.js"), list(), Globber.Operation.SUBPACKAGES, true)) + .isEmpty(); + } + + @Test + public void testSubpackages_simpleDoubleStar() throws Exception { assertThat(cache.globUnsorted(list("**"), list(), Globber.Operation.SUBPACKAGES, true)) .containsExactly("sub"); } + @Test + public void testSubpackages_onlySub() throws Exception { + assertThat(cache.globUnsorted(list("sub"), list(), Globber.Operation.SUBPACKAGES, true)) + .containsExactly("sub"); + } + + @Test + public void testSubpackages_singleStarsAfterSub() throws Exception { + assertThat(cache.globUnsorted(list("sub/*"), list(), Globber.Operation.SUBPACKAGES, true)) + .isEmpty(); + } + + @Test + public void testSubpackages_doubleStarsAfterSub() throws Exception { + assertThat(cache.globUnsorted(list("sub/**"), list(), Globber.Operation.SUBPACKAGES, true)) + .containsExactly("sub"); + } + + @Test + public void testSubpackages_twoDoubleStarsAfterSub() throws Exception { + // Both `**`s are considered to match no path fragments. + assertThat(cache.globUnsorted(list("sub/**/**"), list(), Globber.Operation.SUBPACKAGES, true)) + .containsExactly("sub"); + } + + @Test + public void testSubpackages_doubleStarsAndOtherPathAfterSub() throws Exception { + assertThat(cache.globUnsorted(list("sub/**/foo"), list(), Globber.Operation.SUBPACKAGES, true)) + .isEmpty(); + } + + @Test + public void testSubpackages_doubleStarWithTrailingPattern() throws Exception { + assertThat(cache.globUnsorted(list("**/bar"), list(), Globber.Operation.SUBPACKAGES, true)) + .isEmpty(); + } + private void assertEmpty(Collection<?> glob) { assertThat(glob).isEmpty(); }
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 index 91f3410..f7b41a6 100644 --- a/src/test/java/com/google/devtools/build/lib/packages/NativeSubpackagesTest.java +++ b/src/test/java/com/google/devtools/build/lib/packages/NativeSubpackagesTest.java
@@ -26,15 +26,16 @@ 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 com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; 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) +@RunWith(TestParameterInjector.class) public class NativeSubpackagesTest extends BuildViewTestCase { private static final String ALL_SUBDIRS = "**"; @@ -50,7 +51,7 @@ @Test public void subpackages_simple_include() throws Exception { - makeSubpackageFileGroup("test/starlark/BUILD", "sub1/**", null, null); + makeSubpackageFileGroup("test/starlark/BUILD", "sub1", null, null); makeFilesSubPackage("test/starlark/sub"); makeFilesSubPackage("test/starlark/sub1"); @@ -232,19 +233,26 @@ @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/*'])]"); - + scratch.file("foo/BUILD", "[sh_library(name = p) for p in subpackages(include = ['subdir'])]"); getConfiguredTargetAndData("//foo:subdir"); } @Test - public void includeValidSubMatchSubdir() throws Exception { + public void includeValidSubMatchSubdir( + @TestParameter({ + "subdir/*/deeper", + "subdir/sub*/deeper", + "subdir/**", + "subdir/*/deeper/**", + "subdir/**/deeper/**" + }) + String expression) + 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); + makeSubpackageFileGroup("test/starlark/BUILD", expression, null, null); assertAttrLabelList( "//test/starlark:files",
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 320b8e8..0b0bd54 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
@@ -73,7 +73,9 @@ import com.google.devtools.build.skyframe.SkyKey; import com.google.devtools.build.skyframe.SkyValue; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -106,7 +108,7 @@ private static final PackageIdentifier PKG_ID = PackageIdentifier.createInMainRepo("pkg"); @Before - public final void setUp() throws Exception { + public final void setUp() throws Exception { fs = new CustomInMemoryFs(new ManualClock()); root = fs.getPath("/root/workspace"); writableRoot = fs.getPath("/writableRoot/workspace"); @@ -180,7 +182,7 @@ .getPackageFactoryBuilderForTesting(directories) .build(ruleClassProvider, fs), directories, - /*bzlLoadFunctionForInlining=*/ null)); + /* bzlLoadFunctionForInlining= */ null)); skyFunctions.put( SkyFunctions.EXTERNAL_PACKAGE, new ExternalPackageFunction(BazelSkyframeExecutorConstants.EXTERNAL_PACKAGE_HELPER)); @@ -454,9 +456,7 @@ private void assertGlobsEqual(String pattern1, String pattern2) throws Exception { GlobValue value1 = runGlob(pattern1, Globber.Operation.FILES_AND_DIRS); GlobValue value2 = runGlob(pattern2, Globber.Operation.FILES_AND_DIRS); - new EqualsTester() - .addEqualityGroup(value1, value2) - .testEquals(); + new EqualsTester().addEqualityGroup(value1, value2).testEquals(); } private GlobValue runGlob(String pattern, Globber.Operation globberOperation) throws Exception { @@ -536,9 +536,7 @@ PathFragment.EMPTY_FRAGMENT)); } - /** - * 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 aDotB = pkgPath.getChild("a.b"); @@ -702,7 +700,7 @@ fs.stubStat(pkgPath, null); RootedPath pkgRootedPath = RootedPath.toRootedPath(Root.fromPath(root), pkgPath); FileStateValue pkgDirFileStateValue = - FileStateValue.create(pkgRootedPath, SyscallCache.NO_CACHE, /*tsgm=*/ null); + FileStateValue.create(pkgRootedPath, SyscallCache.NO_CACHE, /* tsgm= */ null); FileValue pkgDirValue = FileValue.value( ImmutableList.of(pkgRootedPath), @@ -899,83 +897,8 @@ } @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"); + public void subpackages_doubleStarPatternWithNamedChild() throws Exception { + assertSubpackageMatches("**/bar"); } @Test @@ -993,6 +916,112 @@ } @Test + public void subpackages_zeroLevelDeep() throws Exception { + makeEmptyPackage("sub"); + + assertSubpackageMatches("sub/*"); + + // `**` is considered to matching nothing below. + assertSubpackageMatches("sub/**", "sub"); + assertSubpackageMatches("sub/**/**", "sub"); + + assertSubpackageMatches("sub/**/foo"); + assertSubpackageMatches("sub/**/foo/**"); + } + + @Test + public void subpackages_zeroAndOneLevelDeep() throws Exception { + makeEmptyPackage("sub"); + makeEmptyPackage("sub/subOfSub"); + + assertSubpackageMatches("sub/*"); + + // `**` is considered to matching nothing below. + assertSubpackageMatches("sub/**", "sub"); + assertSubpackageMatches("sub/**/**", "sub"); + + assertSubpackageMatches("sub/**/foo"); + assertSubpackageMatches("sub/**/foo/**"); + } + + @Test + public void subpackages_oneLevelDeep() throws Exception { + makeEmptyPackage("base/sub"); + makeEmptyPackage("base/sub2"); + makeEmptyPackage("base/sub3"); + + List<String> matchingPatterns = + Arrays.asList("base/*", "base/**", "base/**/**", "base/**/sub*", "base/**/sub*/**"); + + for (String pattern : matchingPatterns) { + assertSubpackageMatches(pattern, /* => */ "base/sub", "base/sub2", "base/sub3"); + } + } + + @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")); + + // * doesn't go deep enough, so no matches + assertSubpackageMatches("base/*"); + + List<String> matchingPatterns = + Arrays.asList("base/**", "base/*/*", "base/*/*/**", "base/*/*/**/**", "base/**/sub*/**"); + + for (String pattern : matchingPatterns) { + assertSubpackageMatches( + pattern, + "base/sub/1", + "base/sub/2", + "base/sub2/3", + "base/sub2/4", + "base/sub3/5", + "base/sub3/6"); + } + } + + @Test + public void subpackages_middleWildcard() throws Exception { + makeEmptyPackage("base/same"); + makeEmptyPackage("base/sub1/same"); + makeEmptyPackage("base/sub2/same"); + makeEmptyPackage("base/sub3/same"); + makeEmptyPackage("base/sub4/same"); + makeEmptyPackage("base/sub5/same"); + makeEmptyPackage("base/sub6/same"); + makeEmptyPackage("base/sub7/sub8/same"); + makeEmptyPackage("base/sub9/sub10/sub11/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/same", + "base/sub1/same", + "base/sub2/same", + "base/sub3/same", + "base/sub4/same", + "base/sub5/same", + "base/sub6/same", + "base/sub7/sub8/same", + "base/sub9/sub10/sub11/same"); + } + + @Test public void subpackages_testSymlinks() throws Exception { Path newPackagePath = pkgPath.getRelative("path/to/pkg"); makeEmptyPackage(newPackagePath);