Open-source LocalDiffAwarenessIntegrationTest.
PiperOrigin-RevId: 420127324
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD
index cbc1c1d..3a70cfb 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD
@@ -98,6 +98,7 @@
"//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
"//src/test/java/com/google/devtools/build/lib/vfs/util",
"//third_party:guava",
+ "//third_party:guava-testlib",
"//third_party:jsr305",
"//third_party:junit4",
"//third_party:truth",
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/SkyframeIntegrationTestBase.java b/src/test/java/com/google/devtools/build/lib/buildtool/util/SkyframeIntegrationTestBase.java
new file mode 100644
index 0000000..6976b49
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/SkyframeIntegrationTestBase.java
@@ -0,0 +1,88 @@
+// Copyright 2022 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.buildtool.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.testing.GcFinalization;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Infrastructure to support Skyframe integration tests. */
+public abstract class SkyframeIntegrationTestBase extends BuildIntegrationTestCase {
+
+ protected SkyframeExecutor skyframeExecutor() {
+ return runtimeWrapper.getSkyframeExecutor();
+ }
+
+ protected static List<WeakReference<?>> weakRefs(Object... strongRefs) throws Exception {
+ List<WeakReference<?>> result = new ArrayList<>();
+ for (Object ref : strongRefs) {
+ result.add(new WeakReference<>(ref));
+ }
+ return result;
+ }
+
+ protected static void assertAllReleased(Iterable<WeakReference<?>> refs) {
+ for (WeakReference<?> ref : refs) {
+ GcFinalization.awaitClear(ref);
+ }
+ }
+
+ private String makeGenruleContents(String value) {
+ return String.format(
+ "genrule(name='target', outs=['out'], cmd='/bin/echo %s > $(location out)')", value);
+ }
+
+ protected void writeGenrule(String filename, String value) throws Exception {
+ write(filename, makeGenruleContents(value));
+ }
+
+ protected void writeGenruleAbsolute(Path file, String value) throws Exception {
+ writeAbsolute(file, makeGenruleContents(value));
+ }
+
+ protected void assertCharContentsIgnoringOrderAndWhitespace(
+ String expectedCharContents, String target) throws Exception {
+ Path path = Iterables.getOnlyElement(getArtifacts(target)).getPath();
+ char[] actualChars = FileSystemUtils.readContentAsLatin1(path);
+ char[] expectedChars = expectedCharContents.toCharArray();
+ Arrays.sort(actualChars);
+ Arrays.sort(expectedChars);
+ assertThat(new String(actualChars).trim()).isEqualTo(new String(expectedChars).trim());
+ }
+
+ protected void assertContents(String expectedContents, String target) throws Exception {
+ assertContents(expectedContents, Iterables.getOnlyElement(getArtifacts(target)).getPath());
+ }
+
+ protected void assertContents(String expectedContents, Path path) throws Exception {
+ String actualContents = new String(FileSystemUtils.readContentAsLatin1(path));
+ assertThat(actualContents.trim()).isEqualTo(expectedContents);
+ }
+
+ protected ImmutableList<String> getOnlyOutputContentAsLines(String target) throws Exception {
+ return FileSystemUtils.readLines(
+ Iterables.getOnlyElement(getArtifacts(target)).getPath(), UTF_8);
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
index 68cd782..271862a 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -20,6 +20,7 @@
# Tests that are broken out from the SkyframeTests target into separate targets.
EXCLUDED_FROM_SKYFRAME_TESTS = [
+ "LocalDiffAwarenessIntegrationTest.java",
"PrepareDepsOfTargetsUnderDirectoryFunctionTest.java", # b/179148968
] + CROSS_PLATFORM_WINDOWS_TESTS
@@ -378,3 +379,28 @@
"//third_party:truth",
],
)
+
+java_test(
+ name = "LocalDiffAwarenessIntegrationTest",
+ srcs = ["LocalDiffAwarenessIntegrationTest.java"],
+ # TODO(pcloudy): Even with --experimental_windows_watchfs, there's an extra
+ # getValues() on the second build in
+ # externalSymlink_doesNotTriggerFullGraphTraversal with Windows, and
+ # non-deterministic failure to detect changes (watchfs bug?).
+ tags = ["no_windows"],
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib:runtime",
+ "//src/main/java/com/google/devtools/build/lib/analysis:blaze_directories",
+ "//src/main/java/com/google/devtools/build/lib/skyframe:local_diff_awareness",
+ "//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception",
+ "//src/main/java/com/google/devtools/build/lib/util:os",
+ "//src/main/java/com/google/devtools/build/lib/vfs",
+ "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+ "//src/main/java/com/google/devtools/common/options",
+ "//src/test/java/com/google/devtools/build/lib/buildtool/util",
+ "//src/test/java/com/google/devtools/build/skyframe:testutil",
+ "//third_party:guava",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/LocalDiffAwarenessIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/LocalDiffAwarenessIntegrationTest.java
new file mode 100644
index 0000000..2129bc2
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/LocalDiffAwarenessIntegrationTest.java
@@ -0,0 +1,180 @@
+// Copyright 2022 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 org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.buildtool.util.SkyframeIntegrationTestBase;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.WorkspaceBuilder;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.vfs.DelegateFileSystem;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.NotifyingHelper;
+import com.google.devtools.common.options.OptionsBase;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for local diff awareness. A good place for general tests of Bazel's interactions with
+ * "smart" filesystems, so that open-source changes don't break Google-internal features around
+ * smart filesystems.
+ */
+@RunWith(JUnit4.class)
+public class LocalDiffAwarenessIntegrationTest extends SkyframeIntegrationTestBase {
+ private final Map<PathFragment, IOException> throwOnNextStatIfFound = new HashMap<>();
+
+ @Override
+ protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
+ return super.getRuntimeBuilder()
+ .addBlazeModule(
+ new BlazeModule() {
+ @Override
+ public void workspaceInit(
+ BlazeRuntime runtime, BlazeDirectories directories, WorkspaceBuilder builder) {
+ builder.addDiffAwarenessFactory(new LocalDiffAwareness.Factory(ImmutableList.of()));
+ }
+
+ @Override
+ public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
+ return ImmutableList.of(LocalDiffAwareness.Options.class);
+ }
+ });
+ }
+
+ @Override
+ public FileSystem createFileSystem() throws Exception {
+ return new DelegateFileSystem(super.createFileSystem()) {
+ @Override
+ protected FileStatus statIfFound(PathFragment path, boolean followSymlinks)
+ throws IOException {
+ IOException e = throwOnNextStatIfFound.remove(path);
+ if (e != null) {
+ throw e;
+ }
+ return super.statIfFound(path, followSymlinks);
+ }
+ };
+ }
+
+ @Before
+ public void addOptions() {
+ addOptions("--watchfs", "--experimental_windows_watchfs");
+ }
+
+ @After
+ public void checkExceptionsThrown() {
+ assertWithMessage("Injected exception(s) not thrown").that(throwOnNextStatIfFound).isEmpty();
+ }
+
+ @Test
+ public void changedFile_detectsChange() throws Exception {
+ // TODO(bazel-team): Understand why these tests are flaky on Mac. Probably real watchfs bug?
+ Assume.assumeFalse(OS.DARWIN.equals(OS.getCurrent()));
+ write("foo/BUILD", "genrule(name='foo', outs=['out'], cmd='echo hello > $@')");
+ buildTarget("//foo");
+ assertContents("hello", "//foo");
+ write("foo/BUILD", "genrule(name='foo', outs=['out'], cmd='echo there > $@')");
+
+ buildTarget("//foo");
+
+ assertContents("there", "//foo");
+ }
+
+ @Test
+ public void changedFile_statFails_throwsError() throws Exception {
+ Assume.assumeFalse(OS.DARWIN.equals(OS.getCurrent()));
+ write("foo/BUILD", "genrule(name='foo', outs=['out'], cmd='echo hello > $@')");
+ buildTarget("//foo");
+ assertContents("hello", "//foo");
+ Path buildFile = write("foo/BUILD", "genrule(name='foo', outs=['out'], cmd='echo there > $@')");
+ IOException injectedException = new IOException("oh no!");
+ throwOnNextStatIfFound.put(buildFile.asFragment(), injectedException);
+
+ AbruptExitException e = assertThrows(AbruptExitException.class, () -> buildTarget("//foo"));
+
+ assertThat(e.getCause()).hasCauseThat().hasCauseThat().isSameInstanceAs(injectedException);
+ }
+
+ @Test
+ public void externalSymlink_doesNotTriggerFullGraphTraversal() throws Exception {
+ addOptions("--symlink_prefix=/");
+ AtomicInteger calledGetValues = new AtomicInteger(0);
+ skyframeExecutor()
+ .getEvaluator()
+ .injectGraphTransformerForTesting(
+ NotifyingHelper.makeNotifyingTransformer(
+ (key, type, order, context) -> {
+ if (type == NotifyingHelper.EventType.GET_VALUES) {
+ calledGetValues.incrementAndGet();
+ }
+ }));
+ write(
+ "hello/BUILD",
+ "genrule(name='target', srcs = ['external'], outs=['out'], cmd='/bin/cat $(SRCS) > $@')");
+ String externalLink = System.getenv("TEST_TMPDIR") + "/target";
+ write(externalLink, "one");
+ createSymlink(externalLink, "hello/external");
+
+ // Trivial build: external symlink is not seen, so normal diff awareness is in play.
+ buildTarget("//hello:BUILD");
+ // New package path on first build triggers full-graph work.
+ calledGetValues.set(0);
+ // getValues() called during output file checking (although if an output service is able to
+ // report modified files in practice there is no iteration).
+ // If external repositories are being used, getValues called because of that too.
+ // TODO(bazel-team): get rid of this when we can disable checks for external repositories.
+ int numGetValuesInFullDiffAwarenessBuild =
+ 1 + ("bazel".equals(this.getRuntime().getProductName()) ? 1 : 0);
+
+ buildTarget("//hello:BUILD");
+ assertThat(calledGetValues.getAndSet(0)).isEqualTo(numGetValuesInFullDiffAwarenessBuild);
+
+ // Now bring the external symlink into Bazel's awareness.
+ buildTarget("//hello:target");
+ assertContents("one", "//hello:target");
+ assertThat(calledGetValues.getAndSet(0)).isEqualTo(numGetValuesInFullDiffAwarenessBuild);
+
+ // Builds that follow a build containing an external file don't trigger a traversal.
+ buildTarget("//hello:target");
+ assertContents("one", "//hello:target");
+ assertThat(calledGetValues.getAndSet(0)).isEqualTo(numGetValuesInFullDiffAwarenessBuild);
+
+ write(externalLink, "two");
+
+ buildTarget("//hello:target");
+ // External file changes are tracked.
+ assertContents("two", "//hello:target");
+ assertThat(calledGetValues.getAndSet(0)).isEqualTo(numGetValuesInFullDiffAwarenessBuild);
+ }
+}