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