Allow `DiffAwareness` to share precomputed information about the workspace and propagate it to the `WorkspaceStatusAction`. Each successful Bazel build issue 2 `BuildInfo` events -- one based on the result of running the command indicated by `workspace_status_command` and a dummy one based on a subset of available information. Receivers of those discard all but the first one. If a build fails before execution phase, the dummy event will be the only even issued, hence it will be used. `DiffAwareness` operates on the workspace to figure out the diffs and in the process probes properties of it. Allow `DiffAwareness` to share such information with the intention to make it available for the rest of the build. Propagate unanimous precomputed workspace information when it is available from `DiffAwareness` through the `DiffAwarenessManager` and `SkyframeExecutor` to `CommandEnvironment` and store it there to make it available for the rest of the build. Make precomputed workspace information available to the dummy `BuildInfo` event. PiperOrigin-RevId: 355072552
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD index 32a3459..e4aa445 100644 --- a/src/main/java/com/google/devtools/build/lib/BUILD +++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -366,6 +366,7 @@ "//src/main/java/com/google/devtools/build/lib/skyframe:skyframe_cluster", "//src/main/java/com/google/devtools/build/lib/skyframe:target_pattern_phase_value", "//src/main/java/com/google/devtools/build/lib/skyframe:top_down_action_cache", + "//src/main/java/com/google/devtools/build/lib/skyframe:workspace_info", "//src/main/java/com/google/devtools/build/lib/unix", "//src/main/java/com/google/devtools/build/lib/util", "//src/main/java/com/google/devtools/build/lib/util:TestType",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BUILD b/src/main/java/com/google/devtools/build/lib/analysis/BUILD index 220d4d5..52ef94a 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/BUILD +++ b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
@@ -1206,6 +1206,7 @@ "//src/main/java/com/google/devtools/build/lib/actions:artifacts", "//src/main/java/com/google/devtools/build/lib/collect/nestedset", "//src/main/java/com/google/devtools/build/lib/shell", + "//src/main/java/com/google/devtools/build/lib/skyframe:workspace_info", "//src/main/java/com/google/devtools/build/lib/util", "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code", "//src/main/java/com/google/devtools/build/lib/vfs", @@ -1213,6 +1214,7 @@ "//src/main/java/com/google/devtools/common/options", "//src/main/protobuf:failure_details_java_proto", "//third_party:guava", + "//third_party:jsr305", ], )
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java b/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java index a4cd247..3e1205f 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java
@@ -26,6 +26,7 @@ import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; import com.google.devtools.build.lib.server.FailureDetails.WorkspaceStatus; import com.google.devtools.build.lib.server.FailureDetails.WorkspaceStatus.Code; +import com.google.devtools.build.lib.skyframe.WorkspaceInfoFromDiff; import com.google.devtools.build.lib.util.DetailedExitCode; import com.google.devtools.build.lib.util.OptionsUtils; import com.google.devtools.build.lib.vfs.FileSystemUtils; @@ -42,6 +43,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; /** * An action writing the workspace status files. @@ -176,6 +178,10 @@ public interface DummyEnvironment { Path getWorkspace(); + /** Returns optional precomputed workspace info to include in the build info event. */ + @Nullable + WorkspaceInfoFromDiff getWorkspaceInfoFromDiff(); + String getBuildRequestId(); OptionsProvider getOptions();
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java index 09ddea4..7cc89e5 100644 --- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java +++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
@@ -55,6 +55,7 @@ import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration; import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor; +import com.google.devtools.build.lib.skyframe.WorkspaceInfoFromDiff; import com.google.devtools.build.lib.skyframe.actiongraph.v2.ActionGraphDump; import com.google.devtools.build.lib.skyframe.actiongraph.v2.AqueryOutputHandler; import com.google.devtools.build.lib.skyframe.actiongraph.v2.AqueryOutputHandler.OutputType; @@ -274,6 +275,12 @@ public OptionsProvider getOptions() { return env.getOptions(); } + + @Nullable + @Override + public WorkspaceInfoFromDiff getWorkspaceInfoFromDiff() { + return env.getWorkspaceInfoFromDiff(); + } }))); } }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java b/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java index 7639bcc..39ddcc9 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java
@@ -14,6 +14,8 @@ package com.google.devtools.build.lib.runtime; +import static com.google.common.base.Preconditions.checkState; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -41,6 +43,7 @@ import com.google.devtools.build.lib.skyframe.SkyframeBuildView; import com.google.devtools.build.lib.skyframe.SkyframeExecutor; import com.google.devtools.build.lib.skyframe.TopDownActionCache; +import com.google.devtools.build.lib.skyframe.WorkspaceInfoFromDiff; import com.google.devtools.build.lib.util.AbruptExitException; import com.google.devtools.build.lib.util.DetailedExitCode; import com.google.devtools.build.lib.util.io.OutErr; @@ -103,6 +106,7 @@ private TopDownActionCache topDownActionCache; private String workspaceName; private boolean hasSyncedPackageLoading = false; + @Nullable private WorkspaceInfoFromDiff workspaceInfoFromDiff; // This AtomicReference is set to: // - null, if neither BlazeModuleEnvironment#exit nor #precompleteCommand have been called @@ -513,7 +517,7 @@ } public void setWorkspaceName(String workspaceName) { - Preconditions.checkState(this.workspaceName == null, "workspace name can only be set once"); + checkState(this.workspaceName == null, "workspace name can only be set once"); this.workspaceName = workspaceName; eventBus.post(new ExecRootEvent(getExecRoot())); } @@ -577,6 +581,18 @@ this.outputService = outputService; } + /** + * Returns precomputed workspace information or null. + * + * <p>Precomputed workspace info is an optimization allowing to share information about the + * workspace if it was derived at the time of synchronizing the workspace. This way we can make it + * available earlier during the build and avoid retrieving it again. + */ + @Nullable + public WorkspaceInfoFromDiff getWorkspaceInfoFromDiff() { + return workspaceInfoFromDiff; + } + public ActionCache getPersistentActionCache() throws IOException { return workspace.getPersistentActionCache(reporter); } @@ -675,16 +691,17 @@ "We should never call this method more than once over the course of a single command"); } hasSyncedPackageLoading = true; - getSkyframeExecutor() - .sync( - reporter, - options.getOptions(PackageOptions.class), - packageLocator, - options.getOptions(BuildLanguageOptions.class), - getCommandId(), - clientEnv, - timestampGranularityMonitor, - options); + workspaceInfoFromDiff = + getSkyframeExecutor() + .sync( + reporter, + options.getOptions(PackageOptions.class), + packageLocator, + options.getOptions(BuildLanguageOptions.class), + getCommandId(), + clientEnv, + timestampGranularityMonitor, + options); } public void recordLastExecutionTime() {
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD index a021adf..8b07bc2 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD +++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -214,6 +214,7 @@ ":tree_artifact_value", ":unloaded_toolchain_context", ":unloaded_toolchain_context_impl", + ":workspace_info", ":workspace_name_function", ":workspace_name_value", ":workspace_status_function", @@ -1286,6 +1287,7 @@ deps = [ ":broken_diff_awareness_exception", ":incompatible_view_exception", + ":workspace_info", "//src/main/java/com/google/devtools/build/lib/vfs", "//src/main/java/com/google/devtools/common/options", "//third_party:jsr305", @@ -1299,6 +1301,7 @@ ":broken_diff_awareness_exception", ":diff_awareness", ":incompatible_view_exception", + ":workspace_info", "//src/main/java/com/google/devtools/build/lib/events", "//src/main/java/com/google/devtools/build/lib/vfs", "//src/main/java/com/google/devtools/common/options", @@ -2799,6 +2802,11 @@ ) java_library( + name = "workspace_info", + srcs = ["WorkspaceInfoFromDiff.java"], +) + +java_library( name = "workspace_name_function", srcs = ["WorkspaceNameFunction.java"], deps = [
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java b/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java index da0ee7d..a1e3ad7 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java
@@ -13,7 +13,7 @@ // limitations under the License. package com.google.devtools.build.lib.skyframe; -import com.google.common.base.Preconditions; +import static com.google.common.base.Preconditions.checkNotNull; /** * Thrown on {@link DiffAwareness#getDiff} to indicate that something is wrong with the @@ -22,6 +22,10 @@ public class BrokenDiffAwarenessException extends Exception { public BrokenDiffAwarenessException(String msg) { - super(Preconditions.checkNotNull(msg)); + super(checkNotNull(msg)); + } + + public BrokenDiffAwarenessException(String msg, Throwable cause) { + super(checkNotNull(msg), cause); } }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java index 0579d9e..b05b070 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java
@@ -45,6 +45,11 @@ /** Opaque view of the filesystem under a package path entry at a specific point in time. */ interface View { + /** Returns workspace info unanimously associated with the package path or null. */ + @Nullable + default WorkspaceInfoFromDiff getWorkspaceInfo() { + return null; + } } /**
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java index ed56b41..1958441 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java
@@ -69,6 +69,9 @@ public interface ProcessableModifiedFileSet { ModifiedFileSet getModifiedFileSet(); + @Nullable + WorkspaceInfoFromDiff getWorkspaceInfo(); + /** * This should be called when the changes have been noted. Otherwise, the result from the next * call to {@link #getDiff} will be from the baseline of the old, unprocessed, diff. @@ -99,7 +102,7 @@ if (baselineView == null) { logger.atInfo().log("Initial baseline view for %s is %s", pathEntry, newView); diffAwarenessState.baselineView = newView; - return BrokenProcessableModifiedFileSet.INSTANCE; + return new InitialModifiedFileSet(newView.getWorkspaceInfo()); } ModifiedFileSet diff; @@ -172,6 +175,12 @@ return modifiedFileSet; } + @Nullable + @Override + public WorkspaceInfoFromDiff getWorkspaceInfo() { + return nextView.getWorkspaceInfo(); + } + @Override public void markProcessed() { DiffAwarenessState diffAwarenessState = currentDiffAwarenessStates.get(pathEntry); @@ -191,6 +200,36 @@ return ModifiedFileSet.EVERYTHING_MODIFIED; } + @Nullable + @Override + public WorkspaceInfoFromDiff getWorkspaceInfo() { + return null; + } + + @Override + public void markProcessed() {} + } + + /** Modified file set for a clean build. */ + private static class InitialModifiedFileSet implements ProcessableModifiedFileSet { + + @Nullable private final WorkspaceInfoFromDiff workspaceInfo; + + InitialModifiedFileSet(@Nullable WorkspaceInfoFromDiff workspaceInfo) { + this.workspaceInfo = workspaceInfo; + } + + @Override + public ModifiedFileSet getModifiedFileSet() { + return ModifiedFileSet.EVERYTHING_MODIFIED; + } + + @Nullable + @Override + public WorkspaceInfoFromDiff getWorkspaceInfo() { + return workspaceInfo; + } + @Override public void markProcessed() { }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java index 35fdfe2..5f704bf 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java
@@ -226,8 +226,9 @@ return recordingDiffer; } + @Nullable @Override - public void sync( + public WorkspaceInfoFromDiff sync( ExtendedEventHandler eventHandler, PackageOptions packageOptions, PathPackageLocator packageLocator, @@ -262,11 +263,13 @@ tsgm, options); long startTime = System.nanoTime(); - handleDiffs(eventHandler, packageOptions.checkOutputFiles, options); + WorkspaceInfoFromDiff workspaceInfo = + handleDiffs(eventHandler, packageOptions.checkOutputFiles, options); long stopTime = System.nanoTime(); Profiler.instance().logSimpleTask(startTime, stopTime, ProfilerTask.INFO, "handleDiffs"); long duration = stopTime - startTime; sourceDiffCheckingDuration = duration > 0 ? Duration.ofNanos(duration) : Duration.ZERO; + return workspaceInfo; } /** @@ -341,7 +344,8 @@ handleDiffs(eventHandler, /*checkOutputFiles=*/false, OptionsProvider.EMPTY); } - private void handleDiffs( + @Nullable + private WorkspaceInfoFromDiff handleDiffs( ExtendedEventHandler eventHandler, boolean checkOutputFiles, OptionsProvider options) throws InterruptedException, AbruptExitException { TimestampGranularityMonitor tsgm = this.tsgm.get(); @@ -354,13 +358,18 @@ invalidateCachedWorkspacePathsStates(); } + WorkspaceInfoFromDiff workspaceInfo = null; Map<Root, DiffAwarenessManager.ProcessableModifiedFileSet> modifiedFilesByPathEntry = Maps.newHashMap(); Set<Pair<Root, DiffAwarenessManager.ProcessableModifiedFileSet>> pathEntriesWithoutDiffInformation = Sets.newHashSet(); - for (Root pathEntry : pkgLocator.get().getPathEntries()) { + ImmutableList<Root> pkgRoots = pkgLocator.get().getPathEntries(); + for (Root pathEntry : pkgRoots) { DiffAwarenessManager.ProcessableModifiedFileSet modifiedFileSet = diffAwarenessManager.getDiff(eventHandler, pathEntry, options); + if (pkgRoots.size() == 1) { + workspaceInfo = modifiedFileSet.getWorkspaceInfo(); + } if (modifiedFileSet.getModifiedFileSet().treatEverythingAsModified()) { pathEntriesWithoutDiffInformation.add(Pair.of(pathEntry, modifiedFileSet)); } else { @@ -381,6 +390,7 @@ managedDirectoriesChanged, fsvcThreads); handleClientEnvironmentChanges(); + return workspaceInfo; } /**
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java index 07713cf..52f8e24 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -2668,8 +2668,13 @@ /** * Initializes and syncs the graph with the given options, readying it for the next evaluation. + * + * <p>Returns precomputed information about the workspace if it is available at this stage. This + * is an optimization allowing implementations which have such information to make it available + * early in the build. */ - public void sync( + @Nullable + public WorkspaceInfoFromDiff sync( ExtendedEventHandler eventHandler, PackageOptions packageOptions, PathPackageLocator pathPackageLocator, @@ -2696,6 +2701,7 @@ dropConfiguredTargetsNow(eventHandler); lastAnalysisDiscarded = false; } + return null; } protected void syncPackageLoading(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceInfoFromDiff.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceInfoFromDiff.java new file mode 100644 index 0000000..c8520d3 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceInfoFromDiff.java
@@ -0,0 +1,17 @@ +// Copyright 2021 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; + +/** Information for a workspace computed at the time of collecting diff. */ +public interface WorkspaceInfoFromDiff {}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD index 6b2a59e..bc02a34 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD +++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -87,7 +87,6 @@ ], deps = select({ "//src/conditions:darwin": [ - "//src/main/java/com/google/devtools/build/lib/skyframe:incompatible_view_exception", "//src/main/java/com/google/devtools/build/lib/skyframe:local_diff_awareness", "//src/main/java/com/google/devtools/build/lib/testing/common:fake-options", ], @@ -201,6 +200,7 @@ "//src/main/java/com/google/devtools/build/lib/skyframe:glob_descriptor", "//src/main/java/com/google/devtools/build/lib/skyframe:glob_function", "//src/main/java/com/google/devtools/build/lib/skyframe:glob_value", + "//src/main/java/com/google/devtools/build/lib/skyframe:incompatible_view_exception", "//src/main/java/com/google/devtools/build/lib/skyframe:local_repository_lookup_value", "//src/main/java/com/google/devtools/build/lib/skyframe:managed_directories_knowledge", "//src/main/java/com/google/devtools/build/lib/skyframe:metadata_consumer_for_metrics", @@ -234,6 +234,7 @@ "//src/main/java/com/google/devtools/build/lib/skyframe:tree_artifact_value", "//src/main/java/com/google/devtools/build/lib/skyframe:unloaded_toolchain_context", "//src/main/java/com/google/devtools/build/lib/skyframe:toolchain_context_key", + "//src/main/java/com/google/devtools/build/lib/skyframe:workspace_info", "//src/main/java/com/google/devtools/build/lib/skyframe:workspace_name_value", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManagerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManagerTest.java index 8cd2ecb..853ebe7 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManagerTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManagerTest.java
@@ -15,11 +15,16 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.devtools.build.lib.events.util.EventCollectionApparatus; +import com.google.devtools.build.lib.skyframe.DiffAwareness.View; import com.google.devtools.build.lib.skyframe.DiffAwarenessManager.ProcessableModifiedFileSet; import com.google.devtools.build.lib.vfs.DigestHashFunction; import com.google.devtools.build.lib.vfs.FileSystem; @@ -195,6 +200,135 @@ processableDiff.markProcessed(); } + @Test + public void getDiff_cleanBuild_propagatesWorkspaceInfo() throws Exception { + Root pathEntry = Root.fromPath(fs.getPath("/path")); + WorkspaceInfoFromDiff workspaceInfo = new WorkspaceInfoFromDiff() {}; + DiffAwareness diffAwareness = mock(DiffAwareness.class); + when(diffAwareness.getCurrentView(any())).thenReturn(createView(workspaceInfo)); + DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class); + when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness); + DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory)); + + ProcessableModifiedFileSet diff = + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + assertThat(diff.getWorkspaceInfo()).isSameInstanceAs(workspaceInfo); + } + + @Test + public void getDiff_incrementalBuild_propagatesLatestWorkspaceInfo() throws Exception { + Root pathEntry = Root.fromPath(fs.getPath("/path")); + WorkspaceInfoFromDiff workspaceInfo1 = new WorkspaceInfoFromDiff() {}; + WorkspaceInfoFromDiff workspaceInfo2 = new WorkspaceInfoFromDiff() {}; + DiffAwareness diffAwareness = mock(DiffAwareness.class); + View view1 = createView(workspaceInfo1); + View view2 = createView(workspaceInfo2); + when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2); + when(diffAwareness.getDiff(view1, view2)) + .thenReturn(ModifiedFileSet.builder().modify(PathFragment.create("file")).build()); + DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class); + when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness); + DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory)); + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + ProcessableModifiedFileSet diff = + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + assertThat(diff.getWorkspaceInfo()).isSameInstanceAs(workspaceInfo2); + } + + @Test + public void getDiff_incrementalBuildNoChange_propagatesNewWorkspaceInfo() throws Exception { + Root pathEntry = Root.fromPath(fs.getPath("/path")); + WorkspaceInfoFromDiff workspaceInfo1 = new WorkspaceInfoFromDiff() {}; + WorkspaceInfoFromDiff workspaceInfo2 = new WorkspaceInfoFromDiff() {}; + DiffAwareness diffAwareness = mock(DiffAwareness.class); + View view1 = createView(workspaceInfo1); + View view2 = createView(workspaceInfo2); + when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2); + when(diffAwareness.getDiff(view1, view2)).thenReturn(ModifiedFileSet.NOTHING_MODIFIED); + DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class); + when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness); + DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory)); + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + ProcessableModifiedFileSet diff = + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + assertThat(diff.getWorkspaceInfo()).isSameInstanceAs(workspaceInfo2); + } + + @Test + public void getDiff_incrementalBuildWithNoWorkspaceInfo_returnsDiffWithNullWorkspaceInfo() + throws Exception { + Root pathEntry = Root.fromPath(fs.getPath("/path")); + DiffAwareness diffAwareness = mock(DiffAwareness.class); + View view1 = createView(new WorkspaceInfoFromDiff() {}); + View view2 = createView(/*workspaceInfo=*/ null); + when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2); + when(diffAwareness.getDiff(view1, view2)) + .thenReturn(ModifiedFileSet.builder().modify(PathFragment.create("file")).build()); + DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class); + when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness); + DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory)); + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + ProcessableModifiedFileSet diff = + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + assertThat(diff.getWorkspaceInfo()).isNull(); + } + + @Test + public void getDiff_brokenDiffAwareness_returnsDiffWithNullWorkspaceInfo() throws Exception { + Root pathEntry = Root.fromPath(fs.getPath("/path")); + WorkspaceInfoFromDiff workspaceInfo1 = new WorkspaceInfoFromDiff() {}; + WorkspaceInfoFromDiff workspaceInfo2 = new WorkspaceInfoFromDiff() {}; + DiffAwareness diffAwareness = mock(DiffAwareness.class); + View view1 = createView(workspaceInfo1); + View view2 = createView(workspaceInfo2); + when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2); + when(diffAwareness.getDiff(view1, view2)).thenThrow(BrokenDiffAwarenessException.class); + DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class); + when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness); + DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory)); + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + ProcessableModifiedFileSet diff = + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + assertThat(diff.getWorkspaceInfo()).isNull(); + } + + @Test + public void getDiff_incompatibleDiff_fails() throws Exception { + Root pathEntry = Root.fromPath(fs.getPath("/path")); + DiffAwareness diffAwareness = mock(DiffAwareness.class); + View view1 = createView(/*workspaceInfo=*/ null); + View view2 = createView(/*workspaceInfo=*/ null); + when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2); + when(diffAwareness.getDiff(view1, view2)).thenThrow(IncompatibleViewException.class); + DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class); + when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness); + DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory)); + manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY); + + assertThrows( + IllegalStateException.class, + () -> manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY)); + } + + private static View createView(@Nullable WorkspaceInfoFromDiff workspaceInfo) { + return new View() { + @Nullable + @Override + public WorkspaceInfoFromDiff getWorkspaceInfo() { + return workspaceInfo; + } + }; + } + private static class DiffAwarenessFactoryStub implements DiffAwareness.Factory { private final Map<Root, DiffAwareness> diffAwarenesses = Maps.newHashMap();