| // Copyright 2024 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.skyframe; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.common.base.Suppliers; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.collect.Maps; | |
| import com.google.common.testing.EqualsTester; | |
| import com.google.devtools.build.lib.actions.FileStateValue; | |
| import com.google.devtools.build.lib.actions.FileValue; | |
| import com.google.devtools.build.lib.analysis.BlazeDirectories; | |
| import com.google.devtools.build.lib.analysis.ServerDirectories; | |
| import com.google.devtools.build.lib.analysis.util.AnalysisMock; | |
| import com.google.devtools.build.lib.bazel.bzlmod.BzlmodRepoRuleValue; | |
| import com.google.devtools.build.lib.clock.BlazeClock; | |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; | |
| import com.google.devtools.build.lib.events.NullEventHandler; | |
| import com.google.devtools.build.lib.io.FileSymlinkCycleUniquenessFunction; | |
| 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.packages.Globber.Operation; | |
| import com.google.devtools.build.lib.packages.RuleClassProvider; | |
| import com.google.devtools.build.lib.packages.WorkspaceFileValue; | |
| import com.google.devtools.build.lib.pkgcache.PathPackageLocator; | |
| import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction; | |
| import com.google.devtools.build.lib.skyframe.ExternalFilesHelper.ExternalFileAction; | |
| import com.google.devtools.build.lib.skyframe.PackageLookupFunction.CrossRepositoryLabelViolationStrategy; | |
| import com.google.devtools.build.lib.testutil.ManualClock; | |
| import com.google.devtools.build.lib.testutil.TestConstants; | |
| import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor; | |
| import com.google.devtools.build.lib.vfs.DigestHashFunction; | |
| import com.google.devtools.build.lib.vfs.Dirent; | |
| import com.google.devtools.build.lib.vfs.FileStateKey; | |
| import com.google.devtools.build.lib.vfs.FileStatus; | |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.Root; | |
| import com.google.devtools.build.lib.vfs.RootedPath; | |
| import com.google.devtools.build.lib.vfs.SyscallCache; | |
| import com.google.devtools.build.lib.vfs.UnixGlob; | |
| import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; | |
| import com.google.devtools.build.skyframe.Differencer.DiffWithDelta.Delta; | |
| import com.google.devtools.build.skyframe.ErrorInfo; | |
| import com.google.devtools.build.skyframe.EvaluationContext; | |
| import com.google.devtools.build.skyframe.EvaluationResult; | |
| import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator; | |
| import com.google.devtools.build.skyframe.MemoizingEvaluator; | |
| import com.google.devtools.build.skyframe.RecordingDifferencer; | |
| import com.google.devtools.build.skyframe.SequencedRecordingDifferencer; | |
| import com.google.devtools.build.skyframe.SkyFunction; | |
| import com.google.devtools.build.skyframe.SkyFunctionName; | |
| import com.google.devtools.build.skyframe.SkyKey; | |
| import com.google.devtools.build.skyframe.SkyValue; | |
| import com.google.errorprone.annotations.ForOverride; | |
| import com.google.testing.junit.testparameterinjector.TestParameter; | |
| 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; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import javax.annotation.Nullable; | |
| import net.starlark.java.eval.StarlarkSemantics; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| public abstract class GlobTestBase { | |
| protected static final EvaluationContext EVALUATION_OPTIONS = | |
| EvaluationContext.newBuilder() | |
| .setKeepGoing(false) | |
| .setParallelism(SkyframeExecutor.DEFAULT_THREAD_COUNT) | |
| .setEventHandler(NullEventHandler.INSTANCE) | |
| .build(); | |
| private CustomInMemoryFs fs; | |
| protected MemoizingEvaluator evaluator; | |
| private RecordingDifferencer differencer; | |
| protected Path root; | |
| private Path writableRoot; | |
| protected Path pkgPath; | |
| private AtomicReference<PathPackageLocator> pkgLocator; | |
| protected static final PackageIdentifier PKG_ID = PackageIdentifier.createInMainRepo("pkg"); | |
| @Before | |
| public final void setUp() throws Exception { | |
| fs = new CustomInMemoryFs(new ManualClock()); | |
| root = fs.getPath("/root/workspace"); | |
| writableRoot = fs.getPath("/writableRoot/workspace"); | |
| pkgPath = root.getRelative(PKG_ID.getPackageFragment()); | |
| pkgLocator = | |
| new AtomicReference<>( | |
| new PathPackageLocator( | |
| fs.getPath("/output_base"), | |
| ImmutableList.of(Root.fromPath(writableRoot), Root.fromPath(root)), | |
| BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY)); | |
| differencer = new SequencedRecordingDifferencer(); | |
| evaluator = new InMemoryMemoizingEvaluator(createFunctionMap(), differencer); | |
| PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID()); | |
| PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator.get()); | |
| PrecomputedValue.STARLARK_SEMANTICS.set(differencer, StarlarkSemantics.DEFAULT); | |
| RepositoryDelegatorFunction.RESOLVED_FILE_INSTEAD_OF_WORKSPACE.set( | |
| differencer, Optional.empty()); | |
| createTestFiles(); | |
| } | |
| private Map<SkyFunctionName, SkyFunction> createFunctionMap() { | |
| AtomicReference<ImmutableSet<PackageIdentifier>> deletedPackages = | |
| new AtomicReference<>(ImmutableSet.of()); | |
| BlazeDirectories directories = | |
| new BlazeDirectories( | |
| new ServerDirectories(root, root, root), | |
| root, | |
| /* defaultSystemJavabase= */ null, | |
| TestConstants.PRODUCT_NAME); | |
| ExternalFilesHelper externalFilesHelper = | |
| ExternalFilesHelper.createForTesting( | |
| pkgLocator, | |
| ExternalFileAction.DEPEND_ON_EXTERNAL_PKG_FOR_EXTERNAL_REPO_PATHS, | |
| directories); | |
| Map<SkyFunctionName, SkyFunction> skyFunctions = new HashMap<>(); | |
| createGlobSkyFunction(skyFunctions); | |
| skyFunctions.put( | |
| SkyFunctions.DIRECTORY_LISTING_STATE, | |
| new DirectoryListingStateFunction(externalFilesHelper, SyscallCache.NO_CACHE)); | |
| skyFunctions.put(SkyFunctions.DIRECTORY_LISTING, new DirectoryListingFunction()); | |
| skyFunctions.put( | |
| SkyFunctions.PACKAGE_LOOKUP, | |
| new PackageLookupFunction( | |
| deletedPackages, | |
| CrossRepositoryLabelViolationStrategy.ERROR, | |
| BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY, | |
| BazelSkyframeExecutorConstants.EXTERNAL_PACKAGE_HELPER)); | |
| skyFunctions.put( | |
| SkyFunctions.IGNORED_PACKAGE_PREFIXES, | |
| BazelSkyframeExecutorConstants.IGNORED_PACKAGE_PREFIXES_FUNCTION); | |
| skyFunctions.put( | |
| FileStateKey.FILE_STATE, | |
| new FileStateFunction( | |
| Suppliers.ofInstance(new TimestampGranularityMonitor(BlazeClock.instance())), | |
| SyscallCache.NO_CACHE, | |
| externalFilesHelper)); | |
| skyFunctions.put( | |
| FileSymlinkInfiniteExpansionUniquenessFunction.NAME, | |
| new FileSymlinkCycleUniquenessFunction()); | |
| skyFunctions.put(FileValue.FILE, new FileFunction(pkgLocator, directories)); | |
| skyFunctions.put( | |
| FileSymlinkCycleUniquenessFunction.NAME, new FileSymlinkCycleUniquenessFunction()); | |
| AnalysisMock analysisMock = AnalysisMock.get(); | |
| RuleClassProvider ruleClassProvider = analysisMock.createRuleClassProvider(); | |
| skyFunctions.put( | |
| WorkspaceFileValue.WORKSPACE_FILE, | |
| new WorkspaceFileFunction( | |
| ruleClassProvider, | |
| analysisMock | |
| .getPackageFactoryBuilderForTesting(directories) | |
| .build(ruleClassProvider, fs), | |
| directories, | |
| /* bzlLoadFunctionForInlining= */ null)); | |
| skyFunctions.put( | |
| SkyFunctions.EXTERNAL_PACKAGE, | |
| new ExternalPackageFunction(BazelSkyframeExecutorConstants.EXTERNAL_PACKAGE_HELPER)); | |
| skyFunctions.put( | |
| SkyFunctions.LOCAL_REPOSITORY_LOOKUP, | |
| new LocalRepositoryLookupFunction(BazelSkyframeExecutorConstants.EXTERNAL_PACKAGE_HELPER)); | |
| skyFunctions.put( | |
| SkyFunctions.REPOSITORY_MAPPING, | |
| new SkyFunction() { | |
| @Override | |
| public SkyValue compute(SkyKey skyKey, Environment env) { | |
| return RepositoryMappingValue.VALUE_FOR_ROOT_MODULE_WITHOUT_REPOS; | |
| } | |
| }); | |
| skyFunctions.put( | |
| BzlmodRepoRuleValue.BZLMOD_REPO_RULE, | |
| new SkyFunction() { | |
| @Override | |
| public SkyValue compute(SkyKey skyKey, Environment env) { | |
| return BzlmodRepoRuleValue.REPO_RULE_NOT_FOUND_VALUE; | |
| } | |
| }); | |
| return skyFunctions; | |
| } | |
| @ForOverride | |
| protected abstract void createGlobSkyFunction(Map<SkyFunctionName, SkyFunction> skyFunctions); | |
| private void createTestFiles() throws IOException { | |
| pkgPath.createDirectoryAndParents(); | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("BUILD")); | |
| for (String dir : | |
| ImmutableList.of( | |
| "foo/bar/wiz", "foo/barnacle/wiz", "food/barnacle/wiz", "fool/barnacle/wiz")) { | |
| pkgPath.getRelative(dir).createDirectoryAndParents(); | |
| } | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/bar/wiz/file")); | |
| // Used for testing the behavior of globbing into nested subpackages. | |
| for (String dir : ImmutableList.of("a1/b1/c", "a2/b2/c")) { | |
| pkgPath.getRelative(dir).createDirectoryAndParents(); | |
| } | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("a2/b2/BUILD")); | |
| } | |
| @Test | |
| public void testSimple() throws Exception { | |
| assertSingleGlobMatches("food", /* => */ "food"); | |
| } | |
| @Test | |
| public void testIgnoreList() throws Exception { | |
| FileSystemUtils.writeContentAsLatin1(root.getRelative(".bazelignore"), "pkg/foo/bar"); | |
| assertSingleGlobMatches("foo/**", "foo/barnacle/wiz", "foo/barnacle", "foo"); | |
| differencer.invalidate( | |
| ImmutableList.of( | |
| FileStateValue.key( | |
| RootedPath.toRootedPath( | |
| Root.fromPath(root), PathFragment.create(".bazelignore"))))); | |
| FileSystemUtils.createEmptyFile(root.getRelative(".bazelignore")); | |
| assertSingleGlobMatches( | |
| "foo/**", | |
| "foo/bar/wiz", | |
| "foo/bar/wiz/file", | |
| "foo/bar", | |
| "foo/barnacle/wiz", | |
| "foo/barnacle", | |
| "foo"); | |
| } | |
| @Test | |
| public void testStartsWithStar() throws Exception { | |
| assertSingleGlobMatches("*oo", /* => */ "foo"); | |
| } | |
| @Test | |
| public void testStartsWithStarWithMiddleStar() throws Exception { | |
| assertSingleGlobMatches("*f*o", /* => */ "foo"); | |
| } | |
| @Test | |
| public void testSingleMatchEqual() throws Exception { | |
| assertGlobsEqual("*oo", "*f*o"); // both produce "foo" | |
| } | |
| @Test | |
| public void testEndsWithStar() throws Exception { | |
| assertSingleGlobMatches("foo*", /* => */ "foo", "food", "fool"); | |
| } | |
| @Test | |
| public void testEndsWithStarWithMiddleStar() throws Exception { | |
| assertSingleGlobMatches("f*oo*", /* => */ "foo", "food", "fool"); | |
| } | |
| @Test | |
| public void testMultipleMatchesEqual() throws Exception { | |
| assertGlobsEqual("foo*", "f*oo*"); // both produce "foo", "food", "fool" | |
| } | |
| @Test | |
| public void testMiddleStar() throws Exception { | |
| assertSingleGlobMatches("f*o", /* => */ "foo"); | |
| } | |
| @Test | |
| public void testTwoMiddleStars() throws Exception { | |
| assertSingleGlobMatches("f*o*o", /* => */ "foo"); | |
| } | |
| @Test | |
| public void testSingleStarPatternWithNamedChild() throws Exception { | |
| assertSingleGlobMatches("*/bar", /* => */ "foo/bar"); | |
| } | |
| @Test | |
| public void testDeepSubpackages() throws Exception { | |
| assertSingleGlobMatches("*/*/c", /* => */ "a1/b1/c"); | |
| } | |
| @Test | |
| public void testSingleStarPatternWithChildGlob() throws Exception { | |
| assertSingleGlobMatches( | |
| "*/bar*", /* => */ "foo/bar", "foo/barnacle", "food/barnacle", "fool/barnacle"); | |
| } | |
| @Test | |
| public void testSingleStarAsChildGlob() throws Exception { | |
| assertSingleGlobMatches("foo/*/wiz", /* => */ "foo/bar/wiz", "foo/barnacle/wiz"); | |
| } | |
| @Test | |
| public void testNoAsteriskAndFilesDontExist() throws Exception { | |
| // Note un-UNIX like semantics: | |
| assertSingleGlobMatches("ceci/n'est/pas/une/globbe" /* => nothing */); | |
| } | |
| @Test | |
| public void testSingleAsteriskUnderNonexistentDirectory() throws Exception { | |
| // Note un-UNIX like semantics: | |
| assertSingleGlobMatches("not-there/*" /* => nothing */); | |
| } | |
| @Test | |
| public void testDifferentGlobsSameResultEqual() throws Exception { | |
| // Once the globs are run, it doesn't matter what pattern ran; only the output. | |
| assertGlobsEqual("not-there/*", "syzygy/*"); // Both produce nothing. | |
| } | |
| @Test | |
| public void testGlobUnderFile() throws Exception { | |
| assertSingleGlobMatches("foo/bar/wiz/file/*" /* => nothing */); | |
| } | |
| @Test | |
| public void testGlobEqualsHashCode() throws Exception { | |
| // Each "equality group" forms a set of elements that are all equals() to one another, | |
| // and also produce the same hashCode. | |
| new EqualsTester() | |
| .addEqualityGroup( | |
| runSingleGlob("no-such-file", Globber.Operation.FILES_AND_DIRS)) // Matches nothing. | |
| .addEqualityGroup( | |
| runSingleGlob("BUILD", Globber.Operation.FILES_AND_DIRS), | |
| runSingleGlob("BUILD", Globber.Operation.FILES)) // Matches BUILD. | |
| .addEqualityGroup( | |
| runSingleGlob("**", Globber.Operation.FILES_AND_DIRS)) // Matches lots of things. | |
| .addEqualityGroup( | |
| runSingleGlob("f*o/bar*", Globber.Operation.FILES_AND_DIRS), | |
| runSingleGlob( | |
| "foo/bar*", Globber.Operation.FILES_AND_DIRS)) // Matches foo/bar and foo/barnacle. | |
| .testEquals(); | |
| } | |
| @Test | |
| public void testGlobDoesNotCrossPackageBoundary() throws Exception { | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/BUILD")); | |
| // "foo/bar" should not be in the results because foo is a separate package. | |
| assertSingleGlobMatches("f*/*", /* => */ "food/barnacle", "fool/barnacle"); | |
| } | |
| @Test | |
| public void testGlobDirectoryMatchDoesNotCrossPackageBoundary() throws Exception { | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/bar/BUILD")); | |
| // "foo/bar" should not be in the results because foo/bar is a separate package. | |
| assertSingleGlobMatches("foo/*", /* => */ "foo/barnacle"); | |
| } | |
| @Test | |
| public void testStarStarDoesNotCrossPackageBoundary() throws Exception { | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/bar/BUILD")); | |
| // "foo/bar" should not be in the results because foo/bar is a separate package. | |
| assertSingleGlobMatches("foo/**", /* => */ "foo/barnacle/wiz", "foo/barnacle", "foo"); | |
| } | |
| @Test | |
| public void testGlobDoesNotCrossPackageBoundaryUnderOtherPackagePath() throws Exception { | |
| writableRoot.getRelative("pkg/foo/bar").createDirectoryAndParents(); | |
| FileSystemUtils.createEmptyFile(writableRoot.getRelative("pkg/foo/bar/BUILD")); | |
| // "foo/bar" should not be in the results because foo/bar is detected as a separate package, | |
| // even though it is under a different package path. | |
| assertSingleGlobMatches("foo/**", /* => */ "foo/barnacle/wiz", "foo/barnacle", "foo"); | |
| } | |
| @Test | |
| public void testGlobDoesNotCrossRepositoryBoundary() throws Exception { | |
| FileSystemUtils.appendIsoLatin1( | |
| root.getRelative("WORKSPACE"), "local_repository(name='local', path='pkg/foo')"); | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/WORKSPACE")); | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/BUILD")); | |
| // "foo/bar" should not be in the results because foo is a separate repository. | |
| assertSingleGlobMatches("f*/*", /* => */ "food/barnacle", "fool/barnacle"); | |
| } | |
| @Test | |
| public void testGlobDirectoryMatchDoesNotCrossRepositoryBoundary() throws Exception { | |
| FileSystemUtils.appendIsoLatin1( | |
| root.getRelative("WORKSPACE"), "local_repository(name='local', path='pkg/foo/bar')"); | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/bar/WORKSPACE")); | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/bar/BUILD")); | |
| // "foo/bar" should not be in the results because foo/bar is a separate repository. | |
| assertSingleGlobMatches("foo/*", /* => */ "foo/barnacle"); | |
| } | |
| @Test | |
| public void testStarStarDoesNotCrossRepositoryBoundary() throws Exception { | |
| FileSystemUtils.appendIsoLatin1( | |
| root.getRelative("WORKSPACE"), "local_repository(name='local', path='pkg/foo/bar')"); | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/bar/WORKSPACE")); | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/bar/BUILD")); | |
| // "foo/bar" should not be in the results because foo/bar is a separate repository. | |
| assertSingleGlobMatches("foo/**", /* => */ "foo/barnacle/wiz", "foo/barnacle", "foo"); | |
| } | |
| @Test | |
| public void testGlobDoesNotCrossRepositoryBoundaryUnderOtherPackagePath() throws Exception { | |
| FileSystemUtils.appendIsoLatin1( | |
| root.getRelative("WORKSPACE"), | |
| "local_repository(name='local', path='" | |
| + writableRoot.getRelative("pkg/foo/bar").getPathString() | |
| + "')"); | |
| writableRoot.getRelative("pkg/foo/bar").createDirectoryAndParents(); | |
| FileSystemUtils.createEmptyFile(writableRoot.getRelative("pkg/foo/bar/WORKSPACE")); | |
| FileSystemUtils.createEmptyFile(writableRoot.getRelative("pkg/foo/bar/BUILD")); | |
| // "foo/bar" should not be in the results because foo/bar is detected as a separate package, | |
| // even though it is under a different package path. | |
| assertSingleGlobMatches("foo/**", /* => */ "foo/barnacle/wiz", "foo/barnacle", "foo"); | |
| } | |
| /** | |
| * For {@link GlobFunctionTest}, creates a {@link GlobDescriptor} using the input pattern. | |
| * | |
| * <p>For {@link GlobsFunctionTest}, creates a {@link GlobsValue.Key} whose {@code globRequests} | |
| * member contains only one element. The sole element's pattern is the input one. | |
| * | |
| * <p>Queries the {@link GlobDescriptor} or {@link GlobsValue.Key} in Skyframe and asserts that | |
| * matches in the result {@link GlobValue} or {@link GlobsValue} is equal to the input {@code | |
| * expecteds}. | |
| */ | |
| private void assertSingleGlobMatches(String pattern, String... expecteds) throws Exception { | |
| assertSingleGlobMatches(pattern, Operation.FILES_AND_DIRS, expecteds); | |
| } | |
| protected abstract void assertSingleGlobMatches( | |
| String pattern, Globber.Operation globberOperation, String... expecteds) throws Exception; | |
| private void assertGlobWithoutDirsMatches(String pattern, String... expecteds) throws Exception { | |
| assertSingleGlobMatches(pattern, Globber.Operation.FILES, expecteds); | |
| } | |
| protected void assertGlobsEqual(String pattern1, String pattern2) throws Exception { | |
| SkyValue value1 = runSingleGlob(pattern1, Globber.Operation.FILES_AND_DIRS); | |
| SkyValue value2 = runSingleGlob(pattern2, Globber.Operation.FILES_AND_DIRS); | |
| new EqualsTester().addEqualityGroup(value1, value2).testEquals(); | |
| } | |
| protected abstract SkyValue runSingleGlob(String pattern, Globber.Operation globberOperation) | |
| throws Exception; | |
| @Test | |
| public void testGlobWithoutWildcardsDoesNotCallReaddir() throws Exception { | |
| String pattern = "foo/bar/wiz/file"; | |
| assertSingleGlobMatches(pattern, "foo/bar/wiz/file"); | |
| // Ensure that the glob depends on the FileValue and not on the DirectoryListingValue. | |
| pkgPath.getRelative("foo/bar/wiz/file").delete(); | |
| // Nothing has been invalidated yet, so the cached result is returned. | |
| assertSingleGlobMatches(pattern, "foo/bar/wiz/file"); | |
| differencer.invalidate( | |
| ImmutableList.of( | |
| DirectoryListingStateValue.key( | |
| RootedPath.toRootedPath(Root.fromPath(root), pkgPath.getRelative("foo/bar/wiz"))))); | |
| // The result should not rely on the DirectoryListingValue, so it's still a cache hit. | |
| assertSingleGlobMatches(pattern, "foo/bar/wiz/file"); | |
| differencer.invalidate( | |
| ImmutableList.of( | |
| FileStateValue.key( | |
| RootedPath.toRootedPath( | |
| Root.fromPath(root), pkgPath.getRelative("foo/bar/wiz/file"))))); | |
| // This should have invalidated the glob result. | |
| assertSingleGlobMatches(pattern /* => nothing */); | |
| } | |
| @Test | |
| public void testIllegalPatterns() { | |
| assertIllegalPattern("foo**bar"); | |
| assertIllegalPattern("?"); | |
| assertIllegalPattern(""); | |
| assertIllegalPattern("."); | |
| assertIllegalPattern("/foo"); | |
| assertIllegalPattern("./foo"); | |
| assertIllegalPattern("foo/"); | |
| assertIllegalPattern("foo/./bar"); | |
| assertIllegalPattern("../foo/bar"); | |
| assertIllegalPattern("foo//bar"); | |
| } | |
| @Test | |
| public void testIllegalRecursivePatterns() throws Exception { | |
| for (String prefix : Lists.newArrayList("", "*/", "**/", "ba/")) { | |
| String suffix = ("/" + prefix).substring(0, prefix.length()); | |
| for (String pattern : Lists.newArrayList("**fo", "fo**", "**fo**", "fo**fo", "fo**fo**fo")) { | |
| assertIllegalPattern(prefix + pattern); | |
| assertIllegalPattern(pattern + suffix); | |
| } | |
| } | |
| } | |
| protected abstract void assertIllegalPattern(String pattern); | |
| /** Tests that globs can contain Java regular expression special characters */ | |
| @Test | |
| public void testSpecialRegexCharacter() throws Exception { | |
| Path aDotB = pkgPath.getChild("a.b"); | |
| FileSystemUtils.createEmptyFile(aDotB); | |
| FileSystemUtils.createEmptyFile(pkgPath.getChild("aab")); | |
| // Note: this contains two asterisks because otherwise a RE is not built, | |
| // as an optimization. | |
| assertThat( | |
| new UnixGlob.Builder(pkgPath, SyscallCache.NO_CACHE) | |
| .addPattern("*a.b*") | |
| .globInterruptible()) | |
| .containsExactly(aDotB); | |
| } | |
| @Test | |
| public void testMatchesCallWithNoCache() { | |
| assertThat(UnixGlob.matches("*a*b", "CaCb", null)).isTrue(); | |
| } | |
| @Test | |
| public void testHiddenFiles() throws Exception { | |
| for (String dir : ImmutableList.of(".hidden", "..also.hidden", "not.hidden")) { | |
| pkgPath.getRelative(dir).createDirectoryAndParents(); | |
| } | |
| // Note that these are not in the result: ".", ".." | |
| assertSingleGlobMatches( | |
| "*", "..also.hidden", ".hidden", "BUILD", "a1", "a2", "foo", "food", "fool", "not.hidden"); | |
| assertSingleGlobMatches("*.hidden", "not.hidden"); | |
| } | |
| @Test | |
| public void testDoubleStar() throws Exception { | |
| assertSingleGlobMatches( | |
| "**", | |
| "a1/b1/c", | |
| "a1/b1", | |
| "a1", | |
| "a2", | |
| "foo/bar/wiz", | |
| "foo/bar/wiz/file", | |
| "foo/bar", | |
| "foo/barnacle/wiz", | |
| "foo/barnacle", | |
| "foo", | |
| "food/barnacle/wiz", | |
| "food/barnacle", | |
| "food", | |
| "fool/barnacle/wiz", | |
| "fool/barnacle", | |
| "fool", | |
| "BUILD"); | |
| } | |
| @Test | |
| public void testDoubleStarExcludeDirs() throws Exception { | |
| assertGlobWithoutDirsMatches("**", "foo/bar/wiz/file", "BUILD"); | |
| } | |
| @Test | |
| public void testDoubleDoubleStar() throws Exception { | |
| assertSingleGlobMatches( | |
| "**/**", | |
| "a1/b1/c", | |
| "a1/b1", | |
| "a1", | |
| "a2", | |
| "foo/bar/wiz", | |
| "foo/bar/wiz/file", | |
| "foo/bar", | |
| "foo/barnacle/wiz", | |
| "foo/barnacle", | |
| "foo", | |
| "food/barnacle/wiz", | |
| "food/barnacle", | |
| "food", | |
| "fool/barnacle/wiz", | |
| "fool/barnacle", | |
| "fool", | |
| "BUILD"); | |
| } | |
| @Test | |
| public void testDirectoryWithDoubleStar() throws Exception { | |
| assertSingleGlobMatches( | |
| "foo/**", | |
| "foo/bar/wiz", | |
| "foo/bar/wiz/file", | |
| "foo/bar", | |
| "foo/barnacle/wiz", | |
| "foo/barnacle", | |
| "foo"); | |
| } | |
| @Test | |
| public void testDoubleStarPatternWithNamedChild() throws Exception { | |
| assertSingleGlobMatches("**/bar", "foo/bar"); | |
| } | |
| @Test | |
| public void testDoubleStarPatternWithErrorChild() throws Exception { | |
| FileSystemUtils.ensureSymbolicLink(pkgPath.getChild("self"), "self"); | |
| IOException ioException = | |
| assertThrows(IOException.class, () -> runSingleGlob("**/self", Operation.FILES)); | |
| assertThat(ioException).hasMessageThat().matches("Symlink cycle"); | |
| } | |
| @Test | |
| public void testDoubleStarPatternWithChildGlob() throws Exception { | |
| assertSingleGlobMatches("**/ba*", "foo/bar", "foo/barnacle", "food/barnacle", "fool/barnacle"); | |
| } | |
| @Test | |
| public void testDoubleStarAsChildGlob() throws Exception { | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("foo/barnacle/wiz/wiz")); | |
| pkgPath.getRelative("foo/barnacle/baz/wiz").createDirectoryAndParents(); | |
| assertSingleGlobMatches( | |
| "foo/**/wiz", | |
| "foo/bar/wiz", | |
| "foo/barnacle/wiz", | |
| "foo/barnacle/baz/wiz", | |
| "foo/barnacle/wiz/wiz"); | |
| } | |
| @Test | |
| public void testDoubleStarUnderNonexistentDirectory() throws Exception { | |
| assertSingleGlobMatches("not-there/**" /* => nothing */); | |
| } | |
| @Test | |
| public void testDoubleStarUnderFile() throws Exception { | |
| assertSingleGlobMatches("foo/bar/wiz/file/**" /* => nothing */); | |
| } | |
| protected abstract SkyKey createdGlobRelatedSkyKey( | |
| String pattern, Globber.Operation globberOperation) throws InvalidGlobPatternException; | |
| /** Regression test for b/13319874: Directory listing crash. */ | |
| @Test | |
| public void testResilienceToFilesystemInconsistencies_directoryExistence() throws Exception { | |
| // Our custom filesystem says "pkgPath/BUILD" exists but "pkgPath" does not exist. | |
| fs.stubStat(pkgPath, null); | |
| RootedPath pkgRootedPath = RootedPath.toRootedPath(Root.fromPath(root), pkgPath); | |
| FileStateValue pkgDirFileStateValue = | |
| FileStateValue.create(pkgRootedPath, SyscallCache.NO_CACHE, /* tsgm= */ null); | |
| FileValue pkgDirValue = | |
| FileValue.value( | |
| ImmutableList.of(pkgRootedPath), | |
| null, | |
| null, | |
| pkgRootedPath, | |
| pkgDirFileStateValue, | |
| pkgRootedPath, | |
| pkgDirFileStateValue); | |
| differencer.inject(ImmutableMap.of(FileValue.key(pkgRootedPath), Delta.justNew(pkgDirValue))); | |
| String expectedMessage = "/root/workspace/pkg is no longer an existing directory"; | |
| SkyKey skyKey = createdGlobRelatedSkyKey("*/foo", Operation.FILES_AND_DIRS); | |
| EvaluationResult<GlobValue> result = | |
| evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class); | |
| assertThat(errorInfo.getException()).hasMessageThat().contains(expectedMessage); | |
| } | |
| @Test | |
| public void testResilienceToFilesystemInconsistencies_subdirectoryExistence() throws Exception { | |
| // Our custom filesystem says directory "pkgPath/foo/bar" contains a subdirectory "wiz" but a | |
| // direct stat on "pkgPath/foo/bar/wiz" says it does not exist. | |
| Path fooBarDir = pkgPath.getRelative("foo/bar"); | |
| fs.stubStat(fooBarDir.getRelative("wiz"), null); | |
| RootedPath fooBarDirRootedPath = RootedPath.toRootedPath(Root.fromPath(root), fooBarDir); | |
| SkyValue fooBarDirListingValue = | |
| DirectoryListingStateValue.create( | |
| ImmutableList.of(new Dirent("wiz", Dirent.Type.DIRECTORY))); | |
| differencer.inject( | |
| ImmutableMap.of( | |
| DirectoryListingStateValue.key(fooBarDirRootedPath), | |
| Delta.justNew(fooBarDirListingValue))); | |
| String expectedMessage = "/root/workspace/pkg/foo/bar/wiz is no longer an existing directory."; | |
| SkyKey skyKey = createdGlobRelatedSkyKey("**/wiz", Globber.Operation.FILES_AND_DIRS); | |
| EvaluationResult<GlobValue> result = | |
| evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class); | |
| assertThat(errorInfo.getException()).hasMessageThat().contains(expectedMessage); | |
| } | |
| @Test | |
| public void testResilienceToFilesystemInconsistencies_symlinkType() throws Exception { | |
| RootedPath wizRootedPath = | |
| RootedPath.toRootedPath(Root.fromPath(root), pkgPath.getRelative("foo/bar/wiz")); | |
| RootedPath fileRootedPath = | |
| RootedPath.toRootedPath(Root.fromPath(root), pkgPath.getRelative("foo/bar/wiz/file")); | |
| final FileStatus realStat = fileRootedPath.asPath().stat(); | |
| fs.stubStat( | |
| fileRootedPath.asPath(), | |
| new FileStatus() { | |
| @Override | |
| public boolean isFile() { | |
| // The stat says foo/bar/wiz/file is a real file, not a symlink. | |
| return true; | |
| } | |
| @Override | |
| public boolean isSpecialFile() { | |
| return false; | |
| } | |
| @Override | |
| public boolean isDirectory() { | |
| return false; | |
| } | |
| @Override | |
| public boolean isSymbolicLink() { | |
| return false; | |
| } | |
| @Override | |
| public long getSize() throws IOException { | |
| return realStat.getSize(); | |
| } | |
| @Override | |
| public long getLastModifiedTime() throws IOException { | |
| return realStat.getLastModifiedTime(); | |
| } | |
| @Override | |
| public long getLastChangeTime() throws IOException { | |
| return realStat.getLastChangeTime(); | |
| } | |
| @Override | |
| public long getNodeId() throws IOException { | |
| return realStat.getNodeId(); | |
| } | |
| }); | |
| // But the dir listing say foo/bar/wiz/file is a symlink. | |
| SkyValue wizDirListingValue = | |
| DirectoryListingStateValue.create( | |
| ImmutableList.of(new Dirent("file", Dirent.Type.SYMLINK))); | |
| differencer.inject( | |
| ImmutableMap.of( | |
| DirectoryListingStateValue.key(wizRootedPath), Delta.justNew(wizDirListingValue))); | |
| String expectedMessage = | |
| "readdir and stat disagree about whether " + fileRootedPath.asPath() + " is a symlink"; | |
| SkyKey skyKey = createdGlobRelatedSkyKey("foo/bar/wiz/*", Globber.Operation.FILES_AND_DIRS); | |
| EvaluationResult<GlobValue> result = | |
| evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class); | |
| assertThat(errorInfo.getException()).hasMessageThat().contains(expectedMessage); | |
| } | |
| /** | |
| * When globbing symlinks, the returned path should use the path of the symlink source instead of | |
| * the symlink target, regardless of whether glob pattern contains wildcard character or not. | |
| */ | |
| @Test | |
| public void testSymlinks(@TestParameter boolean withWildcard) throws Exception { | |
| pkgPath.getRelative("symlinks").createDirectoryAndParents(); | |
| FileSystemUtils.ensureSymbolicLink(pkgPath.getRelative("symlinks/dangling.txt"), "nope"); | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("symlinks/yup")); | |
| FileSystemUtils.ensureSymbolicLink(pkgPath.getRelative("symlinks/existing.txt"), "yup"); | |
| String globPattern = withWildcard ? "symlinks/*.txt" : "symlinks/existing.txt"; | |
| assertSingleGlobMatches(globPattern, "symlinks/existing.txt"); | |
| } | |
| @Test | |
| public void testSymlinks_symlinkPointToDirectory() throws Exception { | |
| root.getRelative("target_direc").createDirectoryAndParents(); | |
| FileSystemUtils.createEmptyFile(root.getRelative("target_direc/file1")); | |
| root.getRelative("target_direc/sub").createDirectoryAndParents(); | |
| FileSystemUtils.createEmptyFile(root.getRelative("target_direc/sub/file2")); | |
| FileSystemUtils.ensureSymbolicLink( | |
| pkgPath.getRelative("symlink"), root.getRelative("target_direc")); | |
| assertSingleGlobMatches( | |
| "symlink/**", "symlink/sub", "symlink/sub/file2", "symlink", "symlink/file1"); | |
| } | |
| @Test | |
| public void symlinkFileValueWithError_symlinkCycleToSelf() throws Exception { | |
| FileSystemUtils.ensureSymbolicLink(pkgPath.getChild("self"), "self"); | |
| IOException ioException = | |
| assertThrows(IOException.class, () -> runSingleGlob("self", Operation.FILES_AND_DIRS)); | |
| assertThat(ioException).hasMessageThat().matches("Symlink cycle"); | |
| } | |
| @Test | |
| public void symlinkFileValueWithError_symlinkCycleBetweenTwoSymlinks( | |
| @TestParameter boolean withWildcard) throws Exception { | |
| pkgPath.getRelative("foo").createDirectoryAndParents(); | |
| pkgPath.getRelative("bar").createDirectoryAndParents(); | |
| FileSystemUtils.ensureSymbolicLink(pkgPath.getRelative("foo/a"), pkgPath.getRelative("bar/b")); | |
| FileSystemUtils.ensureSymbolicLink(pkgPath.getRelative("bar/b"), pkgPath.getRelative("foo/a")); | |
| String globPattern = withWildcard ? "foo/*" : "foo/a"; | |
| IOException ioException = | |
| assertThrows(IOException.class, () -> runSingleGlob(globPattern, Operation.FILES_AND_DIRS)); | |
| assertThat(ioException).hasMessageThat().matches("Symlink cycle"); | |
| } | |
| @Test | |
| public void symlinkSubdirValueWithError() throws Exception { | |
| Path cycle = pkgPath.getChild("cycle"); | |
| FileSystemUtils.ensureSymbolicLink(cycle.getChild("self"), "self"); | |
| FileSystemUtils.ensureSymbolicLink(pkgPath.getChild("symlink"), cycle); | |
| IOException ioException = | |
| assertThrows( | |
| IOException.class, () -> runSingleGlob("symlink/self", Operation.FILES_AND_DIRS)); | |
| assertThat(ioException).hasMessageThat().matches("Symlink cycle"); | |
| } | |
| @Test | |
| public void testSymlinks_unboundedSymlinkExpansion(@TestParameter boolean withRecursiveWildcard) | |
| throws Exception { | |
| pkgPath.getRelative("parent/sub").createDirectoryAndParents(); | |
| FileSystemUtils.ensureSymbolicLink( | |
| pkgPath.getRelative("parent/sub/symlink"), pkgPath.getRelative("parent")); | |
| String globPattern = withRecursiveWildcard ? "parent/**" : "parent/sub/symlink"; | |
| SkyKey skyKey = createdGlobRelatedSkyKey(globPattern, Globber.Operation.FILES_AND_DIRS); | |
| EvaluationResult<GlobValue> result = | |
| evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS); | |
| if (withRecursiveWildcard) { | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| assertThat(errorInfo.getException()) | |
| .isInstanceOf(FileSymlinkInfiniteExpansionException.class); | |
| assertThat(errorInfo.getException()).hasMessageThat().contains("Infinite symlink expansion"); | |
| } else { | |
| assertThat(result.hasError()).isFalse(); | |
| } | |
| } | |
| /** | |
| * Covers the scenario when a directory has two symlinks of different status. | |
| * | |
| * <p>One of the symlinks is a normal one whose path should be accepted by {@code | |
| * SymlinkProducer}. | |
| * | |
| * <p>The other symlink shows different {@code readdir} and {@code stat} status. {@code readdir} | |
| * shows that it is a symlink but {@code stat} shows that it is a normal file. A {@link | |
| * InconsistentFilesystemException} should be accepted for this path by {@code SymlinkProducer}. | |
| * | |
| * <p>{@code PatternWithWildcardProducer} immediately returns {@code DONE} when knowing the number | |
| * of accepted symlink paths (1) is smaller than the number of symlink queried (2). The size | |
| * mismatch indicates that one of the symlinks goes wrong. | |
| */ | |
| @Test | |
| public void testSymlinks_oneNormalOneInconsistencyFilesystemError() throws Exception { | |
| pkgPath.getRelative("inconsistent").createDirectoryAndParents(); | |
| FileSystemUtils.createEmptyFile(pkgPath.getRelative("target")); | |
| FileSystemUtils.ensureSymbolicLink( | |
| pkgPath.getRelative("inconsistent/good"), pkgPath.getRelative("target")); | |
| FileSystemUtils.ensureSymbolicLink( | |
| pkgPath.getRelative("inconsistent/bad"), pkgPath.getRelative("target")); | |
| RootedPath badRootedPath = | |
| RootedPath.toRootedPath(Root.fromPath(root), pkgPath.getRelative("inconsistent/bad")); | |
| final FileStatus realStat = badRootedPath.asPath().stat(); | |
| fs.stubStat( | |
| badRootedPath.asPath(), | |
| new FileStatus() { | |
| @Override | |
| public boolean isFile() { | |
| // Intentionally set `isFile` as true, which disagree with filesystem. | |
| return true; | |
| } | |
| @Override | |
| public boolean isSpecialFile() { | |
| return false; | |
| } | |
| @Override | |
| public boolean isDirectory() { | |
| return false; | |
| } | |
| @Override | |
| public boolean isSymbolicLink() { | |
| // Intentionally set `isSymbolicLink` as false, which disagree with filesystem. | |
| return false; | |
| } | |
| @Override | |
| public long getSize() throws IOException { | |
| return realStat.getSize(); | |
| } | |
| @Override | |
| public long getLastModifiedTime() throws IOException { | |
| return realStat.getLastModifiedTime(); | |
| } | |
| @Override | |
| public long getLastChangeTime() throws IOException { | |
| return realStat.getLastChangeTime(); | |
| } | |
| @Override | |
| public long getNodeId() throws IOException { | |
| return realStat.getNodeId(); | |
| } | |
| }); | |
| SkyKey skyKey = createdGlobRelatedSkyKey("inconsistent/*", Globber.Operation.FILES_AND_DIRS); | |
| EvaluationResult<GlobValue> result = | |
| evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS); | |
| assertThat(result.hasError()).isTrue(); | |
| ErrorInfo errorInfo = result.getError(skyKey); | |
| assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class); | |
| assertThat(errorInfo.getException()) | |
| .hasMessageThat() | |
| .contains("Inconsistent filesystem operations. readdir and stat disagree"); | |
| } | |
| /** | |
| * The test below covers the case when {@link DirectoryListingValue} contains multiple symlinks, | |
| * which is common for bazel shell integration tests. Bazel shell integration tests usually create | |
| * symlinks for all source files. | |
| * | |
| * <p>Expect all matches to be returned when globbing multiple symlinks under the directory. | |
| */ | |
| @Test | |
| public void testSymlinksUnderDirectory_shouldAllBeGlobbed() throws Exception { | |
| root.getRelative("targets").createDirectoryAndParents(); | |
| pkgPath.getRelative("symlinks").createDirectoryAndParents(); | |
| for (char c = 'a'; c <= 'z'; ++c) { | |
| FileSystemUtils.createEmptyFile(root.getRelative("targets/" + c + ".bzl")); | |
| FileSystemUtils.ensureSymbolicLink( | |
| pkgPath.getRelative("symlinks/" + c + ".bzl"), root.getRelative("targets/" + c + ".bzl")); | |
| } | |
| String[] allExpectedPathsInStr = new String[26]; | |
| for (int i = 0; i < 26; ++i) { | |
| allExpectedPathsInStr[i] = "symlinks/" + (char) ('a' + i) + ".bzl"; | |
| } | |
| assertSingleGlobMatches("symlinks/*.bzl", Operation.FILES_AND_DIRS, allExpectedPathsInStr); | |
| } | |
| static final class CustomInMemoryFs extends InMemoryFileSystem { | |
| private final Map<PathFragment, FileStatus> stubbedStats = Maps.newHashMap(); | |
| CustomInMemoryFs(ManualClock manualClock) { | |
| super(manualClock, DigestHashFunction.SHA256); | |
| } | |
| public void stubStat(Path path, @Nullable FileStatus stubbedResult) { | |
| stubbedStats.put(path.asFragment(), stubbedResult); | |
| } | |
| @Override | |
| public FileStatus statIfFound(PathFragment path, boolean followSymlinks) throws IOException { | |
| if (stubbedStats.containsKey(path)) { | |
| return stubbedStats.get(path); | |
| } | |
| return super.statIfFound(path, followSymlinks); | |
| } | |
| } | |
| private void assertSubpackageMatches(String pattern, String... expecteds) throws Exception { | |
| assertThat(getSubpackagesMatches(pattern)) | |
| .containsExactlyElementsIn(ImmutableList.copyOf(expecteds)); | |
| } | |
| protected abstract Iterable<String> getSubpackagesMatches(String pattern) throws Exception; | |
| 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_doubleStarPatternWithNamedChild() throws Exception { | |
| assertSubpackageMatches("**/bar"); | |
| } | |
| @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_zeroLevelDeep(@TestParameter boolean withDeeperSubpackage) | |
| throws Exception { | |
| makeEmptyPackage("sub"); | |
| if (withDeeperSubpackage) { | |
| 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); | |
| 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"); | |
| } | |
| } |