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());
+ }
+}