janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 1 | // 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 | |
| 15 | package com.google.devtools.build.lib.skyframe; |
| 16 | |
| 17 | import static com.google.common.truth.Truth.assertThat; |
| 18 | import static com.google.common.truth.Truth.assertWithMessage; |
Googler | 54998a70 | 2022-07-19 11:03:35 -0700 | [diff] [blame] | 19 | import static com.google.common.truth.TruthJUnit.assume; |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 20 | import static org.junit.Assert.assertThrows; |
Googler | 703c023 | 2022-07-14 10:43:07 -0700 | [diff] [blame] | 21 | import static org.junit.Assert.fail; |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 22 | |
| 23 | import com.google.common.collect.ImmutableList; |
Googler | 703c023 | 2022-07-14 10:43:07 -0700 | [diff] [blame] | 24 | import com.google.common.eventbus.Subscribe; |
| 25 | import com.google.devtools.build.lib.actions.ChangedFilesMessage; |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 26 | import com.google.devtools.build.lib.analysis.BlazeDirectories; |
| 27 | import com.google.devtools.build.lib.buildtool.util.SkyframeIntegrationTestBase; |
| 28 | import com.google.devtools.build.lib.runtime.BlazeModule; |
| 29 | import com.google.devtools.build.lib.runtime.BlazeRuntime; |
| 30 | import com.google.devtools.build.lib.runtime.Command; |
| 31 | import com.google.devtools.build.lib.runtime.WorkspaceBuilder; |
| 32 | import com.google.devtools.build.lib.util.AbruptExitException; |
Googler | 54998a70 | 2022-07-19 11:03:35 -0700 | [diff] [blame] | 33 | import com.google.devtools.build.lib.util.OS; |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 34 | import com.google.devtools.build.lib.vfs.DelegateFileSystem; |
| 35 | import com.google.devtools.build.lib.vfs.FileStatus; |
| 36 | import com.google.devtools.build.lib.vfs.FileSystem; |
janakr | 8ef29dd | 2022-01-06 15:19:16 -0800 | [diff] [blame] | 37 | import com.google.devtools.build.lib.vfs.FileSystemUtils; |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 38 | import com.google.devtools.build.lib.vfs.Path; |
| 39 | import com.google.devtools.build.lib.vfs.PathFragment; |
| 40 | import com.google.devtools.build.skyframe.NotifyingHelper; |
| 41 | import com.google.devtools.common.options.OptionsBase; |
| 42 | import java.io.IOException; |
| 43 | import java.util.HashMap; |
| 44 | import java.util.Map; |
Googler | 703c023 | 2022-07-14 10:43:07 -0700 | [diff] [blame] | 45 | import java.util.concurrent.atomic.AtomicBoolean; |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 46 | import java.util.concurrent.atomic.AtomicInteger; |
| 47 | import org.junit.After; |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 48 | import org.junit.Before; |
| 49 | import org.junit.Test; |
| 50 | import org.junit.runner.RunWith; |
| 51 | import 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) |
| 59 | public 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 { |
Googler | 54998a70 | 2022-07-19 11:03:35 -0700 | [diff] [blame] | 107 | // TODO(b/238606809): Understand why these tests are flaky on Mac. Probably real watchfs bug? |
| 108 | assume().that(OS.getCurrent()).isNotEqualTo(OS.DARWIN); |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 109 | 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 | |
Googler | 703c023 | 2022-07-14 10:43:07 -0700 | [diff] [blame] | 114 | buildTargetWithRetryUntilSeesChange("//foo", "foo/BUILD"); |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 115 | |
| 116 | assertContents("there", "//foo"); |
| 117 | } |
| 118 | |
| 119 | @Test |
Sushain Cherivirala | 4dabe43 | 2023-07-18 08:17:53 -0700 | [diff] [blame] | 120 | 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 |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 172 | public void changedFile_statFails_throwsError() throws Exception { |
Googler | 54998a70 | 2022-07-19 11:03:35 -0700 | [diff] [blame] | 173 | // TODO(b/238606809): Understand why these tests are flaky on Mac. Probably real watchfs bug? |
| 174 | assume().that(OS.getCurrent()).isNotEqualTo(OS.DARWIN); |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 175 | 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 | |
Googler | 703c023 | 2022-07-14 10:43:07 -0700 | [diff] [blame] | 182 | AbruptExitException e = |
| 183 | assertThrows( |
| 184 | AbruptExitException.class, |
| 185 | () -> buildTargetWithRetryUntilSeesChange("//foo", "foo/BUILD")); |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 186 | |
Googler | 33a689c | 2023-06-29 07:57:25 -0700 | [diff] [blame] | 187 | assertThat(e).hasCauseThat().hasCauseThat().hasCauseThat().isInstanceOf(IOException.class); |
Googler | 703c023 | 2022-07-14 10:43:07 -0700 | [diff] [blame] | 188 | } |
| 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"); |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 214 | } |
| 215 | |
janakr | 416ae37 | 2022-01-07 10:16:27 -0800 | [diff] [blame] | 216 | // 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. |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 219 | @Test |
janakr | 8ef29dd | 2022-01-06 15:19:16 -0800 | [diff] [blame] | 220 | public void ignoreOutputFilesThenCheckAgainDoesCheck() throws Exception { |
Paul Tarjan | 123da96 | 2022-01-19 07:25:59 -0800 | [diff] [blame] | 221 | if ("bazel".equals(this.getRuntime().getProductName())) { |
| 222 | // Repository options only in Bazel. |
| 223 | addOptions("--noexperimental_check_external_repository_files"); |
| 224 | } |
janakr | 8ef29dd | 2022-01-06 15:19:16 -0800 | [diff] [blame] | 225 | 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 |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 252 | public void externalSymlink_doesNotTriggerFullGraphTraversal() throws Exception { |
| 253 | addOptions("--symlink_prefix=/"); |
Paul Tarjan | 123da96 | 2022-01-19 07:25:59 -0800 | [diff] [blame] | 254 | if ("bazel".equals(this.getRuntime().getProductName())) { |
| 255 | // Repository options only in Bazel. |
| 256 | addOptions("--noexperimental_check_external_repository_files"); |
| 257 | } |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 258 | 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); |
Googler | b5052b8 | 2022-11-03 11:33:56 -0700 | [diff] [blame] | 279 | // getValuesAndExceptions() called during output file checking (although if an output service is |
| 280 | // able to report modified files in practice there is no iteration). |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 281 | |
| 282 | buildTarget("//hello:BUILD"); |
Paul Tarjan | 123da96 | 2022-01-19 07:25:59 -0800 | [diff] [blame] | 283 | assertThat(calledGetValues.getAndSet(0)).isEqualTo(1); |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 284 | |
| 285 | // Now bring the external symlink into Bazel's awareness. |
| 286 | buildTarget("//hello:target"); |
| 287 | assertContents("one", "//hello:target"); |
Paul Tarjan | 123da96 | 2022-01-19 07:25:59 -0800 | [diff] [blame] | 288 | assertThat(calledGetValues.getAndSet(0)).isEqualTo(1); |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 289 | |
| 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 Tarjan | 123da96 | 2022-01-19 07:25:59 -0800 | [diff] [blame] | 293 | assertThat(calledGetValues.getAndSet(0)).isEqualTo(1); |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 294 | |
| 295 | write(externalLink, "two"); |
| 296 | |
| 297 | buildTarget("//hello:target"); |
| 298 | // External file changes are tracked. |
| 299 | assertContents("two", "//hello:target"); |
Paul Tarjan | 123da96 | 2022-01-19 07:25:59 -0800 | [diff] [blame] | 300 | assertThat(calledGetValues.getAndSet(0)).isEqualTo(1); |
janakr | ac8dcc6 | 2022-01-06 13:09:06 -0800 | [diff] [blame] | 301 | } |
| 302 | } |