Open source some skyframe/bazel tests.

--
MOS_MIGRATED_REVID=106308990
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java
new file mode 100644
index 0000000..e699f0a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunctionTest.java
@@ -0,0 +1,853 @@
+// 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.common.truth.Truth.assertWithMessage;
+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 com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode;
+import com.google.devtools.build.lib.actions.FilesetTraversalParamsFactory;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.events.NullEventHandler;
+import com.google.devtools.build.lib.packages.FilesetEntry.SymlinkBehavior;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.testutil.FoundationTestCase;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.Fingerprint;
+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.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 java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/** Tests for {@link FilesetEntryFunction}. */
+public final class FilesetEntryFunctionTest extends FoundationTestCase {
+
+  private TimestampGranularityMonitor tsgm = new TimestampGranularityMonitor(BlazeClock.instance());
+  private MemoizingEvaluator evaluator;
+  private SequentialBuildDriver driver;
+  private RecordingDifferencer differencer;
+  private AtomicReference<PathPackageLocator> pkgLocator;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+
+    pkgLocator = new AtomicReference<>(new PathPackageLocator(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.FILESET_ENTRY, new FilesetEntryFunction());
+
+    differencer = new RecordingDifferencer();
+    evaluator = new InMemoryMemoizingEvaluator(skyFunctions, differencer);
+    driver = new SequentialBuildDriver(evaluator);
+    PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID());
+    PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator.get());
+  }
+
+  private Artifact getSourceArtifact(String path) throws Exception {
+    return new Artifact(new PathFragment(path), Root.asSourceRoot(rootDirectory));
+  }
+
+  private Artifact createSourceArtifact(String path) throws Exception {
+    Artifact result = getSourceArtifact(path);
+    createFile(result, "foo");
+    return result;
+  }
+
+  private static RootedPath rootedPath(Artifact artifact) {
+    return RootedPath.toRootedPath(artifact.getRoot().getPath(), artifact.getRootRelativePath());
+  }
+
+  private static RootedPath childOf(Artifact artifact, String relative) {
+    return RootedPath.toRootedPath(
+        artifact.getRoot().getPath(), artifact.getRootRelativePath().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 {
+    createFile(path.asPath(), contents);
+    return path;
+  }
+
+  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 FilesetEntryValue evalFilesetTraversal(FilesetTraversalParams params) throws Exception {
+    SkyKey key = FilesetEntryValue.key(params);
+    EvaluationResult<FilesetEntryValue> result = eval(key);
+    assertThat(result.hasError()).isFalse();
+    return result.get(key);
+  }
+
+  private static FilesetOutputSymlink symlink(String from, Artifact to) {
+    return new FilesetOutputSymlink(new PathFragment(from), to.getPath().asFragment());
+  }
+
+  private static FilesetOutputSymlink symlink(String from, String to) {
+    return new FilesetOutputSymlink(new PathFragment(from), new PathFragment(to));
+  }
+
+  private static FilesetOutputSymlink symlink(String from, RootedPath to) {
+    return new FilesetOutputSymlink(new PathFragment(from), to.asPath().asFragment());
+  }
+
+  private void assertSymlinksInOrder(
+      FilesetTraversalParams request, FilesetOutputSymlink... expectedSymlinks) throws Exception {
+    List<FilesetOutputSymlink> expected = Arrays.asList(expectedSymlinks);
+    Collection<FilesetOutputSymlink> actual =
+        Collections2.transform(
+            evalFilesetTraversal(request).getSymlinks(),
+            // Strip the metadata from the actual results.
+            new Function<FilesetOutputSymlink, FilesetOutputSymlink>() {
+              @Override
+              public FilesetOutputSymlink apply(FilesetOutputSymlink input) {
+                return new FilesetOutputSymlink(input.name, input.target);
+              }
+            });
+    assertThat(actual).containsExactlyElementsIn(expected).inOrder();
+  }
+
+  private static Label label(String label) throws Exception {
+    return Label.parseAbsolute(label);
+  }
+
+  public void testFileTraversalForFile() throws Exception {
+    Artifact file = createSourceArtifact("foo/file.real");
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.fileTraversal(
+            /*ownerLabel=*/ label("//foo"),
+            /*fileToTraverse=*/ file,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    assertSymlinksInOrder(params, symlink("output-name", file));
+  }
+
+  private void assertFileTraversalForFileSymlink(SymlinkBehavior symlinks) throws Exception {
+    Artifact file = createSourceArtifact("foo/file.real");
+    Artifact symlink = getSourceArtifact("foo/file.sym");
+    symlink.getPath().createSymbolicLink(new PathFragment("file.real"));
+
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.fileTraversal(
+            /*ownerLabel=*/ label("//foo"),
+            /*fileToTraverse=*/ symlink,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*symlinkBehaviorMode=*/ symlinks,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    switch (symlinks) {
+      case COPY:
+        assertSymlinksInOrder(params, symlink("output-name", "file.real"));
+        break;
+      case DEREFERENCE:
+        assertSymlinksInOrder(params, symlink("output-name", file));
+        break;
+      default:
+        throw new IllegalStateException(symlinks.toString());
+    }
+  }
+
+  public void testFileTraversalForFileSymlinkNoFollow() throws Exception {
+    assertFileTraversalForFileSymlink(SymlinkBehavior.COPY);
+  }
+
+  public void testFileTraversalForFileSymlinkFollow() throws Exception {
+    assertFileTraversalForFileSymlink(SymlinkBehavior.DEREFERENCE);
+  }
+
+  public void testFileTraversalForDirectory() throws Exception {
+    Artifact dir = getSourceArtifact("foo/dir_real");
+    RootedPath fileA = createFile(childOf(dir, "file.a"), "hello");
+    RootedPath fileB = createFile(childOf(dir, "sub/file.b"), "world");
+
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.fileTraversal(
+            /*ownerLabel=*/ label("//foo"),
+            /*fileToTraverse=*/ dir,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    assertSymlinksInOrder(
+        params, symlink("output-name/file.a", fileA), symlink("output-name/sub/file.b", fileB));
+  }
+
+  private void assertFileTraversalForDirectorySymlink(SymlinkBehavior symlinks) throws Exception {
+    Artifact dir = getSourceArtifact("foo/dir_real");
+    Artifact symlink = getSourceArtifact("foo/dir_sym");
+    createFile(childOf(dir, "file.a"), "hello");
+    createFile(childOf(dir, "sub/file.b"), "world");
+    symlink.getPath().createSymbolicLink(new PathFragment("dir_real"));
+
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.fileTraversal(
+            /*ownerLabel=*/ label("//foo"),
+            /*fileToTraverse=*/ symlink,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*symlinkBehaviorMode=*/ symlinks,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    switch (symlinks) {
+      case COPY:
+        assertSymlinksInOrder(params, symlink("output-name", "dir_real"));
+        break;
+      case DEREFERENCE:
+        assertSymlinksInOrder(params, symlink("output-name", dir));
+        break;
+      default:
+        throw new IllegalStateException(symlinks.toString());
+    }
+  }
+
+  public void testFileTraversalForDirectorySymlinkFollow() throws Exception {
+    assertFileTraversalForDirectorySymlink(SymlinkBehavior.COPY);
+  }
+
+  public void testFileTraversalForDirectorySymlinkNoFollow() throws Exception {
+    assertFileTraversalForDirectorySymlink(SymlinkBehavior.DEREFERENCE);
+  }
+
+  private void assertRecursiveTraversalForDirectory(
+      SymlinkBehavior symlinks, PackageBoundaryMode pkgBoundaryMode) throws Exception {
+    Artifact dir = getSourceArtifact("foo/dir");
+    RootedPath fileA = createFile(childOf(dir, "file.a"), "blah");
+    RootedPath fileAsym = childOf(dir, "subdir/file.a.sym");
+    RootedPath buildFile = createFile(childOf(dir, "subpkg/BUILD"), "blah");
+    RootedPath fileB = createFile(childOf(dir, "subpkg/file.b"), "blah");
+    fileAsym.asPath().getParentDirectory().createDirectory();
+    fileAsym.asPath().createSymbolicLink(new PathFragment("../file.a"));
+
+    FilesetOutputSymlink outA = symlink("output-name/file.a", childOf(dir, "file.a"));
+    FilesetOutputSymlink outAsym = null;
+    FilesetOutputSymlink outBuild = symlink("output-name/subpkg/BUILD", buildFile);
+    FilesetOutputSymlink outB = symlink("output-name/subpkg/file.b", fileB);
+    switch (symlinks) {
+      case COPY:
+        outAsym = symlink("output-name/subdir/file.a.sym", "../file.a");
+        break;
+      case DEREFERENCE:
+        outAsym = symlink("output-name/subdir/file.a.sym", fileA);
+        break;
+      default:
+        throw new IllegalStateException(symlinks.toString());
+    }
+
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.recursiveTraversalOfDirectory(
+            /*ownerLabel=*/ label("//foo"),
+            /*directoryToTraverse=*/ dir,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*excludes=*/ null,
+            /*symlinkBehaviorMode=*/ symlinks,
+            /*pkgBoundaryMode=*/ pkgBoundaryMode);
+    switch (pkgBoundaryMode) {
+      case CROSS:
+        assertSymlinksInOrder(params, outA, outAsym, outBuild, outB);
+        break;
+      case DONT_CROSS:
+        assertSymlinksInOrder(params, outA, outAsym);
+        break;
+      case REPORT_ERROR:
+        SkyKey key = FilesetEntryValue.key(params);
+        EvaluationResult<SkyValue> result = eval(key);
+        assertThat(result.hasError()).isTrue();
+        assertThat(result.getError(key).getException().getMessage())
+            .contains("'foo/dir' crosses package boundary into package rooted at foo/dir/subpkg");
+        break;
+      default:
+        throw new IllegalStateException(pkgBoundaryMode.toString());
+    }
+  }
+
+  public void testRecursiveTraversalForDirectoryCrossNoFollow() throws Exception {
+    assertRecursiveTraversalForDirectory(SymlinkBehavior.COPY, CROSS);
+  }
+
+  public void testRecursiveTraversalForDirectoryDontCrossNoFollow() throws Exception {
+    assertRecursiveTraversalForDirectory(SymlinkBehavior.COPY, DONT_CROSS);
+  }
+
+  public void testRecursiveTraversalForDirectoryReportErrorNoFollow() throws Exception {
+    assertRecursiveTraversalForDirectory(SymlinkBehavior.COPY, REPORT_ERROR);
+  }
+
+  public void testRecursiveTraversalForDirectoryCrossFollow() throws Exception {
+    assertRecursiveTraversalForDirectory(SymlinkBehavior.DEREFERENCE, CROSS);
+  }
+
+  public void testRecursiveTraversalForDirectoryDontCrossFollow() throws Exception {
+    assertRecursiveTraversalForDirectory(SymlinkBehavior.DEREFERENCE, DONT_CROSS);
+  }
+
+  public void testRecursiveTraversalForDirectoryReportErrorFollow() throws Exception {
+    assertRecursiveTraversalForDirectory(SymlinkBehavior.DEREFERENCE, REPORT_ERROR);
+  }
+
+  private void assertRecursiveTraversalForDirectorySymlink(
+      SymlinkBehavior symlinks, PackageBoundaryMode pkgBoundaryMode) throws Exception {
+    Artifact dir = getSourceArtifact("foo/dir_real");
+    Artifact symlink = getSourceArtifact("foo/dir_sym");
+    createFile(childOf(dir, "file.a"), "blah");
+    RootedPath fileAsym = childOf(dir, "subdir/file.a.sym");
+    createFile(childOf(dir, "subpkg/BUILD"), "blah");
+    createFile(childOf(dir, "subpkg/file.b"), "blah");
+    fileAsym.asPath().getParentDirectory().createDirectory();
+    fileAsym.asPath().createSymbolicLink(new PathFragment("../file.a"));
+    symlink.getPath().createSymbolicLink(new PathFragment("dir_real"));
+
+    FilesetOutputSymlink outA = symlink("output-name/file.a", childOf(symlink, "file.a"));
+    FilesetOutputSymlink outASym = null;
+    FilesetOutputSymlink outBuild =
+        symlink("output-name/subpkg/BUILD", childOf(symlink, "subpkg/BUILD"));
+    FilesetOutputSymlink outB =
+        symlink("output-name/subpkg/file.b", childOf(symlink, "subpkg/file.b"));
+    switch (symlinks) {
+      case COPY:
+        outASym = symlink("output-name/subdir/file.a.sym", "../file.a");
+        break;
+      case DEREFERENCE:
+        outASym = symlink("output-name/subdir/file.a.sym", childOf(dir, "file.a"));
+        break;
+      default:
+        throw new IllegalStateException(symlinks.toString());
+    }
+
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.recursiveTraversalOfDirectory(
+            /*ownerLabel=*/ label("//foo"),
+            /*directoryToTraverse=*/ symlink,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*excludes=*/ null,
+            /*symlinkBehaviorMode=*/ symlinks,
+            /*pkgBoundaryMode=*/ pkgBoundaryMode);
+    switch (pkgBoundaryMode) {
+      case CROSS:
+        assertSymlinksInOrder(params, outA, outASym, outBuild, outB);
+        break;
+      case DONT_CROSS:
+        assertSymlinksInOrder(params, outA, outASym);
+        break;
+      case REPORT_ERROR:
+        SkyKey key = FilesetEntryValue.key(params);
+        EvaluationResult<SkyValue> result = eval(key);
+        assertThat(result.hasError()).isTrue();
+        assertThat(result.getError(key).getException().getMessage())
+            .contains(
+                "'foo/dir_sym' crosses package boundary into package rooted at foo/dir_sym/subpkg");
+        break;
+      default:
+        throw new IllegalStateException(pkgBoundaryMode.toString());
+    }
+  }
+
+  public void testRecursiveTraversalForDirectorySymlinkNoFollowCross() throws Exception {
+    assertRecursiveTraversalForDirectorySymlink(SymlinkBehavior.COPY, CROSS);
+  }
+
+  public void testRecursiveTraversalForDirectorySymlinkNoFollowDontCross() throws Exception {
+    assertRecursiveTraversalForDirectorySymlink(SymlinkBehavior.COPY, DONT_CROSS);
+  }
+
+  public void testRecursiveTraversalForDirectorySymlinkNoFollowReportError() throws Exception {
+    assertRecursiveTraversalForDirectorySymlink(SymlinkBehavior.COPY, REPORT_ERROR);
+  }
+
+  public void testRecursiveTraversalForDirectorySymlinkFollowCross() throws Exception {
+    assertRecursiveTraversalForDirectorySymlink(SymlinkBehavior.DEREFERENCE, CROSS);
+  }
+
+  public void testRecursiveTraversalForDirectorySymlinkFollowDontCross() throws Exception {
+    assertRecursiveTraversalForDirectorySymlink(SymlinkBehavior.DEREFERENCE, DONT_CROSS);
+  }
+
+  public void testRecursiveTraversalForDirectorySymlinkFollowReportError() throws Exception {
+    assertRecursiveTraversalForDirectorySymlink(SymlinkBehavior.DEREFERENCE, REPORT_ERROR);
+  }
+
+  private void assertRecursiveTraversalForPackage(
+      SymlinkBehavior symlinks, PackageBoundaryMode pkgBoundaryMode) throws Exception {
+    Artifact buildFile = createSourceArtifact("foo/BUILD");
+    Artifact subpkgBuildFile = createSourceArtifact("foo/subpkg/BUILD");
+    Artifact subpkgSymlink = getSourceArtifact("foo/subpkg_sym");
+
+    RootedPath fileA = createFile(siblingOf(buildFile, "file.a"), "blah");
+    RootedPath fileAsym = siblingOf(buildFile, "subdir/file.a.sym");
+    RootedPath fileB = createFile(siblingOf(subpkgBuildFile, "file.b"), "blah");
+
+    scratch.dir(fileAsym.asPath().getParentDirectory().getPathString());
+    fileAsym.asPath().createSymbolicLink(new PathFragment("../file.a"));
+    subpkgSymlink.getPath().createSymbolicLink(new PathFragment("subpkg"));
+
+    FilesetOutputSymlink outBuild = symlink("output-name/BUILD", buildFile);
+    FilesetOutputSymlink outA = symlink("output-name/file.a", fileA);
+    FilesetOutputSymlink outAsym = null;
+    FilesetOutputSymlink outSubpkgBuild = symlink("output-name/subpkg/BUILD", subpkgBuildFile);
+    FilesetOutputSymlink outSubpkgB = symlink("output-name/subpkg/file.b", fileB);
+    FilesetOutputSymlink outSubpkgSymBuild;
+    switch (symlinks) {
+      case COPY:
+        outAsym = symlink("output-name/subdir/file.a.sym", "../file.a");
+        outSubpkgSymBuild = symlink("output-name/subpkg_sym", "subpkg");
+        break;
+      case DEREFERENCE:
+        outAsym = symlink("output-name/subdir/file.a.sym", fileA);
+        outSubpkgSymBuild = symlink("output-name/subpkg_sym", getSourceArtifact("foo/subpkg"));
+        break;
+      default:
+        throw new IllegalStateException(symlinks.toString());
+    }
+
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.recursiveTraversalOfPackage(
+            /*ownerLabel=*/ label("//foo"),
+            /*directoryToTraverse=*/ buildFile,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*excludes=*/ null,
+            /*symlinkBehaviorMode=*/ symlinks,
+            /*pkgBoundaryMode=*/ pkgBoundaryMode);
+    switch (pkgBoundaryMode) {
+      case CROSS:
+        assertSymlinksInOrder(
+            params, outBuild, outA, outSubpkgSymBuild, outAsym, outSubpkgBuild, outSubpkgB);
+        break;
+      case DONT_CROSS:
+        assertSymlinksInOrder(params, outBuild, outA, outAsym);
+        break;
+      case REPORT_ERROR:
+        SkyKey key = FilesetEntryValue.key(params);
+        EvaluationResult<SkyValue> result = eval(key);
+        assertThat(result.hasError()).isTrue();
+        assertThat(result.getError(key).getException().getMessage())
+            .contains("'foo' crosses package boundary into package rooted at foo/subpkg");
+        break;
+      default:
+        throw new IllegalStateException(pkgBoundaryMode.toString());
+    }
+  }
+
+  public void testRecursiveTraversalForPackageNoFollowCross() throws Exception {
+    assertRecursiveTraversalForPackage(SymlinkBehavior.COPY, CROSS);
+  }
+
+  public void testRecursiveTraversalForPackageNoFollowDontCross() throws Exception {
+    assertRecursiveTraversalForPackage(SymlinkBehavior.COPY, DONT_CROSS);
+  }
+
+  public void testRecursiveTraversalForPackageNoFollowReportError() throws Exception {
+    assertRecursiveTraversalForPackage(SymlinkBehavior.COPY, REPORT_ERROR);
+  }
+
+  public void testRecursiveTraversalForPackageFollowCross() throws Exception {
+    assertRecursiveTraversalForPackage(SymlinkBehavior.DEREFERENCE, CROSS);
+  }
+
+  public void testRecursiveTraversalForPackageFollowDontCross() throws Exception {
+    assertRecursiveTraversalForPackage(SymlinkBehavior.DEREFERENCE, DONT_CROSS);
+  }
+
+  public void testRecursiveTraversalForPackageFollowReportError() throws Exception {
+    assertRecursiveTraversalForPackage(SymlinkBehavior.DEREFERENCE, REPORT_ERROR);
+  }
+
+  public void testNestedFileFilesetTraversal() throws Exception {
+    Artifact path = getSourceArtifact("foo/bar.file");
+    createFile(path, "blah");
+    FilesetTraversalParams inner =
+        FilesetTraversalParamsFactory.fileTraversal(
+            /*ownerLabel=*/ label("//foo"),
+            /*fileToTraverse=*/ path,
+            /*destPath=*/ new PathFragment("inner-out"),
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    FilesetTraversalParams outer =
+        FilesetTraversalParamsFactory.nestedTraversal(
+            /*ownerLabel=*/ label("//foo:bar"),
+            /*nested=*/ inner,
+            /*destDir=*/ new PathFragment("outer-out"),
+            /*excludes=*/ null);
+    assertSymlinksInOrder(outer, symlink("outer-out/inner-out", rootedPath(path)));
+  }
+
+  private void assertNestedRecursiveFilesetTraversal(boolean useInnerDir) throws Exception {
+    Artifact dir = getSourceArtifact("foo/dir");
+    RootedPath fileA = createFile(childOf(dir, "file.a"), "hello");
+    RootedPath fileB = createFile(childOf(dir, "file.b"), "hello");
+    RootedPath fileC = createFile(childOf(dir, "sub/file.c"), "world");
+
+    FilesetTraversalParams inner =
+        FilesetTraversalParamsFactory.recursiveTraversalOfDirectory(
+            /*ownerLabel=*/ label("//foo"),
+            /*directoryToTraverse=*/ dir,
+            /*destPath=*/ new PathFragment(useInnerDir ? "inner-dir" : ""),
+            /*excludes=*/ null,
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    FilesetTraversalParams outer =
+        FilesetTraversalParamsFactory.nestedTraversal(
+            /*ownerLabel=*/ label("//foo"),
+            /*nested=*/ inner,
+            /*destDir=*/ new PathFragment("outer-dir"),
+            ImmutableSet.<String>of("file.a", "sub/file.c"));
+
+    if (useInnerDir) {
+      assertSymlinksInOrder(
+          outer,
+          // no file is excluded, since no files from "inner" are top-level in the outer Fileset
+          symlink("outer-dir/inner-dir/file.a", fileA),
+          symlink("outer-dir/inner-dir/file.b", fileB),
+          symlink("outer-dir/inner-dir/sub/file.c", fileC)); // only top-level files are excluded
+    } else {
+      assertSymlinksInOrder(
+          outer,
+          // file.a can be excluded because it's top-level (there's no output directory for "inner")
+          symlink("outer-dir/file.b", fileB),
+          symlink("outer-dir/sub/file.c", fileC)); // only top-level files could be excluded
+    }
+  }
+
+  public void testNestedRecursiveFilesetTraversalWithInnerDestDir() throws Exception {
+    assertNestedRecursiveFilesetTraversal(true);
+  }
+
+  public void testNestedRecursiveFilesetTraversalWithoutInnerDestDir() throws Exception {
+    assertNestedRecursiveFilesetTraversal(false);
+  }
+
+  public void testFileTraversalForDanglingSymlink() throws Exception {
+    Artifact linkName = getSourceArtifact("foo/dangling.sym");
+    RootedPath linkTarget = createFile(siblingOf(linkName, "target.file"), "blah");
+    linkName.getPath().createSymbolicLink(new PathFragment("target.file"));
+    linkTarget.asPath().delete();
+
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.fileTraversal(
+            /*ownerLabel=*/ label("//foo"),
+            /*fileToTraverse=*/ linkName,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    assertSymlinksInOrder(params); // expect empty results
+  }
+
+  public void testFileTraversalForNonExistentFile() throws Exception {
+    Artifact path = getSourceArtifact("foo/non-existent");
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.fileTraversal(
+            /*ownerLabel=*/ label("//foo"),
+            /*fileToTraverse=*/ path,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    assertSymlinksInOrder(params); // expect empty results
+  }
+
+  public void testRecursiveTraversalForDanglingSymlink() throws Exception {
+    Artifact linkName = getSourceArtifact("foo/dangling.sym");
+    RootedPath linkTarget = createFile(siblingOf(linkName, "target.file"), "blah");
+    linkName.getPath().createSymbolicLink(new PathFragment("target.file"));
+    linkTarget.asPath().delete();
+
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.recursiveTraversalOfDirectory(
+            /*ownerLabel=*/ label("//foo"),
+            /*directoryToTraverse=*/ linkName,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*excludes=*/ null,
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    assertSymlinksInOrder(params); // expect empty results
+  }
+
+  public void testRecursiveTraversalForNonExistentFile() throws Exception {
+    Artifact path = getSourceArtifact("foo/non-existent");
+
+    FilesetTraversalParams params =
+        FilesetTraversalParamsFactory.recursiveTraversalOfDirectory(
+            /*ownerLabel=*/ label("//foo"),
+            /*directoryToTraverse=*/ path,
+            /*destPath=*/ new PathFragment("output-name"),
+            /*excludes=*/ null,
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+    assertSymlinksInOrder(params); // expect empty results
+  }
+
+  /**
+   * Tests that the fingerprint is a function of all arguments of the factory method.
+   *
+   * <p>Implementations must provide:
+   * <ul>
+   * <li>two different values (a domain) for each argument of the factory method and whether or not
+   * it is expected to influence the fingerprint
+   * <li>a way to instantiate {@link FilesetTraversalParams} with a given set of arguments from the
+   * specified domains
+   * </ul>
+   *
+   * <p>The tests will instantiate pairs of {@link FilesetTraversalParams} objects with only a given
+   * attribute differing, and observe whether the fingerprints differ (if they are expected to) or
+   * are the same (otherwise).
+   */
+  private abstract static class FingerprintTester {
+    private final Map<String, Domain> domains;
+
+    FingerprintTester(Map<String, Domain> domains) {
+      this.domains = domains;
+    }
+
+    abstract FilesetTraversalParams create(Map<String, ?> kwArgs) throws Exception;
+
+    private Map<String, ?> getDefaultArgs() {
+      return getKwArgs(null);
+    }
+
+    private Map<String, ?> getKwArgs(@Nullable String useAlternateFor) {
+      Map<String, Object> values = new HashMap<>();
+      for (Map.Entry<String, Domain> d : domains.entrySet()) {
+        values.put(
+            d.getKey(),
+            d.getKey().equals(useAlternateFor) ? d.getValue().valueA : d.getValue().valueB);
+      }
+      return values;
+    }
+
+    public void doTest() throws Exception {
+      Fingerprint fp = new Fingerprint();
+
+      create(getDefaultArgs()).fingerprint(fp);
+      String primary = fp.hexDigestAndReset();
+
+      for (String argName : domains.keySet()) {
+        create(getKwArgs(argName)).fingerprint(fp);
+        String secondary = fp.hexDigestAndReset();
+
+        if (domains.get(argName).includedInFingerprint) {
+          assertWithMessage(
+                  "Argument '"
+                      + argName
+                      + "' was expected to be included in the"
+                      + " fingerprint, but wasn't")
+              .that(primary)
+              .isNotEqualTo(secondary);
+        } else {
+          assertWithMessage(
+                  "Argument '"
+                      + argName
+                      + "' was expected not to be included in the"
+                      + " fingerprint, but was")
+              .that(primary)
+              .isEqualTo(secondary);
+        }
+      }
+    }
+  }
+
+  private static final class Domain {
+    boolean includedInFingerprint;
+    Object valueA;
+    Object valueB;
+
+    Domain(boolean includedInFingerprint, Object valueA, Object valueB) {
+      this.includedInFingerprint = includedInFingerprint;
+      this.valueA = valueA;
+      this.valueB = valueB;
+    }
+  }
+
+  private static Domain partOfFingerprint(Object valueA, Object valueB) {
+    return new Domain(true, valueA, valueB);
+  }
+
+  private static Domain notPartOfFingerprint(Object valueA, Object valueB) {
+    return new Domain(false, valueA, valueB);
+  }
+
+  public void testFingerprintOfFileTraversal() throws Exception {
+    new FingerprintTester(
+        ImmutableMap.<String, Domain>of(
+            "ownerLabel", notPartOfFingerprint("//foo", "//bar"),
+            "fileToTraverse", partOfFingerprint("foo/file.a", "bar/file.b"),
+            "destPath", partOfFingerprint("out1", "out2"),
+            "symlinkBehaviorMode",
+                partOfFingerprint(SymlinkBehavior.COPY, SymlinkBehavior.DEREFERENCE),
+            "pkgBoundaryMode", partOfFingerprint(CROSS, DONT_CROSS))) {
+      @Override
+      FilesetTraversalParams create(Map<String, ?> kwArgs) throws Exception {
+        return FilesetTraversalParamsFactory.fileTraversal(
+            label((String) kwArgs.get("ownerLabel")),
+            getSourceArtifact((String) kwArgs.get("fileToTraverse")),
+            new PathFragment((String) kwArgs.get("destPath")),
+            ((SymlinkBehavior) kwArgs.get("symlinkBehaviorMode")),
+            (PackageBoundaryMode) kwArgs.get("pkgBoundaryMode"));
+      }
+    }.doTest();
+  }
+
+  public void testFingerprintOfDirectoryTraversal() throws Exception {
+    new FingerprintTester(
+        ImmutableMap.<String, Domain>builder()
+            .put("ownerLabel", notPartOfFingerprint("//foo", "//bar"))
+            .put("directoryToTraverse", partOfFingerprint("foo/dir_a", "bar/dir_b"))
+            .put("destPath", partOfFingerprint("out1", "out2"))
+            .put(
+                "excludes",
+                partOfFingerprint(ImmutableSet.<String>of(), ImmutableSet.<String>of("blah")))
+            .put(
+                "symlinkBehaviorMode",
+                partOfFingerprint(SymlinkBehavior.COPY, SymlinkBehavior.DEREFERENCE))
+            .put("pkgBoundaryMode", partOfFingerprint(CROSS, DONT_CROSS))
+            .build()) {
+      @SuppressWarnings("unchecked")
+      @Override
+      FilesetTraversalParams create(Map<String, ?> kwArgs) throws Exception {
+        return FilesetTraversalParamsFactory.recursiveTraversalOfDirectory(
+            label((String) kwArgs.get("ownerLabel")),
+            getSourceArtifact((String) kwArgs.get("directoryToTraverse")),
+            new PathFragment((String) kwArgs.get("destPath")),
+            (Set<String>) kwArgs.get("excludes"),
+            ((SymlinkBehavior) kwArgs.get("symlinkBehaviorMode")),
+            (PackageBoundaryMode) kwArgs.get("pkgBoundaryMode"));
+      }
+    }.doTest();
+  }
+
+  public void testFingerprintOfPackageTraversal() throws Exception {
+    new FingerprintTester(
+        ImmutableMap.<String, Domain>builder()
+            .put("ownerLabel", notPartOfFingerprint("//foo", "//bar"))
+            .put("buildFile", partOfFingerprint("foo/BUILD", "bar/BUILD"))
+            .put("destPath", partOfFingerprint("out1", "out2"))
+            .put(
+                "excludes",
+                partOfFingerprint(ImmutableSet.<String>of(), ImmutableSet.<String>of("blah")))
+            .put(
+                "symlinkBehaviorMode",
+                partOfFingerprint(SymlinkBehavior.COPY, SymlinkBehavior.DEREFERENCE))
+            .put("pkgBoundaryMode", partOfFingerprint(CROSS, DONT_CROSS))
+            .build()) {
+      @SuppressWarnings("unchecked")
+      @Override
+      FilesetTraversalParams create(Map<String, ?> kwArgs) throws Exception {
+        return FilesetTraversalParamsFactory.recursiveTraversalOfPackage(
+            label((String) kwArgs.get("ownerLabel")),
+            getSourceArtifact((String) kwArgs.get("buildFile")),
+            new PathFragment((String) kwArgs.get("destPath")),
+            (Set<String>) kwArgs.get("excludes"),
+            ((SymlinkBehavior) kwArgs.get("symlinkBehaviorMode")),
+            (PackageBoundaryMode) kwArgs.get("pkgBoundaryMode"));
+      }
+    }.doTest();
+  }
+
+  public void testFingerprintOfNestedTraversal() throws Exception {
+    FilesetTraversalParams n1 =
+        FilesetTraversalParamsFactory.fileTraversal(
+            /*ownerLabel=*/ label("//blah"),
+            /*fileToTraverse=*/ getSourceArtifact("blah/file.a"),
+            /*destPath=*/ new PathFragment("output-name"),
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+
+    FilesetTraversalParams n2 =
+        FilesetTraversalParamsFactory.fileTraversal(
+            /*ownerLabel=*/ label("//blah"),
+            /*fileToTraverse=*/ getSourceArtifact("meow/file.b"),
+            /*destPath=*/ new PathFragment("output-name"),
+            /*symlinkBehaviorMode=*/ SymlinkBehavior.COPY,
+            /*pkgBoundaryMode=*/ DONT_CROSS);
+
+    new FingerprintTester(
+        ImmutableMap.<String, Domain>of(
+            "ownerLabel", notPartOfFingerprint("//foo", "//bar"),
+            "nested", partOfFingerprint(n1, n2),
+            "destDir", partOfFingerprint("out1", "out2"),
+            "excludes",
+                partOfFingerprint(ImmutableSet.<String>of(), ImmutableSet.<String>of("x")))) {
+      @SuppressWarnings("unchecked")
+      @Override
+      FilesetTraversalParams create(Map<String, ?> kwArgs) throws Exception {
+        return FilesetTraversalParamsFactory.nestedTraversal(
+            label((String) kwArgs.get("ownerLabel")),
+            (FilesetTraversalParams) kwArgs.get("nested"),
+            new PathFragment((String) kwArgs.get("destDir")),
+            (Set<String>) kwArgs.get("excludes"));
+      }
+    }.doTest();
+  }
+}