blob: d94b8dd06c0dbe2e2f64ab835b6c88e8da422dac [file] [log] [blame]
// 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.actions.FilesetTraversalParams.PackageBoundaryMode.CROSS;
import static com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode.DONT_CROSS;
import static com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode.REPORT_ERROR;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode;
import com.google.devtools.build.lib.actions.Root;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.events.NullEventHandler;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile;
import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.TraversalRequest;
import com.google.devtools.build.lib.testutil.FoundationTestCaseForJunit4;
import com.google.devtools.build.lib.util.BlazeClock;
import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
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.skyframe.ErrorInfo;
import com.google.devtools.build.skyframe.EvaluationProgressReceiver;
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 org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
/** Tests for {@link RecursiveFilesystemTraversalFunction}. */
@RunWith(JUnit4.class)
public final class RecursiveFilesystemTraversalFunctionTest extends FoundationTestCaseForJunit4 {
private TimestampGranularityMonitor tsgm = new TimestampGranularityMonitor(BlazeClock.instance());
private RecordingEvaluationProgressReceiver progressReceiver;
private MemoizingEvaluator evaluator;
private SequentialBuildDriver driver;
private RecordingDifferencer differencer;
private AtomicReference<PathPackageLocator> pkgLocator;
@Before
public final void setUp() throws Exception {
pkgLocator = new AtomicReference<>(
new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)));
AtomicReference<ImmutableSet<PackageIdentifier>> deletedPackages =
new AtomicReference<>(ImmutableSet.<PackageIdentifier>of());
ExternalFilesHelper externalFilesHelper = new ExternalFilesHelper(pkgLocator);
Map<SkyFunctionName, SkyFunction> skyFunctions = new HashMap<>();
skyFunctions.put(SkyFunctions.FILE_STATE, new FileStateFunction(tsgm, externalFilesHelper));
skyFunctions.put(SkyFunctions.FILE, new FileFunction(pkgLocator, tsgm, externalFilesHelper));
skyFunctions.put(SkyFunctions.DIRECTORY_LISTING, new DirectoryListingFunction());
skyFunctions.put(
SkyFunctions.DIRECTORY_LISTING_STATE,
new DirectoryListingStateFunction(externalFilesHelper));
skyFunctions.put(
SkyFunctions.RECURSIVE_FILESYSTEM_TRAVERSAL, new RecursiveFilesystemTraversalFunction());
skyFunctions.put(SkyFunctions.PACKAGE_LOOKUP, new PackageLookupFunction(deletedPackages));
skyFunctions.put(SkyFunctions.BLACKLISTED_PACKAGE_PREFIXES,
new BlacklistedPackagePrefixesFunction());
progressReceiver = new RecordingEvaluationProgressReceiver();
differencer = new RecordingDifferencer();
evaluator = new InMemoryMemoizingEvaluator(skyFunctions, differencer, progressReceiver);
driver = new SequentialBuildDriver(evaluator);
PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID());
PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator.get());
PrecomputedValue.BLACKLISTED_PACKAGE_PREFIXES_FILE.set(
differencer, PathFragment.EMPTY_FRAGMENT);
}
private Artifact sourceArtifact(String path) {
return new Artifact(new PathFragment(path), Root.asSourceRoot(rootDirectory));
}
private Artifact sourceArtifactUnderPackagePath(String path, String packagePath) {
return new Artifact(
new PathFragment(path), Root.asSourceRoot(rootDirectory.getRelative(packagePath)));
}
private Artifact derivedArtifact(String path) {
PathFragment execPath = new PathFragment("out").getRelative(path);
Path fullPath = rootDirectory.getRelative(execPath);
Artifact output =
new Artifact(
fullPath,
Root.asDerivedRoot(rootDirectory, rootDirectory.getRelative("out")),
execPath);
return output;
}
private static RootedPath rootedPath(Artifact artifact) {
return RootedPath.toRootedPath(artifact.getRoot().getPath(), artifact.getRootRelativePath());
}
private RootedPath rootedPath(String path, String packagePath) {
return RootedPath.toRootedPath(rootDirectory.getRelative(packagePath), new PathFragment(path));
}
private static RootedPath childOf(Artifact artifact, String relative) {
return RootedPath.toRootedPath(
artifact.getRoot().getPath(), artifact.getRootRelativePath().getRelative(relative));
}
private static RootedPath childOf(RootedPath path, String relative) {
return RootedPath.toRootedPath(path.getRoot(), path.getRelativePath().getRelative(relative));
}
private static RootedPath parentOf(RootedPath path) {
PathFragment parent = Preconditions.checkNotNull(path.getRelativePath().getParentDirectory());
return RootedPath.toRootedPath(path.getRoot(), parent);
}
private static RootedPath siblingOf(RootedPath path, String relative) {
PathFragment parent = Preconditions.checkNotNull(path.getRelativePath().getParentDirectory());
return RootedPath.toRootedPath(path.getRoot(), parent.getRelative(relative));
}
private static RootedPath siblingOf(Artifact artifact, String relative) {
PathFragment parent =
Preconditions.checkNotNull(artifact.getRootRelativePath().getParentDirectory());
return RootedPath.toRootedPath(artifact.getRoot().getPath(), parent.getRelative(relative));
}
private void createFile(Path path, String... contents) throws Exception {
if (!path.getParentDirectory().exists()) {
scratch.dir(path.getParentDirectory().getPathString());
}
scratch.file(path.getPathString(), contents);
}
private void createFile(Artifact artifact, String... contents) throws Exception {
createFile(artifact.getPath(), contents);
}
private RootedPath createFile(RootedPath path, String... contents) throws Exception {
scratch.dir(parentOf(path).asPath().getPathString());
createFile(path.asPath(), contents);
return path;
}
private static TraversalRequest fileLikeRoot(Artifact file, PackageBoundaryMode pkgBoundaryMode) {
return new TraversalRequest(
rootedPath(file), !file.isSourceArtifact(), pkgBoundaryMode, false, null, null);
}
private static TraversalRequest pkgRoot(
RootedPath pkgDirectory, PackageBoundaryMode pkgBoundaryMode) {
return new TraversalRequest(pkgDirectory, false, pkgBoundaryMode, true, null, null);
}
private <T extends SkyValue> EvaluationResult<T> eval(SkyKey key) throws Exception {
return driver.evaluate(
ImmutableList.of(key),
false,
SkyframeExecutor.DEFAULT_THREAD_COUNT,
NullEventHandler.INSTANCE);
}
private RecursiveFilesystemTraversalValue evalTraversalRequest(TraversalRequest params)
throws Exception {
SkyKey key = rftvSkyKey(params);
EvaluationResult<RecursiveFilesystemTraversalValue> result = eval(key);
assertThat(result.hasError()).isFalse();
return result.get(key);
}
private static SkyKey rftvSkyKey(TraversalRequest params) {
return RecursiveFilesystemTraversalValue.key(params);
}
/**
* Asserts that the requested SkyValue can be built and results in the expected set of files.
*
* <p>The metadata of files is ignored in comparing the actual results with the expected ones.
* The returned object however contains the actual metadata.
*/
@SafeVarargs
private final RecursiveFilesystemTraversalValue traverseAndAssertFiles(
TraversalRequest params, ResolvedFile... expectedFilesIgnoringMetadata) throws Exception {
Set<ResolvedFile> expectedMap = new HashSet<>();
for (ResolvedFile exp : expectedFilesIgnoringMetadata) {
// Strip metadata so only the type and path of the objects are compared.
expectedMap.add(exp.stripMetadataForTesting());
}
RecursiveFilesystemTraversalValue result = evalTraversalRequest(params);
Set<ResolvedFile> actualMap = new HashSet<>();
for (ResolvedFile act : result.getTransitiveFiles()) {
// Strip metadata so only the type and path of the objects are compared.
actualMap.add(act.stripMetadataForTesting());
}
// First just assert equality of the keys, so in case of a mismatch the error message is easier
// to read.
assertThat(expectedMap).isEqualTo(actualMap);
// The returned object still has the unstripped metadata.
return result;
}
private void appendToFile(RootedPath rootedPath, String content) throws Exception {
Path path = rootedPath.asPath();
if (path.exists()) {
try (OutputStream os = path.getOutputStream(/*append=*/ true)) {
os.write(content.getBytes(StandardCharsets.UTF_8));
}
differencer.invalidate(ImmutableList.of(FileStateValue.key(rootedPath)));
} else {
createFile(path, content);
}
}
private void appendToFile(Artifact file, String content) throws Exception {
appendToFile(rootedPath(file), content);
}
private void invalidateDirectory(RootedPath path) {
differencer.invalidate(ImmutableList.of(DirectoryListingStateValue.key(path)));
}
private void invalidateDirectory(Artifact directoryArtifact) {
invalidateDirectory(rootedPath(directoryArtifact));
}
private static final class RecordingEvaluationProgressReceiver
implements EvaluationProgressReceiver {
Set<SkyKey> invalidations;
Set<SkyValue> evaluations;
RecordingEvaluationProgressReceiver() {
clear();
}
void clear() {
invalidations = Sets.newConcurrentHashSet();
evaluations = Sets.newConcurrentHashSet();
}
@Override
public void invalidated(SkyKey skyKey, InvalidationState state) {
invalidations.add(skyKey);
}
@Override
public void enqueueing(SkyKey skyKey) {}
@Override
public void computed(SkyKey skyKey, long elapsedTimeNanos) {}
@Override
public void evaluated(
SkyKey skyKey, Supplier<SkyValue> skyValueSupplier, EvaluationState state) {
SkyValue value = skyValueSupplier.get();
if (value != null) {
evaluations.add(value);
}
}
}
private ResolvedFile resolvedFile(RootedPath path) throws Exception {
return ResolvedFile.regularFile(path, FileStateValue.create(path, tsgm));
}
private ResolvedFile resolvedDanglingSymlink(RootedPath linkNamePath, PathFragment linkTargetPath)
throws Exception {
return ResolvedFile.danglingSymlink(
linkNamePath, linkTargetPath, FileStateValue.create(linkNamePath, tsgm));
}
private ResolvedFile resolvedSymlinkToFile(
RootedPath targetPath, RootedPath linkNamePath, PathFragment linkTargetPath)
throws Exception {
return ResolvedFile.symlinkToFile(
targetPath, linkNamePath, linkTargetPath, FileStateValue.create(linkNamePath, tsgm));
}
private ResolvedFile resolvedSymlinkToDir(
RootedPath targetPath, RootedPath linkNamePath, PathFragment linkTargetPath)
throws Exception {
return ResolvedFile.symlinkToDirectory(
targetPath, linkNamePath, linkTargetPath, FileStateValue.create(linkNamePath, tsgm));
}
private void assertTraversalOfFile(Artifact rootArtifact) throws Exception {
TraversalRequest traversalRoot = fileLikeRoot(rootArtifact, DONT_CROSS);
RootedPath rootedPath = createFile(rootedPath(rootArtifact), "foo");
// Assert that the SkyValue is built and looks right.
ResolvedFile expected = resolvedFile(rootedPath);
RecursiveFilesystemTraversalValue v1 = traverseAndAssertFiles(traversalRoot, expected);
assertThat(progressReceiver.invalidations).isEmpty();
assertThat(progressReceiver.evaluations).contains(v1);
progressReceiver.clear();
// Edit the file and verify that the value is rebuilt.
appendToFile(rootArtifact, "bar");
RecursiveFilesystemTraversalValue v2 = traverseAndAssertFiles(traversalRoot, expected);
assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot));
assertThat(progressReceiver.evaluations).contains(v2);
assertThat(v2).isNotEqualTo(v1);
progressReceiver.clear();
}
@Test
public void testTraversalOfSourceFile() throws Exception {
assertTraversalOfFile(sourceArtifact("foo/bar.txt"));
}
@Test
public void testTraversalOfGeneratedFile() throws Exception {
assertTraversalOfFile(derivedArtifact("foo/bar.txt"));
}
@Test
public void testTraversalOfSymlinkToFile() throws Exception {
Artifact linkNameArtifact = sourceArtifact("foo/baz/qux.sym");
Artifact linkTargetArtifact = sourceArtifact("foo/bar/baz.txt");
PathFragment linkValue = new PathFragment("../bar/baz.txt");
TraversalRequest traversalRoot = fileLikeRoot(linkNameArtifact, DONT_CROSS);
createFile(linkTargetArtifact);
scratch.dir(linkNameArtifact.getExecPath().getParentDirectory().getPathString());
rootDirectory.getRelative(linkNameArtifact.getExecPath()).createSymbolicLink(linkValue);
// Assert that the SkyValue is built and looks right.
RootedPath symlinkNamePath = rootedPath(linkNameArtifact);
RootedPath symlinkTargetPath = rootedPath(linkTargetArtifact);
ResolvedFile expected = resolvedSymlinkToFile(symlinkTargetPath, symlinkNamePath, linkValue);
RecursiveFilesystemTraversalValue v1 = traverseAndAssertFiles(traversalRoot, expected);
assertThat(progressReceiver.invalidations).isEmpty();
assertThat(progressReceiver.evaluations).contains(v1);
progressReceiver.clear();
// Edit the target of the symlink and verify that the value is rebuilt.
appendToFile(linkTargetArtifact, "bar");
RecursiveFilesystemTraversalValue v2 = traverseAndAssertFiles(traversalRoot, expected);
assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot));
assertThat(progressReceiver.evaluations).contains(v2);
assertThat(v2).isNotEqualTo(v1);
}
@Test
public void testTraversalOfTransitiveSymlinkToFile() throws Exception {
Artifact directLinkArtifact = sourceArtifact("direct/file.sym");
Artifact transitiveLinkArtifact = sourceArtifact("transitive/sym.sym");
RootedPath fileA = createFile(rootedPath(sourceArtifact("a/file.a")));
RootedPath directLink = rootedPath(directLinkArtifact);
RootedPath transitiveLink = rootedPath(transitiveLinkArtifact);
PathFragment directLinkPath = new PathFragment("../a/file.a");
PathFragment transitiveLinkPath = new PathFragment("../direct/file.sym");
parentOf(directLink).asPath().createDirectory();
parentOf(transitiveLink).asPath().createDirectory();
directLink.asPath().createSymbolicLink(directLinkPath);
transitiveLink.asPath().createSymbolicLink(transitiveLinkPath);
traverseAndAssertFiles(
fileLikeRoot(directLinkArtifact, DONT_CROSS),
resolvedSymlinkToFile(fileA, directLink, directLinkPath));
traverseAndAssertFiles(
fileLikeRoot(transitiveLinkArtifact, DONT_CROSS),
resolvedSymlinkToFile(fileA, transitiveLink, transitiveLinkPath));
}
private void assertTraversalOfDirectory(Artifact directoryArtifact) throws Exception {
// Create files under the directory.
// Use the root + root-relative path of the rootArtifact to create these files, rather than
// using the rootDirectory + execpath of the rootArtifact. The resulting paths are the same
// but the RootedPaths are different:
// in the 1st case, it is: RootedPath(/root/execroot, relative), in the second it is
// in the 2nd case, it is: RootedPath(/root, execroot/relative).
// Creating the files will also create the parent directories.
RootedPath file1 = createFile(childOf(directoryArtifact, "bar.txt"));
RootedPath file2 = createFile(childOf(directoryArtifact, "baz/qux.txt"));
TraversalRequest traversalRoot = fileLikeRoot(directoryArtifact, DONT_CROSS);
// Assert that the SkyValue is built and looks right.
ResolvedFile expected1 = resolvedFile(file1);
ResolvedFile expected2 = resolvedFile(file2);
RecursiveFilesystemTraversalValue v1 =
traverseAndAssertFiles(traversalRoot, expected1, expected2);
assertThat(progressReceiver.invalidations).isEmpty();
assertThat(progressReceiver.evaluations).contains(v1);
progressReceiver.clear();
// Add a new file to the directory and see that the value is rebuilt.
RootedPath file3 = createFile(childOf(directoryArtifact, "foo.txt"));
invalidateDirectory(directoryArtifact);
ResolvedFile expected3 = resolvedFile(file3);
RecursiveFilesystemTraversalValue v2 =
traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3);
assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot));
assertThat(progressReceiver.evaluations).contains(v2);
assertThat(v2).isNotEqualTo(v1);
progressReceiver.clear();
// Edit a file in the directory and see that the value is rebuilt.
appendToFile(file1, "bar");
RecursiveFilesystemTraversalValue v3 =
traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3);
assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot));
assertThat(progressReceiver.evaluations).contains(v3);
assertThat(v3).isNotEqualTo(v2);
progressReceiver.clear();
// Add a new file *outside* of the directory and see that the value is *not* rebuilt.
Artifact someFile = sourceArtifact("somewhere/else/a.file");
createFile(someFile, "new file");
appendToFile(someFile, "not all changes are treated equal");
RecursiveFilesystemTraversalValue v4 =
traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3);
assertThat(v4).isEqualTo(v3);
assertThat(progressReceiver.invalidations).doesNotContain(rftvSkyKey(traversalRoot));
}
@Test
public void testTraversalOfSourceDirectory() throws Exception {
assertTraversalOfDirectory(sourceArtifact("dir"));
}
@Test
public void testTraversalOfGeneratedDirectory() throws Exception {
assertTraversalOfDirectory(derivedArtifact("dir"));
}
@Test
public void testTraversalOfTransitiveSymlinkToDirectory() throws Exception {
Artifact directLinkArtifact = sourceArtifact("direct/dir.sym");
Artifact transitiveLinkArtifact = sourceArtifact("transitive/sym.sym");
RootedPath fileA = createFile(rootedPath(sourceArtifact("a/file.a")));
RootedPath directLink = rootedPath(directLinkArtifact);
RootedPath transitiveLink = rootedPath(transitiveLinkArtifact);
PathFragment directLinkPath = new PathFragment("../a");
PathFragment transitiveLinkPath = new PathFragment("../direct/dir.sym");
parentOf(directLink).asPath().createDirectory();
parentOf(transitiveLink).asPath().createDirectory();
directLink.asPath().createSymbolicLink(directLinkPath);
transitiveLink.asPath().createSymbolicLink(transitiveLinkPath);
// Expect the file as if was a child of the direct symlink, not of the actual directory.
traverseAndAssertFiles(
fileLikeRoot(directLinkArtifact, DONT_CROSS),
resolvedSymlinkToDir(parentOf(fileA), directLink, directLinkPath),
resolvedFile(childOf(directLinkArtifact, "file.a")));
// Expect the file as if was a child of the transitive symlink, not of the actual directory.
traverseAndAssertFiles(
fileLikeRoot(transitiveLinkArtifact, DONT_CROSS),
resolvedSymlinkToDir(parentOf(fileA), transitiveLink, transitiveLinkPath),
resolvedFile(childOf(transitiveLinkArtifact, "file.a")));
}
@Test
public void testTraversePackage() throws Exception {
Artifact buildFile = sourceArtifact("pkg/BUILD");
RootedPath buildFilePath = createFile(rootedPath(buildFile));
RootedPath file1 = createFile(siblingOf(buildFile, "subdir/file.a"));
traverseAndAssertFiles(
pkgRoot(parentOf(buildFilePath), DONT_CROSS),
resolvedFile(buildFilePath),
resolvedFile(file1));
}
@Test
public void testTraversalOfSymlinkToDirectory() throws Exception {
Artifact linkNameArtifact = sourceArtifact("link/foo.sym");
Artifact linkTargetArtifact = sourceArtifact("dir");
RootedPath linkName = rootedPath(linkNameArtifact);
PathFragment linkValue = new PathFragment("../dir");
RootedPath file1 = createFile(childOf(linkTargetArtifact, "file.1"));
createFile(childOf(linkTargetArtifact, "sub/file.2"));
scratch.dir(parentOf(linkName).asPath().getPathString());
linkName.asPath().createSymbolicLink(linkValue);
// Assert that the SkyValue is built and looks right.
TraversalRequest traversalRoot = fileLikeRoot(linkNameArtifact, DONT_CROSS);
ResolvedFile expected1 =
resolvedSymlinkToDir(rootedPath(linkTargetArtifact), linkName, linkValue);
ResolvedFile expected2 = resolvedFile(childOf(linkNameArtifact, "file.1"));
ResolvedFile expected3 = resolvedFile(childOf(linkNameArtifact, "sub/file.2"));
// We expect to see all the files from the symlink'd directory, under the symlink's path, not
// under the symlink target's path.
RecursiveFilesystemTraversalValue v1 =
traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3);
assertThat(progressReceiver.invalidations).isEmpty();
assertThat(progressReceiver.evaluations).contains(v1);
progressReceiver.clear();
// Add a new file to the directory and see that the value is rebuilt.
createFile(childOf(linkTargetArtifact, "file.3"));
invalidateDirectory(linkTargetArtifact);
ResolvedFile expected4 = resolvedFile(childOf(linkNameArtifact, "file.3"));
RecursiveFilesystemTraversalValue v2 =
traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3, expected4);
assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot));
assertThat(progressReceiver.evaluations).contains(v2);
assertThat(v2).isNotEqualTo(v1);
progressReceiver.clear();
// Edit a file in the directory and see that the value is rebuilt.
appendToFile(file1, "bar");
RecursiveFilesystemTraversalValue v3 =
traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3, expected4);
assertThat(progressReceiver.invalidations).contains(rftvSkyKey(traversalRoot));
assertThat(progressReceiver.evaluations).contains(v3);
assertThat(v3).isNotEqualTo(v2);
progressReceiver.clear();
// Add a new file *outside* of the directory and see that the value is *not* rebuilt.
Artifact someFile = sourceArtifact("somewhere/else/a.file");
createFile(someFile, "new file");
appendToFile(someFile, "not all changes are treated equal");
RecursiveFilesystemTraversalValue v4 =
traverseAndAssertFiles(traversalRoot, expected1, expected2, expected3, expected4);
assertThat(v4).isEqualTo(v3);
assertThat(progressReceiver.invalidations).doesNotContain(rftvSkyKey(traversalRoot));
}
@Test
public void testTraversalOfDanglingSymlink() throws Exception {
Artifact linkArtifact = sourceArtifact("a/dangling.sym");
RootedPath link = rootedPath(linkArtifact);
PathFragment linkTarget = new PathFragment("non_existent");
parentOf(link).asPath().createDirectory();
link.asPath().createSymbolicLink(linkTarget);
traverseAndAssertFiles(
fileLikeRoot(linkArtifact, DONT_CROSS), resolvedDanglingSymlink(link, linkTarget));
}
@Test
public void testTraversalOfDanglingSymlinkInADirectory() throws Exception {
Artifact dirArtifact = sourceArtifact("a");
RootedPath file = createFile(childOf(dirArtifact, "file.txt"));
RootedPath link = rootedPath(sourceArtifact("a/dangling.sym"));
PathFragment linkTarget = new PathFragment("non_existent");
parentOf(link).asPath().createDirectory();
link.asPath().createSymbolicLink(linkTarget);
traverseAndAssertFiles(
fileLikeRoot(dirArtifact, DONT_CROSS),
resolvedFile(file),
resolvedDanglingSymlink(link, linkTarget));
}
private void assertTraverseSubpackages(PackageBoundaryMode traverseSubpackages) throws Exception {
Artifact pkgDirArtifact = sourceArtifact("pkg1/foo");
Artifact subpkgDirArtifact = sourceArtifact("pkg1/foo/subdir/subpkg");
RootedPath pkgBuildFile = childOf(pkgDirArtifact, "BUILD");
RootedPath subpkgBuildFile = childOf(subpkgDirArtifact, "BUILD");
scratch.dir(rootedPath(pkgDirArtifact).asPath().getPathString());
scratch.dir(rootedPath(subpkgDirArtifact).asPath().getPathString());
createFile(pkgBuildFile);
createFile(subpkgBuildFile);
TraversalRequest traversalRoot = pkgRoot(parentOf(pkgBuildFile), traverseSubpackages);
ResolvedFile expected1 = resolvedFile(pkgBuildFile);
ResolvedFile expected2 = resolvedFile(subpkgBuildFile);
switch (traverseSubpackages) {
case CROSS:
traverseAndAssertFiles(traversalRoot, expected1, expected2);
break;
case DONT_CROSS:
traverseAndAssertFiles(traversalRoot, expected1);
break;
case REPORT_ERROR:
SkyKey key = rftvSkyKey(traversalRoot);
EvaluationResult<SkyValue> result = eval(key);
assertThat(result.hasError()).isTrue();
assertThat(result.getError().getException().getMessage())
.contains("crosses package boundary into package rooted at");
break;
default:
throw new IllegalStateException(traverseSubpackages.toString());
}
}
@Test
public void testTraverseSubpackages() throws Exception {
assertTraverseSubpackages(CROSS);
}
@Test
public void testDoNotTraverseSubpackages() throws Exception {
assertTraverseSubpackages(DONT_CROSS);
}
@Test
public void testReportErrorWhenTraversingSubpackages() throws Exception {
assertTraverseSubpackages(REPORT_ERROR);
}
@Test
public void testSwitchPackageRootsWhenUsingMultiplePackagePaths() throws Exception {
// Layout:
// pp1://a/BUILD
// pp1://a/file.a
// pp1://a/b.sym -> b/ (only created later)
// pp1://a/b/
// pp1://a/b/file.fake
// pp1://a/subdir/file.b
//
// pp2://a/BUILD
// pp2://a/b/
// pp2://a/b/BUILD
// pp2://a/b/file.a
// pp2://a/subdir.fake/
// pp2://a/subdir.fake/file.fake
//
// Notice that pp1://a/b will be overlaid by pp2://a/b as the latter has a BUILD file and that
// takes precedence. On the other hand the package definition pp2://a/BUILD will be ignored
// since package //a is already defined under pp1.
//
// Notice also that pp1://a/b.sym is a relative symlink pointing to b/. This should be resolved
// to the definition of //a/b/ under pp1, not under pp2.
// Set the package paths.
pkgLocator.set(new PathPackageLocator(outputBase,
ImmutableList.of(rootDirectory.getRelative("pp1"), rootDirectory.getRelative("pp2"))));
PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator.get());
Artifact aBuildArtifact = sourceArtifactUnderPackagePath("a/BUILD", "pp1");
Artifact bBuildArtifact = sourceArtifactUnderPackagePath("a/b/BUILD", "pp2");
RootedPath pp1aBuild = createFile(rootedPath(aBuildArtifact));
RootedPath pp1aFileA = createFile(siblingOf(pp1aBuild, "file.a"));
RootedPath pp1bFileFake = createFile(siblingOf(pp1aBuild, "b/file.fake"));
RootedPath pp1aSubdirFileB = createFile(siblingOf(pp1aBuild, "subdir/file.b"));
RootedPath pp2aBuild = createFile(rootedPath("a/BUILD", "pp2"));
RootedPath pp2bBuild = createFile(rootedPath(bBuildArtifact));
RootedPath pp2bFileA = createFile(siblingOf(pp2bBuild, "file.a"));
createFile(siblingOf(pp2aBuild, "subdir.fake/file.fake"));
// Traverse //a including subpackages. The result should contain the pp1-definition of //a and
// the pp2-definition of //a/b.
traverseAndAssertFiles(
pkgRoot(parentOf(rootedPath(aBuildArtifact)), CROSS),
resolvedFile(pp1aBuild),
resolvedFile(pp1aFileA),
resolvedFile(pp1aSubdirFileB),
resolvedFile(pp2bBuild),
resolvedFile(pp2bFileA));
// Traverse //a excluding subpackages. The result should only contain files from //a and not
// from //a/b.
traverseAndAssertFiles(
pkgRoot(parentOf(rootedPath(aBuildArtifact)), DONT_CROSS),
resolvedFile(pp1aBuild),
resolvedFile(pp1aFileA),
resolvedFile(pp1aSubdirFileB));
// Create a relative symlink pp1://a/b.sym -> b/. It will be resolved to the subdirectory
// pp1://a/b, even though a package definition pp2://a/b exists.
RootedPath pp1aBsym = siblingOf(pp1aFileA, "b.sym");
pp1aBsym.asPath().createSymbolicLink(new PathFragment("b"));
invalidateDirectory(parentOf(pp1aBsym));
// Traverse //a excluding subpackages. The relative symlink //a/b.sym points to the subdirectory
// a/b, i.e. the pp1-definition, even though there is a pp2-defined package //a/b and we expect
// to see b.sym/b.fake (not b/b.fake).
traverseAndAssertFiles(
pkgRoot(parentOf(rootedPath(aBuildArtifact)), DONT_CROSS),
resolvedFile(pp1aBuild),
resolvedFile(pp1aFileA),
resolvedFile(childOf(pp1aBsym, "file.fake")),
resolvedSymlinkToDir(parentOf(pp1bFileFake), pp1aBsym, new PathFragment("b")),
resolvedFile(pp1aSubdirFileB));
}
@Test
public void testFileDigestChangeCausesRebuild() throws Exception {
Artifact artifact = sourceArtifact("foo/bar.txt");
RootedPath path = rootedPath(artifact);
createFile(path, "hello");
// Assert that the SkyValue is built and looks right.
TraversalRequest params = fileLikeRoot(artifact, DONT_CROSS);
ResolvedFile expected = resolvedFile(path);
RecursiveFilesystemTraversalValue v1 = traverseAndAssertFiles(params, expected);
assertThat(progressReceiver.evaluations).contains(v1);
progressReceiver.clear();
// Change the digest of the file. See that the value is rebuilt.
appendToFile(path, "world");
RecursiveFilesystemTraversalValue v2 = traverseAndAssertFiles(params, expected);
assertThat(progressReceiver.invalidations).contains(rftvSkyKey(params));
assertThat(v2).isNotEqualTo(v1);
}
@Test
public void testFileMtimeChangeDoesNotCauseRebuildIfDigestIsUnchanged() throws Exception {
Artifact artifact = sourceArtifact("foo/bar.txt");
RootedPath path = rootedPath(artifact);
createFile(path, "hello");
// Assert that the SkyValue is built and looks right.
TraversalRequest params = fileLikeRoot(artifact, DONT_CROSS);
ResolvedFile expected = resolvedFile(path);
RecursiveFilesystemTraversalValue v1 = traverseAndAssertFiles(params, expected);
assertThat(progressReceiver.evaluations).contains(v1);
progressReceiver.clear();
// Change the mtime of the file but not the digest. See that the value is *not* rebuilt.
long mtime = path.asPath().getLastModifiedTime();
mtime += 1000000L; // more than the timestamp granularity of any filesystem
path.asPath().setLastModifiedTime(mtime);
RecursiveFilesystemTraversalValue v2 = traverseAndAssertFiles(params, expected);
assertThat(v2).isEqualTo(v1);
}
@Test
public void testRegexp() throws Exception {
Artifact wantedArtifact = sourceArtifact("foo/bar/baz.txt");
Artifact unwantedArtifact = sourceArtifact("foo/boo/baztxt.bak");
RootedPath wantedPath = rootedPath(wantedArtifact);
createFile(wantedPath, "hello");
createFile(unwantedArtifact, "nope");
Artifact pkgDirArtifact = sourceArtifact("foo");
RootedPath dir = rootedPath(pkgDirArtifact);
scratch.dir(dir.asPath().getPathString());
TraversalRequest traversalRoot =
new TraversalRequest(
dir, false, PackageBoundaryMode.REPORT_ERROR, true, null, Pattern.compile(".*\\.txt"));
ResolvedFile expected = resolvedFile(wantedPath);
traverseAndAssertFiles(traversalRoot, expected);
}
@Test
public void testGeneratedDirectoryConflictsWithPackage() throws Exception {
Artifact genDir = derivedArtifact("a/b");
createFile(rootedPath(sourceArtifact("a/b/c/file.real")));
createFile(rootedPath(derivedArtifact("a/b/c/file.fake")));
createFile(sourceArtifact("a/b/c/BUILD"));
SkyKey key = rftvSkyKey(fileLikeRoot(genDir, CROSS));
EvaluationResult<SkyValue> result = eval(key);
assertThat(result.hasError()).isTrue();
ErrorInfo error = result.getError(key);
assertThat(error.isTransient()).isFalse();
assertThat(error.getException().getMessage())
.contains("Generated directory a/b/c conflicts with package under the same path.");
}
}