| // Copyright 2015 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 com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode.CROSS; | 
 | import static com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode.DONT_CROSS; | 
 | import static com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode.REPORT_ERROR; | 
 | import static com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFileFactoryForTesting.danglingSymlinkForTesting; | 
 | import static com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFileFactoryForTesting.regularFileForTesting; | 
 | import static com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFileFactoryForTesting.symlinkToDirectoryForTesting; | 
 | import static com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFileFactoryForTesting.symlinkToFileForTesting; | 
 |  | 
 | import com.google.common.base.Preconditions; | 
 | import com.google.common.base.Supplier; | 
 | import com.google.common.collect.ImmutableList; | 
 | import com.google.common.collect.ImmutableSet; | 
 | import com.google.common.collect.Sets; | 
 | import com.google.devtools.build.lib.actions.Artifact; | 
 | import com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode; | 
 | import com.google.devtools.build.lib.actions.Root; | 
 | import com.google.devtools.build.lib.cmdline.PackageIdentifier; | 
 | import com.google.devtools.build.lib.events.NullEventHandler; | 
 | import com.google.devtools.build.lib.pkgcache.PathPackageLocator; | 
 | import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile; | 
 | import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.TraversalRequest; | 
 | import com.google.devtools.build.lib.testutil.FoundationTestCase; | 
 | import com.google.devtools.build.lib.util.BlazeClock; | 
 | import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor; | 
 | import com.google.devtools.build.lib.vfs.Path; | 
 | import com.google.devtools.build.lib.vfs.PathFragment; | 
 | import com.google.devtools.build.lib.vfs.RootedPath; | 
 | import com.google.devtools.build.skyframe.ErrorInfo; | 
 | import com.google.devtools.build.skyframe.EvaluationProgressReceiver; | 
 | 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.SequentialBuildDriver; | 
 | 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 org.junit.Before; | 
 | import org.junit.Test; | 
 | import org.junit.runner.RunWith; | 
 | import org.junit.runners.JUnit4; | 
 |  | 
 | import java.io.OutputStream; | 
 | import java.nio.charset.StandardCharsets; | 
 | import java.util.HashMap; | 
 | import java.util.HashSet; | 
 | import java.util.Map; | 
 | import java.util.Set; | 
 | import java.util.UUID; | 
 | import java.util.concurrent.atomic.AtomicReference; | 
 | import java.util.regex.Pattern; | 
 |  | 
 | /** Tests for {@link RecursiveFilesystemTraversalFunction}. */ | 
 | @RunWith(JUnit4.class) | 
 | public final class RecursiveFilesystemTraversalFunctionTest extends FoundationTestCase { | 
 |  | 
 |   private final TimestampGranularityMonitor tsgm = | 
 |       new TimestampGranularityMonitor(BlazeClock.instance()); | 
 |   private RecordingEvaluationProgressReceiver progressReceiver; | 
 |   private MemoizingEvaluator evaluator; | 
 |   private SequentialBuildDriver driver; | 
 |   private RecordingDifferencer differencer; | 
 |   private AtomicReference<PathPackageLocator> pkgLocator; | 
 |  | 
 |   @Before | 
 |   public final void setUp() throws Exception  { | 
 |     pkgLocator = new AtomicReference<>( | 
 |         new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory))); | 
 |     AtomicReference<ImmutableSet<PackageIdentifier>> deletedPackages = | 
 |         new AtomicReference<>(ImmutableSet.<PackageIdentifier>of()); | 
 |     ExternalFilesHelper externalFilesHelper = new ExternalFilesHelper(pkgLocator); | 
 |  | 
 |     Map<SkyFunctionName, SkyFunction> skyFunctions = new HashMap<>(); | 
 |  | 
 |     skyFunctions.put(SkyFunctions.FILE_STATE, new FileStateFunction(tsgm, externalFilesHelper)); | 
 |     skyFunctions.put(SkyFunctions.FILE, new FileFunction(pkgLocator, tsgm, externalFilesHelper)); | 
 |     skyFunctions.put(SkyFunctions.DIRECTORY_LISTING, new DirectoryListingFunction()); | 
 |     skyFunctions.put( | 
 |         SkyFunctions.DIRECTORY_LISTING_STATE, | 
 |         new DirectoryListingStateFunction(externalFilesHelper)); | 
 |     skyFunctions.put( | 
 |         SkyFunctions.RECURSIVE_FILESYSTEM_TRAVERSAL, new RecursiveFilesystemTraversalFunction()); | 
 |     skyFunctions.put(SkyFunctions.PACKAGE_LOOKUP, new PackageLookupFunction(deletedPackages)); | 
 |     skyFunctions.put(SkyFunctions.BLACKLISTED_PACKAGE_PREFIXES, | 
 |         new BlacklistedPackagePrefixesFunction()); | 
 |  | 
 |     progressReceiver = new RecordingEvaluationProgressReceiver(); | 
 |     differencer = new RecordingDifferencer(); | 
 |     evaluator = new InMemoryMemoizingEvaluator(skyFunctions, differencer, progressReceiver); | 
 |     driver = new SequentialBuildDriver(evaluator); | 
 |     PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID()); | 
 |     PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator.get()); | 
 |     PrecomputedValue.BLACKLISTED_PACKAGE_PREFIXES_FILE.set( | 
 |         differencer, PathFragment.EMPTY_FRAGMENT); | 
 |   } | 
 |  | 
 |   private Artifact sourceArtifact(String path) { | 
 |     return new Artifact(new PathFragment(path), Root.asSourceRoot(rootDirectory)); | 
 |   } | 
 |  | 
 |   private Artifact sourceArtifactUnderPackagePath(String path, String packagePath) { | 
 |     return new Artifact( | 
 |         new PathFragment(path), Root.asSourceRoot(rootDirectory.getRelative(packagePath))); | 
 |   } | 
 |  | 
 |   private Artifact derivedArtifact(String path) { | 
 |     PathFragment execPath = new PathFragment("out").getRelative(path); | 
 |     Path fullPath = rootDirectory.getRelative(execPath); | 
 |     Artifact output = | 
 |         new Artifact( | 
 |             fullPath, | 
 |             Root.asDerivedRoot(rootDirectory, rootDirectory.getRelative("out")), | 
 |             execPath); | 
 |     return output; | 
 |   } | 
 |  | 
 |   private static RootedPath rootedPath(Artifact artifact) { | 
 |     return RootedPath.toRootedPath(artifact.getRoot().getPath(), artifact.getRootRelativePath()); | 
 |   } | 
 |  | 
 |   private RootedPath rootedPath(String path, String packagePath) { | 
 |     return RootedPath.toRootedPath(rootDirectory.getRelative(packagePath), new PathFragment(path)); | 
 |   } | 
 |  | 
 |   private static RootedPath childOf(Artifact artifact, String relative) { | 
 |     return RootedPath.toRootedPath( | 
 |         artifact.getRoot().getPath(), artifact.getRootRelativePath().getRelative(relative)); | 
 |   } | 
 |  | 
 |   private static RootedPath childOf(RootedPath path, String relative) { | 
 |     return RootedPath.toRootedPath(path.getRoot(), path.getRelativePath().getRelative(relative)); | 
 |   } | 
 |  | 
 |   private static RootedPath parentOf(RootedPath path) { | 
 |     PathFragment parent = Preconditions.checkNotNull(path.getRelativePath().getParentDirectory()); | 
 |     return RootedPath.toRootedPath(path.getRoot(), parent); | 
 |   } | 
 |  | 
 |   private static RootedPath siblingOf(RootedPath path, String relative) { | 
 |     PathFragment parent = Preconditions.checkNotNull(path.getRelativePath().getParentDirectory()); | 
 |     return RootedPath.toRootedPath(path.getRoot(), parent.getRelative(relative)); | 
 |   } | 
 |  | 
 |   private static RootedPath siblingOf(Artifact artifact, String relative) { | 
 |     PathFragment parent = | 
 |         Preconditions.checkNotNull(artifact.getRootRelativePath().getParentDirectory()); | 
 |     return RootedPath.toRootedPath(artifact.getRoot().getPath(), parent.getRelative(relative)); | 
 |   } | 
 |  | 
 |   private void createFile(Path path, String... contents) throws Exception { | 
 |     if (!path.getParentDirectory().exists()) { | 
 |       scratch.dir(path.getParentDirectory().getPathString()); | 
 |     } | 
 |     scratch.file(path.getPathString(), contents); | 
 |   } | 
 |  | 
 |   private void createFile(Artifact artifact, String... contents) throws Exception { | 
 |     createFile(artifact.getPath(), contents); | 
 |   } | 
 |  | 
 |   private RootedPath createFile(RootedPath path, String... contents) throws Exception { | 
 |     scratch.dir(parentOf(path).asPath().getPathString()); | 
 |     createFile(path.asPath(), contents); | 
 |     return path; | 
 |   } | 
 |  | 
 |   private static TraversalRequest fileLikeRoot(Artifact file, PackageBoundaryMode pkgBoundaryMode) { | 
 |     return new TraversalRequest( | 
 |         rootedPath(file), !file.isSourceArtifact(), pkgBoundaryMode, false, null, null); | 
 |   } | 
 |  | 
 |   private static TraversalRequest pkgRoot( | 
 |       RootedPath pkgDirectory, PackageBoundaryMode pkgBoundaryMode) { | 
 |     return new TraversalRequest(pkgDirectory, false, pkgBoundaryMode, true, null, null); | 
 |   } | 
 |  | 
 |   private <T extends SkyValue> EvaluationResult<T> eval(SkyKey key) throws Exception { | 
 |     return driver.evaluate( | 
 |         ImmutableList.of(key), | 
 |         false, | 
 |         SkyframeExecutor.DEFAULT_THREAD_COUNT, | 
 |         NullEventHandler.INSTANCE); | 
 |   } | 
 |  | 
 |   private RecursiveFilesystemTraversalValue evalTraversalRequest(TraversalRequest params) | 
 |       throws Exception { | 
 |     SkyKey key = rftvSkyKey(params); | 
 |     EvaluationResult<RecursiveFilesystemTraversalValue> result = eval(key); | 
 |     assertThat(result.hasError()).isFalse(); | 
 |     return result.get(key); | 
 |   } | 
 |  | 
 |   private static SkyKey rftvSkyKey(TraversalRequest params) { | 
 |     return RecursiveFilesystemTraversalValue.key(params); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Asserts that the requested SkyValue can be built and results in the expected set of files. | 
 |    * | 
 |    * <p>The metadata of files is ignored in comparing the actual results with the expected ones. | 
 |    * The returned object however contains the actual metadata. | 
 |    */ | 
 |   @SafeVarargs | 
 |   private final RecursiveFilesystemTraversalValue traverseAndAssertFiles( | 
 |       TraversalRequest params, ResolvedFile... expectedFilesIgnoringMetadata) throws Exception { | 
 |     RecursiveFilesystemTraversalValue result = evalTraversalRequest(params); | 
 |     Set<ResolvedFile> actual = new HashSet<>(); | 
 |     for (ResolvedFile act : result.getTransitiveFiles()) { | 
 |       // Strip metadata so only the type and path of the objects are compared. | 
 |       actual.add(act.stripMetadataForTesting()); | 
 |     } | 
 |     // First just assert equality of the keys, so in case of a mismatch the error message is easier | 
 |     // to read. | 
 |     assertThat(actual).containsExactly((Object[]) expectedFilesIgnoringMetadata); | 
 |  | 
 |     // The returned object still has the unstripped metadata. | 
 |     return result; | 
 |   } | 
 |  | 
 |   private void appendToFile(RootedPath rootedPath, String content) throws Exception { | 
 |     Path path = rootedPath.asPath(); | 
 |     if (path.exists()) { | 
 |       try (OutputStream os = path.getOutputStream(/*append=*/ true)) { | 
 |         os.write(content.getBytes(StandardCharsets.UTF_8)); | 
 |       } | 
 |       differencer.invalidate(ImmutableList.of(FileStateValue.key(rootedPath))); | 
 |     } else { | 
 |       createFile(path, content); | 
 |     } | 
 |   } | 
 |  | 
 |   private void appendToFile(Artifact file, String content) throws Exception { | 
 |     appendToFile(rootedPath(file), content); | 
 |   } | 
 |  | 
 |   private void invalidateDirectory(RootedPath path) { | 
 |     differencer.invalidate(ImmutableList.of(DirectoryListingStateValue.key(path))); | 
 |   } | 
 |  | 
 |   private void invalidateDirectory(Artifact directoryArtifact) { | 
 |     invalidateDirectory(rootedPath(directoryArtifact)); | 
 |   } | 
 |  | 
 |   private static final class RecordingEvaluationProgressReceiver | 
 |       implements EvaluationProgressReceiver { | 
 |     Set<SkyKey> invalidations; | 
 |     Set<SkyValue> evaluations; | 
 |  | 
 |     RecordingEvaluationProgressReceiver() { | 
 |       clear(); | 
 |     } | 
 |  | 
 |     void clear() { | 
 |       invalidations = Sets.newConcurrentHashSet(); | 
 |       evaluations = Sets.newConcurrentHashSet(); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void invalidated(SkyKey skyKey, InvalidationState state) { | 
 |       invalidations.add(skyKey); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void enqueueing(SkyKey skyKey) {} | 
 |  | 
 |     @Override | 
 |     public void computed(SkyKey skyKey, long elapsedTimeNanos) {} | 
 |  | 
 |     @Override | 
 |     public void evaluated( | 
 |         SkyKey skyKey, Supplier<SkyValue> skyValueSupplier, EvaluationState state) { | 
 |       SkyValue value = skyValueSupplier.get(); | 
 |       if (value != null) { | 
 |         evaluations.add(value); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |  | 
 |   private void assertTraversalOfFile(Artifact rootArtifact) throws Exception { | 
 |     TraversalRequest traversalRoot = fileLikeRoot(rootArtifact, DONT_CROSS); | 
 |     RootedPath rootedPath = createFile(rootedPath(rootArtifact), "foo"); | 
 |  | 
 |     // Assert that the SkyValue is built and looks right. | 
 |     ResolvedFile expected = regularFileForTesting(rootedPath); | 
 |     RecursiveFilesystemTraversalValue v1 = traverseAndAssertFiles(traversalRoot, expected); | 
 |     assertThat(progressReceiver.invalidations).isEmpty(); | 
 |     assertThat(progressReceiver.evaluations).contains(v1); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Edit the file and verify that the value is rebuilt. | 
 |     appendToFile(rootArtifact, "bar"); | 
 |     RecursiveFilesystemTraversalValue v2 = traverseAndAssertFiles(traversalRoot, expected); | 
 |     assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot)); | 
 |     assertThat(progressReceiver.evaluations).contains(v2); | 
 |     assertThat(v2).isNotEqualTo(v1); | 
 |     progressReceiver.clear(); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfSourceFile() throws Exception { | 
 |     assertTraversalOfFile(sourceArtifact("foo/bar.txt")); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfGeneratedFile() throws Exception { | 
 |     assertTraversalOfFile(derivedArtifact("foo/bar.txt")); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfSymlinkToFile() throws Exception { | 
 |     Artifact linkNameArtifact = sourceArtifact("foo/baz/qux.sym"); | 
 |     Artifact linkTargetArtifact = sourceArtifact("foo/bar/baz.txt"); | 
 |     PathFragment linkValue = new PathFragment("../bar/baz.txt"); | 
 |     TraversalRequest traversalRoot = fileLikeRoot(linkNameArtifact, DONT_CROSS); | 
 |     createFile(linkTargetArtifact); | 
 |     scratch.dir(linkNameArtifact.getExecPath().getParentDirectory().getPathString()); | 
 |     rootDirectory.getRelative(linkNameArtifact.getExecPath()).createSymbolicLink(linkValue); | 
 |  | 
 |     // Assert that the SkyValue is built and looks right. | 
 |     RootedPath symlinkNamePath = rootedPath(linkNameArtifact); | 
 |     RootedPath symlinkTargetPath = rootedPath(linkTargetArtifact); | 
 |     ResolvedFile expected = symlinkToFileForTesting(symlinkTargetPath, symlinkNamePath, linkValue); | 
 |     RecursiveFilesystemTraversalValue v1 = traverseAndAssertFiles(traversalRoot, expected); | 
 |     assertThat(progressReceiver.invalidations).isEmpty(); | 
 |     assertThat(progressReceiver.evaluations).contains(v1); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Edit the target of the symlink and verify that the value is rebuilt. | 
 |     appendToFile(linkTargetArtifact, "bar"); | 
 |     RecursiveFilesystemTraversalValue v2 = traverseAndAssertFiles(traversalRoot, expected); | 
 |     assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot)); | 
 |     assertThat(progressReceiver.evaluations).contains(v2); | 
 |     assertThat(v2).isNotEqualTo(v1); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfTransitiveSymlinkToFile() throws Exception { | 
 |     Artifact directLinkArtifact = sourceArtifact("direct/file.sym"); | 
 |     Artifact transitiveLinkArtifact = sourceArtifact("transitive/sym.sym"); | 
 |     RootedPath fileA = createFile(rootedPath(sourceArtifact("a/file.a"))); | 
 |     RootedPath directLink = rootedPath(directLinkArtifact); | 
 |     RootedPath transitiveLink = rootedPath(transitiveLinkArtifact); | 
 |     PathFragment directLinkPath = new PathFragment("../a/file.a"); | 
 |     PathFragment transitiveLinkPath = new PathFragment("../direct/file.sym"); | 
 |  | 
 |     parentOf(directLink).asPath().createDirectory(); | 
 |     parentOf(transitiveLink).asPath().createDirectory(); | 
 |     directLink.asPath().createSymbolicLink(directLinkPath); | 
 |     transitiveLink.asPath().createSymbolicLink(transitiveLinkPath); | 
 |  | 
 |     traverseAndAssertFiles( | 
 |         fileLikeRoot(directLinkArtifact, DONT_CROSS), | 
 |         symlinkToFileForTesting(fileA, directLink, directLinkPath)); | 
 |  | 
 |     traverseAndAssertFiles( | 
 |         fileLikeRoot(transitiveLinkArtifact, DONT_CROSS), | 
 |         symlinkToFileForTesting(fileA, transitiveLink, transitiveLinkPath)); | 
 |   } | 
 |  | 
 |   private void assertTraversalOfDirectory(Artifact directoryArtifact) throws Exception { | 
 |     // Create files under the directory. | 
 |     // Use the root + root-relative path of the rootArtifact to create these files, rather than | 
 |     // using the rootDirectory + execpath of the rootArtifact. The resulting paths are the same | 
 |     // but the RootedPaths are different: | 
 |     // in the 1st case, it is: RootedPath(/root/execroot, relative), in the second it is | 
 |     // in the 2nd case, it is: RootedPath(/root, execroot/relative). | 
 |     // Creating the files will also create the parent directories. | 
 |     RootedPath file1 = createFile(childOf(directoryArtifact, "bar.txt")); | 
 |     RootedPath file2 = createFile(childOf(directoryArtifact, "baz/qux.txt")); | 
 |  | 
 |     TraversalRequest traversalRoot = fileLikeRoot(directoryArtifact, DONT_CROSS); | 
 |  | 
 |     // Assert that the SkyValue is built and looks right. | 
 |     ResolvedFile expected1 = regularFileForTesting(file1); | 
 |     ResolvedFile expected2 = regularFileForTesting(file2); | 
 |     RecursiveFilesystemTraversalValue v1 = | 
 |         traverseAndAssertFiles(traversalRoot, expected1, expected2); | 
 |     assertThat(progressReceiver.invalidations).isEmpty(); | 
 |     assertThat(progressReceiver.evaluations).contains(v1); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Add a new file to the directory and see that the value is rebuilt. | 
 |     RootedPath file3 = createFile(childOf(directoryArtifact, "foo.txt")); | 
 |     invalidateDirectory(directoryArtifact); | 
 |     ResolvedFile expected3 = regularFileForTesting(file3); | 
 |     RecursiveFilesystemTraversalValue v2 = | 
 |         traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3); | 
 |     assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot)); | 
 |     assertThat(progressReceiver.evaluations).contains(v2); | 
 |     assertThat(v2).isNotEqualTo(v1); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Edit a file in the directory and see that the value is rebuilt. | 
 |     appendToFile(file1, "bar"); | 
 |     RecursiveFilesystemTraversalValue v3 = | 
 |         traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3); | 
 |     assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot)); | 
 |     assertThat(progressReceiver.evaluations).contains(v3); | 
 |     assertThat(v3).isNotEqualTo(v2); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Add a new file *outside* of the directory and see that the value is *not* rebuilt. | 
 |     Artifact someFile = sourceArtifact("somewhere/else/a.file"); | 
 |     createFile(someFile, "new file"); | 
 |     appendToFile(someFile, "not all changes are treated equal"); | 
 |     RecursiveFilesystemTraversalValue v4 = | 
 |         traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3); | 
 |     assertThat(v4).isEqualTo(v3); | 
 |     assertThat(progressReceiver.invalidations).doesNotContain(rftvSkyKey(traversalRoot)); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfSourceDirectory() throws Exception { | 
 |     assertTraversalOfDirectory(sourceArtifact("dir")); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfGeneratedDirectory() throws Exception { | 
 |     assertTraversalOfDirectory(derivedArtifact("dir")); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfTransitiveSymlinkToDirectory() throws Exception { | 
 |     Artifact directLinkArtifact = sourceArtifact("direct/dir.sym"); | 
 |     Artifact transitiveLinkArtifact = sourceArtifact("transitive/sym.sym"); | 
 |     RootedPath fileA = createFile(rootedPath(sourceArtifact("a/file.a"))); | 
 |     RootedPath directLink = rootedPath(directLinkArtifact); | 
 |     RootedPath transitiveLink = rootedPath(transitiveLinkArtifact); | 
 |     PathFragment directLinkPath = new PathFragment("../a"); | 
 |     PathFragment transitiveLinkPath = new PathFragment("../direct/dir.sym"); | 
 |  | 
 |     parentOf(directLink).asPath().createDirectory(); | 
 |     parentOf(transitiveLink).asPath().createDirectory(); | 
 |     directLink.asPath().createSymbolicLink(directLinkPath); | 
 |     transitiveLink.asPath().createSymbolicLink(transitiveLinkPath); | 
 |  | 
 |     // Expect the file as if was a child of the direct symlink, not of the actual directory. | 
 |     traverseAndAssertFiles( | 
 |         fileLikeRoot(directLinkArtifact, DONT_CROSS), | 
 |         symlinkToDirectoryForTesting(parentOf(fileA), directLink, directLinkPath), | 
 |         regularFileForTesting(childOf(directLinkArtifact, "file.a"))); | 
 |  | 
 |     // Expect the file as if was a child of the transitive symlink, not of the actual directory. | 
 |     traverseAndAssertFiles( | 
 |         fileLikeRoot(transitiveLinkArtifact, DONT_CROSS), | 
 |         symlinkToDirectoryForTesting(parentOf(fileA), transitiveLink, transitiveLinkPath), | 
 |         regularFileForTesting(childOf(transitiveLinkArtifact, "file.a"))); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversePackage() throws Exception { | 
 |     Artifact buildFile = sourceArtifact("pkg/BUILD"); | 
 |     RootedPath buildFilePath = createFile(rootedPath(buildFile)); | 
 |     RootedPath file1 = createFile(siblingOf(buildFile, "subdir/file.a")); | 
 |  | 
 |     traverseAndAssertFiles( | 
 |         pkgRoot(parentOf(buildFilePath), DONT_CROSS), | 
 |         regularFileForTesting(buildFilePath), | 
 |         regularFileForTesting(file1)); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfSymlinkToDirectory() throws Exception { | 
 |     Artifact linkNameArtifact = sourceArtifact("link/foo.sym"); | 
 |     Artifact linkTargetArtifact = sourceArtifact("dir"); | 
 |     RootedPath linkName = rootedPath(linkNameArtifact); | 
 |     PathFragment linkValue = new PathFragment("../dir"); | 
 |     RootedPath file1 = createFile(childOf(linkTargetArtifact, "file.1")); | 
 |     createFile(childOf(linkTargetArtifact, "sub/file.2")); | 
 |     scratch.dir(parentOf(linkName).asPath().getPathString()); | 
 |     linkName.asPath().createSymbolicLink(linkValue); | 
 |  | 
 |     // Assert that the SkyValue is built and looks right. | 
 |     TraversalRequest traversalRoot = fileLikeRoot(linkNameArtifact, DONT_CROSS); | 
 |     ResolvedFile expected1 = | 
 |         symlinkToDirectoryForTesting(rootedPath(linkTargetArtifact), linkName, linkValue); | 
 |     ResolvedFile expected2 = regularFileForTesting(childOf(linkNameArtifact, "file.1")); | 
 |     ResolvedFile expected3 = regularFileForTesting(childOf(linkNameArtifact, "sub/file.2")); | 
 |     // We expect to see all the files from the symlink'd directory, under the symlink's path, not | 
 |     // under the symlink target's path. | 
 |     RecursiveFilesystemTraversalValue v1 = | 
 |         traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3); | 
 |     assertThat(progressReceiver.invalidations).isEmpty(); | 
 |     assertThat(progressReceiver.evaluations).contains(v1); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Add a new file to the directory and see that the value is rebuilt. | 
 |     createFile(childOf(linkTargetArtifact, "file.3")); | 
 |     invalidateDirectory(linkTargetArtifact); | 
 |     ResolvedFile expected4 = regularFileForTesting(childOf(linkNameArtifact, "file.3")); | 
 |     RecursiveFilesystemTraversalValue v2 = | 
 |         traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3, expected4); | 
 |     assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot)); | 
 |     assertThat(progressReceiver.evaluations).contains(v2); | 
 |     assertThat(v2).isNotEqualTo(v1); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Edit a file in the directory and see that the value is rebuilt. | 
 |     appendToFile(file1, "bar"); | 
 |     RecursiveFilesystemTraversalValue v3 = | 
 |         traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3, expected4); | 
 |     assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot)); | 
 |     assertThat(progressReceiver.evaluations).contains(v3); | 
 |     assertThat(v3).isNotEqualTo(v2); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Add a new file *outside* of the directory and see that the value is *not* rebuilt. | 
 |     Artifact someFile = sourceArtifact("somewhere/else/a.file"); | 
 |     createFile(someFile, "new file"); | 
 |     appendToFile(someFile, "not all changes are treated equal"); | 
 |     RecursiveFilesystemTraversalValue v4 = | 
 |         traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3, expected4); | 
 |     assertThat(v4).isEqualTo(v3); | 
 |     assertThat(progressReceiver.invalidations).doesNotContain(rftvSkyKey(traversalRoot)); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfDanglingSymlink() throws Exception { | 
 |     Artifact linkArtifact = sourceArtifact("a/dangling.sym"); | 
 |     RootedPath link = rootedPath(linkArtifact); | 
 |     PathFragment linkTarget = new PathFragment("non_existent"); | 
 |     parentOf(link).asPath().createDirectory(); | 
 |     link.asPath().createSymbolicLink(linkTarget); | 
 |     traverseAndAssertFiles( | 
 |         fileLikeRoot(linkArtifact, DONT_CROSS), danglingSymlinkForTesting(link, linkTarget)); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraversalOfDanglingSymlinkInADirectory() throws Exception { | 
 |     Artifact dirArtifact = sourceArtifact("a"); | 
 |     RootedPath file = createFile(childOf(dirArtifact, "file.txt")); | 
 |     RootedPath link = rootedPath(sourceArtifact("a/dangling.sym")); | 
 |     PathFragment linkTarget = new PathFragment("non_existent"); | 
 |     parentOf(link).asPath().createDirectory(); | 
 |     link.asPath().createSymbolicLink(linkTarget); | 
 |     traverseAndAssertFiles( | 
 |         fileLikeRoot(dirArtifact, DONT_CROSS), | 
 |         regularFileForTesting(file), | 
 |         danglingSymlinkForTesting(link, linkTarget)); | 
 |   } | 
 |  | 
 |   private void assertTraverseSubpackages(PackageBoundaryMode traverseSubpackages) throws Exception { | 
 |     Artifact pkgDirArtifact = sourceArtifact("pkg1/foo"); | 
 |     Artifact subpkgDirArtifact = sourceArtifact("pkg1/foo/subdir/subpkg"); | 
 |     RootedPath pkgBuildFile = childOf(pkgDirArtifact, "BUILD"); | 
 |     RootedPath subpkgBuildFile = childOf(subpkgDirArtifact, "BUILD"); | 
 |     scratch.dir(rootedPath(pkgDirArtifact).asPath().getPathString()); | 
 |     scratch.dir(rootedPath(subpkgDirArtifact).asPath().getPathString()); | 
 |     createFile(pkgBuildFile); | 
 |     createFile(subpkgBuildFile); | 
 |  | 
 |     TraversalRequest traversalRoot = pkgRoot(parentOf(pkgBuildFile), traverseSubpackages); | 
 |  | 
 |     ResolvedFile expected1 = regularFileForTesting(pkgBuildFile); | 
 |     ResolvedFile expected2 = regularFileForTesting(subpkgBuildFile); | 
 |     switch (traverseSubpackages) { | 
 |       case CROSS: | 
 |         traverseAndAssertFiles(traversalRoot, expected1, expected2); | 
 |         break; | 
 |       case DONT_CROSS: | 
 |         traverseAndAssertFiles(traversalRoot, expected1); | 
 |         break; | 
 |       case REPORT_ERROR: | 
 |         SkyKey key = rftvSkyKey(traversalRoot); | 
 |         EvaluationResult<SkyValue> result = eval(key); | 
 |         assertThat(result.hasError()).isTrue(); | 
 |         assertThat(result.getError().getException().getMessage()) | 
 |             .contains("crosses package boundary into package rooted at"); | 
 |         break; | 
 |       default: | 
 |         throw new IllegalStateException(traverseSubpackages.toString()); | 
 |     } | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testTraverseSubpackages() throws Exception { | 
 |     assertTraverseSubpackages(CROSS); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testDoNotTraverseSubpackages() throws Exception { | 
 |     assertTraverseSubpackages(DONT_CROSS); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testReportErrorWhenTraversingSubpackages() throws Exception { | 
 |     assertTraverseSubpackages(REPORT_ERROR); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testSwitchPackageRootsWhenUsingMultiplePackagePaths() throws Exception { | 
 |     // Layout: | 
 |     //   pp1://a/BUILD | 
 |     //   pp1://a/file.a | 
 |     //   pp1://a/b.sym -> b/   (only created later) | 
 |     //   pp1://a/b/ | 
 |     //   pp1://a/b/file.fake | 
 |     //   pp1://a/subdir/file.b | 
 |     // | 
 |     //   pp2://a/BUILD | 
 |     //   pp2://a/b/ | 
 |     //   pp2://a/b/BUILD | 
 |     //   pp2://a/b/file.a | 
 |     //   pp2://a/subdir.fake/ | 
 |     //   pp2://a/subdir.fake/file.fake | 
 |     // | 
 |     // Notice that pp1://a/b will be overlaid by pp2://a/b as the latter has a BUILD file and that | 
 |     // takes precedence. On the other hand the package definition pp2://a/BUILD will be ignored | 
 |     // since package //a is already defined under pp1. | 
 |     // | 
 |     // Notice also that pp1://a/b.sym is a relative symlink pointing to b/. This should be resolved | 
 |     // to the definition of //a/b/ under pp1, not under pp2. | 
 |  | 
 |     // Set the package paths. | 
 |     pkgLocator.set(new PathPackageLocator(outputBase, | 
 |         ImmutableList.of(rootDirectory.getRelative("pp1"), rootDirectory.getRelative("pp2")))); | 
 |     PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator.get()); | 
 |  | 
 |     Artifact aBuildArtifact = sourceArtifactUnderPackagePath("a/BUILD", "pp1"); | 
 |     Artifact bBuildArtifact = sourceArtifactUnderPackagePath("a/b/BUILD", "pp2"); | 
 |  | 
 |     RootedPath pp1aBuild = createFile(rootedPath(aBuildArtifact)); | 
 |     RootedPath pp1aFileA = createFile(siblingOf(pp1aBuild, "file.a")); | 
 |     RootedPath pp1bFileFake = createFile(siblingOf(pp1aBuild, "b/file.fake")); | 
 |     RootedPath pp1aSubdirFileB = createFile(siblingOf(pp1aBuild, "subdir/file.b")); | 
 |  | 
 |     RootedPath pp2aBuild = createFile(rootedPath("a/BUILD", "pp2")); | 
 |     RootedPath pp2bBuild = createFile(rootedPath(bBuildArtifact)); | 
 |     RootedPath pp2bFileA = createFile(siblingOf(pp2bBuild, "file.a")); | 
 |     createFile(siblingOf(pp2aBuild, "subdir.fake/file.fake")); | 
 |  | 
 |     // Traverse //a including subpackages. The result should contain the pp1-definition of //a and | 
 |     // the pp2-definition of //a/b. | 
 |     traverseAndAssertFiles( | 
 |         pkgRoot(parentOf(rootedPath(aBuildArtifact)), CROSS), | 
 |         regularFileForTesting(pp1aBuild), | 
 |         regularFileForTesting(pp1aFileA), | 
 |         regularFileForTesting(pp1aSubdirFileB), | 
 |         regularFileForTesting(pp2bBuild), | 
 |         regularFileForTesting(pp2bFileA)); | 
 |  | 
 |     // Traverse //a excluding subpackages. The result should only contain files from //a and not | 
 |     // from //a/b. | 
 |     traverseAndAssertFiles( | 
 |         pkgRoot(parentOf(rootedPath(aBuildArtifact)), DONT_CROSS), | 
 |         regularFileForTesting(pp1aBuild), | 
 |         regularFileForTesting(pp1aFileA), | 
 |         regularFileForTesting(pp1aSubdirFileB)); | 
 |  | 
 |     // Create a relative symlink pp1://a/b.sym -> b/. It will be resolved to the subdirectory | 
 |     // pp1://a/b, even though a package definition pp2://a/b exists. | 
 |     RootedPath pp1aBsym = siblingOf(pp1aFileA, "b.sym"); | 
 |     pp1aBsym.asPath().createSymbolicLink(new PathFragment("b")); | 
 |     invalidateDirectory(parentOf(pp1aBsym)); | 
 |  | 
 |     // Traverse //a excluding subpackages. The relative symlink //a/b.sym points to the subdirectory | 
 |     // a/b, i.e. the pp1-definition, even though there is a pp2-defined package //a/b and we expect | 
 |     // to see b.sym/b.fake (not b/b.fake). | 
 |     traverseAndAssertFiles( | 
 |         pkgRoot(parentOf(rootedPath(aBuildArtifact)), DONT_CROSS), | 
 |         regularFileForTesting(pp1aBuild), | 
 |         regularFileForTesting(pp1aFileA), | 
 |         regularFileForTesting(childOf(pp1aBsym, "file.fake")), | 
 |         symlinkToDirectoryForTesting(parentOf(pp1bFileFake), pp1aBsym, new PathFragment("b")), | 
 |         regularFileForTesting(pp1aSubdirFileB)); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testFileDigestChangeCausesRebuild() throws Exception { | 
 |     Artifact artifact = sourceArtifact("foo/bar.txt"); | 
 |     RootedPath path = rootedPath(artifact); | 
 |     createFile(path, "hello"); | 
 |  | 
 |     // Assert that the SkyValue is built and looks right. | 
 |     TraversalRequest params = fileLikeRoot(artifact, DONT_CROSS); | 
 |     ResolvedFile expected = regularFileForTesting(path); | 
 |     RecursiveFilesystemTraversalValue v1 = traverseAndAssertFiles(params, expected); | 
 |     assertThat(progressReceiver.evaluations).contains(v1); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Change the digest of the file. See that the value is rebuilt. | 
 |     appendToFile(path, "world"); | 
 |     RecursiveFilesystemTraversalValue v2 = traverseAndAssertFiles(params, expected); | 
 |     assertThat(progressReceiver.invalidations).contains(rftvSkyKey(params)); | 
 |     assertThat(v2).isNotEqualTo(v1); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testFileMtimeChangeDoesNotCauseRebuildIfDigestIsUnchanged() throws Exception { | 
 |     Artifact artifact = sourceArtifact("foo/bar.txt"); | 
 |     RootedPath path = rootedPath(artifact); | 
 |     createFile(path, "hello"); | 
 |  | 
 |     // Assert that the SkyValue is built and looks right. | 
 |     TraversalRequest params = fileLikeRoot(artifact, DONT_CROSS); | 
 |     ResolvedFile expected = regularFileForTesting(path); | 
 |     RecursiveFilesystemTraversalValue v1 = traverseAndAssertFiles(params, expected); | 
 |     assertThat(progressReceiver.evaluations).contains(v1); | 
 |     progressReceiver.clear(); | 
 |  | 
 |     // Change the mtime of the file but not the digest. See that the value is *not* rebuilt. | 
 |     long mtime = path.asPath().getLastModifiedTime(); | 
 |     mtime += 1000000L; // more than the timestamp granularity of any filesystem | 
 |     path.asPath().setLastModifiedTime(mtime); | 
 |     RecursiveFilesystemTraversalValue v2 = traverseAndAssertFiles(params, expected); | 
 |     assertThat(v2).isEqualTo(v1); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testRegexp() throws Exception { | 
 |     Artifact wantedArtifact = sourceArtifact("foo/bar/baz.txt"); | 
 |     Artifact unwantedArtifact = sourceArtifact("foo/boo/baztxt.bak"); | 
 |     RootedPath wantedPath = rootedPath(wantedArtifact); | 
 |     createFile(wantedPath, "hello"); | 
 |     createFile(unwantedArtifact, "nope"); | 
 |     Artifact pkgDirArtifact = sourceArtifact("foo"); | 
 |     RootedPath dir = rootedPath(pkgDirArtifact); | 
 |     scratch.dir(dir.asPath().getPathString()); | 
 |  | 
 |     TraversalRequest traversalRoot = | 
 |         new TraversalRequest( | 
 |             dir, false, PackageBoundaryMode.REPORT_ERROR, true, null, Pattern.compile(".*\\.txt")); | 
 |  | 
 |     ResolvedFile expected = regularFileForTesting(wantedPath); | 
 |     traverseAndAssertFiles(traversalRoot, expected); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testGeneratedDirectoryConflictsWithPackage() throws Exception { | 
 |     Artifact genDir = derivedArtifact("a/b"); | 
 |     createFile(rootedPath(sourceArtifact("a/b/c/file.real"))); | 
 |     createFile(rootedPath(derivedArtifact("a/b/c/file.fake"))); | 
 |     createFile(sourceArtifact("a/b/c/BUILD")); | 
 |  | 
 |     SkyKey key = rftvSkyKey(fileLikeRoot(genDir, CROSS)); | 
 |     EvaluationResult<SkyValue> result = eval(key); | 
 |     assertThat(result.hasError()).isTrue(); | 
 |     ErrorInfo error = result.getError(key); | 
 |     assertThat(error.isTransient()).isFalse(); | 
 |     assertThat(error.getException().getMessage()) | 
 |         .contains("Generated directory a/b/c conflicts with package under the same path."); | 
 |   } | 
 | } |