blob: 7dd4eed5221952e060abf8ef5d83309bf21235e5 [file] [log] [blame]
// Copyright 2024 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.skyframe;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.FileValue;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ServerDirectories;
import com.google.devtools.build.lib.analysis.util.AnalysisMock;
import com.google.devtools.build.lib.clock.BlazeClock;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
import com.google.devtools.build.lib.skyframe.ExternalFilesHelper.ExternalFileAction;
import com.google.devtools.build.lib.testutil.FoundationTestCase;
import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
import com.google.devtools.build.lib.vfs.FileStateKey;
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.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.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import java.util.concurrent.atomic.AtomicReference;
import net.starlark.java.eval.StarlarkSemantics;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class DirectoryTreeDigestFunctionTest extends FoundationTestCase {
private RecordingDifferencer differencer;
private ImmutableMap<SkyFunctionName, SkyFunction> skyFunctions;
private EvaluationContext evaluationContext;
@Before
public void setup() throws Exception {
differencer = new SequencedRecordingDifferencer();
evaluationContext =
EvaluationContext.newBuilder().setParallelism(8).setEventHandler(reporter).build();
AtomicReference<PathPackageLocator> packageLocator =
new AtomicReference<>(
new PathPackageLocator(
outputBase,
ImmutableList.of(Root.fromPath(rootDirectory)),
BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY));
BlazeDirectories directories =
new BlazeDirectories(
new ServerDirectories(rootDirectory, outputBase, rootDirectory),
rootDirectory,
/* defaultSystemJavabase= */ null,
AnalysisMock.get().getProductName());
ExternalFilesHelper externalFilesHelper =
ExternalFilesHelper.createForTesting(
packageLocator,
ExternalFileAction.DEPEND_ON_EXTERNAL_PKG_FOR_EXTERNAL_REPO_PATHS,
directories);
skyFunctions =
ImmutableMap.<SkyFunctionName, SkyFunction>builder()
.put(FileValue.FILE, new FileFunction(packageLocator, directories))
.put(
FileStateKey.FILE_STATE,
new FileStateFunction(
Suppliers.ofInstance(new TimestampGranularityMonitor(BlazeClock.instance())),
SyscallCache.NO_CACHE,
externalFilesHelper))
.put(SkyFunctions.PRECOMPUTED, new PrecomputedFunction())
.put(SkyFunctions.DIRECTORY_LISTING, new DirectoryListingFunction())
.put(
SkyFunctions.DIRECTORY_LISTING_STATE,
new DirectoryListingStateFunction(externalFilesHelper, SyscallCache.NO_CACHE))
.put(SkyFunctions.DIRECTORY_TREE_DIGEST, new DirectoryTreeDigestFunction())
.buildOrThrow();
PrecomputedValue.STARLARK_SEMANTICS.set(differencer, StarlarkSemantics.DEFAULT);
PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, packageLocator.get());
}
private String getTreeDigest(String path) throws Exception {
RootedPath rootedPath =
RootedPath.toRootedPath(Root.absoluteRoot(fileSystem), scratch.resolve(path));
SkyKey key = DirectoryTreeDigestValue.key(rootedPath);
MemoizingEvaluator evaluator = new InMemoryMemoizingEvaluator(skyFunctions, differencer);
var result = evaluator.evaluate(ImmutableList.of(key), evaluationContext);
if (result.hasError()) {
throw result.getError().getException();
}
return ((DirectoryTreeDigestValue) result.get(key)).hexDigest();
}
@Test
public void basic() throws Exception {
scratch.file("a", "a");
scratch.file("b/b", "b");
scratch.file("c", "c");
String oldDigest = getTreeDigest("/");
scratch.overwriteFile("b/b", "something else");
assertThat(getTreeDigest("/")).isNotEqualTo(oldDigest);
}
@Test
public void addFile() throws Exception {
scratch.file("a", "a");
scratch.file("b/b", "b");
scratch.file("c", "c");
String oldDigest = getTreeDigest("/");
scratch.file("b/d", "something else");
assertThat(getTreeDigest("/")).isNotEqualTo(oldDigest);
}
@Test
public void removeFile() throws Exception {
scratch.file("a", "a");
scratch.file("b/b", "b");
scratch.file("c", "c");
String oldDigest = getTreeDigest("/");
scratch.deleteFile("b/b");
assertThat(getTreeDigest("/")).isNotEqualTo(oldDigest);
}
@Test
public void renameFile() throws Exception {
scratch.file("a", "a");
scratch.file("b/b", "b");
scratch.file("c", "c");
String oldDigest = getTreeDigest("/");
scratch.deleteFile("b/b");
scratch.file("b/b1", "b");
assertThat(getTreeDigest("/")).isNotEqualTo(oldDigest);
}
@Test
public void swapDirAndFile() throws Exception {
scratch.file("a", "a");
scratch.file("b", "b");
scratch.file("c/inner", "inner");
String oldDigest = getTreeDigest("/");
scratch.resolve("c").deleteTree();
scratch.deleteFile("b");
scratch.file("b/inner", "inner");
scratch.file("c", "b");
assertThat(getTreeDigest("/")).isNotEqualTo(oldDigest);
}
@Test
public void changeMtime() throws Exception {
scratch.file("a", "a");
scratch.file("b", "b");
scratch.file("c", "c");
String oldDigest = getTreeDigest("/");
// We don't digest mtimes so this shouldn't affect anything.
scratch.resolve("c").setLastModifiedTime(2024L);
assertThat(getTreeDigest("/")).isEqualTo(oldDigest);
}
@Test
public void symlink() throws Exception {
scratch.file("dir/a", "a");
scratch.resolve("dir/b").createSymbolicLink(scratch.resolve("otherdir"));
scratch.file("dir/c", "c");
scratch.file("otherdir/b", "b");
scratch.file("otherdir/sub/sub", "sub");
String oldDigest = getTreeDigest("dir");
scratch.deleteFile("dir/b");
scratch.resolve("dir/b").createSymbolicLink(scratch.resolve("yetotherdir"));
scratch.file("yetotherdir/crazy", "stuff");
assertThat(getTreeDigest("dir")).isNotEqualTo(oldDigest);
}
@Test
public void danglingSymlink() throws Exception {
scratch.file("dir/a", "a");
scratch.resolve("dir/b").createSymbolicLink(scratch.resolve("otherdir"));
scratch.file("dir/c", "c");
String oldDigest = getTreeDigest("dir");
scratch.file("otherdir/b", "b");
assertThat(getTreeDigest("dir")).isNotEqualTo(oldDigest);
}
@Test
public void symlinkPointingToSameContents() throws Exception {
scratch.file("dir/a", "a");
scratch.file("dir/b/b", "b");
scratch.file("dir/b/sub/sub", "sub");
scratch.file("dir/c", "c");
String oldDigest = getTreeDigest("dir");
// replace dir/b with a symlink pointing to otherdir/, which contains the same contents.
// this shouldn't affect the tree digest.
scratch.resolve("dir/b").deleteTree();
scratch.resolve("dir/b").createSymbolicLink(scratch.resolve("otherdir"));
scratch.file("otherdir/b", "b");
scratch.file("otherdir/sub/sub", "sub");
assertThat(getTreeDigest("dir")).isEqualTo(oldDigest);
}
}