| // 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; | |
| private Path testCaseIgnoredDir; | |
| @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(); | |
| testCaseIgnoredDir = testCaseRoot.getChild("ignored-dir"); | |
| testCaseIgnoredDir.createDirectoryAndParents(); | |
| localDiff = | |
| (LocalDiffAwareness) | |
| factory.maybeCreate(Root.fromPath(testCaseRoot), ImmutableSet.of(testCaseIgnoredDir)); | |
| 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 testIgnoredFileModified() throws Exception { | |
| captureFirstView(watchFsEnabledProvider); | |
| touch(testCaseIgnoredDir.getRelative("foo").getPathString()); | |
| new ModifiedFileSetChecker().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; | |
| } | |
| } | |
| } |