blob: cefc4a9b5b8fe258dae2733ef25be3fe6b03ceab [file] [log] [blame]
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +00001// Copyright 2016 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
jmmvb77b2882020-04-20 13:38:20 -070017import static org.junit.Assume.assumeFalse;
18
19import com.google.common.collect.HashMultimap;
juliexxia4fa081c2018-08-21 09:45:49 -070020import com.google.common.collect.ImmutableMap;
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000021import com.google.common.collect.ImmutableSet;
jmmvb77b2882020-04-20 13:38:20 -070022import com.google.common.collect.Iterables;
23import com.google.common.collect.Multimap;
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000024import com.google.devtools.build.lib.skyframe.DiffAwareness.View;
Ulf Adamsde14ade2016-10-14 14:20:31 +000025import com.google.devtools.build.lib.skyframe.LocalDiffAwareness.Options;
jmmvb77b2882020-04-20 13:38:20 -070026import com.google.devtools.build.lib.vfs.ModifiedFileSet;
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000027import com.google.devtools.build.lib.vfs.PathFragment;
Ulf Adamsde14ade2016-10-14 14:20:31 +000028import com.google.devtools.common.options.OptionsBase;
juliexxia618a0762018-08-17 08:33:52 -070029import com.google.devtools.common.options.OptionsProvider;
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000030import java.io.IOException;
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000031import java.nio.file.FileVisitResult;
32import java.nio.file.Files;
33import java.nio.file.Path;
34import java.nio.file.SimpleFileVisitor;
35import java.nio.file.attribute.BasicFileAttributes;
jmmvb77b2882020-04-20 13:38:20 -070036import java.util.Arrays;
jmmv37131332020-02-12 11:57:34 -080037import java.util.HashSet;
juliexxia4fa081c2018-08-21 09:45:49 -070038import java.util.Map;
jmmv37131332020-02-12 11:57:34 -080039import java.util.Set;
jmmvb77b2882020-04-20 13:38:20 -070040import java.util.concurrent.CountDownLatch;
41import java.util.concurrent.ExecutorService;
42import java.util.concurrent.Executors;
43import java.util.concurrent.atomic.AtomicReference;
44import java.util.logging.Logger;
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000045import org.junit.After;
46import org.junit.Before;
philwo4f2a56a2020-05-04 05:41:28 -070047import org.junit.Ignore;
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000048import org.junit.Test;
49import org.junit.runner.RunWith;
50import org.junit.runners.JUnit4;
51
52/** Tests for {@link MacOSXFsEventsDiffAwareness} */
53@RunWith(JUnit4.class)
54public class MacOSXFsEventsDiffAwarenessTest {
55
jmmvb77b2882020-04-20 13:38:20 -070056 private static Logger logger = Logger.getLogger(MacOSXFsEventsDiffAwarenessTest.class.getName());
57
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000058 private static void rmdirs(Path directory) throws IOException {
59 Files.walkFileTree(
60 directory,
61 new SimpleFileVisitor<Path>() {
62 @Override
63 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
64 throws IOException {
65 Files.delete(file);
66 return FileVisitResult.CONTINUE;
67 }
68
69 @Override
70 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
71 Files.delete(dir);
72 return FileVisitResult.CONTINUE;
73 }
74 });
75 }
76
77 private MacOSXFsEventsDiffAwareness underTest;
78 private Path watchedPath;
juliexxia618a0762018-08-17 08:33:52 -070079 private OptionsProvider watchFsEnabledProvider;
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000080
81 @Before
82 public void setUp() throws Exception {
83 watchedPath = com.google.common.io.Files.createTempDir().getCanonicalFile().toPath();
84 underTest = new MacOSXFsEventsDiffAwareness(watchedPath.toString());
Ulf Adamsde14ade2016-10-14 14:20:31 +000085 LocalDiffAwareness.Options localDiffOptions = new LocalDiffAwareness.Options();
86 localDiffOptions.watchFS = true;
87 watchFsEnabledProvider = new LocalDiffAwarenessOptionsProvider(localDiffOptions);
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000088 }
89
90 @After
91 public void tearDown() throws Exception {
92 underTest.close();
93 rmdirs(watchedPath);
94 }
95
jmmvb77b2882020-04-20 13:38:20 -070096 private void scratchDir(String path) throws IOException {
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000097 Path p = watchedPath.resolve(path);
jmmvb77b2882020-04-20 13:38:20 -070098 p.toFile().mkdirs();
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +000099 }
100
101 private void scratchFile(String path) throws IOException {
jmmvb77b2882020-04-20 13:38:20 -0700102 Path p = watchedPath.resolve(path);
103 com.google.common.io.Files.write(new byte[] {}, p.toFile());
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +0000104 }
105
jmmv37131332020-02-12 11:57:34 -0800106 /**
107 * Checks that the union of the diffs between the current view and each member of some consecutive
108 * sequence of views is the specific set of given files.
109 *
110 * @param view1 the view to compare to
111 * @param rawPaths the files to expect in the view
112 * @return the new view
113 */
jmmvb77b2882020-04-20 13:38:20 -0700114 private View assertDiff(View view1, Iterable<String> rawPaths)
jmmv37131332020-02-12 11:57:34 -0800115 throws IncompatibleViewException, BrokenDiffAwarenessException, InterruptedException {
116 Set<PathFragment> pathsYetToBeSeen = new HashSet<>();
117 for (String path : rawPaths) {
118 pathsYetToBeSeen.add(PathFragment.create(path));
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +0000119 }
jmmv37131332020-02-12 11:57:34 -0800120
121 // fsevents may be delayed (especially under machine load), which means that we may not notice
122 // all file system changes in one go. Try enough times (multiple seconds) for the events to be
123 // delivered. Given that each time we call getCurrentView we may get a subset of the total
124 // events we expect, track the events we have already seen by subtracting them from the
125 // pathsYetToBeSeen set.
126 int attempts = 0;
127 for (; ; ) {
128 View view2 = underTest.getCurrentView(watchFsEnabledProvider);
129
jmmvb77b2882020-04-20 13:38:20 -0700130 ModifiedFileSet diff = underTest.getDiff(view1, view2);
131 // If fsevents lost events (e.g. because we weren't fast enough processing them or because
132 // too many happened at the same time), there is nothing we can do. Yes, this means that if
133 // our fsevents monitor always returns "everything modified", we aren't really testing
134 // anything here... but let's assume we don't have such an obvious bug...
135 assumeFalse("Lost events; diff unknown", diff.equals(ModifiedFileSet.EVERYTHING_MODIFIED));
136
137 ImmutableSet<PathFragment> modifiedSourceFiles = diff.modifiedSourceFiles();
jmmv37131332020-02-12 11:57:34 -0800138 pathsYetToBeSeen.removeAll(modifiedSourceFiles);
139 if (pathsYetToBeSeen.isEmpty()) {
140 // Found all paths that we wanted to see as modified.
141 return view2;
142 }
143
144 if (attempts == 600) {
145 throw new AssertionError("Paths " + pathsYetToBeSeen + " not found as modified");
146 }
jmmvb77b2882020-04-20 13:38:20 -0700147 logger.info("Still have to see " + pathsYetToBeSeen.size() + " paths");
jmmv37131332020-02-12 11:57:34 -0800148 Thread.sleep(100);
149 attempts++;
150 view1 = view2; // getDiff requires views to be sequential if we want to get meaningful data.
151 }
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +0000152 }
153
154 @Test
philwo4f2a56a2020-05-04 05:41:28 -0700155 @Ignore("Test is flaky; see https://github.com/bazelbuild/bazel/issues/10776")
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +0000156 public void testSimple() throws Exception {
Ulf Adamsde14ade2016-10-14 14:20:31 +0000157 View view1 = underTest.getCurrentView(watchFsEnabledProvider);
jmmv37131332020-02-12 11:57:34 -0800158
jmmvb77b2882020-04-20 13:38:20 -0700159 scratchDir("a/b");
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +0000160 scratchFile("a/b/c");
jmmvb77b2882020-04-20 13:38:20 -0700161 scratchDir("b/c");
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +0000162 scratchFile("b/c/d");
jmmvb77b2882020-04-20 13:38:20 -0700163 View view2 = assertDiff(view1, Arrays.asList("a", "a/b", "a/b/c", "b", "b/c", "b/c/d"));
jmmv37131332020-02-12 11:57:34 -0800164
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +0000165 rmdirs(watchedPath.resolve("a"));
166 rmdirs(watchedPath.resolve("b"));
jmmvb77b2882020-04-20 13:38:20 -0700167 assertDiff(view2, Arrays.asList("a", "a/b", "a/b/c", "b", "b/c", "b/c/d"));
168 }
169
170 @Test
philwo4f2a56a2020-05-04 05:41:28 -0700171 @Ignore("Test is flaky; see https://github.com/bazelbuild/bazel/issues/10776")
jmmvb77b2882020-04-20 13:38:20 -0700172 public void testStress() throws Exception {
173 View view1 = underTest.getCurrentView(watchFsEnabledProvider);
174
175 // Attempt to cause fsevents to drop events by performing a lot of concurrent file accesses
176 // which then may result in our own callback in fsevents.cc not being able to keep up.
177 // There is no guarantee that we'll trigger this condition, but on 2020-02-28 on a Mac Pro
178 // 2013, this happened pretty predictably with the settings below.
179 logger.info("Starting file creation under " + watchedPath);
180 ExecutorService executor = Executors.newCachedThreadPool();
181 int nThreads = 100;
182 int nFilesPerThread = 100;
183 Multimap<String, String> dirToFilesToCreate = HashMultimap.create();
184 for (int i = 0; i < nThreads; i++) {
185 String dir = "" + i;
186 for (int j = 0; j < nFilesPerThread; j++) {
187 String file = dir + "/" + j;
188 dirToFilesToCreate.put(dir, file);
189 }
190 }
191 CountDownLatch latch = new CountDownLatch(nThreads);
192 AtomicReference<IOException> firstError = new AtomicReference<>(null);
193 dirToFilesToCreate
194 .asMap()
195 .forEach(
196 (dir, files) ->
197 executor.submit(
198 () -> {
199 try {
200 scratchDir(dir);
201 for (String file : files) {
202 scratchFile(file);
203 }
204 } catch (IOException e) {
205 firstError.compareAndSet(null, e);
206 }
207 latch.countDown();
208 }));
209 latch.await();
210 executor.shutdown();
211 IOException e = firstError.get();
212 if (e != null) {
213 throw e;
214 }
215
216 assertDiff(view1, Iterables.concat(dirToFilesToCreate.keySet(), dirToFilesToCreate.values()));
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +0000217 }
Ulf Adamsde14ade2016-10-14 14:20:31 +0000218
219 /**
220 * Only returns a fixed options class for {@link LocalDiffAwareness.Options}.
221 */
juliexxia618a0762018-08-17 08:33:52 -0700222 private static final class LocalDiffAwarenessOptionsProvider implements OptionsProvider {
Ulf Adamsde14ade2016-10-14 14:20:31 +0000223 private final Options localDiffOptions;
224
225 private LocalDiffAwarenessOptionsProvider(Options localDiffOptions) {
226 this.localDiffOptions = localDiffOptions;
227 }
228
229 @Override
230 public <O extends OptionsBase> O getOptions(Class<O> optionsClass) {
231 if (optionsClass.equals(LocalDiffAwareness.Options.class)) {
232 return optionsClass.cast(localDiffOptions);
233 }
234 return null;
235 }
juliexxia4fa081c2018-08-21 09:45:49 -0700236
237 @Override
juliexxia692d1482018-11-29 09:24:44 -0800238 public Map<String, Object> getStarlarkOptions() {
juliexxia4fa081c2018-08-21 09:45:49 -0700239 return ImmutableMap.of();
240 }
Ulf Adamsde14ade2016-10-14 14:20:31 +0000241 }
Damien Martin-Guillerez2988e102016-10-13 20:29:41 +0000242}