blob: 537543361aba879017ac9a56b7fead6271fc09db [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.common.truth.Truth.assertWithMessage;
import static com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode.CROSS;
import static com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode.DONT_CROSS;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.actions.FileValue;
import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
import com.google.devtools.build.lib.actions.FilesetTraversalParams;
import com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode;
import com.google.devtools.build.lib.actions.FilesetTraversalParamsFactory;
import com.google.devtools.build.lib.actions.FilesetTraversalParamsFactory.SymlinkBehavior;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ServerDirectories;
import com.google.devtools.build.lib.clock.BlazeClock;
import com.google.devtools.build.lib.cmdline.Label;
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.skyframe.PackageLookupFunction.CrossRepositoryLabelViolationStrategy;
import com.google.devtools.build.lib.testutil.FoundationTestCase;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
import com.google.devtools.build.lib.vfs.FileStateKey;
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.SyscallCache;
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.MemoizingEvaluator;
import com.google.devtools.build.skyframe.RecordingDifferencer;
import com.google.devtools.build.skyframe.SequencedRecordingDifferencer;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import net.starlark.java.eval.StarlarkSemantics;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link FilesetEntryFunction}. */
@RunWith(JUnit4.class)
public final class FilesetEntryFunctionTest extends FoundationTestCase {
private MemoizingEvaluator evaluator;
private RecordingDifferencer differencer;
@Before
public void setUp() throws Exception {
AtomicReference<PathPackageLocator> pkgLocator =
new AtomicReference<>(
new PathPackageLocator(
outputBase,
ImmutableList.of(Root.fromPath(rootDirectory)),
BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY));
BlazeDirectories directories =
new BlazeDirectories(
new ServerDirectories(outputBase, outputBase, outputBase),
rootDirectory,
/* defaultSystemJavabase= */ null,
TestConstants.PRODUCT_NAME);
ExternalFilesHelper externalFilesHelper =
ExternalFilesHelper.createForTesting(
pkgLocator,
ExternalFileAction.DEPEND_ON_EXTERNAL_PKG_FOR_EXTERNAL_REPO_PATHS,
directories);
Map<SkyFunctionName, SkyFunction> skyFunctions = new HashMap<>();
skyFunctions.put(
FileStateKey.FILE_STATE,
new FileStateFunction(
Suppliers.ofInstance(new TimestampGranularityMonitor(BlazeClock.instance())),
SyscallCache.NO_CACHE,
externalFilesHelper));
skyFunctions.put(FileValue.FILE, new FileFunction(pkgLocator, directories));
skyFunctions.put(SkyFunctions.DIRECTORY_LISTING, new DirectoryListingFunction());
skyFunctions.put(
SkyFunctions.DIRECTORY_LISTING_STATE,
new DirectoryListingStateFunction(externalFilesHelper, SyscallCache.NO_CACHE));
skyFunctions.put(
SkyFunctions.RECURSIVE_FILESYSTEM_TRAVERSAL,
new RecursiveFilesystemTraversalFunction(SyscallCache.NO_CACHE));
skyFunctions.put(
SkyFunctions.PACKAGE_LOOKUP,
new PackageLookupFunction(
new AtomicReference<>(ImmutableSet.of()),
CrossRepositoryLabelViolationStrategy.ERROR,
BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY,
BazelSkyframeExecutorConstants.EXTERNAL_PACKAGE_HELPER));
skyFunctions.put(
SkyFunctions.IGNORED_PACKAGE_PREFIXES,
new IgnoredPackagePrefixesFunction(
/*ignoredPackagePrefixesFile=*/ PathFragment.EMPTY_FRAGMENT));
skyFunctions.put(
SkyFunctions.FILESET_ENTRY, new FilesetEntryFunction((unused) -> rootDirectory));
skyFunctions.put(SkyFunctions.WORKSPACE_NAME, new TestWorkspaceNameFunction());
skyFunctions.put(
SkyFunctions.LOCAL_REPOSITORY_LOOKUP,
new LocalRepositoryLookupFunction(BazelSkyframeExecutorConstants.EXTERNAL_PACKAGE_HELPER));
differencer = new SequencedRecordingDifferencer();
evaluator = new InMemoryMemoizingEvaluator(skyFunctions, differencer);
PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID());
PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator.get());
PrecomputedValue.STARLARK_SEMANTICS.set(differencer, StarlarkSemantics.DEFAULT);
}
private Artifact getSourceArtifact(String path) {
return ActionsTestUtil.createArtifact(
ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory)), path);
}
private Artifact createSourceArtifact(String path) throws Exception {
Artifact result = getSourceArtifact(path);
createFile(result, "foo");
return result;
}
private static RootedPath childOf(Artifact artifact, String relative) {
return RootedPath.toRootedPath(
artifact.getRoot().getRoot(), artifact.getRootRelativePath().getRelative(relative));
}
private static RootedPath siblingOf(Artifact artifact, String relative) {
PathFragment parent =
Preconditions.checkNotNull(artifact.getRootRelativePath().getParentDirectory());
return RootedPath.toRootedPath(artifact.getRoot().getRoot(), 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 {
createFile(path.asPath(), contents);
return path;
}
private <T extends SkyValue> EvaluationResult<T> eval(SkyKey key) throws Exception {
EvaluationContext evaluationContext =
EvaluationContext.newBuilder()
.setKeepGoing(false)
.setParallelism(SkyframeExecutor.DEFAULT_THREAD_COUNT)
.setEventHandler(NullEventHandler.INSTANCE)
.build();
return evaluator.evaluate(ImmutableList.of(key), evaluationContext);
}
private FilesetEntryValue evalFilesetTraversal(FilesetTraversalParams params) throws Exception {
SkyKey key = FilesetEntryKey.key(params);
EvaluationResult<FilesetEntryValue> result = eval(key);
assertThat(result.hasError()).isFalse();
return result.get(key);
}
private FilesetOutputSymlink symlink(String from, Artifact to) {
return symlink(PathFragment.create(from), to.getPath().asFragment());
}
private FilesetOutputSymlink symlink(String from, String to) {
return symlink(PathFragment.create(from), PathFragment.create(to));
}
private FilesetOutputSymlink symlink(String from, RootedPath to) {
return symlink(PathFragment.create(from), to.asPath().asFragment());
}
private FilesetOutputSymlink symlink(PathFragment from, PathFragment to) {
return FilesetOutputSymlink.createForTesting(from, to, rootDirectory.asFragment());
}
private void assertSymlinksCreatedInOrder(
FilesetTraversalParams request, FilesetOutputSymlink... expectedSymlinks) throws Exception {
Collection<FilesetOutputSymlink> actual =
Collections2.transform(
evalFilesetTraversal(request).getSymlinks(),
// Strip the metadata from the actual results.
(input) ->
FilesetOutputSymlink.createAlreadyRelativizedForTesting(
input.getName(), input.getTargetPath(), input.isRelativeToExecRoot()));
assertThat(actual).containsExactlyElementsIn(expectedSymlinks).inOrder();
}
private static Label label(String label) throws Exception {
return Label.parseCanonical(label);
}
@Test
public void testFileTraversalForFile() throws Exception {
Artifact file = createSourceArtifact("foo/file.real");
FilesetTraversalParams params =
FilesetTraversalParamsFactory.fileTraversal(
/* ownerLabel= */ label("//foo"),
/* fileToTraverse= */ file,
PathFragment.create("output-name"),
/* symlinkBehaviorMode= */ FilesetTraversalParamsFactory.SymlinkBehavior.COPY,
/* pkgBoundaryMode= */ DONT_CROSS,
/* strictFilesetOutput= */ false,
/* permitDirectories= */ false);
assertSymlinksCreatedInOrder(params, symlink("output-name", file));
}
private void assertFileTraversalForFileSymlink(SymlinkBehavior symlinks) throws Exception {
Artifact file = createSourceArtifact("foo/file.real");
Artifact symlink = getSourceArtifact("foo/file.sym");
symlink.getPath().createSymbolicLink(PathFragment.create("file.real"));
FilesetTraversalParams params =
FilesetTraversalParamsFactory.fileTraversal(
/* ownerLabel= */ label("//foo"),
/* fileToTraverse= */ symlink,
PathFragment.create("output-name"),
/* symlinkBehaviorMode= */ symlinks,
/* pkgBoundaryMode= */ DONT_CROSS,
/* strictFilesetOutput= */ false,
/* permitDirectories= */ false);
switch (symlinks) {
case COPY:
assertSymlinksCreatedInOrder(params, symlink("output-name", "file.real"));
break;
case DEREFERENCE:
assertSymlinksCreatedInOrder(params, symlink("output-name", file));
break;
default:
throw new IllegalStateException(symlinks.toString());
}
}
@Test
public void testFileTraversalForFileSymlinkNoFollow() throws Exception {
assertFileTraversalForFileSymlink(FilesetTraversalParamsFactory.SymlinkBehavior.COPY);
}
@Test
public void testFileTraversalForFileSymlinkFollow() throws Exception {
assertFileTraversalForFileSymlink(FilesetTraversalParamsFactory.SymlinkBehavior.DEREFERENCE);
}
@Test
public void testFileTraversalForDirectory() throws Exception {
Artifact dir = getSourceArtifact("foo/dir_real");
RootedPath fileA = createFile(childOf(dir, "file.a"), "hello");
RootedPath fileB = createFile(childOf(dir, "sub/file.b"), "world");
FilesetTraversalParams params =
FilesetTraversalParamsFactory.fileTraversal(
/* ownerLabel= */ label("//foo"),
/* fileToTraverse= */ dir,
PathFragment.create("output-name"),
/* symlinkBehaviorMode= */ FilesetTraversalParamsFactory.SymlinkBehavior.COPY,
/* pkgBoundaryMode= */ DONT_CROSS,
/* strictFilesetOutput= */ false,
/* permitDirectories= */ true);
assertSymlinksCreatedInOrder(
params, symlink("output-name/file.a", fileA), symlink("output-name/sub/file.b", fileB));
}
@Test
public void testFileTraversalForDisallowedDirectoryThrows() throws Exception {
Artifact dir = getSourceArtifact("foo/dir_real");
createFile(childOf(dir, "file.a"), "hello");
createFile(childOf(dir, "sub/file.b"), "world");
FilesetTraversalParams params =
FilesetTraversalParamsFactory.fileTraversal(
/* ownerLabel= */ label("//foo"),
/* fileToTraverse= */ dir,
PathFragment.create("output-name"),
/* symlinkBehaviorMode= */ FilesetTraversalParamsFactory.SymlinkBehavior.COPY,
/* pkgBoundaryMode= */ DONT_CROSS,
/* strictFilesetOutput= */ false,
/* permitDirectories= */ false);
SkyKey key = FilesetEntryKey.key(params);
EvaluationResult<FilesetEntryValue> result = eval(key);
assertThat(result.hasError()).isTrue();
assertThat(result.getError(key).getException())
.hasMessageThat()
.contains("foo contains a directory");
}
private void assertFileTraversalForDirectorySymlink(SymlinkBehavior symlinks) throws Exception {
Artifact dir = getSourceArtifact("foo/dir_real");
Artifact symlink = getSourceArtifact("foo/dir_sym");
createFile(childOf(dir, "file.a"), "hello");
createFile(childOf(dir, "sub/file.b"), "world");
symlink.getPath().createSymbolicLink(PathFragment.create("dir_real"));
FilesetTraversalParams params =
FilesetTraversalParamsFactory.fileTraversal(
/* ownerLabel= */ label("//foo"),
/* fileToTraverse= */ symlink,
PathFragment.create("output-name"),
/* symlinkBehaviorMode= */ symlinks,
/* pkgBoundaryMode= */ DONT_CROSS,
/* strictFilesetOutput= */ false,
/* permitDirectories= */ true);
switch (symlinks) {
case COPY:
assertSymlinksCreatedInOrder(params, symlink("output-name", "dir_real"));
break;
case DEREFERENCE:
assertSymlinksCreatedInOrder(params, symlink("output-name", dir));
break;
default:
throw new IllegalStateException(symlinks.toString());
}
}
@Test
public void testFileTraversalForDirectorySymlinkFollow() throws Exception {
assertFileTraversalForDirectorySymlink(FilesetTraversalParamsFactory.SymlinkBehavior.COPY);
}
@Test
public void testFileTraversalForDirectorySymlinkNoFollow() throws Exception {
assertFileTraversalForDirectorySymlink(
FilesetTraversalParamsFactory.SymlinkBehavior.DEREFERENCE);
}
@Test
public void testFileTraversalForDanglingSymlink() throws Exception {
Artifact linkName = getSourceArtifact("foo/dangling.sym");
RootedPath linkTarget = createFile(siblingOf(linkName, "target.file"), "blah");
linkName.getPath().createSymbolicLink(PathFragment.create("target.file"));
linkTarget.asPath().delete();
FilesetTraversalParams params =
FilesetTraversalParamsFactory.fileTraversal(
/* ownerLabel= */ label("//foo"),
/* fileToTraverse= */ linkName,
PathFragment.create("output-name"),
/* symlinkBehaviorMode= */ FilesetTraversalParamsFactory.SymlinkBehavior.COPY,
/* pkgBoundaryMode= */ DONT_CROSS,
/* strictFilesetOutput= */ false,
/* permitDirectories= */ false);
assertSymlinksCreatedInOrder(params); // expect empty results
}
@Test
public void testFileTraversalForNonExistentFile() throws Exception {
Artifact path = getSourceArtifact("foo/non-existent");
FilesetTraversalParams params =
FilesetTraversalParamsFactory.fileTraversal(
/* ownerLabel= */ label("//foo"),
/* fileToTraverse= */ path,
PathFragment.create("output-name"),
/* symlinkBehaviorMode= */ FilesetTraversalParamsFactory.SymlinkBehavior.COPY,
/* pkgBoundaryMode= */ DONT_CROSS,
/* strictFilesetOutput= */ false,
/* permitDirectories= */ false);
assertSymlinksCreatedInOrder(params); // expect empty results
}
/**
* Tests that the fingerprint is a function of all arguments of the factory method.
*
* <p>Implementations must provide:
* <ul>
* <li>two different values (a domain) for each argument of the factory method and whether or not
* it is expected to influence the fingerprint
* <li>a way to instantiate {@link FilesetTraversalParams} with a given set of arguments from the
* specified domains
* </ul>
*
* <p>The tests will instantiate pairs of {@link FilesetTraversalParams} objects with only a given
* attribute differing, and observe whether the fingerprints differ (if they are expected to) or
* are the same (otherwise).
*/
private abstract static class FingerprintTester {
private final Map<String, Domain> domains;
FingerprintTester(Map<String, Domain> domains) {
this.domains = domains;
}
abstract FilesetTraversalParams create(Map<String, ?> kwArgs) throws Exception;
private Map<String, ?> getDefaultArgs() {
return getKwArgs(null);
}
private Map<String, ?> getKwArgs(@Nullable String useAlternateFor) {
Map<String, Object> values = new HashMap<>();
for (Map.Entry<String, Domain> d : domains.entrySet()) {
values.put(
d.getKey(),
d.getKey().equals(useAlternateFor) ? d.getValue().valueA : d.getValue().valueB);
}
return values;
}
public void doTest() throws Exception {
Fingerprint fp = new Fingerprint();
create(getDefaultArgs()).fingerprint(fp);
String primary = fp.hexDigestAndReset();
for (String argName : domains.keySet()) {
create(getKwArgs(argName)).fingerprint(fp);
String secondary = fp.hexDigestAndReset();
if (domains.get(argName).includedInFingerprint) {
assertWithMessage(
"Argument '"
+ argName
+ "' was expected to be included in the"
+ " fingerprint, but wasn't")
.that(primary)
.isNotEqualTo(secondary);
} else {
assertWithMessage(
"Argument '"
+ argName
+ "' was expected not to be included in the"
+ " fingerprint, but was")
.that(primary)
.isEqualTo(secondary);
}
}
}
}
private static final class Domain {
boolean includedInFingerprint;
Object valueA;
Object valueB;
Domain(boolean includedInFingerprint, Object valueA, Object valueB) {
this.includedInFingerprint = includedInFingerprint;
this.valueA = valueA;
this.valueB = valueB;
}
}
private static Domain partOfFingerprint(Object valueA, Object valueB) {
return new Domain(true, valueA, valueB);
}
private static Domain notPartOfFingerprint(Object valueB) {
return new Domain(false, "//foo", valueB);
}
@Test
public void testFingerprintOfFileTraversal() throws Exception {
new FingerprintTester(
ImmutableMap.<String, Domain>builder()
.put("ownerLabel", notPartOfFingerprint("//bar"))
.put("fileToTraverse", partOfFingerprint("foo/file.a", "bar/file.b"))
.put("destPath", partOfFingerprint("out1", "out2"))
.put(
"symlinkBehaviorMode",
partOfFingerprint(
FilesetTraversalParamsFactory.SymlinkBehavior.COPY,
FilesetTraversalParamsFactory.SymlinkBehavior.DEREFERENCE))
.put("pkgBoundaryMode", partOfFingerprint(CROSS, DONT_CROSS))
.put("strictFilesetOutput", partOfFingerprint(true, false))
.put("permitDirectories", partOfFingerprint(true, false))
.buildOrThrow()) {
@Override
FilesetTraversalParams create(Map<String, ?> kwArgs) throws Exception {
return FilesetTraversalParamsFactory.fileTraversal(
label((String) kwArgs.get("ownerLabel")),
getSourceArtifact((String) kwArgs.get("fileToTraverse")),
PathFragment.create((String) kwArgs.get("destPath")),
((SymlinkBehavior) kwArgs.get("symlinkBehaviorMode")),
(PackageBoundaryMode) kwArgs.get("pkgBoundaryMode"),
(Boolean) kwArgs.get("strictFilesetOutput"),
(Boolean) kwArgs.get("permitDirectories"));
}
}.doTest();
}
@Test
public void testFingerprintOfNestedTraversal() throws Exception {
Artifact nested1 = getSourceArtifact("a/b");
Artifact nested2 = getSourceArtifact("a/c");
new FingerprintTester(
ImmutableMap.of(
"ownerLabel", notPartOfFingerprint("//bar"),
"nestedArtifact", partOfFingerprint(nested1, nested2),
"destDir", partOfFingerprint("out1", "out2"),
"excludes", partOfFingerprint(ImmutableSet.<String>of(), ImmutableSet.of("x")))) {
@SuppressWarnings("unchecked")
@Override
FilesetTraversalParams create(Map<String, ?> kwArgs) throws Exception {
return FilesetTraversalParamsFactory.nestedTraversal(
label((String) kwArgs.get("ownerLabel")),
(Artifact) kwArgs.get("nestedArtifact"),
PathFragment.create((String) kwArgs.get("destDir")),
(Set<String>) kwArgs.get("excludes"));
}
}.doTest();
}
private static class TestWorkspaceNameFunction implements SkyFunction {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env)
throws SkyFunctionException, InterruptedException {
return WorkspaceNameValue.withName("workspace");
}
}
}