blob: 47e75802b924f0b4fe513562fc85fdeb194210c0 [file] [log] [blame]
janakrac8dcc62022-01-06 13:09:06 -08001// Copyright 2022 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.skyframe;
16
17import static com.google.common.truth.Truth.assertThat;
18import static com.google.common.truth.Truth.assertWithMessage;
Googler54998a702022-07-19 11:03:35 -070019import static com.google.common.truth.TruthJUnit.assume;
janakrac8dcc62022-01-06 13:09:06 -080020import static org.junit.Assert.assertThrows;
Googler703c0232022-07-14 10:43:07 -070021import static org.junit.Assert.fail;
janakrac8dcc62022-01-06 13:09:06 -080022
23import com.google.common.collect.ImmutableList;
Googler703c0232022-07-14 10:43:07 -070024import com.google.common.eventbus.Subscribe;
25import com.google.devtools.build.lib.actions.ChangedFilesMessage;
janakrac8dcc62022-01-06 13:09:06 -080026import com.google.devtools.build.lib.analysis.BlazeDirectories;
27import com.google.devtools.build.lib.buildtool.util.SkyframeIntegrationTestBase;
28import com.google.devtools.build.lib.runtime.BlazeModule;
29import com.google.devtools.build.lib.runtime.BlazeRuntime;
30import com.google.devtools.build.lib.runtime.Command;
31import com.google.devtools.build.lib.runtime.WorkspaceBuilder;
32import com.google.devtools.build.lib.util.AbruptExitException;
Googler54998a702022-07-19 11:03:35 -070033import com.google.devtools.build.lib.util.OS;
janakrac8dcc62022-01-06 13:09:06 -080034import com.google.devtools.build.lib.vfs.DelegateFileSystem;
35import com.google.devtools.build.lib.vfs.FileStatus;
36import com.google.devtools.build.lib.vfs.FileSystem;
janakr8ef29dd2022-01-06 15:19:16 -080037import com.google.devtools.build.lib.vfs.FileSystemUtils;
janakrac8dcc62022-01-06 13:09:06 -080038import com.google.devtools.build.lib.vfs.Path;
39import com.google.devtools.build.lib.vfs.PathFragment;
40import com.google.devtools.build.skyframe.NotifyingHelper;
41import com.google.devtools.common.options.OptionsBase;
42import java.io.IOException;
43import java.util.HashMap;
44import java.util.Map;
Googler703c0232022-07-14 10:43:07 -070045import java.util.concurrent.atomic.AtomicBoolean;
janakrac8dcc62022-01-06 13:09:06 -080046import java.util.concurrent.atomic.AtomicInteger;
47import org.junit.After;
janakrac8dcc62022-01-06 13:09:06 -080048import org.junit.Before;
49import org.junit.Test;
50import org.junit.runner.RunWith;
51import org.junit.runners.JUnit4;
52
53/**
54 * Tests for local diff awareness. A good place for general tests of Bazel's interactions with
55 * "smart" filesystems, so that open-source changes don't break Google-internal features around
56 * smart filesystems.
57 */
58@RunWith(JUnit4.class)
59public class LocalDiffAwarenessIntegrationTest extends SkyframeIntegrationTestBase {
60 private final Map<PathFragment, IOException> throwOnNextStatIfFound = new HashMap<>();
61
62 @Override
63 protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
64 return super.getRuntimeBuilder()
65 .addBlazeModule(
66 new BlazeModule() {
67 @Override
68 public void workspaceInit(
69 BlazeRuntime runtime, BlazeDirectories directories, WorkspaceBuilder builder) {
70 builder.addDiffAwarenessFactory(new LocalDiffAwareness.Factory(ImmutableList.of()));
71 }
72
73 @Override
74 public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
75 return ImmutableList.of(LocalDiffAwareness.Options.class);
76 }
77 });
78 }
79
80 @Override
81 public FileSystem createFileSystem() throws Exception {
82 return new DelegateFileSystem(super.createFileSystem()) {
83 @Override
84 protected FileStatus statIfFound(PathFragment path, boolean followSymlinks)
85 throws IOException {
86 IOException e = throwOnNextStatIfFound.remove(path);
87 if (e != null) {
88 throw e;
89 }
90 return super.statIfFound(path, followSymlinks);
91 }
92 };
93 }
94
95 @Before
96 public void addOptions() {
97 addOptions("--watchfs", "--experimental_windows_watchfs");
98 }
99
100 @After
101 public void checkExceptionsThrown() {
102 assertWithMessage("Injected exception(s) not thrown").that(throwOnNextStatIfFound).isEmpty();
103 }
104
105 @Test
106 public void changedFile_detectsChange() throws Exception {
Googler54998a702022-07-19 11:03:35 -0700107 // TODO(b/238606809): Understand why these tests are flaky on Mac. Probably real watchfs bug?
108 assume().that(OS.getCurrent()).isNotEqualTo(OS.DARWIN);
janakrac8dcc62022-01-06 13:09:06 -0800109 write("foo/BUILD", "genrule(name='foo', outs=['out'], cmd='echo hello > $@')");
110 buildTarget("//foo");
111 assertContents("hello", "//foo");
112 write("foo/BUILD", "genrule(name='foo', outs=['out'], cmd='echo there > $@')");
113
Googler703c0232022-07-14 10:43:07 -0700114 buildTargetWithRetryUntilSeesChange("//foo", "foo/BUILD");
janakrac8dcc62022-01-06 13:09:06 -0800115
116 assertContents("there", "//foo");
117 }
118
119 @Test
Sushain Cherivirala4dabe432023-07-18 08:17:53 -0700120 public void changedIgnoredFile_ignoresChange() throws Exception {
121 // MacOSXFsEventsDiffAwareness doesn't currently support not registering
122 // watches for ignored paths.
123 assume().that(OS.getCurrent()).isNotEqualTo(OS.DARWIN);
124
125 String notIgnoredFilePath = "foo/BUILD";
126 String ignoredFilePath = "foo/ignored-dir/BUILD";
127
128 write(".bazelignore", "foo/ignored-dir");
129
130 write(ignoredFilePath, "");
131 write(notIgnoredFilePath, "genrule(name='foo', outs=['out'], cmd='echo hello > $@')");
132 buildTarget("//foo");
133 assertContents("hello", "//foo");
134
135 write(notIgnoredFilePath, "genrule(name='foo', outs=['out'], cmd='echo there > $@')");
136 write(ignoredFilePath, "A = 1");
137
138 AtomicBoolean ignoredFileChanged = new AtomicBoolean();
139 AtomicBoolean notIgnoredFileChanged = new AtomicBoolean();
140 runtimeWrapper.registerSubscriber(
141 new Object() {
142 @Subscribe
143 private void onChangedFiles(ChangedFilesMessage changedFiles) {
144 ignoredFileChanged.compareAndSet(
145 false, changedFiles.changedFiles().contains(PathFragment.create(ignoredFilePath)));
146 notIgnoredFileChanged.compareAndSet(
147 false,
148 changedFiles.changedFiles().contains(PathFragment.create(notIgnoredFilePath)));
149 }
150 });
151
152 // Work around the inherent raciness of LocalDiffAwareness where the FS events are
153 // delivered asynchronously and fast running test can trigger an incremental build
154 // before the change is observed.
155 for (int attempt = 0; attempt < 10; ++attempt) {
156 buildTarget("//foo");
157 if (notIgnoredFileChanged.get() && !ignoredFileChanged.get()) {
158 assertContents("there", "//foo");
159 return;
160 }
161 }
162
163 if (!notIgnoredFileChanged.get()) {
164 fail("Didn't observe file change within allowed number of retries");
165 }
166 if (ignoredFileChanged.get()) {
167 fail("Observed ignored file change");
168 }
169 }
170
171 @Test
janakrac8dcc62022-01-06 13:09:06 -0800172 public void changedFile_statFails_throwsError() throws Exception {
Googler54998a702022-07-19 11:03:35 -0700173 // TODO(b/238606809): Understand why these tests are flaky on Mac. Probably real watchfs bug?
174 assume().that(OS.getCurrent()).isNotEqualTo(OS.DARWIN);
janakrac8dcc62022-01-06 13:09:06 -0800175 write("foo/BUILD", "genrule(name='foo', outs=['out'], cmd='echo hello > $@')");
176 buildTarget("//foo");
177 assertContents("hello", "//foo");
178 Path buildFile = write("foo/BUILD", "genrule(name='foo', outs=['out'], cmd='echo there > $@')");
179 IOException injectedException = new IOException("oh no!");
180 throwOnNextStatIfFound.put(buildFile.asFragment(), injectedException);
181
Googler703c0232022-07-14 10:43:07 -0700182 AbruptExitException e =
183 assertThrows(
184 AbruptExitException.class,
185 () -> buildTargetWithRetryUntilSeesChange("//foo", "foo/BUILD"));
janakrac8dcc62022-01-06 13:09:06 -0800186
Googler33a689c2023-06-29 07:57:25 -0700187 assertThat(e).hasCauseThat().hasCauseThat().hasCauseThat().isInstanceOf(IOException.class);
Googler703c0232022-07-14 10:43:07 -0700188 }
189
190 /**
191 * Runs {@link #buildTarget(String...)} repeatedly until we observe a change for the given path.
192 *
193 * <p>This allows to work around the inherent raciness of {@code LocalDiffAwareness} where the FS
194 * events are delivered asynchronously and fast running test can trigger an incremental build
195 * before the change is observed.
196 */
197 private void buildTargetWithRetryUntilSeesChange(String target, String path) throws Exception {
198 AtomicBoolean changed = new AtomicBoolean();
199 runtimeWrapper.registerSubscriber(
200 new Object() {
201 @Subscribe
202 private void onChangedFiles(ChangedFilesMessage changedFiles) {
203 changed.compareAndSet(
204 false, changedFiles.changedFiles().contains(PathFragment.create(path)));
205 }
206 });
207 for (int attempt = 0; attempt < 10; ++attempt) {
208 buildTarget(target);
209 if (changed.get()) {
210 return;
211 }
212 }
213 fail("Didn't observe file change within allowed number of retries");
janakrac8dcc62022-01-06 13:09:06 -0800214 }
215
janakr416ae372022-01-07 10:16:27 -0800216 // This test doesn't use --watchfs functionality, but if the source filesystem doesn't offer diffs
217 // Bazel must scan the full Skyframe graph anyway, so a bug in checking output files wouldn't be
218 // detected without --watchfs.
janakrac8dcc62022-01-06 13:09:06 -0800219 @Test
janakr8ef29dd2022-01-06 15:19:16 -0800220 public void ignoreOutputFilesThenCheckAgainDoesCheck() throws Exception {
Paul Tarjan123da962022-01-19 07:25:59 -0800221 if ("bazel".equals(this.getRuntime().getProductName())) {
222 // Repository options only in Bazel.
223 addOptions("--noexperimental_check_external_repository_files");
224 }
janakr8ef29dd2022-01-06 15:19:16 -0800225 Path buildFile =
226 write(
227 "foo/BUILD",
228 "genrule(name = 'foo', outs = ['out'], cmd = 'cp $< $@', srcs = ['link'])");
229 Path outputFile = directories.getOutputBase().getChild("linkTarget");
230 FileSystemUtils.writeContentAsLatin1(outputFile, "one");
231 buildFile.getParentDirectory().getChild("link").createSymbolicLink(outputFile.asFragment());
232
233 buildTarget("//foo:foo");
234
235 assertContents("one", "//foo:foo");
236
237 addOptions("--noexperimental_check_output_files");
238 FileSystemUtils.writeContentAsLatin1(outputFile, "two");
239
240 buildTarget("//foo:foo");
241
242 assertContents("one", "//foo:foo");
243
244 addOptions("--experimental_check_output_files");
245
246 buildTarget("//foo:foo");
247
248 assertContents("two", "//foo:foo");
249 }
250
251 @Test
janakrac8dcc62022-01-06 13:09:06 -0800252 public void externalSymlink_doesNotTriggerFullGraphTraversal() throws Exception {
253 addOptions("--symlink_prefix=/");
Paul Tarjan123da962022-01-19 07:25:59 -0800254 if ("bazel".equals(this.getRuntime().getProductName())) {
255 // Repository options only in Bazel.
256 addOptions("--noexperimental_check_external_repository_files");
257 }
janakrac8dcc62022-01-06 13:09:06 -0800258 AtomicInteger calledGetValues = new AtomicInteger(0);
259 skyframeExecutor()
260 .getEvaluator()
261 .injectGraphTransformerForTesting(
262 NotifyingHelper.makeNotifyingTransformer(
263 (key, type, order, context) -> {
264 if (type == NotifyingHelper.EventType.GET_VALUES) {
265 calledGetValues.incrementAndGet();
266 }
267 }));
268 write(
269 "hello/BUILD",
270 "genrule(name='target', srcs = ['external'], outs=['out'], cmd='/bin/cat $(SRCS) > $@')");
271 String externalLink = System.getenv("TEST_TMPDIR") + "/target";
272 write(externalLink, "one");
273 createSymlink(externalLink, "hello/external");
274
275 // Trivial build: external symlink is not seen, so normal diff awareness is in play.
276 buildTarget("//hello:BUILD");
277 // New package path on first build triggers full-graph work.
278 calledGetValues.set(0);
Googlerb5052b82022-11-03 11:33:56 -0700279 // getValuesAndExceptions() called during output file checking (although if an output service is
280 // able to report modified files in practice there is no iteration).
janakrac8dcc62022-01-06 13:09:06 -0800281
282 buildTarget("//hello:BUILD");
Paul Tarjan123da962022-01-19 07:25:59 -0800283 assertThat(calledGetValues.getAndSet(0)).isEqualTo(1);
janakrac8dcc62022-01-06 13:09:06 -0800284
285 // Now bring the external symlink into Bazel's awareness.
286 buildTarget("//hello:target");
287 assertContents("one", "//hello:target");
Paul Tarjan123da962022-01-19 07:25:59 -0800288 assertThat(calledGetValues.getAndSet(0)).isEqualTo(1);
janakrac8dcc62022-01-06 13:09:06 -0800289
290 // Builds that follow a build containing an external file don't trigger a traversal.
291 buildTarget("//hello:target");
292 assertContents("one", "//hello:target");
Paul Tarjan123da962022-01-19 07:25:59 -0800293 assertThat(calledGetValues.getAndSet(0)).isEqualTo(1);
janakrac8dcc62022-01-06 13:09:06 -0800294
295 write(externalLink, "two");
296
297 buildTarget("//hello:target");
298 // External file changes are tracked.
299 assertContents("two", "//hello:target");
Paul Tarjan123da962022-01-19 07:25:59 -0800300 assertThat(calledGetValues.getAndSet(0)).isEqualTo(1);
janakrac8dcc62022-01-06 13:09:06 -0800301 }
302}