New SkyFunction: PathCasingLookupFunction

The new PathCasingLookupFunction is a SkyFunction
that can validate the casing of a path.

On case-insensitive filesystems if "foo/BUILD"
exists then "FOO/BUILD" also exists (because they
mean the same file), but this should not imply
that the "//foo" and "//FOO" packages both exist.

The new SkyFunction can validate whether
"FOO/BUILD" is the correct casing. The casing is
correct if the file's name in its parent
directory's listing is exactly the same
(case-sensitively) as the expected name.

In a later PR we can change the
PackageLookupFunction to deny loading if the path
is not correctly cased.

Motivation: https://github.com/bazelbuild/bazel/issues/8799

Change-Id: Ibb2a5f4f6c6041bdfc537b33d0c6fc55e568b998

Closes #10362.

Change-Id: Ie25b9b202a1aef3b2594064b4842fc0f2fb2776b
PiperOrigin-RevId: 283939452
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PathCasingLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PathCasingLookupFunction.java
new file mode 100644
index 0000000..644fe24
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PathCasingLookupFunction.java
@@ -0,0 +1,116 @@
+// Copyright 2019 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 com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.FileValue;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.RootedPathAndCasing;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import java.io.IOException;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** SkyFunction for {@link PathCasingLookupValue}s. */
+public final class PathCasingLookupFunction implements SkyFunction {
+
+  @Override
+  public PathCasingLookupValue compute(SkyKey skyKey, Environment env)
+      throws PathComponentIsNotDirectoryException, InterruptedException {
+    RootedPathAndCasing arg = (RootedPathAndCasing) skyKey.argument();
+    if (arg.getPath().getRootRelativePath().isEmpty()) {
+      // This is a Root, e.g. "[/foo/bar]/[]".
+      // As of 2019-12-04, PathCasingLookupValue is not used anywhere. But it's planned to be used
+      // in PackageLookupFunction to validate the package part's casing, so the RootedPath's Root's
+      // casing doesn't even matter, so if the relative part is empty then for our use case this is
+      // a correctly cased RootedPath.
+      return PathCasingLookupValue.GOOD;
+    }
+
+    RootedPath parent = arg.getPath().getParentDirectory();
+    Preconditions.checkNotNull(parent, arg.getPath());
+    Preconditions.checkNotNull(parent.getRootRelativePath(), arg.getPath());
+
+    SkyKey parentCasingKey = PathCasingLookupValue.key(parent);
+    SkyKey parentFileKey = FileValue.key(parent);
+    SkyKey childFileKey = FileValue.key(arg.getPath());
+    Map<SkyKey, SkyValue> values =
+        env.getValues(ImmutableList.of(parentCasingKey, parentFileKey, childFileKey));
+    if (env.valuesMissing()) {
+      return null;
+    }
+    if (!((PathCasingLookupValue) values.get(parentCasingKey)).isCorrect()) {
+      // Parent's casing is bad, so this path's casing is also bad.
+      return PathCasingLookupValue.BAD;
+    }
+
+    FileValue parentFile = (FileValue) values.get(parentFileKey);
+    if (!parentFile.exists()) {
+      // Parent's casing is good, because it's missing.
+      // That means this path is also missing, so by definition its casing is good.
+      return PathCasingLookupValue.GOOD;
+    }
+    if (!parentFile.isDirectory()) {
+      // Parent's casing is good, but it's not a directory.
+      throw new PathComponentIsNotDirectoryException(
+          new IOException(
+              "Cannot check path casing of "
+                  + arg.getPath()
+                  + ": its parent exists but is not a directory"));
+    }
+
+    FileValue childFile = (FileValue) values.get(childFileKey);
+    if (!childFile.exists()) {
+      // Parent's casing is good, but this file is missing.
+      // That means this path is missing, so by definition its casing is good.
+      return PathCasingLookupValue.GOOD;
+    }
+
+    // The parent file must exist, otherwise the DirectoryListingFunction will throw.
+    SkyKey parentListingKey = DirectoryListingValue.key(parent);
+    DirectoryListingValue parentList = (DirectoryListingValue) env.getValue(parentListingKey);
+    if (parentList == null) {
+      return null;
+    }
+
+    String expected = arg.getPath().getRootRelativePath().getBaseName();
+    // We already handled RootedPaths with empty relative part.
+    Preconditions.checkState(!Strings.isNullOrEmpty(expected), arg.getPath());
+
+    Dirent child = parentList.getDirents().maybeGetDirent(expected);
+    return (child != null && expected.equals(child.getName()))
+        ? PathCasingLookupValue.GOOD
+        : PathCasingLookupValue.BAD;
+  }
+
+  @Nullable
+  @Override
+  public String extractTag(SkyKey skyKey) {
+    return null;
+  }
+
+  /** Thrown if a non-terminal path component exists, but it's not a directory. */
+  public static final class PathComponentIsNotDirectoryException extends SkyFunctionException {
+    public PathComponentIsNotDirectoryException(IOException e) {
+      super(e, Transience.PERSISTENT);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PathCasingLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PathCasingLookupValue.java
new file mode 100644
index 0000000..0248da7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PathCasingLookupValue.java
@@ -0,0 +1,115 @@
+// Copyright 2019 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 com.google.common.base.Preconditions;
+import com.google.common.collect.Interner;
+import com.google.devtools.build.lib.concurrent.BlazeInterners;
+import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.RootedPathAndCasing;
+import com.google.devtools.build.skyframe.AbstractSkyKey;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * Value that represents whether a certain path is correctly cased.
+ *
+ * <p>Most filesystems preserve uppercase and lowercase letters when creating entries: {@code
+ * mkdir("Abc1")} creates the directory "Abc1", and not "ABC1" nor "abc1".
+ *
+ * <p>Some filesystems differentiate casing when looking up entries, but some don't. Suppose we have
+ * an empty directory and create the file "Abc1" underneath it. Then, calling {@code exists("Abc1")}
+ * succeeds and {@code exists("ABC1")} fails on ext4 (Linux) but both calls succeed on APFS (macOS)
+ * and NTFS (Windows). Reason for this difference is that ext4 is case-sensitive, so "Abc1" and
+ * "ABC1" mean different files, but APFS and NTFS are case-ignoring (or case-insensitive), so both
+ * paths mean the same file.
+ *
+ * <p>This object represents whether an existing path on a case-ignoring filesystem (APFS and NTFS)
+ * is correctly or incorrectly cased, i.e. whether the exact use of upper and lower case letters
+ * matches the entry on disk. In the previous example, "Abc1" is correctly cased while "ABC1" is
+ * incorrectly cased.
+ *
+ * <p>Paths on case-sensitive filesystems (ext4) are always correctly cased, because the filesystem
+ * requires exact case matching when accessing files.
+ */
+public abstract class PathCasingLookupValue implements SkyValue {
+
+  @AutoCodec public static final BadPathCasing BAD = new BadPathCasing();
+
+  @AutoCodec public static final CorrectPathCasing GOOD = new CorrectPathCasing();
+
+  /** Singleton {@link PathCasingLookupValue} instance for incorrectly cased paths. */
+  public static class BadPathCasing extends PathCasingLookupValue {
+    @Override
+    public boolean isCorrect() {
+      return false;
+    }
+  }
+
+  /** Singleton {@link PathCasingLookupValue} instance for correctly cased paths. */
+  public static class CorrectPathCasing extends PathCasingLookupValue {
+    @Override
+    public boolean isCorrect() {
+      return true;
+    }
+  }
+
+  /**
+   * Creates a {@code SkyKey} to request this {@code PathCasingLookupValue} from Skyframe.
+   *
+   * <p>The argument is a {@link RootedPath} and not a {@link Path} for two reasons:
+   *
+   * <ul>
+   *   <li>as of 2019-12-04 the {@code PathCasingLookupFunction} depends on {@code
+   *       DirectoryListingValue} whose {@code SkyKey} requires a {@code RootedPath}
+   *   <li>as of 2019-12-04 the {@code PathCasingLookupValue} is only used to validate that a
+   *       package label is correctly cased, and package labels are always relative to a package
+   *       root, so using a {@code RootedPath} is adequate and the Root part of it doesn't even have
+   *       to be correctly cased.
+   * </ul>
+   */
+  public static SkyKey key(RootedPath path) {
+    return Key.create(RootedPathAndCasing.create(path));
+  }
+
+  private PathCasingLookupValue() {}
+
+  public abstract boolean isCorrect();
+
+  /** {@link SkyKey} for {@link PathCasingLookupValue} computation. */
+  @AutoCodec.VisibleForSerialization
+  @AutoCodec
+  public static class Key extends AbstractSkyKey<RootedPathAndCasing> {
+    private static final Interner<Key> interner = BlazeInterners.newWeakInterner();
+
+    private Key(RootedPathAndCasing arg) {
+      super(arg);
+    }
+
+    @AutoCodec.VisibleForSerialization
+    @AutoCodec.Instantiator
+    static Key create(RootedPathAndCasing arg) {
+      Preconditions.checkNotNull(arg);
+      return interner.intern(new Key(arg));
+    }
+
+    @Override
+    public SkyFunctionName functionName() {
+      return SkyFunctions.PATH_CASING_LOOKUP;
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
index 3c732ee..d8f9a1f 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
@@ -107,6 +107,8 @@
   public static final SkyFunctionName ACTION_EXECUTION = ActionLookupData.NAME;
   public static final SkyFunctionName ARTIFACT_NESTED_SET =
       SkyFunctionName.createHermetic("ARTIFACT_NESTED_SET");
+  public static final SkyFunctionName PATH_CASING_LOOKUP =
+      SkyFunctionName.createHermetic("PATH_CASING_LOOKUP");
 
   @VisibleForTesting
   public static final SkyFunctionName RECURSIVE_FILESYSTEM_TRAVERSAL =
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index fdc155a..03bd599 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -9,6 +9,7 @@
 )
 
 # Tests for Windows-specific functionality that can run cross-platform.
+# These don't need to run on Windows, they merely use Windows- and case-insensitive path semantics.
 CROSS_PLATFORM_WINDOWS_TESTS = [
     "util/DependencySetWindowsTest.java",
     "vfs/PathFragmentWindowsTest.java",
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
index b380cac..61db0e4 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -12,6 +12,12 @@
     visibility = ["//src/test/java/com/google/devtools/build/lib:__pkg__"],
 )
 
+# Tests for Windows-specific functionality that can run cross-platform.
+# These don't need to run on Windows, they merely use Windows- and case-insensitive path semantics.
+CROSS_PLATFORM_WINDOWS_TESTS = [
+    "PathCasingLookupFunctionTest.java",
+]
+
 java_library(
     name = "testutil",
     srcs = glob([
@@ -53,11 +59,17 @@
 java_test(
     name = "SkyframeTests",
     srcs = select({
-        "//src/conditions:darwin": glob(["*.java"]),
-        "//src/conditions:darwin_x86_64": glob(["*.java"]),
+        "//src/conditions:darwin": glob(
+            ["*.java"],
+            exclude = CROSS_PLATFORM_WINDOWS_TESTS,
+        ),
+        "//src/conditions:darwin_x86_64": glob(
+            ["*.java"],
+            exclude = CROSS_PLATFORM_WINDOWS_TESTS,
+        ),
         "//conditions:default": glob(
             ["*.java"],
-            exclude = ["MacOSXFsEventsDiffAwarenessTest.java"],
+            exclude = ["MacOSXFsEventsDiffAwarenessTest.java"] + CROSS_PLATFORM_WINDOWS_TESTS,
         ),
     }),
     shard_count = 20,
@@ -117,6 +129,41 @@
     ],
 )
 
+# Tests that exercise Windows-specific (or case-insensitive-filesystem specific) functionality.
+# These don't need to run on Windows, they merely use Windows- and case-insensitive path semantics.
+java_test(
+    name = "windows_test",
+    srcs = CROSS_PLATFORM_WINDOWS_TESTS,
+    jvm_flags = [
+        "-Dblaze.os=Windows",
+        "-Dbazel.windows_unix_root=C:/fake/msys",
+    ],
+    tags = ["skyframe"],
+    test_class = "com.google.devtools.build.lib.AllTests",
+    deps = [
+        ":testutil",
+        "//src/main/java/com/google/devtools/build/lib:build-base",
+        "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/main/java/com/google/devtools/build/lib:packages-internal",
+        "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/java/com/google/devtools/build/lib/cmdline",
+        "//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
+        "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
+        "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils",
+        "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
+        "//src/main/java/com/google/devtools/build/skyframe",
+        "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
+        "//src/test/java/com/google/devtools/build/lib:analysis_testutil",
+        "//src/test/java/com/google/devtools/build/lib:testutil",
+        "//third_party:guava",
+        "//third_party:guava-testlib",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
 test_suite(
     name = "windows_tests",
     tags = [
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PathCasingLookupFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PathCasingLookupFunctionTest.java
new file mode 100644
index 0000000..0058570
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PathCasingLookupFunctionTest.java
@@ -0,0 +1,228 @@
+// Copyright 2019 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.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.FileStateValue;
+import com.google.devtools.build.lib.actions.FileValue;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.ServerDirectories;
+import com.google.devtools.build.lib.analysis.util.AnalysisMock;
+import com.google.devtools.build.lib.events.NullEventHandler;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.skyframe.ExternalFilesHelper.ExternalFileAction;
+import com.google.devtools.build.lib.testutil.FoundationTestCase;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Root;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.RootedPathAndCasing;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+import com.google.devtools.build.skyframe.EvaluationContext;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator;
+import com.google.devtools.build.skyframe.RecordingDifferencer;
+import com.google.devtools.build.skyframe.SequencedRecordingDifferencer;
+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 java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link PathCasingLookupFunction}. */
+@RunWith(JUnit4.class)
+public final class PathCasingLookupFunctionTest extends FoundationTestCase {
+
+  private SequentialBuildDriver driver;
+  private RecordingDifferencer differencer;
+
+  @Before
+  public final void setUp() {
+    AtomicReference<PathPackageLocator> pkgLocator =
+        new AtomicReference<>(
+            new PathPackageLocator(
+                outputBase,
+                ImmutableList.of(Root.fromPath(rootDirectory)),
+                BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY));
+    BlazeDirectories directories =
+        new BlazeDirectories(
+            new ServerDirectories(rootDirectory, outputBase, rootDirectory),
+            rootDirectory,
+            null,
+            AnalysisMock.get().getProductName());
+    ExternalFilesHelper externalFilesHelper =
+        ExternalFilesHelper.createForTesting(
+            pkgLocator,
+            ExternalFileAction.DEPEND_ON_EXTERNAL_PKG_FOR_EXTERNAL_REPO_PATHS,
+            directories);
+
+    AtomicReference<UnixGlob.FilesystemCalls> syscalls =
+        new AtomicReference<>(UnixGlob.DEFAULT_SYSCALLS);
+    Map<SkyFunctionName, SkyFunction> skyFunctions = new HashMap<>();
+    skyFunctions.put(
+        FileStateValue.FILE_STATE,
+        new FileStateFunction(new AtomicReference<>(), syscalls, externalFilesHelper));
+    skyFunctions.put(FileValue.FILE, new FileFunction(pkgLocator));
+    skyFunctions.put(SkyFunctions.DIRECTORY_LISTING, new DirectoryListingFunction());
+    skyFunctions.put(
+        SkyFunctions.DIRECTORY_LISTING_STATE,
+        new DirectoryListingStateFunction(externalFilesHelper, syscalls));
+    skyFunctions.put(SkyFunctions.PATH_CASING_LOOKUP, new PathCasingLookupFunction());
+
+    differencer = new SequencedRecordingDifferencer();
+    driver =
+        new SequentialBuildDriver(new InMemoryMemoizingEvaluator(skyFunctions, differencer, null));
+  }
+
+  private RootedPath rootedPath(String relative) {
+    return RootedPath.toRootedPath(Root.fromPath(rootDirectory), PathFragment.create(relative));
+  }
+
+  @Test
+  public void testSanityCheckFilesystemIsCaseInsensitive() {
+    Path p1 = rootDirectory.getRelative("Foo/Bar");
+    Path p2 = rootDirectory.getRelative("FOO/BAR");
+    Path p3 = rootDirectory.getRelative("control");
+    assertThat(p1).isNotSameInstanceAs(p2);
+    assertThat(p1).isNotSameInstanceAs(p3);
+    assertThat(p2).isNotSameInstanceAs(p3);
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1).isNotEqualTo(p3);
+  }
+
+  @Test
+  public void testPathCasingLookup() throws Exception {
+    RootedPath a = rootedPath("Foo/Bar/Baz");
+    RootedPath b = rootedPath("fOO/baR/BAZ");
+    createFile(a);
+    assertThat(a).isEqualTo(b);
+    assertThat(RootedPathAndCasing.create(a)).isNotEqualTo(RootedPathAndCasing.create(b));
+    assertThat(expectEvalSuccess(a).isCorrect()).isTrue();
+    assertThat(expectEvalSuccess(b).isCorrect()).isFalse();
+  }
+
+  @Test
+  public void testNonExistentPath() throws Exception {
+    RootedPath file = rootedPath("Foo/Bar/Baz.txt");
+    createFile(file);
+    RootedPath missing1 = rootedPath("Foo/Bar/x/y");
+    RootedPath missing2 = rootedPath("Foo/BAR/x/y");
+    // Non-existent paths are correct if their existing part is correct.
+    assertThat(expectEvalSuccess(missing1).isCorrect()).isTrue();
+    assertThat(expectEvalSuccess(missing2).isCorrect()).isFalse();
+    // Non-existent paths are illegal if their parent exists but is not a directory.
+    RootedPath bad = rootedPath("Foo/Bar/Baz.txt/x/y");
+    Exception e = expectEvalFailure(bad);
+    assertThat(e).hasMessageThat().contains("its parent exists but is not a directory");
+  }
+
+  @Test
+  public void testNonExistentPathThatComesIntoExistence() throws Exception {
+    RootedPath a = rootedPath("Foo/Bar/Baz");
+    RootedPath b = rootedPath("fOO/baR/BAZ");
+    assertThat(a).isEqualTo(b);
+    // Expecting RootedPath.toRootedPath not to intern instances, otherwise 'a' would be the same
+    // instance as 'b' which would nullify this test.
+    assertThat(a).isNotSameInstanceAs(b);
+    assertThat(a.toString()).isNotEqualTo(b.toString());
+    assertThat(RootedPathAndCasing.create(a)).isNotEqualTo(RootedPathAndCasing.create(b));
+    // Path does not exist, so both casings are correct!
+    assertThat(expectEvalSuccess(a).isCorrect()).isTrue();
+    assertThat(expectEvalSuccess(b).isCorrect()).isTrue();
+    // Path comes into existence.
+    createFile(a);
+    // Now only one casing is correct.
+    assertThat(expectEvalSuccess(a).isCorrect()).isTrue();
+    assertThat(expectEvalSuccess(b).isCorrect()).isFalse();
+  }
+
+  @Test
+  public void testExistingPathThatIsThenDeleted() throws Exception {
+    RootedPath a = rootedPath("Foo/Bar/Baz");
+    RootedPath b = rootedPath("Foo/Bar/BAZ");
+    createFile(a);
+    // Path exists, so only one casing is correct.
+    assertThat(expectEvalSuccess(a).isCorrect()).isTrue();
+    assertThat(expectEvalSuccess(b).isCorrect()).isFalse();
+    // Path no longer exists, both casings are correct.
+    deleteFile(a);
+    assertThat(expectEvalSuccess(a).isCorrect()).isTrue();
+    assertThat(expectEvalSuccess(b).isCorrect()).isTrue();
+  }
+
+  private void createFile(RootedPath p) throws IOException {
+    Path path = p.asPath();
+    if (!path.getParentDirectory().exists()) {
+      scratch.dir(path.getParentDirectory().getPathString());
+    }
+    scratch.file(path.getPathString());
+    invalidateFileAndParents(p);
+  }
+
+  private void deleteFile(RootedPath p) throws IOException {
+    Path path = p.asPath();
+    scratch.deleteFile(path.getPathString());
+    invalidateFileAndParents(p);
+  }
+
+  private EvaluationResult<PathCasingLookupValue> evaluate(SkyKey key) throws Exception {
+    EvaluationContext evaluationContext =
+        EvaluationContext.newBuilder()
+            .setKeepGoing(false)
+            .setNumThreads(SkyframeExecutor.DEFAULT_THREAD_COUNT)
+            .setEventHander(NullEventHandler.INSTANCE)
+            .build();
+    return driver.evaluate(ImmutableList.of(key), evaluationContext);
+  }
+
+  private PathCasingLookupValue expectEvalSuccess(RootedPath path) throws Exception {
+    SkyKey key = PathCasingLookupValue.key(path);
+    EvaluationResult<PathCasingLookupValue> result = evaluate(key);
+    assertThat(result.hasError()).isFalse();
+    return result.get(key);
+  }
+
+  private Exception expectEvalFailure(RootedPath path) throws Exception {
+    SkyKey key = PathCasingLookupValue.key(path);
+    EvaluationResult<PathCasingLookupValue> result = evaluate(key);
+    assertThat(result.hasError()).isTrue();
+    return result.getError().getException();
+  }
+
+  private void invalidateFile(RootedPath path) {
+    differencer.invalidate(ImmutableList.of(FileStateValue.key(path)));
+  }
+
+  private void invalidateDirectory(RootedPath path) {
+    invalidateFile(path);
+    differencer.invalidate(ImmutableList.of(DirectoryListingStateValue.key(path)));
+  }
+
+  private void invalidateFileAndParents(RootedPath p) {
+    invalidateFile(p);
+    do {
+      p = p.getParentDirectory();
+      invalidateDirectory(p);
+    } while (!p.getRootRelativePath().isEmpty());
+  }
+}