blob: 47e75802b924f0b4fe513562fc85fdeb194210c0 [file] [log] [blame]
// 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 com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.actions.ChangedFilesMessage;
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.FileSystemUtils;
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.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
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(b/238606809): Understand why these tests are flaky on Mac. Probably real watchfs bug?
assume().that(OS.getCurrent()).isNotEqualTo(OS.DARWIN);
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 > $@')");
buildTargetWithRetryUntilSeesChange("//foo", "foo/BUILD");
assertContents("there", "//foo");
}
@Test
public void changedIgnoredFile_ignoresChange() throws Exception {
// MacOSXFsEventsDiffAwareness doesn't currently support not registering
// watches for ignored paths.
assume().that(OS.getCurrent()).isNotEqualTo(OS.DARWIN);
String notIgnoredFilePath = "foo/BUILD";
String ignoredFilePath = "foo/ignored-dir/BUILD";
write(".bazelignore", "foo/ignored-dir");
write(ignoredFilePath, "");
write(notIgnoredFilePath, "genrule(name='foo', outs=['out'], cmd='echo hello > $@')");
buildTarget("//foo");
assertContents("hello", "//foo");
write(notIgnoredFilePath, "genrule(name='foo', outs=['out'], cmd='echo there > $@')");
write(ignoredFilePath, "A = 1");
AtomicBoolean ignoredFileChanged = new AtomicBoolean();
AtomicBoolean notIgnoredFileChanged = new AtomicBoolean();
runtimeWrapper.registerSubscriber(
new Object() {
@Subscribe
private void onChangedFiles(ChangedFilesMessage changedFiles) {
ignoredFileChanged.compareAndSet(
false, changedFiles.changedFiles().contains(PathFragment.create(ignoredFilePath)));
notIgnoredFileChanged.compareAndSet(
false,
changedFiles.changedFiles().contains(PathFragment.create(notIgnoredFilePath)));
}
});
// Work around the inherent raciness of LocalDiffAwareness where the FS events are
// delivered asynchronously and fast running test can trigger an incremental build
// before the change is observed.
for (int attempt = 0; attempt < 10; ++attempt) {
buildTarget("//foo");
if (notIgnoredFileChanged.get() && !ignoredFileChanged.get()) {
assertContents("there", "//foo");
return;
}
}
if (!notIgnoredFileChanged.get()) {
fail("Didn't observe file change within allowed number of retries");
}
if (ignoredFileChanged.get()) {
fail("Observed ignored file change");
}
}
@Test
public void changedFile_statFails_throwsError() throws Exception {
// TODO(b/238606809): Understand why these tests are flaky on Mac. Probably real watchfs bug?
assume().that(OS.getCurrent()).isNotEqualTo(OS.DARWIN);
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,
() -> buildTargetWithRetryUntilSeesChange("//foo", "foo/BUILD"));
assertThat(e).hasCauseThat().hasCauseThat().hasCauseThat().isInstanceOf(IOException.class);
}
/**
* Runs {@link #buildTarget(String...)} repeatedly until we observe a change for the given path.
*
* <p>This allows to work around the inherent raciness of {@code LocalDiffAwareness} where the FS
* events are delivered asynchronously and fast running test can trigger an incremental build
* before the change is observed.
*/
private void buildTargetWithRetryUntilSeesChange(String target, String path) throws Exception {
AtomicBoolean changed = new AtomicBoolean();
runtimeWrapper.registerSubscriber(
new Object() {
@Subscribe
private void onChangedFiles(ChangedFilesMessage changedFiles) {
changed.compareAndSet(
false, changedFiles.changedFiles().contains(PathFragment.create(path)));
}
});
for (int attempt = 0; attempt < 10; ++attempt) {
buildTarget(target);
if (changed.get()) {
return;
}
}
fail("Didn't observe file change within allowed number of retries");
}
// This test doesn't use --watchfs functionality, but if the source filesystem doesn't offer diffs
// Bazel must scan the full Skyframe graph anyway, so a bug in checking output files wouldn't be
// detected without --watchfs.
@Test
public void ignoreOutputFilesThenCheckAgainDoesCheck() throws Exception {
if ("bazel".equals(this.getRuntime().getProductName())) {
// Repository options only in Bazel.
addOptions("--noexperimental_check_external_repository_files");
}
Path buildFile =
write(
"foo/BUILD",
"genrule(name = 'foo', outs = ['out'], cmd = 'cp $< $@', srcs = ['link'])");
Path outputFile = directories.getOutputBase().getChild("linkTarget");
FileSystemUtils.writeContentAsLatin1(outputFile, "one");
buildFile.getParentDirectory().getChild("link").createSymbolicLink(outputFile.asFragment());
buildTarget("//foo:foo");
assertContents("one", "//foo:foo");
addOptions("--noexperimental_check_output_files");
FileSystemUtils.writeContentAsLatin1(outputFile, "two");
buildTarget("//foo:foo");
assertContents("one", "//foo:foo");
addOptions("--experimental_check_output_files");
buildTarget("//foo:foo");
assertContents("two", "//foo:foo");
}
@Test
public void externalSymlink_doesNotTriggerFullGraphTraversal() throws Exception {
addOptions("--symlink_prefix=/");
if ("bazel".equals(this.getRuntime().getProductName())) {
// Repository options only in Bazel.
addOptions("--noexperimental_check_external_repository_files");
}
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);
// getValuesAndExceptions() called during output file checking (although if an output service is
// able to report modified files in practice there is no iteration).
buildTarget("//hello:BUILD");
assertThat(calledGetValues.getAndSet(0)).isEqualTo(1);
// Now bring the external symlink into Bazel's awareness.
buildTarget("//hello:target");
assertContents("one", "//hello:target");
assertThat(calledGetValues.getAndSet(0)).isEqualTo(1);
// 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(1);
write(externalLink, "two");
buildTarget("//hello:target");
// External file changes are tracked.
assertContents("two", "//hello:target");
assertThat(calledGetValues.getAndSet(0)).isEqualTo(1);
}
}