Open source skyframe tests

--
MOS_MIGRATED_REVID=107983315
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java
new file mode 100644
index 0000000..8fe6c9d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java
@@ -0,0 +1,557 @@
+// 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 com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.packages.ConstantRuleVisibility;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.packages.util.SubincludePreprocessor;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils;
+import com.google.devtools.build.lib.testutil.ManualClock;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+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.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.RecordingDifferencer;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+/**
+ * Unit tests of specific functionality of PackageFunction. Note that it's already tested
+ * indirectly in several other places.
+ */
+public class PackageFunctionTest extends BuildViewTestCase {
+
+  private CustomInMemoryFs fs = new CustomInMemoryFs(new ManualClock());
+
+  @Override
+  protected Preprocessor.Factory.Supplier getPreprocessorFactorySupplier() {
+    return new SubincludePreprocessor.FactorySupplier(scratch.getFileSystem());
+  }
+
+  @Override
+  protected FileSystem createFileSystem() {
+    return fs;
+  }
+
+  private PackageValue validPackage(SkyKey skyKey) throws InterruptedException {
+    EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter);
+    if (result.hasError()) {
+      fail(result.getError(skyKey).getException().getMessage());
+    }
+    PackageValue value = result.get(skyKey);
+    assertFalse(value.getPackage().containsErrors());
+    return value;
+  }
+
+  public void testInconsistentNewPackage() throws Exception {
+    scratch.file("pkg/BUILD", "subinclude('//foo:sub')");
+    scratch.file("foo/sub");
+
+    getSkyframeExecutor().preparePackageLoading(
+        new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)),
+        ConstantRuleVisibility.PUBLIC, true,
+        7, "", UUID.randomUUID());
+
+    SkyKey pkgLookupKey = PackageLookupValue.key(new PathFragment("foo"));
+    EvaluationResult<PackageLookupValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), pkgLookupKey, /*keepGoing=*/false, reporter);
+    assertFalse(result.hasError());
+    assertFalse(result.get(pkgLookupKey).packageExists());
+
+    scratch.file("foo/BUILD");
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("pkg"));
+    result = SkyframeExecutorTestUtils.evaluate(getSkyframeExecutor(),
+        skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    Throwable exception = result.getError(skyKey).getException();
+    assertThat(exception.getMessage()).contains("Inconsistent filesystem operations");
+    assertThat(exception.getMessage()).contains("Unexpected package");
+  }
+
+  public void testInconsistentMissingPackage() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    Path root1 = fs.getPath("/root1");
+    scratch.file("/root1/WORKSPACE");
+    scratch.file("/root1/foo/sub");
+    scratch.file("/root1/pkg/BUILD", "subinclude('//foo:sub')");
+
+    Path root2 = fs.getPath("/root2");
+    scratch.file("/root2/foo/BUILD");
+    scratch.file("/root2/foo/sub");
+
+    getSkyframeExecutor().preparePackageLoading(
+        new PathPackageLocator(outputBase, ImmutableList.of(root1, root2)),
+        ConstantRuleVisibility.PUBLIC, true,
+        7, "", UUID.randomUUID());
+
+    SkyKey pkgLookupKey = PackageLookupValue.key(PackageIdentifier.parse("foo"));
+    EvaluationResult<PackageLookupValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), pkgLookupKey, /*keepGoing=*/false, reporter);
+    assertFalse(result.hasError());
+    assertEquals(root2, result.get(pkgLookupKey).getRoot());
+
+    scratch.file("/root1/foo/BUILD");
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("pkg"));
+    result = SkyframeExecutorTestUtils.evaluate(getSkyframeExecutor(),
+        skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    Throwable exception = result.getError(skyKey).getException();
+    System.out.println("exception: " + exception.getMessage());
+    assertThat(exception.getMessage()).contains("Inconsistent filesystem operations");
+    assertThat(exception.getMessage()).contains("Inconsistent package location");
+  }
+
+  public void testPropagatesFilesystemInconsistencies() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    RecordingDifferencer differencer = getSkyframeExecutor().getDifferencerForTesting();
+    Path pkgRoot = getSkyframeExecutor().getPathEntries().get(0);
+    Path fooBuildFile = scratch.file("foo/BUILD");
+    Path fooDir = fooBuildFile.getParentDirectory();
+
+    // Our custom filesystem says "foo/BUILD" exists but its parent "foo" is a file.
+    FileStatus inconsistentParentFileStatus = new FileStatus() {
+      @Override
+      public boolean isFile() {
+        return true;
+      }
+
+      @Override
+      public boolean isDirectory() {
+        return false;
+      }
+
+      @Override
+      public boolean isSymbolicLink() {
+        return false;
+      }
+
+      @Override
+      public boolean isSpecialFile() {
+        return false;
+      }
+
+      @Override
+      public long getSize() throws IOException {
+        return 0;
+      }
+
+      @Override
+      public long getLastModifiedTime() throws IOException {
+        return 0;
+      }
+
+      @Override
+      public long getLastChangeTime() throws IOException {
+        return 0;
+      }
+
+      @Override
+      public long getNodeId() throws IOException {
+        return 0;
+      }
+    };
+    fs.stubStat(fooDir, inconsistentParentFileStatus);
+    RootedPath pkgRootedPath = RootedPath.toRootedPath(pkgRoot, fooDir);
+    SkyValue fooDirValue = FileStateValue.create(pkgRootedPath,
+        getSkyframeExecutor().getTimestampGranularityMonitorForTesting());
+    differencer.inject(ImmutableMap.of(FileStateValue.key(pkgRootedPath), fooDirValue));
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo"));
+    String expectedMessage = "/workspace/foo/BUILD exists but its parent path /workspace/foo isn't "
+        + "an existing directory";
+    EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    ErrorInfo errorInfo = result.getError(skyKey);
+    String errorMessage = errorInfo.getException().getMessage();
+    assertThat(errorMessage).contains("Inconsistent filesystem operations");
+    assertThat(errorMessage).contains(expectedMessage);
+  }
+
+  public void testPropagatesFilesystemInconsistencies_Globbing() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    RecordingDifferencer differencer = getSkyframeExecutor().getDifferencerForTesting();
+    Path pkgRoot = getSkyframeExecutor().getPathEntries().get(0);
+    scratch.file("foo/BUILD",
+        "subinclude('//a:a')",
+        "sh_library(name = 'foo', srcs = glob(['bar/**/baz.sh']))");
+    scratch.file("a/BUILD");
+    scratch.file("a/a");
+    Path bazFile = scratch.file("foo/bar/baz/baz.sh");
+    Path bazDir = bazFile.getParentDirectory();
+    Path barDir = bazDir.getParentDirectory();
+
+    long bazFileNodeId = bazFile.stat().getNodeId();
+    // Our custom filesystem says "foo/bar/baz" does not exist but it also says that "foo/bar"
+    // has a child directory "baz".
+    fs.stubStat(bazDir, null);
+    RootedPath barDirRootedPath = RootedPath.toRootedPath(pkgRoot, barDir);
+    FileStateValue barDirFileStateValue = FileStateValue.create(barDirRootedPath,
+        getSkyframeExecutor().getTimestampGranularityMonitorForTesting());
+    FileValue barDirFileValue = FileValue.value(barDirRootedPath, barDirFileStateValue,
+        barDirRootedPath, barDirFileStateValue);
+    DirectoryListingValue barDirListing = DirectoryListingValue.value(barDirRootedPath,
+        barDirFileValue, DirectoryListingStateValue.create(ImmutableList.of(
+            new Dirent("baz", Dirent.Type.DIRECTORY))));
+    differencer.inject(ImmutableMap.of(DirectoryListingValue.key(barDirRootedPath), barDirListing));
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo"));
+    String expectedMessage = "Some filesystem operations implied /workspace/foo/bar/baz/baz.sh was "
+        + "a regular file with size of 0 and mtime of 0 and nodeId of " + bazFileNodeId + " and "
+        + "mtime of 0 but others made us think it was a nonexistent path";
+    EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    ErrorInfo errorInfo = result.getError(skyKey);
+    String errorMessage = errorInfo.getException().getMessage();
+    assertThat(errorMessage).contains("Inconsistent filesystem operations");
+    assertThat(errorMessage).contains(expectedMessage);
+  }
+
+  /** Regression test for unexpected exception type from PackageValue. */
+  public void testDiscrepancyBetweenLegacyAndSkyframePackageLoadingErrors() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    Path fooBuildFile = scratch.file("foo/BUILD",
+        "sh_library(name = 'foo', srcs = glob(['bar/*.sh']))");
+    Path fooDir = fooBuildFile.getParentDirectory();
+    Path barDir = fooDir.getRelative("bar");
+    scratch.file("foo/bar/baz.sh");
+    fs.scheduleMakeUnreadableAfterReaddir(barDir);
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo"));
+    String expectedMessage = "Encountered error 'Directory is not readable'";
+    EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    ErrorInfo errorInfo = result.getError(skyKey);
+    String errorMessage = errorInfo.getException().getMessage();
+    assertThat(errorMessage).contains("Inconsistent filesystem operations");
+    assertThat(errorMessage).contains(expectedMessage);
+  }
+
+  public void testMultipleSubincludesFromSamePackage() throws Exception {
+    scratch.file("foo/BUILD",
+        "subinclude('//bar:a')",
+        "subinclude('//bar:b')");
+    scratch.file("bar/BUILD",
+        "exports_files(['a', 'b'])");
+    scratch.file("bar/a");
+    scratch.file("bar/b");
+
+    getSkyframeExecutor().preparePackageLoading(
+        new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)),
+        ConstantRuleVisibility.PUBLIC, true,
+        7, "", UUID.randomUUID());
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo"));
+    validPackage(skyKey);
+  }
+
+  public void testTransitiveSubincludesStoredInPackage() throws Exception {
+    scratch.file("foo/BUILD",
+        "subinclude('//bar:a')");
+    scratch.file("bar/BUILD",
+        "exports_files(['a'])");
+    scratch.file("bar/a",
+        "subinclude('//baz:b')");
+    scratch.file("baz/BUILD",
+        "exports_files(['b', 'c'])");
+    scratch.file("baz/b");
+    scratch.file("baz/c");
+
+    getSkyframeExecutor().preparePackageLoading(
+        new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)),
+        ConstantRuleVisibility.PUBLIC, true,
+        7, "", UUID.randomUUID());
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo"));
+    PackageValue value = validPackage(skyKey);
+    assertThat(value.getPackage().getSubincludeLabels()).containsExactly(
+        Label.parseAbsolute("//bar:a"), Label.parseAbsolute("//baz:b"));
+
+    scratch.overwriteFile("bar/a",
+        "subinclude('//baz:c')");
+    getSkyframeExecutor().invalidateFilesUnderPathForTesting(reporter,
+        ModifiedFileSet.builder().modify(new PathFragment("bar/a")).build(), rootDirectory);
+
+    value = validPackage(skyKey);
+    assertThat(value.getPackage().getSubincludeLabels()).containsExactly(
+        Label.parseAbsolute("//bar:a"), Label.parseAbsolute("//baz:c"));
+  }
+
+  public void testTransitiveSkylarkDepsStoredInPackage() throws Exception {
+    scratch.file("foo/BUILD",
+        "load('/bar/ext', 'a')");
+    scratch.file("bar/BUILD");
+    scratch.file("bar/ext.bzl",
+        "load('/baz/ext', 'b')",
+        "a = b");
+    scratch.file("baz/BUILD");
+    scratch.file("baz/ext.bzl",
+        "b = 1");
+    scratch.file("qux/BUILD");
+    scratch.file("qux/ext.bzl",
+        "c = 1");
+
+    getSkyframeExecutor().preparePackageLoading(
+        new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)),
+        ConstantRuleVisibility.PUBLIC, true,
+        7, "", UUID.randomUUID());
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo"));
+    PackageValue value = validPackage(skyKey);
+    assertThat(value.getPackage().getSkylarkFileDependencies()).containsExactly(
+        Label.parseAbsolute("//bar:ext.bzl"), Label.parseAbsolute("//baz:ext.bzl"));
+
+    scratch.overwriteFile("bar/ext.bzl",
+        "load('/qux/ext', 'c')",
+        "a = c");
+    getSkyframeExecutor().invalidateFilesUnderPathForTesting(reporter,
+        ModifiedFileSet.builder().modify(new PathFragment("bar/ext.bzl")).build(), rootDirectory);
+
+    value = validPackage(skyKey);
+    assertThat(value.getPackage().getSkylarkFileDependencies()).containsExactly(
+        Label.parseAbsolute("//bar:ext.bzl"), Label.parseAbsolute("//qux:ext.bzl"));
+  }
+
+  public void testNonExistingSkylarkExtension() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file("test/skylark/BUILD",
+        "load('/test/skylark/bad_extension', 'some_symbol')",
+        "genrule(name = gr,",
+        "    outs = ['out.txt'],",
+        "    cmd = 'echo hello >@')");
+    invalidatePackages();
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("test/skylark"));
+    EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    ErrorInfo errorInfo = result.getError(skyKey);
+    assertThat(errorInfo.getException())
+        .hasMessage("error loading package 'test/skylark': Extension file not found. "
+            + "Unable to load file '//test/skylark:bad_extension.bzl': "
+            + "file doesn't exist or isn't a file");
+  }
+
+  public void testNonExistingSkylarkExtensionWithPythonPreprocessing() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file("foo/BUILD",
+        "exports_files(['a'])");
+    scratch.file("foo/a",
+        "load('/test/skylark/bad_extension', 'some_symbol')");
+    scratch.file("test/skylark/BUILD",
+        "subinclude('//foo:a')");
+    invalidatePackages();
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("test/skylark"));
+    EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    assertContainsEvent("Extension file not found. "
+        + "Unable to load file '//test/skylark:bad_extension.bzl': "
+        + "file doesn't exist or isn't a file");
+  }
+
+  public void testNonExistingSkylarkExtensionFromExtension() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file("test/skylark/extension.bzl",
+        "load('/test/skylark/bad_extension', 'some_symbol')",
+        "a = 'a'");
+    scratch.file("test/skylark/BUILD",
+        "load('/test/skylark/extension', 'a')",
+        "genrule(name = gr,",
+        "    outs = ['out.txt'],",
+        "    cmd = 'echo hello >@')");
+    invalidatePackages();
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("test/skylark"));
+    EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    ErrorInfo errorInfo = result.getError(skyKey);
+    assertThat(errorInfo.getException())
+        .hasMessage("error loading package 'test/skylark': Extension file not found. "
+            + "Unable to load file '//test/skylark:bad_extension.bzl': "
+            + "file doesn't exist or isn't a file");
+  }
+
+  public void testSymlinkCycleWithSkylarkExtension() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    Path extensionFilePath = scratch.resolve("/workspace/test/skylark/extension.bzl");
+    FileSystemUtils.ensureSymbolicLink(extensionFilePath, new PathFragment("extension.bzl"));
+    scratch.file("test/skylark/BUILD",
+        "load('/test/skylark/extension', 'a')",
+        "genrule(name = gr,",
+        "    outs = ['out.txt'],",
+        "    cmd = 'echo hello >@')");
+    invalidatePackages();
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("test/skylark"));
+    EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    ErrorInfo errorInfo = result.getError(skyKey);
+    assertEquals(skyKey, errorInfo.getRootCauseOfException());
+    assertThat(errorInfo.getException())
+        .hasMessage(
+            "error loading package 'test/skylark': Encountered error while reading extension "
+            + "file 'test/skylark/extension.bzl': Symlink cycle");
+  }
+
+  public void testIOErrorLookingForSubpackageForLabelIsHandled() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file("foo/BUILD",
+        "sh_library(name = 'foo', srcs = ['bar/baz.sh'])");
+    Path barBuildFile = scratch.file("foo/bar/BUILD");
+    fs.stubStatError(barBuildFile, new IOException("nope"));
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("foo"));
+    EvaluationResult<PackageValue> result = SkyframeExecutorTestUtils.evaluate(
+        getSkyframeExecutor(), skyKey, /*keepGoing=*/false, reporter);
+    assertTrue(result.hasError());
+    assertContainsEvent("nope");
+  }
+
+  public void testLoadRelativePath() throws Exception {
+    scratch.file("pkg/BUILD", "load('ext', 'a')");
+    scratch.file("pkg/ext.bzl", "a = 1");
+    validPackage(PackageValue.key(PackageIdentifier.parse("pkg")));
+  }
+
+  public void testLoadAbsolutePath() throws Exception {
+    scratch.file("pkg1/BUILD");
+    scratch.file("pkg2/BUILD",
+        "load('/pkg1/ext', 'a')");
+    scratch.file("pkg1/ext.bzl", "a = 1");
+    validPackage(PackageValue.key(PackageIdentifier.parse("pkg2")));
+  }
+
+  public void testBadWorkspaceFile() throws Exception {
+    Path workspacePath = scratch.overwriteFile("WORKSPACE", "junk");
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.createInDefaultRepo("external"));
+    getSkyframeExecutor()
+        .invalidate(
+            Predicates.equalTo(
+                FileStateValue.key(
+                    RootedPath.toRootedPath(
+                        workspacePath.getParentDirectory(),
+                        new PathFragment(workspacePath.getBaseName())))));
+
+    reporter.removeHandler(failFastHandler);
+    EvaluationResult<PackageValue> result =
+        SkyframeExecutorTestUtils.evaluate(
+            getSkyframeExecutor(), skyKey, /*keepGoing=*/ false, reporter);
+    assertFalse(result.hasError());
+    assertTrue(result.get(skyKey).getPackage().containsErrors());
+  }
+
+  private static class CustomInMemoryFs extends InMemoryFileSystem {
+    private abstract static class FileStatusOrException {
+      abstract FileStatus get() throws IOException;
+
+      private static class ExceptionImpl extends FileStatusOrException {
+        private final IOException exn;
+
+        private ExceptionImpl(IOException exn) {
+          this.exn = exn;
+        }
+
+        @Override
+        FileStatus get() throws IOException {
+          throw exn;
+        }
+      }
+
+      private static class FileStatusImpl extends FileStatusOrException {
+
+        @Nullable
+        private final FileStatus fileStatus;
+
+        private  FileStatusImpl(@Nullable FileStatus fileStatus) {
+          this.fileStatus = fileStatus;
+        }
+
+        @Override
+        @Nullable
+        FileStatus get() {
+          return fileStatus;
+        }
+      }
+    }
+
+    private Map<Path, FileStatusOrException> stubbedStats = Maps.newHashMap();
+    private Set<Path> makeUnreadableAfterReaddir = Sets.newHashSet();
+
+    public CustomInMemoryFs(ManualClock manualClock) {
+      super(manualClock);
+    }
+
+    public void stubStat(Path path, @Nullable FileStatus stubbedResult) {
+      stubbedStats.put(path, new FileStatusOrException.FileStatusImpl(stubbedResult));
+    }
+
+    public void stubStatError(Path path, IOException stubbedResult) {
+      stubbedStats.put(path, new FileStatusOrException.ExceptionImpl(stubbedResult));
+    }
+
+    @Override
+    public FileStatus stat(Path path, boolean followSymlinks) throws IOException {
+      if (stubbedStats.containsKey(path)) {
+        return stubbedStats.get(path).get();
+      }
+      return super.stat(path, followSymlinks);
+    }
+
+    public void scheduleMakeUnreadableAfterReaddir(Path path) {
+      makeUnreadableAfterReaddir.add(path);
+    }
+
+    @Override
+    public Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
+      Collection<Dirent> result = super.readdir(path, followSymlinks);
+      if (makeUnreadableAfterReaddir.contains(path)) {
+        path.setReadable(false);
+      }
+      return result;
+    }
+  }
+}