// 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.devtools.build.lib.skyframe.SkyframeExecutor.DEFAULT_THREAD_COUNT; import static com.google.devtools.build.skyframe.EvaluationResultSubjectFactory.assertThatEvaluationResult; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.common.base.Function; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.testing.EqualsTester; import com.google.devtools.build.lib.analysis.BlazeDirectories; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.cmdline.PackageIdentifier; import com.google.devtools.build.lib.cmdline.RepositoryName; import com.google.devtools.build.lib.events.NullEventHandler; import com.google.devtools.build.lib.events.StoredEventHandler; import com.google.devtools.build.lib.pkgcache.PathPackageLocator; import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction; import com.google.devtools.build.lib.skyframe.ExternalFilesHelper.ExternalFileAction; import com.google.devtools.build.lib.skyframe.PackageLookupFunction.CrossRepositoryLabelViolationStrategy; import com.google.devtools.build.lib.skyframe.PackageLookupValue.BuildFileName; import com.google.devtools.build.lib.testutil.ManualClock; import com.google.devtools.build.lib.testutil.TestConstants; import com.google.devtools.build.lib.testutil.TestRuleClassProvider; import com.google.devtools.build.lib.testutil.TestUtils; import com.google.devtools.build.lib.util.Pair; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor; 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.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.lib.vfs.util.FileSystems; import com.google.devtools.build.skyframe.ErrorInfo; 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.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link FileFunction}. */ @RunWith(JUnit4.class) public class FileFunctionTest { private CustomInMemoryFs fs; private Path pkgRoot; private Path outputBase; private PathPackageLocator pkgLocator; private boolean fastDigest; private ManualClock manualClock; private RecordingDifferencer differencer; @Before public final void createFsAndRoot() throws Exception { fastDigest = true; manualClock = new ManualClock(); createFsAndRoot(new CustomInMemoryFs(manualClock)); } private void createFsAndRoot(CustomInMemoryFs fs) throws IOException { this.fs = fs; pkgRoot = fs.getRootDirectory().getRelative("root"); outputBase = fs.getRootDirectory().getRelative("output_base"); pkgLocator = new PathPackageLocator(outputBase, ImmutableList.of(pkgRoot)); FileSystemUtils.createDirectoryAndParents(pkgRoot); } private SequentialBuildDriver makeDriver() { return makeDriver(ExternalFileAction.DEPEND_ON_EXTERNAL_PKG_FOR_EXTERNAL_REPO_PATHS); } private SequentialBuildDriver makeDriver(ExternalFileAction externalFileAction) { AtomicReference<PathPackageLocator> pkgLocatorRef = new AtomicReference<>(pkgLocator); BlazeDirectories directories = new BlazeDirectories(pkgRoot, outputBase, pkgRoot, TestConstants.PRODUCT_NAME); ExternalFilesHelper externalFilesHelper = new ExternalFilesHelper(pkgLocatorRef, externalFileAction, directories); differencer = new RecordingDifferencer(); MemoizingEvaluator evaluator = new InMemoryMemoizingEvaluator( ImmutableMap.<SkyFunctionName, SkyFunction>builder() .put( SkyFunctions.FILE_STATE, new FileStateFunction( new AtomicReference<TimestampGranularityMonitor>(), externalFilesHelper)) .put( SkyFunctions.FILE_SYMLINK_CYCLE_UNIQUENESS, new FileSymlinkCycleUniquenessFunction()) .put( SkyFunctions.FILE_SYMLINK_INFINITE_EXPANSION_UNIQUENESS, new FileSymlinkInfiniteExpansionUniquenessFunction()) .put(SkyFunctions.FILE, new FileFunction(pkgLocatorRef)) .put( SkyFunctions.PACKAGE, new PackageFunction(null, null, null, null, null, null, null)) .put( SkyFunctions.PACKAGE_LOOKUP, new PackageLookupFunction( new AtomicReference<>(ImmutableSet.<PackageIdentifier>of()), CrossRepositoryLabelViolationStrategy.ERROR, ImmutableList.of(BuildFileName.BUILD_DOT_BAZEL, BuildFileName.BUILD))) .put( SkyFunctions.WORKSPACE_AST, new WorkspaceASTFunction(TestRuleClassProvider.getRuleClassProvider())) .put( SkyFunctions.WORKSPACE_FILE, new WorkspaceFileFunction( TestRuleClassProvider.getRuleClassProvider(), TestConstants.PACKAGE_FACTORY_BUILDER_FACTORY_FOR_TESTING.builder().build( TestRuleClassProvider.getRuleClassProvider(), fs), directories)) .put(SkyFunctions.EXTERNAL_PACKAGE, new ExternalPackageFunction()) .put(SkyFunctions.LOCAL_REPOSITORY_LOOKUP, new LocalRepositoryLookupFunction()) .build(), differencer); PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID()); PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator); RepositoryDelegatorFunction.REPOSITORY_OVERRIDES.set( differencer, ImmutableMap.<RepositoryName, PathFragment>of()); return new SequentialBuildDriver(evaluator); } private FileValue valueForPath(Path path) throws InterruptedException { return valueForPathHelper(pkgRoot, path); } private FileValue valueForPathOutsidePkgRoot(Path path) throws InterruptedException { return valueForPathHelper(fs.getRootDirectory(), path); } private FileValue valueForPathHelper(Path root, Path path) throws InterruptedException { PathFragment pathFragment = path.relativeTo(root); RootedPath rootedPath = RootedPath.toRootedPath(root, pathFragment); SequentialBuildDriver driver = makeDriver(); SkyKey key = FileValue.key(rootedPath); EvaluationResult<FileValue> result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertFalse(result.hasError()); return result.get(key); } @Test public void testFileValueHashCodeAndEqualsContract() throws Exception { Path pathA = file(pkgRoot + "a", "a"); Path pathB = file(pkgRoot + "b", "b"); FileValue valueA1 = valueForPathOutsidePkgRoot(pathA); FileValue valueA2 = valueForPathOutsidePkgRoot(pathA); FileValue valueB1 = valueForPathOutsidePkgRoot(pathB); FileValue valueB2 = valueForPathOutsidePkgRoot(pathB); new EqualsTester() .addEqualityGroup(valueA1, valueA2) .addEqualityGroup(valueB1, valueB2) .testEquals(); } @Test public void testIsDirectory() throws Exception { assertFalse(valueForPath(file("a")).isDirectory()); assertFalse(valueForPath(path("nonexistent")).isDirectory()); assertTrue(valueForPath(directory("dir")).isDirectory()); assertFalse(valueForPath(symlink("sa", "a")).isDirectory()); assertFalse(valueForPath(symlink("smissing", "missing")).isDirectory()); assertTrue(valueForPath(symlink("sdir", "dir")).isDirectory()); assertTrue(valueForPath(symlink("ssdir", "sdir")).isDirectory()); } @Test public void testIsFile() throws Exception { assertTrue(valueForPath(file("a")).isFile()); assertFalse(valueForPath(path("nonexistent")).isFile()); assertFalse(valueForPath(directory("dir")).isFile()); assertTrue(valueForPath(symlink("sa", "a")).isFile()); assertFalse(valueForPath(symlink("smissing", "missing")).isFile()); assertFalse(valueForPath(symlink("sdir", "dir")).isFile()); assertTrue(valueForPath(symlink("ssfile", "sa")).isFile()); } @Test public void testSimpleIndependentFiles() throws Exception { file("a"); file("b"); Set<RootedPath> seenFiles = Sets.newHashSet(); seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("a", false, "b")); seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("b", false, "a")); assertThat(seenFiles).containsExactly(rootedPath("a"), rootedPath("b"), rootedPath("")); } @Test public void testSimpleSymlink() throws Exception { symlink("a", "b"); file("b"); assertValueChangesIfContentsOfFileChanges("a", false, "b"); assertValueChangesIfContentsOfFileChanges("b", true, "a"); } @Test public void testTransitiveSymlink() throws Exception { symlink("a", "b"); symlink("b", "c"); file("c"); assertValueChangesIfContentsOfFileChanges("a", false, "b"); assertValueChangesIfContentsOfFileChanges("a", false, "c"); assertValueChangesIfContentsOfFileChanges("b", true, "a"); assertValueChangesIfContentsOfFileChanges("c", true, "b"); assertValueChangesIfContentsOfFileChanges("c", true, "a"); } @Test public void testFileUnderDirectorySymlink() throws Exception { symlink("a", "b/c"); symlink("b", "d"); assertValueChangesIfContentsOfDirectoryChanges("b", true, "a/e"); } @Test public void testSymlinkInDirectory() throws Exception { symlink("a/aa", "ab"); file("a/ab"); assertValueChangesIfContentsOfFileChanges("a/aa", false, "a/ab"); assertValueChangesIfContentsOfFileChanges("a/ab", true, "a/aa"); } @Test public void testRelativeSymlink() throws Exception { symlink("a/aa/aaa", "../ab/aba"); file("a/ab/aba"); assertValueChangesIfContentsOfFileChanges("a/ab/aba", true, "a/aa/aaa"); } @Test public void testDoubleRelativeSymlink() throws Exception { symlink("a/b/c/d", "../../e/f"); file("a/e/f"); assertValueChangesIfContentsOfFileChanges("a/e/f", true, "a/b/c/d"); } @Test public void testExternalRelativeSymlink() throws Exception { symlink("a", "../outside"); file("b"); file("../outside"); Set<RootedPath> seenFiles = Sets.newHashSet(); seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("b", false, "a")); seenFiles.addAll( getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("../outside", true, "a")); assertThat(seenFiles) .containsExactly( rootedPath("a"), rootedPath(""), RootedPath.toRootedPath(fs.getRootDirectory(), PathFragment.EMPTY_FRAGMENT), RootedPath.toRootedPath(fs.getRootDirectory(), PathFragment.create("outside"))); } @Test public void testAbsoluteSymlink() throws Exception { symlink("a", "/absolute"); file("b"); file("/absolute"); Set<RootedPath> seenFiles = Sets.newHashSet(); seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("b", false, "a")); seenFiles.addAll( getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("/absolute", true, "a")); assertThat(seenFiles) .containsExactly( rootedPath("a"), rootedPath(""), RootedPath.toRootedPath(fs.getRootDirectory(), PathFragment.EMPTY_FRAGMENT), RootedPath.toRootedPath(fs.getRootDirectory(), PathFragment.create("absolute"))); } @Test public void testAbsoluteSymlinkToExternal() throws Exception { String externalPath = outputBase.getRelative(Label.EXTERNAL_PACKAGE_NAME).getRelative("a/b").getPathString(); symlink("a", externalPath); file("b"); file(externalPath); Set<RootedPath> seenFiles = Sets.newHashSet(); seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("b", false, "a")); seenFiles.addAll( getFilesSeenAndAssertValueChangesIfContentsOfFileChanges(externalPath, true, "a")); Path root = fs.getRootDirectory(); assertThat(seenFiles) .containsExactly( rootedPath("WORKSPACE"), rootedPath("a"), rootedPath(""), RootedPath.toRootedPath(root, PathFragment.EMPTY_FRAGMENT), RootedPath.toRootedPath(root, PathFragment.create("output_base")), RootedPath.toRootedPath(root, PathFragment.create("output_base/external")), RootedPath.toRootedPath(root, PathFragment.create("output_base/external/a")), RootedPath.toRootedPath(root, PathFragment.create("output_base/external/a/b"))); } @Test public void testSymlinkAsAncestor() throws Exception { file("a/b/c/d"); symlink("f", "a/b/c"); assertValueChangesIfContentsOfFileChanges("a/b/c/d", true, "f/d"); } @Test public void testSymlinkAsAncestorNested() throws Exception { file("a/b/c/d"); symlink("f", "a/b"); assertValueChangesIfContentsOfFileChanges("a/b/c/d", true, "f/c/d"); } @Test public void testTwoSymlinksInAncestors() throws Exception { file("a/aa/aaa/aaaa"); symlink("b/ba/baa", "../../a/aa"); symlink("c/ca", "../b/ba"); assertValueChangesIfContentsOfFileChanges("c/ca", true, "c/ca/baa/aaa/aaaa"); assertValueChangesIfContentsOfFileChanges("b/ba/baa", true, "c/ca/baa/aaa/aaaa"); assertValueChangesIfContentsOfFileChanges("a/aa/aaa/aaaa", true, "c/ca/baa/aaa/aaaa"); } @Test public void testSelfReferencingSymlink() throws Exception { symlink("a", "a"); assertError("a"); } @Test public void testMutuallyReferencingSymlinks() throws Exception { symlink("a", "b"); symlink("b", "a"); assertError("a"); } @Test public void testRecursiveNestingSymlink() throws Exception { symlink("a/a", "../a"); assertError("a/a/b"); } @Test public void testBrokenSymlink() throws Exception { symlink("a", "b"); Set<RootedPath> seenFiles = Sets.newHashSet(); seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("b", true, "a")); seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("a", false, "b")); assertThat(seenFiles).containsExactly(rootedPath("a"), rootedPath("b"), rootedPath("")); } @Test public void testBrokenDirectorySymlink() throws Exception { symlink("a", "b"); file("c"); assertValueChangesIfContentsOfDirectoryChanges("a", true, "a/aa"); // This just creates the directory "b", which doesn't change the value for "a/aa", since "a/aa" // still has real path "b/aa" and still doesn't exist. assertValueChangesIfContentsOfDirectoryChanges("b", false, "a/aa"); assertValueChangesIfContentsOfFileChanges("c", false, "a/aa"); } @Test public void testTraverseIntoVirtualNonDirectory() throws Exception { file("dir/a"); symlink("vdir", "dir"); // The following evaluation should not throw IOExceptions. assertNoError("vdir/a/aa/aaa"); } @Test public void testFileCreation() throws Exception { FileValue a = valueForPath(path("file")); Path p = file("file"); FileValue b = valueForPath(p); assertFalse(a.equals(b)); } @Test public void testEmptyFile() throws Exception { final byte[] digest = new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; createFsAndRoot( new CustomInMemoryFs(manualClock) { @Override protected byte[] getFastDigest(Path path, HashFunction hf) throws IOException { return digest; } }); Path p = file("file"); p.setLastModifiedTime(0L); FileValue a = valueForPath(p); p.setLastModifiedTime(1L); assertThat(valueForPath(p)).isNotEqualTo(a); p.setLastModifiedTime(0L); assertEquals(a, valueForPath(p)); FileSystemUtils.writeContentAsLatin1(p, "content"); // Same digest, but now non-empty. assertThat(valueForPath(p)).isNotEqualTo(a); } @Test public void testUnreadableFileWithNoFastDigest() throws Exception { Path p = file("unreadable"); p.chmod(0); p.setLastModifiedTime(0L); FileValue value = valueForPath(p); assertTrue(value.exists()); assertThat(value.getDigest()).isNull(); p.setLastModifiedTime(10L); assertThat(valueForPath(p)).isNotEqualTo(value); p.setLastModifiedTime(0L); assertThat(valueForPath(p)).isEqualTo(value); } @Test public void testUnreadableFileWithFastDigest() throws Exception { final byte[] expectedDigest = MessageDigest.getInstance("md5").digest("blah".getBytes(StandardCharsets.UTF_8)); createFsAndRoot( new CustomInMemoryFs(manualClock) { @Override protected byte[] getFastDigest(Path path, HashFunction hf) { return path.getBaseName().equals("unreadable") ? expectedDigest : null; } }); Path p = file("unreadable"); p.chmod(0); FileValue value = valueForPath(p); assertThat(value.exists()).isTrue(); assertThat(value.getDigest()).isNotNull(); } @Test public void testFileModificationModTime() throws Exception { fastDigest = false; Path p = file("file"); FileValue a = valueForPath(p); p.setLastModifiedTime(42); FileValue b = valueForPath(p); assertFalse(a.equals(b)); } @Test public void testFileModificationDigest() throws Exception { fastDigest = true; Path p = file("file"); FileValue a = valueForPath(p); FileSystemUtils.writeContentAsLatin1(p, "goop"); FileValue b = valueForPath(p); assertFalse(a.equals(b)); } @Test public void testModTimeVsDigest() throws Exception { Path p = file("somefile", "fizzley"); fastDigest = true; FileValue aMd5 = valueForPath(p); fastDigest = false; FileValue aModTime = valueForPath(p); assertThat(aModTime).isNotEqualTo(aMd5); new EqualsTester().addEqualityGroup(aMd5).addEqualityGroup(aModTime).testEquals(); } @Test public void testFileDeletion() throws Exception { Path p = file("file"); FileValue a = valueForPath(p); p.delete(); FileValue b = valueForPath(p); assertFalse(a.equals(b)); } @Test public void testFileTypeChange() throws Exception { Path p = file("file"); FileValue a = valueForPath(p); p.delete(); p = symlink("file", "foo"); FileValue b = valueForPath(p); p.delete(); FileSystemUtils.createDirectoryAndParents(pkgRoot.getRelative("file")); FileValue c = valueForPath(p); assertFalse(a.equals(b)); assertFalse(b.equals(c)); assertFalse(a.equals(c)); } @Test public void testSymlinkTargetChange() throws Exception { Path p = symlink("symlink", "foo"); FileValue a = valueForPath(p); p.delete(); p = symlink("symlink", "bar"); FileValue b = valueForPath(p); assertThat(b).isNotEqualTo(a); } @Test public void testSymlinkTargetContentsChangeCTime() throws Exception { fastDigest = false; Path fooPath = file("foo"); FileSystemUtils.writeContentAsLatin1(fooPath, "foo"); Path p = symlink("symlink", "foo"); FileValue a = valueForPath(p); manualClock.advanceMillis(1); fooPath.chmod(0555); manualClock.advanceMillis(1); FileValue b = valueForPath(p); assertThat(b).isNotEqualTo(a); } @Test public void testSymlinkTargetContentsChangeDigest() throws Exception { fastDigest = true; Path fooPath = file("foo"); FileSystemUtils.writeContentAsLatin1(fooPath, "foo"); Path p = symlink("symlink", "foo"); FileValue a = valueForPath(p); FileSystemUtils.writeContentAsLatin1(fooPath, "bar"); FileValue b = valueForPath(p); assertThat(b).isNotEqualTo(a); } @Test public void testRealPath() throws Exception { file("file"); directory("directory"); file("directory/file"); symlink("directory/link", "file"); symlink("directory/doublelink", "link"); symlink("directory/parentlink", "../file"); symlink("directory/doubleparentlink", "../link"); symlink("link", "file"); symlink("deadlink", "missing_file"); symlink("dirlink", "directory"); symlink("doublelink", "link"); symlink("doubledirlink", "dirlink"); checkRealPath("file"); checkRealPath("link"); checkRealPath("doublelink"); for (String dir : new String[] {"directory", "dirlink", "doubledirlink"}) { checkRealPath(dir); checkRealPath(dir + "/file"); checkRealPath(dir + "/link"); checkRealPath(dir + "/doublelink"); checkRealPath(dir + "/parentlink"); } assertRealPath("missing", "missing"); assertRealPath("deadlink", "missing_file"); } @Test public void testRealPathRelativeSymlink() throws Exception { directory("dir"); symlink("dir/link", "../dir2"); directory("dir2"); symlink("dir2/filelink", "../dest"); file("dest"); checkRealPath("dir/link/filelink"); } @Test public void testSymlinkAcrossPackageRoots() throws Exception { Path otherPkgRoot = fs.getRootDirectory().getRelative("other_root"); pkgLocator = new PathPackageLocator(outputBase, ImmutableList.of(pkgRoot, otherPkgRoot)); symlink("a", "/other_root/b"); assertValueChangesIfContentsOfFileChanges("/other_root/b", true, "a"); } @Test public void testFilesOutsideRootIsReEvaluated() throws Exception { Path file = file("/outsideroot"); SequentialBuildDriver driver = makeDriver(); SkyKey key = skyKey("/outsideroot"); EvaluationResult<SkyValue> result; result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); if (result.hasError()) { fail(String.format("Evaluation error for %s: %s", key, result.getError())); } FileValue oldValue = (FileValue) result.get(key); assertTrue(oldValue.exists()); file.delete(); differencer.invalidate(ImmutableList.of(fileStateSkyKey("/outsideroot"))); result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); if (result.hasError()) { fail(String.format("Evaluation error for %s: %s", key, result.getError())); } FileValue newValue = (FileValue) result.get(key); assertNotSame(oldValue, newValue); assertFalse(newValue.exists()); } @Test public void testFilesOutsideRootWhenExternalAssumedNonExistentAndImmutable() throws Exception { file("/outsideroot"); SequentialBuildDriver driver = makeDriver(ExternalFileAction.ASSUME_NON_EXISTENT_AND_IMMUTABLE_FOR_EXTERNAL_PATHS); SkyKey key = skyKey("/outsideroot"); EvaluationResult<SkyValue> result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertThatEvaluationResult(result).hasNoError(); FileValue value = (FileValue) result.get(key); assertThat(value).isNotNull(); assertFalse(value.exists()); } @Test public void testAbsoluteSymlinksToFilesOutsideRootWhenExternalAssumedNonExistentAndImmutable() throws Exception { file("/outsideroot"); symlink("a", "/outsideroot"); SequentialBuildDriver driver = makeDriver(ExternalFileAction.ASSUME_NON_EXISTENT_AND_IMMUTABLE_FOR_EXTERNAL_PATHS); SkyKey key = skyKey("a"); EvaluationResult<SkyValue> result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertThatEvaluationResult(result).hasNoError(); FileValue value = (FileValue) result.get(key); assertThat(value).isNotNull(); assertFalse(value.exists()); } @Test public void testAbsoluteSymlinksReferredByInternalFilesToFilesOutsideRootWhenExternalAssumedNonExistentAndImmutable() throws Exception { file("/outsideroot/src/foo/bar"); symlink("/root/src", "/outsideroot/src"); SequentialBuildDriver driver = makeDriver(ExternalFileAction.ASSUME_NON_EXISTENT_AND_IMMUTABLE_FOR_EXTERNAL_PATHS); SkyKey key = skyKey("/root/src/foo/bar"); EvaluationResult<SkyValue> result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertThatEvaluationResult(result).hasNoError(); FileValue value = (FileValue) result.get(key); assertThat(value).isNotNull(); assertFalse(value.exists()); } @Test public void testRelativeSymlinksToFilesOutsideRootWhenExternalAssumedNonExistentAndImmutable() throws Exception { file("../outsideroot"); symlink("a", "../outsideroot"); SequentialBuildDriver driver = makeDriver(ExternalFileAction.ASSUME_NON_EXISTENT_AND_IMMUTABLE_FOR_EXTERNAL_PATHS); SkyKey key = skyKey("a"); EvaluationResult<SkyValue> result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertThatEvaluationResult(result).hasNoError(); FileValue value = (FileValue) result.get(key); assertThat(value).isNotNull(); assertFalse(value.exists()); } @Test public void testAbsoluteSymlinksBackIntoSourcesOkWhenExternalDisallowed() throws Exception { Path file = file("insideroot"); symlink("a", file.getPathString()); SequentialBuildDriver driver = makeDriver(ExternalFileAction.ASSUME_NON_EXISTENT_AND_IMMUTABLE_FOR_EXTERNAL_PATHS); SkyKey key = skyKey("a"); EvaluationResult<SkyValue> result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertThatEvaluationResult(result).hasNoError(); FileValue value = (FileValue) result.get(key); assertThat(value).isNotNull(); assertTrue(value.exists()); assertThat(value.realRootedPath().getRelativePath().getPathString()).isEqualTo("insideroot"); } @SuppressWarnings({"rawtypes", "unchecked"}) private static Set<RootedPath> filesSeen(MemoizingEvaluator graph) { return ImmutableSet.copyOf( (Iterable<RootedPath>) (Iterable) Iterables.transform( Iterables.filter( graph.getValues().keySet(), SkyFunctionName.functionIs(SkyFunctions.FILE_STATE)), new Function<SkyKey, Object>() { @Override public Object apply(SkyKey skyKey) { return skyKey.argument(); } })); } @Test public void testSize() throws Exception { Path file = file("file"); int fileSize = 20; FileSystemUtils.writeContentAsLatin1(file, Strings.repeat("a", fileSize)); assertEquals(fileSize, valueForPath(file).getSize()); Path dir = directory("directory"); file(dir.getChild("child").getPathString()); try { valueForPath(dir).getSize(); fail(); } catch (IllegalStateException e) { // Expected. } Path nonexistent = fs.getPath("/root/noexist"); try { valueForPath(nonexistent).getSize(); fail(); } catch (IllegalStateException e) { // Expected. } Path symlink = symlink("link", "/root/file"); // Symlink stores size of target, not link. assertEquals(fileSize, valueForPath(symlink).getSize()); assertTrue(symlink.delete()); symlink = symlink("link", "/root/directory"); try { valueForPath(symlink).getSize(); fail(); } catch (IllegalStateException e) { // Expected. } assertTrue(symlink.delete()); symlink = symlink("link", "/root/noexist"); try { valueForPath(symlink).getSize(); fail(); } catch (IllegalStateException e) { // Expected. } } @Test public void testDigest() throws Exception { final AtomicInteger digestCalls = new AtomicInteger(0); int expectedCalls = 0; fs = new CustomInMemoryFs(manualClock) { @Override protected byte[] getMD5Digest(Path path) throws IOException { digestCalls.incrementAndGet(); return super.getMD5Digest(path); } }; pkgRoot = fs.getRootDirectory().getRelative("root"); Path file = file("file"); FileSystemUtils.writeContentAsLatin1(file, Strings.repeat("a", 20)); byte[] digest = file.getMD5Digest(); expectedCalls++; assertEquals(expectedCalls, digestCalls.get()); FileValue value = valueForPath(file); expectedCalls++; assertEquals(expectedCalls, digestCalls.get()); assertArrayEquals(digest, value.getDigest()); // Digest is cached -- no filesystem access. assertEquals(expectedCalls, digestCalls.get()); fastDigest = false; digestCalls.set(0); value = valueForPath(file); // No new digest calls. assertEquals(0, digestCalls.get()); assertNull(value.getDigest()); assertEquals(0, digestCalls.get()); fastDigest = true; Path dir = directory("directory"); try { assertNull(valueForPath(dir).getDigest()); fail(); } catch (IllegalStateException e) { // Expected. } assertEquals(0, digestCalls.get()); // No digest calls made for directory. Path nonexistent = fs.getPath("/root/noexist"); try { assertNull(valueForPath(nonexistent).getDigest()); fail(); } catch (IllegalStateException e) { // Expected. } assertEquals(0, digestCalls.get()); // No digest calls made for nonexistent file. Path symlink = symlink("link", "/root/file"); value = valueForPath(symlink); assertEquals(1, digestCalls.get()); // Symlink stores digest of target, not link. assertArrayEquals(digest, value.getDigest()); assertEquals(1, digestCalls.get()); digestCalls.set(0); assertTrue(symlink.delete()); symlink = symlink("link", "/root/directory"); // Symlink stores digest of target, not link, for directories too. try { assertNull(valueForPath(symlink).getDigest()); fail(); } catch (IllegalStateException e) { // Expected. } assertEquals(0, digestCalls.get()); } @Test public void testDoesntStatChildIfParentDoesntExist() throws Exception { // Our custom filesystem says "a" does not exist, so FileFunction shouldn't bother trying to // think about "a/b". Test for this by having a stat of "a/b" fail with an io error, and // observing that we don't encounter the error. fs.stubStat(path("a"), null); fs.stubStatError(path("a/b"), new IOException("ouch!")); assertThat(valueForPath(path("a/b")).exists()).isFalse(); } @Test public void testFilesystemInconsistencies_ParentIsntADirectory() throws Exception { file("a/b"); // Our custom filesystem says "a/b" exists but its parent "a" is a file. FileStatus inconsistentParentFileStatus = new FileStatus() { @Override public boolean isFile() { return true; } @Override public boolean isSpecialFile() { return false; } @Override public boolean isDirectory() { return false; } @Override public boolean isSymbolicLink() { 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(path("a"), inconsistentParentFileStatus); // Disable fast-path md5 so that we don't try try to md5 the "a" (since it actually physically // is a directory). fastDigest = false; SequentialBuildDriver driver = makeDriver(); SkyKey skyKey = skyKey("a/b"); EvaluationResult<FileValue> result = driver.evaluate( ImmutableList.of(skyKey), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertTrue(result.hasError()); ErrorInfo errorInfo = result.getError(skyKey); assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class); assertThat(errorInfo.getException().getMessage()) .contains("file /root/a/b exists but its parent path /root/a isn't an existing directory"); } @Test public void testFilesystemInconsistencies_GetFastDigest() throws Exception { file("a"); // Our custom filesystem says "a/b" exists but "a" does not exist. fs.stubFastDigestError(path("a"), new IOException("nope")); SequentialBuildDriver driver = makeDriver(); SkyKey skyKey = skyKey("a"); EvaluationResult<FileValue> result = driver.evaluate( ImmutableList.of(skyKey), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertTrue(result.hasError()); ErrorInfo errorInfo = result.getError(skyKey); assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class); assertThat(errorInfo.getException().getMessage()).contains("encountered error 'nope'"); assertThat(errorInfo.getException().getMessage()).contains("/root/a is no longer a file"); } @Test public void testFilesystemInconsistencies_GetFastDigestAndIsReadableFailure() throws Exception { createFsAndRoot( new CustomInMemoryFs(manualClock) { @Override protected boolean isReadable(Path path) throws IOException { if (path.getBaseName().equals("unreadable")) { throw new IOException("isReadable failed"); } return super.isReadable(path); } }); Path p = file("unreadable"); p.chmod(0); SequentialBuildDriver driver = makeDriver(); SkyKey skyKey = skyKey("unreadable"); EvaluationResult<FileValue> result = driver.evaluate( ImmutableList.of(skyKey), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertTrue(result.hasError()); ErrorInfo errorInfo = result.getError(skyKey); assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class); assertThat(errorInfo.getException().getMessage()) .contains("encountered error 'isReadable failed'"); assertThat(errorInfo.getException().getMessage()) .contains("/root/unreadable is no longer a file"); } private void runTestSymlinkCycle(boolean ancestorCycle, boolean startInCycle) throws Exception { symlink("a", "b"); symlink("b", "c"); symlink("c", "d"); symlink("d", "e"); symlink("e", "c"); // We build multiple keys at once to make sure the cycle is reported exactly once. Map<RootedPath, ImmutableList<RootedPath>> startToCycleMap = ImmutableMap.<RootedPath, ImmutableList<RootedPath>>builder() .put( rootedPath("a"), ImmutableList.of(rootedPath("c"), rootedPath("d"), rootedPath("e"))) .put( rootedPath("b"), ImmutableList.of(rootedPath("c"), rootedPath("d"), rootedPath("e"))) .put( rootedPath("d"), ImmutableList.<RootedPath>of(rootedPath("d"), rootedPath("e"), rootedPath("c"))) .put( rootedPath("e"), ImmutableList.<RootedPath>of(rootedPath("e"), rootedPath("c"), rootedPath("d"))) .put( rootedPath("a/some/descendant"), ImmutableList.of(rootedPath("c"), rootedPath("d"), rootedPath("e"))) .put( rootedPath("b/some/descendant"), ImmutableList.of(rootedPath("c"), rootedPath("d"), rootedPath("e"))) .put( rootedPath("d/some/descendant"), ImmutableList.<RootedPath>of(rootedPath("d"), rootedPath("e"), rootedPath("c"))) .put( rootedPath("e/some/descendant"), ImmutableList.<RootedPath>of(rootedPath("e"), rootedPath("c"), rootedPath("d"))) .build(); Map<RootedPath, ImmutableList<RootedPath>> startToPathToCycleMap = ImmutableMap.<RootedPath, ImmutableList<RootedPath>>builder() .put(rootedPath("a"), ImmutableList.of(rootedPath("a"), rootedPath("b"))) .put(rootedPath("b"), ImmutableList.of(rootedPath("b"))) .put(rootedPath("d"), ImmutableList.<RootedPath>of()) .put(rootedPath("e"), ImmutableList.<RootedPath>of()) .put( rootedPath("a/some/descendant"), ImmutableList.of(rootedPath("a"), rootedPath("b"))) .put(rootedPath("b/some/descendant"), ImmutableList.of(rootedPath("b"))) .put(rootedPath("d/some/descendant"), ImmutableList.<RootedPath>of()) .put(rootedPath("e/some/descendant"), ImmutableList.<RootedPath>of()) .build(); ImmutableList<SkyKey> keys; if (ancestorCycle && startInCycle) { keys = ImmutableList.of(skyKey("d/some/descendant"), skyKey("e/some/descendant")); } else if (ancestorCycle && !startInCycle) { keys = ImmutableList.of(skyKey("a/some/descendant"), skyKey("b/some/descendant")); } else if (!ancestorCycle && startInCycle) { keys = ImmutableList.of(skyKey("d"), skyKey("e")); } else { keys = ImmutableList.of(skyKey("a"), skyKey("b")); } StoredEventHandler eventHandler = new StoredEventHandler(); SequentialBuildDriver driver = makeDriver(); EvaluationResult<FileValue> result = driver.evaluate(keys, /*keepGoing=*/ true, DEFAULT_THREAD_COUNT, eventHandler); assertTrue(result.hasError()); for (SkyKey key : keys) { ErrorInfo errorInfo = result.getError(key); // FileFunction detects symlink cycles explicitly. assertThat(errorInfo.getCycleInfo()).isEmpty(); FileSymlinkCycleException fsce = (FileSymlinkCycleException) errorInfo.getException(); RootedPath start = (RootedPath) key.argument(); assertThat(fsce.getPathToCycle()) .containsExactlyElementsIn(startToPathToCycleMap.get(start)) .inOrder(); assertThat(fsce.getCycle()).containsExactlyElementsIn(startToCycleMap.get(start)).inOrder(); } // Check that the unique cycle was reported exactly once. assertThat(eventHandler.getEvents()).hasSize(1); assertThat(Iterables.getOnlyElement(eventHandler.getEvents()).getMessage()) .contains("circular symlinks detected"); } @Test public void testSymlinkCycle_AncestorCycle_StartInCycle() throws Exception { runTestSymlinkCycle(/*ancestorCycle=*/ true, /*startInCycle=*/ true); } @Test public void testSymlinkCycle_AncestorCycle_StartOutOfCycle() throws Exception { runTestSymlinkCycle(/*ancestorCycle=*/ true, /*startInCycle=*/ false); } @Test public void testSymlinkCycle_RegularCycle_StartInCycle() throws Exception { runTestSymlinkCycle(/*ancestorCycle=*/ false, /*startInCycle=*/ true); } @Test public void testSymlinkCycle_RegularCycle_StartOutOfCycle() throws Exception { runTestSymlinkCycle(/*ancestorCycle=*/ false, /*startInCycle=*/ false); } @Test public void testSerialization() throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); FileSystem oldFileSystem = Path.getFileSystemForSerialization(); try { // InMemoryFS is not supported for serialization. FileSystem fs = FileSystems.getJavaIoFileSystem(); Path.setFileSystemForSerialization(fs); pkgRoot = fs.getRootDirectory(); FileValue a = valueForPath(fs.getPath("/")); Path tmp = fs.getPath(TestUtils.tmpDirFile().getAbsoluteFile() + "/file.txt"); FileSystemUtils.writeContentAsLatin1(tmp, "test contents"); FileValue b = valueForPath(tmp); Preconditions.checkState(b.isFile()); FileValue c = valueForPath(fs.getPath("/does/not/exist")); oos.writeObject(a); oos.writeObject(b); oos.writeObject(c); ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); FileValue a2 = (FileValue) ois.readObject(); FileValue b2 = (FileValue) ois.readObject(); FileValue c2 = (FileValue) ois.readObject(); assertEquals(a, a2); assertEquals(b, b2); assertEquals(c, c2); assertFalse(a2.equals(b2)); } finally { Path.setFileSystemForSerialization(oldFileSystem); } } @Test public void testFileStateEquality() throws Exception { file("a"); symlink("b1", "a"); symlink("b2", "a"); symlink("b3", "zzz"); directory("d1"); directory("d2"); SkyKey file = fileStateSkyKey("a"); SkyKey symlink1 = fileStateSkyKey("b1"); SkyKey symlink2 = fileStateSkyKey("b2"); SkyKey symlink3 = fileStateSkyKey("b3"); SkyKey missing1 = fileStateSkyKey("c1"); SkyKey missing2 = fileStateSkyKey("c2"); SkyKey directory1 = fileStateSkyKey("d1"); SkyKey directory2 = fileStateSkyKey("d2"); ImmutableList<SkyKey> keys = ImmutableList.of( file, symlink1, symlink2, symlink3, missing1, missing2, directory1, directory2); SequentialBuildDriver driver = makeDriver(); EvaluationResult<SkyValue> result = driver.evaluate(keys, false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); new EqualsTester() .addEqualityGroup(result.get(file)) .addEqualityGroup(result.get(symlink1), result.get(symlink2)) .addEqualityGroup(result.get(symlink3)) .addEqualityGroup(result.get(missing1), result.get(missing2)) .addEqualityGroup(result.get(directory1), result.get(directory2)) .testEquals(); } @Test public void testSymlinkToPackagePathBoundary() throws Exception { Path path = path("this/is/a/path"); FileSystemUtils.ensureSymbolicLink(path, pkgRoot); assertError("this/is/a/path"); } private void runTestInfiniteSymlinkExpansion(boolean symlinkToAncestor, boolean absoluteSymlink) throws Exception { Path otherPath = path("other"); RootedPath otherRootedPath = RootedPath.toRootedPath(pkgRoot, otherPath.relativeTo(pkgRoot)); Path ancestorPath = path("a"); RootedPath ancestorRootedPath = RootedPath.toRootedPath(pkgRoot, ancestorPath.relativeTo(pkgRoot)); FileSystemUtils.ensureSymbolicLink(otherPath, ancestorPath); Path intermediatePath = path("inter"); RootedPath intermediateRootedPath = RootedPath.toRootedPath(pkgRoot, intermediatePath.relativeTo(pkgRoot)); Path descendantPath = path("a/b/c/d/e"); RootedPath descendantRootedPath = RootedPath.toRootedPath(pkgRoot, descendantPath.relativeTo(pkgRoot)); if (symlinkToAncestor) { FileSystemUtils.ensureSymbolicLink(descendantPath, intermediatePath); if (absoluteSymlink) { FileSystemUtils.ensureSymbolicLink(intermediatePath, ancestorPath); } else { FileSystemUtils.ensureSymbolicLink(intermediatePath, ancestorRootedPath.getRelativePath()); } } else { FileSystemUtils.ensureSymbolicLink(ancestorPath, intermediatePath); if (absoluteSymlink) { FileSystemUtils.ensureSymbolicLink(intermediatePath, descendantPath); } else { FileSystemUtils.ensureSymbolicLink( intermediatePath, descendantRootedPath.getRelativePath()); } } StoredEventHandler eventHandler = new StoredEventHandler(); SequentialBuildDriver driver = makeDriver(); SkyKey ancestorPathKey = FileValue.key(ancestorRootedPath); SkyKey descendantPathKey = FileValue.key(descendantRootedPath); SkyKey otherPathKey = FileValue.key(otherRootedPath); ImmutableList<SkyKey> keys; ImmutableList<SkyKey> errorKeys; ImmutableList<RootedPath> expectedChain; if (symlinkToAncestor) { keys = ImmutableList.of(descendantPathKey, otherPathKey); errorKeys = ImmutableList.of(descendantPathKey); expectedChain = ImmutableList.of(descendantRootedPath, intermediateRootedPath, ancestorRootedPath); } else { keys = ImmutableList.of(ancestorPathKey, otherPathKey); errorKeys = keys; expectedChain = ImmutableList.of(ancestorRootedPath, intermediateRootedPath, descendantRootedPath); } EvaluationResult<FileValue> result = driver.evaluate(keys, /*keepGoing=*/ true, DEFAULT_THREAD_COUNT, eventHandler); assertTrue(result.hasError()); for (SkyKey key : errorKeys) { ErrorInfo errorInfo = result.getError(key); // FileFunction detects infinite symlink expansion explicitly. assertThat(errorInfo.getCycleInfo()).isEmpty(); FileSymlinkInfiniteExpansionException fsiee = (FileSymlinkInfiniteExpansionException) errorInfo.getException(); assertThat(fsiee.getMessage()).contains("Infinite symlink expansion"); assertThat(fsiee.getChain()).containsExactlyElementsIn(expectedChain).inOrder(); } // Check that the unique symlink expansion error was reported exactly once. assertThat(eventHandler.getEvents()).hasSize(1); assertThat(Iterables.getOnlyElement(eventHandler.getEvents()).getMessage()) .contains("infinite symlink expansion detected"); } @Test public void testInfiniteSymlinkExpansion_AbsoluteSymlinkToDescendant() throws Exception { runTestInfiniteSymlinkExpansion(/*ancestor=*/ false, /*absoluteSymlink=*/ true); } @Test public void testInfiniteSymlinkExpansion_RelativeSymlinkToDescendant() throws Exception { runTestInfiniteSymlinkExpansion(/*ancestor=*/ false, /*absoluteSymlink=*/ false); } @Test public void testInfiniteSymlinkExpansion_AbsoluteSymlinkToAncestor() throws Exception { runTestInfiniteSymlinkExpansion(/*ancestor=*/ true, /*absoluteSymlink=*/ true); } @Test public void testInfiniteSymlinkExpansion_RelativeSymlinkToAncestor() throws Exception { runTestInfiniteSymlinkExpansion(/*ancestor=*/ true, /*absoluteSymlink=*/ false); } @Test public void testChildOfNonexistentParent() throws Exception { Path ancestor = directory("this/is/an/ancestor"); Path parent = ancestor.getChild("parent"); Path child = parent.getChild("child"); assertFalse(valueForPath(parent).exists()); assertFalse(valueForPath(child).exists()); } private void checkRealPath(String pathString) throws Exception { Path realPath = pkgRoot.getRelative(pathString).resolveSymbolicLinks(); assertRealPath(pathString, realPath.relativeTo(pkgRoot).toString()); } private void assertRealPath(String pathString, String expectedRealPathString) throws Exception { SequentialBuildDriver driver = makeDriver(); SkyKey key = skyKey(pathString); EvaluationResult<SkyValue> result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); if (result.hasError()) { fail(String.format("Evaluation error for %s: %s", key, result.getError())); } FileValue fileValue = (FileValue) result.get(key); assertEquals( pkgRoot.getRelative(expectedRealPathString).toString(), fileValue.realRootedPath().asPath().toString()); } /** * Returns a callback that, when executed, deletes the given path. Not meant to be called directly * by tests. */ private Runnable makeDeletePathCallback(final Path toDelete) { return new Runnable() { @Override public void run() { try { toDelete.delete(); } catch (IOException e) { e.printStackTrace(); fail(e.getMessage()); } } }; } /** * Returns a callback that, when executed, writes the given bytes to the given file path. Not * meant to be called directly by tests. */ private Runnable makeWriteFileContentCallback(final Path toChange, final byte[] contents) { return new Runnable() { @Override public void run() { OutputStream outputStream; try { outputStream = toChange.getOutputStream(); outputStream.write(contents); outputStream.close(); } catch (IOException e) { e.printStackTrace(); fail(e.getMessage()); } } }; } /** * Returns a callback that, when executed, creates the given directory path. Not meant to be * called directly by tests. */ private Runnable makeCreateDirectoryCallback(final Path toCreate) { return new Runnable() { @Override public void run() { try { toCreate.createDirectory(); } catch (IOException e) { e.printStackTrace(); fail(e.getMessage()); } } }; } /** * Returns a callback that, when executed, makes {@code toLink} a symlink to {@code toTarget}. Not * meant to be called directly by tests. */ private Runnable makeSymlinkCallback(final Path toLink, final PathFragment toTarget) { return new Runnable() { @Override public void run() { try { FileSystemUtils.ensureSymbolicLink(toLink, toTarget); } catch (IOException e) { e.printStackTrace(); fail(e.getMessage()); } } }; } /** Returns the files that would be changed/created if {@code path} were to be changed/created. */ private ImmutableList<String> filesTouchedIfTouched(Path path) { List<String> filesToBeTouched = Lists.newArrayList(); do { filesToBeTouched.add(path.getPathString()); path = path.getParentDirectory(); } while (!path.exists()); return ImmutableList.copyOf(filesToBeTouched); } /** * Changes the contents of the FileValue for the given file in some way e.g. * * <ul> * <li> If it's a regular file, the contents will be changed. * <li> If it's a non-existent file, it will be created. * <ul> * and then returns the file(s) changed paired with a callback to undo the change. Not meant * to be called directly by tests. */ private Pair<ImmutableList<String>, Runnable> changeFile(String fileStringToChange) throws Exception { Path fileToChange = path(fileStringToChange); if (fileToChange.exists()) { final byte[] oldContents = FileSystemUtils.readContent(fileToChange); OutputStream outputStream = fileToChange.getOutputStream(/*append=*/ true); outputStream.write(new byte[] {(byte) 42}, 0, 1); outputStream.close(); return Pair.of( ImmutableList.of(fileStringToChange), makeWriteFileContentCallback(fileToChange, oldContents)); } else { ImmutableList<String> filesTouched = filesTouchedIfTouched(fileToChange); file(fileStringToChange, "new stuff"); return Pair.of(ImmutableList.copyOf(filesTouched), makeDeletePathCallback(fileToChange)); } } /** * Changes the contents of the FileValue for the given directory in some way e.g. * * <ul> * <li> If it exists, the directory will be deleted. * <li> If it doesn't exist, the directory will be created. * <ul> * and then returns the file(s) changed paired with a callback to undo the change. Not meant * to be called directly by tests. */ private Pair<ImmutableList<String>, Runnable> changeDirectory(String directoryStringToChange) throws Exception { final Path directoryToChange = path(directoryStringToChange); if (directoryToChange.exists()) { directoryToChange.delete(); return Pair.of( ImmutableList.of(directoryStringToChange), makeCreateDirectoryCallback(directoryToChange)); } else { directoryToChange.createDirectory(); return Pair.of( ImmutableList.of(directoryStringToChange), makeDeletePathCallback(directoryToChange)); } } /** * Performs filesystem operations to change the file or directory denoted by {@code * changedPathString} and returns the file(s) changed paired with a callback to undo the change. * Not meant to be called directly by tests. * * @param isSupposedToBeFile whether the path denoted by the given string is supposed to be a file * or a directory. This is needed is the path doesn't exist yet, and so the filesystem doesn't * know. */ private Pair<ImmutableList<String>, Runnable> change( String changedPathString, boolean isSupposedToBeFile) throws Exception { final Path changedPath = path(changedPathString); if (changedPath.isSymbolicLink()) { ImmutableList<String> filesTouched = filesTouchedIfTouched(changedPath); PathFragment oldTarget = changedPath.readSymbolicLink(); FileSystemUtils.ensureSymbolicLink(changedPath, oldTarget.getChild("__different_target__")); return Pair.of(filesTouched, makeSymlinkCallback(changedPath, oldTarget)); } else if (isSupposedToBeFile) { return changeFile(changedPathString); } else { return changeDirectory(changedPathString); } } /** * Asserts that if the contents of {@code changedPathString} changes, then the FileValue * corresponding to {@code pathString} will change. Not meant to be called directly by tests. */ private void assertValueChangesIfContentsOfFileChanges( String changedPathString, boolean changes, String pathString) throws Exception { getFilesSeenAndAssertValueChangesIfContentsOfFileChanges( changedPathString, changes, pathString); } /** * Asserts that if the contents of {@code changedPathString} changes, then the FileValue * corresponding to {@code pathString} will change. Returns the paths of all files seen. */ private Set<RootedPath> getFilesSeenAndAssertValueChangesIfContentsOfFileChanges( String changedPathString, boolean changes, String pathString) throws Exception { return assertChangesIfChanges(changedPathString, true, changes, pathString); } /** * Asserts that if the directory {@code changedPathString} changes, then the FileValue * corresponding to {@code pathString} will change. Returns the paths of all files seen. */ private Set<RootedPath> assertValueChangesIfContentsOfDirectoryChanges( String changedPathString, boolean changes, String pathString) throws Exception { return assertChangesIfChanges(changedPathString, false, changes, pathString); } /** * Asserts that if the contents of {@code changedPathString} changes, then the FileValue * corresponding to {@code pathString} will change. Returns the paths of all files seen. Not meant * to be called directly by tests. */ private Set<RootedPath> assertChangesIfChanges( String changedPathString, boolean isFile, boolean changes, String pathString) throws Exception { SequentialBuildDriver driver = makeDriver(); SkyKey key = skyKey(pathString); EvaluationResult<SkyValue> result; result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); if (result.hasError()) { fail(String.format("Evaluation error for %s: %s", key, result.getError())); } SkyValue oldValue = result.get(key); Pair<ImmutableList<String>, Runnable> changeResult = change(changedPathString, isFile); ImmutableList<String> changedPathStrings = changeResult.first; Runnable undoCallback = changeResult.second; differencer.invalidate( Iterables.transform( changedPathStrings, new Function<String, SkyKey>() { @Override public SkyKey apply(String input) { return fileStateSkyKey(input); } })); result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); if (result.hasError()) { fail(String.format("Evaluation error for %s: %s", key, result.getError())); } SkyValue newValue = result.get(key); assertTrue( String.format( "Changing the contents of %s %s should%s change the value for file %s.", isFile ? "file" : "directory", changedPathString, changes ? "" : " not", pathString), changes != newValue.equals(oldValue)); // Restore the original file. undoCallback.run(); return filesSeen(driver.getGraphForTesting()); } /** * Asserts that trying to construct a FileValue for {@code path} succeeds. Returns the paths of * all files seen. */ private Set<RootedPath> assertNoError(String pathString) throws Exception { SequentialBuildDriver driver = makeDriver(); SkyKey key = skyKey(pathString); EvaluationResult<FileValue> result; result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertFalse( "Did not expect error while evaluating " + pathString + ", got " + result.get(key), result.hasError()); return filesSeen(driver.getGraphForTesting()); } /** * Asserts that trying to construct a FileValue for {@code path} fails. Returns the paths of all * files seen. */ private Set<RootedPath> assertError(String pathString) throws Exception { SequentialBuildDriver driver = makeDriver(); SkyKey key = skyKey(pathString); EvaluationResult<FileValue> result; result = driver.evaluate( ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); assertTrue( "Expected error while evaluating " + pathString + ", got " + result.get(key), result.hasError()); assertTrue( !Iterables.isEmpty(result.getError().getCycleInfo()) || result.getError().getException() != null); return filesSeen(driver.getGraphForTesting()); } private Path file(String fileName) throws Exception { Path path = path(fileName); FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); FileSystemUtils.createEmptyFile(path); return path; } private Path file(String fileName, String contents) throws Exception { Path path = path(fileName); FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); FileSystemUtils.writeContentAsLatin1(path, contents); return path; } private Path directory(String directoryName) throws Exception { Path path = path(directoryName); FileSystemUtils.createDirectoryAndParents(path); return path; } private Path symlink(String link, String target) throws Exception { Path path = path(link); FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); path.createSymbolicLink(PathFragment.create(target)); return path; } private Path path(String rootRelativePath) { return pkgRoot.getRelative(PathFragment.create(rootRelativePath)); } private RootedPath rootedPath(String pathString) { Path path = path(pathString); for (Path root : pkgLocator.getPathEntries()) { if (path.startsWith(root)) { return RootedPath.toRootedPath(root, path); } } return RootedPath.toRootedPath(fs.getRootDirectory(), path); } private SkyKey skyKey(String pathString) { return FileValue.key(rootedPath(pathString)); } private SkyKey fileStateSkyKey(String pathString) { return FileStateValue.key(rootedPath(pathString)); } private class CustomInMemoryFs extends InMemoryFileSystem { private final Map<Path, FileStatus> stubbedStats = Maps.newHashMap(); private final Map<Path, IOException> stubbedStatErrors = Maps.newHashMap(); private final Map<Path, IOException> stubbedFastDigestErrors = Maps.newHashMap(); public CustomInMemoryFs(ManualClock manualClock) { super(manualClock); } public void stubFastDigestError(Path path, IOException error) { stubbedFastDigestErrors.put(path, error); } @Override protected byte[] getFastDigest(Path path, HashFunction hashFunction) throws IOException { if (stubbedFastDigestErrors.containsKey(path)) { throw stubbedFastDigestErrors.get(path); } return fastDigest ? getDigest(path) : null; } public void stubStat(Path path, @Nullable FileStatus stubbedResult) { stubbedStats.put(path, stubbedResult); } public void stubStatError(Path path, IOException error) { stubbedStatErrors.put(path, error); } @Override public FileStatus stat(Path path, boolean followSymlinks) throws IOException { if (stubbedStatErrors.containsKey(path)) { throw stubbedStatErrors.get(path); } if (stubbedStats.containsKey(path)) { return stubbedStats.get(path); } return super.stat(path, followSymlinks); } } }