| // 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 org.junit.Assert.assertThrows; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Sets; |
| import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; |
| import com.google.devtools.build.lib.skyframe.DiffAwareness.View; |
| import com.google.devtools.build.lib.skyframe.LocalDiffAwareness.SequentialView; |
| import com.google.devtools.build.lib.testing.common.FakeOptions; |
| import com.google.devtools.build.lib.util.OS; |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; |
| import com.google.devtools.build.lib.vfs.ModifiedFileSet; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.lib.vfs.Root; |
| import com.google.devtools.common.options.OptionsProvider; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import java.io.IOException; |
| import java.nio.file.Paths; |
| import java.util.Set; |
| import org.junit.After; |
| import org.junit.Assume; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.rules.TestName; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * Integration tests for LocalDiffAwareness. |
| */ |
| @RunWith(JUnit4.class) |
| public class LocalDiffAwarenessTest extends BuildIntegrationTestCase { |
| |
| /** Try this many times to pick up file changes. Inotify needs some nanoseconds of patience. */ |
| private static final int MAX_RETRY_COUNT = 20; |
| |
| private OptionsProvider watchFsEnabledProvider; |
| private LocalDiffAwareness localDiff; |
| private DiffAwareness.View oldView; |
| private Path testCaseRoot; |
| |
| @org.junit.Rule |
| public TestName name = new TestName(); |
| |
| @Before |
| public final void initializeSettings() throws Exception { |
| LocalDiffAwareness.Factory factory = new LocalDiffAwareness.Factory(ImmutableList.<String>of()); |
| // Make sure all test functions have their own directory to test |
| testCaseRoot = testRoot.getChild(name.getMethodName()); |
| testCaseRoot.createDirectoryAndParents(); |
| localDiff = (LocalDiffAwareness) factory.maybeCreate(Root.fromPath(testCaseRoot)); |
| |
| LocalDiffAwareness.Options localDiffOptions = new LocalDiffAwareness.Options(); |
| localDiffOptions.watchFS = true; |
| watchFsEnabledProvider = FakeOptions.of(localDiffOptions); |
| |
| // Ignore test failures when run on a Mac. |
| // |
| // On a Mac, LocalDiffAwareness.Factory#maybeCreate will produce a MacOSXFsEventsDiffAwareness. |
| // There's a known issue with the underlying implementation |
| // (https://github.com/bazelbuild/bazel/issues/10776); basically all the test cases in here |
| // consistently fail, presumably due to the same underlying issue with FSEvents. Also, |
| // MacOSXFsEventsDiffAwareness is already unit-tested separately anyway (although not very well |
| // because of the bug). |
| Assume.assumeFalse(OS.DARWIN.equals(OS.getCurrent())); |
| } |
| |
| @After |
| public final void closeLocalDiff() throws Exception { |
| localDiff.close(); |
| } |
| |
| @After |
| public final void deleteTestCaseRoot() throws Exception { |
| testCaseRoot.deleteTree(); |
| } |
| |
| private void captureFirstView(OptionsProvider options) throws BrokenDiffAwarenessException { |
| oldView = localDiff.getCurrentView(options); |
| Preconditions.checkNotNull(localDiff); |
| } |
| |
| @Test |
| public void areInSequenceWithEverythingModifiedShouldAlwaysReturnFalse() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| SequentialView old = (SequentialView) oldView; |
| SequentialView everythingMod = (SequentialView) LocalDiffAwareness.EVERYTHING_MODIFIED; |
| assertThat(LocalDiffAwareness.areInSequence(old, everythingMod)).isFalse(); |
| assertThat(LocalDiffAwareness.areInSequence(everythingMod, old)).isFalse(); |
| assertThat(LocalDiffAwareness.areInSequence(everythingMod, everythingMod)).isFalse(); |
| } |
| |
| @Test |
| public void testFileAdded() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| touch("foo.txt"); |
| new ModifiedFileSetChecker().modify("foo.txt").check(); |
| } |
| |
| @Test |
| public void testSymlink() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| touch("a"); |
| symlink("b", "a"); |
| new ModifiedFileSetChecker().modify("a").modify("b").check(); |
| } |
| |
| @Test |
| public void testSymlinkBroken() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| symlink("b", "a"); |
| new ModifiedFileSetChecker().modify("b").check(); |
| } |
| |
| @Test |
| public void testFileModified() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| |
| touch("foo.txt"); |
| new ModifiedFileSetChecker().modify("foo.txt").check(); |
| |
| new ModifiedFileSetChecker().check(); |
| |
| touch("foo.txt"); |
| new ModifiedFileSetChecker().modify("foo.txt").check(); |
| } |
| |
| @Test |
| public void testFileRemoved() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| |
| touch("foo.txt"); |
| new ModifiedFileSetChecker().modify("foo.txt").check(); |
| |
| rm("foo.txt"); |
| new ModifiedFileSetChecker().modify("foo.txt").check(); |
| } |
| |
| @Test |
| public void testFileAddAndRemove() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| |
| touch("foo.txt"); |
| touch("bar.txt"); |
| new ModifiedFileSetChecker().modify("foo.txt").modify("bar.txt").check(); |
| |
| rm("foo.txt"); |
| touch("foo.txt"); |
| new ModifiedFileSetChecker().modify("foo.txt").check(); |
| } |
| |
| @Test |
| public void testAddDirectory() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| |
| mkdir("equestria"); |
| touch("equestria/foo.txt"); |
| new ModifiedFileSetChecker().modify("equestria").modify("equestria/foo.txt").check(); |
| } |
| |
| @Test |
| public void testRemoveDirectory() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| |
| mkdir("equestria"); |
| touch("equestria/foo.txt"); |
| new ModifiedFileSetChecker().modify("equestria").modify("equestria/foo.txt").check(); |
| |
| |
| rm("equestria"); |
| new ModifiedFileSetChecker().modify("equestria").modify("equestria/foo.txt").check(); |
| } |
| |
| @Test |
| public void testLotsOfChanges() throws Exception { |
| captureFirstView(watchFsEnabledProvider); |
| |
| mkdir("pre"); |
| touch("pre/pre.txt"); |
| new ModifiedFileSetChecker().modify("pre").modify("pre/pre.txt").check(); |
| |
| mkdir("a"); |
| touch("a/a1.txt"); |
| touch("a/a2.txt"); |
| rm("a"); |
| mkdir("a"); |
| touch("a/a1.txt"); |
| touch("a/a2.txt"); |
| mkdir("a/b"); |
| touch("a/b/b1.txt"); |
| new ModifiedFileSetChecker() |
| .modify("a") |
| .modify("a/a1.txt") |
| .modify("a/a2.txt") |
| .modify("a/b") |
| .modify("a/b/b1.txt") |
| .check(); |
| |
| rm("a/b/b1.txt"); |
| touch("a/b/b2.txt"); |
| rm("a/b"); |
| mkdir("a/b"); |
| rm("a/b"); |
| mkdir("a/b"); |
| touch("a/b/b3.txt"); |
| new ModifiedFileSetChecker() |
| .modify("a/b") |
| .modify("a/b/b1.txt") |
| .modify("a/b/b2.txt") |
| .modify("a/b/b3.txt") |
| .check(); |
| |
| rm("a/b/b3.txt"); |
| new ModifiedFileSetChecker().modify("a/b/b3.txt").check(); |
| } |
| |
| private static OptionsProvider createWatchFsDisabledProvider() { |
| final LocalDiffAwareness.Options localDiffOptions = new LocalDiffAwareness.Options(); |
| localDiffOptions.watchFS = false; |
| return FakeOptions.of(localDiffOptions); |
| } |
| |
| @Test |
| public void testEnableWatchFs() throws Exception { |
| OptionsProvider watchFsDisabledProvider = createWatchFsDisabledProvider(); |
| captureFirstView(watchFsDisabledProvider); |
| |
| new ModifiedFileSetChecker().checkEverythingModified(watchFsDisabledProvider); |
| |
| touch("a.txt"); |
| new ModifiedFileSetChecker().checkEverythingModified(watchFsDisabledProvider); |
| |
| touch("b.txt"); |
| new ModifiedFileSetChecker().checkEverythingModified(watchFsEnabledProvider); |
| |
| touch("c.txt"); |
| new ModifiedFileSetChecker().modify("c.txt").check(); |
| } |
| |
| @Test |
| public void testDisableWatchFs() throws Exception { |
| OptionsProvider watchFsDisabledProvider = createWatchFsDisabledProvider(); |
| captureFirstView(watchFsEnabledProvider); |
| |
| new ModifiedFileSetChecker().check(); |
| |
| assertThrows( |
| BrokenDiffAwarenessException.class, |
| () -> localDiff.getCurrentView(watchFsDisabledProvider)); |
| } |
| |
| @Test |
| public void modifiedPathIsntUnderWatchRoot() { |
| java.nio.file.Path otherRootDirectoryNioPath = Paths.get("/notundertestroot"); |
| assertThat(otherRootDirectoryNioPath.startsWith(Paths.get(testCaseRoot.getPathString()))) |
| .isFalse(); |
| |
| View oldView = |
| new LocalDiffAwareness.SequentialView( |
| localDiff, /*position=*/ 0, /*modifiedAbsolutePaths=*/ ImmutableSet.of()); |
| View newView = |
| new LocalDiffAwareness.SequentialView( |
| localDiff, |
| /*position=*/ 1, |
| /*modifiedAbsolutePaths=*/ ImmutableSet.of( |
| otherRootDirectoryNioPath.resolve("foo.txt"))); |
| Throwable throwable = |
| assertThrows(BrokenDiffAwarenessException.class, () -> localDiff.getDiff(oldView, newView)); |
| assertThat(throwable) |
| .hasMessageThat() |
| // Do a round-trip through PathFragment to deal with Windows path separators. |
| .contains( |
| PathFragment.create("/notundertestroot/foo.txt is not under ").getPathString() |
| + PathFragment.create(testCaseRoot.getPathString()).getPathString()); |
| } |
| |
| private void touch(String pathString) throws IOException { |
| Path path = testCaseRoot.getRelative(pathString); |
| FileSystemUtils.createEmptyFile(path); |
| FileSystemUtils.writeIsoLatin1(path, "Sunshine, sunshine, ladybugs awake!"); |
| } |
| |
| private void mkdir(String pathString) throws IOException { |
| Path path = testCaseRoot.getRelative(pathString); |
| path.createDirectoryAndParents(); |
| } |
| |
| private void rm(String pathString) throws IOException { |
| Path path = testCaseRoot.getRelative(pathString); |
| path.deleteTree(); |
| } |
| |
| private void symlink(String from, String to) throws IOException { |
| Path fromPath = testCaseRoot.getRelative(from); |
| Path toPath = testCaseRoot.getRelative(to); |
| FileSystemUtils.ensureSymbolicLink(fromPath, toPath); |
| } |
| |
| private class ModifiedFileSetChecker { |
| private final Set<PathFragment> modified = Sets.newHashSet(); |
| |
| public void check() throws Exception { |
| // Unfortunately, inotify needs a few milliseconds (more than a few in the worst case) |
| // after a change to pick up a list of changed files. Trying a few times to make sure. |
| for (int i = 0; i < MAX_RETRY_COUNT; i++) { |
| Thread.sleep(150); |
| DiffAwareness.View newView = localDiff.getCurrentView(watchFsEnabledProvider); |
| ModifiedFileSet modifiedFileSet = localDiff.getDiff(oldView, newView); |
| oldView = newView; |
| assertThat(modifiedFileSet.treatEverythingAsModified()).isFalse(); |
| if (modifiedFileSet.modifiedSourceFiles().isEmpty()) { |
| continue; |
| } |
| assertThat(modifiedFileSet.modifiedSourceFiles()).isEqualTo(modified); |
| return; |
| } |
| // If we never received any changes, make sure this is what we actually expect. |
| assertThat(modified).isEmpty(); |
| } |
| |
| public void checkEverythingModified(OptionsProvider options) throws Exception { |
| DiffAwareness.View newView = localDiff.getCurrentView(options); |
| ModifiedFileSet modifiedFileSet = localDiff.getDiff(oldView, newView); |
| oldView = newView; |
| assertThat(modifiedFileSet.treatEverythingAsModified()).isTrue(); |
| } |
| |
| @CanIgnoreReturnValue |
| public ModifiedFileSetChecker modify(String filename) { |
| modified.add(PathFragment.create(filename)); |
| return this; |
| } |
| } |
| } |