Open-source AnalysisCachingTest.

--
MOS_MIGRATED_REVID=108496188
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/AnalysisCachingTest.java b/src/test/java/com/google/devtools/build/lib/analysis/AnalysisCachingTest.java
new file mode 100644
index 0000000..124e09c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/AnalysisCachingTest.java
@@ -0,0 +1,455 @@
+// Copyright 2015 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.analysis;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.util.AnalysisCachingTestBase;
+import com.google.devtools.build.lib.rules.java.JavaSourceJarsProvider;
+import com.google.devtools.build.lib.testutil.Suite;
+import com.google.devtools.build.lib.testutil.TestSpec;
+
+import java.util.Set;
+
+/**
+ * Analysis caching tests.
+ */
+@TestSpec(size = Suite.SMALL_TESTS)
+public class AnalysisCachingTest extends AnalysisCachingTestBase {
+
+  public void testSimpleCleanAnalysis() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'])");
+    update("//java/a:A");
+    ConfiguredTarget javaTest = getConfiguredTarget("//java/a:A");
+    assertNotNull(javaTest);
+    assertNotNull(javaTest.getProvider(JavaSourceJarsProvider.class));
+  }
+
+  public void testTickTock() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'])",
+        "java_test(name = 'B',",
+        "          srcs = ['B.java'])");
+    update("//java/a:A");
+    update("//java/a:B");
+    update("//java/a:A");
+  }
+
+  public void testFullyCached() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'])");
+    update("//java/a:A");
+    ConfiguredTarget old = getConfiguredTarget("//java/a:A");
+    update("//java/a:A");
+    ConfiguredTarget current = getConfiguredTarget("//java/a:A");
+    assertSame(old, current);
+  }
+
+  public void testSubsetCached() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'])",
+        "java_test(name = 'B',",
+        "          srcs = ['B.java'])");
+    update("//java/a:A", "//java/a:B");
+    ConfiguredTarget old = getConfiguredTarget("//java/a:A");
+    update("//java/a:A");
+    ConfiguredTarget current = getConfiguredTarget("//java/a:A");
+    assertSame(old, current);
+  }
+
+  public void testDependencyChanged() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'],",
+        "          deps = ['//java/b'])");
+    scratch.file("java/b/BUILD",
+        "java_library(name = 'b',",
+        "             srcs = ['B.java'])");
+    update("//java/a:A");
+    ConfiguredTarget old = getConfiguredTarget("//java/a:A");
+    scratch.overwriteFile("java/b/BUILD",
+        "java_library(name = 'b',",
+        "             srcs = ['C.java'])");
+    update("//java/a:A");
+    ConfiguredTarget current = getConfiguredTarget("//java/a:A");
+    assertNotSame(old, current);
+  }
+
+  public void testTopLevelChanged() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'],",
+        "          deps = ['//java/b'])");
+    scratch.file("java/b/BUILD",
+        "java_library(name = 'b',",
+        "             srcs = ['B.java'])");
+    update("//java/a:A");
+    ConfiguredTarget old = getConfiguredTarget("//java/a:A");
+    scratch.overwriteFile("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'])");
+    update("//java/a:A");
+    ConfiguredTarget current = getConfiguredTarget("//java/a:A");
+    assertNotSame(old, current);
+  }
+
+  // Regression test for:
+  // "action conflict detection is incorrect if conflict is in non-top-level configured targets".
+  public void testActionConflictInDependencyImpliesTopLevelTargetFailure() throws Exception {
+    useConfiguration("--force_pic");
+    scratch.file("conflict/BUILD",
+        "cc_library(name='x', srcs=['foo.cc'])",
+        "cc_binary(name='_objs/x/conflict/foo.pic.o', srcs=['bar.cc'])",
+        "cc_binary(name='foo', deps=['x'], data=['_objs/x/conflict/foo.pic.o'])");
+    reporter.removeHandler(failFastHandler); // expect errors
+    update(defaultFlags().with(Flag.KEEP_GOING), "//conflict:foo");
+    assertContainsEvent("file 'conflict/_objs/x/conflict/foo.pic.o' " + CONFLICT_MSG);
+    assertThat(getAnalysisResult().getTargetsToBuild()).isEmpty();
+  }
+
+  /**
+   * Generating the same output from two targets is ok if we build them on successive builds
+   * and invalidate the first target before we build the second target. This is a strictly weaker
+   * test than if we didn't invalidate the first target, but since Skyframe can't pass then, this
+   * test could be useful for it. Actually, since Skyframe makes multiple update calls, it manages
+   * to unregister actions even when it shouldn't, and so this test can incorrectly pass. However,
+   * {@code SkyframeExecutorTest#testNoActionConflictWithInvalidatedTarget} tests it more
+   * rigorously.
+   */
+  public void testNoActionConflictWithInvalidatedTarget() throws Exception {
+    useConfiguration("--force_pic");
+    scratch.file("conflict/BUILD",
+        "cc_library(name='x', srcs=['foo.cc'])",
+        "cc_binary(name='_objs/x/conflict/foo.pic.o', srcs=['bar.cc'])");
+    update("//conflict:x");
+    ConfiguredTarget conflict = getConfiguredTarget("//conflict:x");
+    Action oldAction = getGeneratingAction(getBinArtifact("_objs/x/conflict/foo.pic.o", conflict));
+    assertEquals("//conflict:x", oldAction.getOwner().getLabel().toString());
+    scratch.overwriteFile("conflict/BUILD",
+        "cc_library(name='newx', srcs=['foo.cc'])", // Rename target.
+        "cc_binary(name='_objs/x/conflict/foo.pic.o', srcs=['bar.cc'])");
+    update(defaultFlags(), "//conflict:_objs/x/conflict/foo.pic.o");
+    ConfiguredTarget objsConflict = getConfiguredTarget("//conflict:_objs/x/conflict/foo.pic.o");
+    Action newAction =
+        getGeneratingAction(getBinArtifact("_objs/x/conflict/foo.pic.o", objsConflict));
+    assertEquals("//conflict:_objs/x/conflict/foo.pic.o",
+        newAction.getOwner().getLabel().toString());
+  }
+
+  /**
+   * Generating the same output from multiple actions is causing an error.
+   */
+  public void testActionConflictCausesError() throws Exception {
+    useConfiguration("--force_pic");
+    scratch.file("conflict/BUILD",
+        "cc_library(name='x', srcs=['foo.cc'])",
+        "cc_binary(name='_objs/x/conflict/foo.pic.o', srcs=['bar.cc'])");
+    reporter.removeHandler(failFastHandler); // expect errors
+    update(defaultFlags().with(Flag.KEEP_GOING),
+        "//conflict:x", "//conflict:_objs/x/conflict/foo.pic.o");
+    assertContainsEvent("file 'conflict/_objs/x/conflict/foo.pic.o' " + CONFLICT_MSG);
+  }
+
+  public void testNoActionConflictErrorAfterClearedAnalysis() throws Exception {
+    useConfiguration("--force_pic");
+    scratch.file("conflict/BUILD",
+                "cc_library(name='x', srcs=['foo.cc'])",
+                "cc_binary(name='_objs/x/conflict/foo.pic.o', srcs=['bar.cc'])");
+    reporter.removeHandler(failFastHandler); // expect errors
+    update(defaultFlags().with(Flag.KEEP_GOING),
+        "//conflict:x", "//conflict:_objs/x/conflict/foo.pic.o");
+    // We want to force a "dropConfiguredTargetsNow" operation, which won't inform the
+    // invalidation receiver about the dropped configured targets.
+    getView().clearAnalysisCache(ImmutableList.<ConfiguredTarget>of());
+    assertContainsEvent("file 'conflict/_objs/x/conflict/foo.pic.o' " + CONFLICT_MSG);
+    eventCollector.clear();
+    scratch.overwriteFile("conflict/BUILD",
+        "cc_library(name='x', srcs=['baz.cc'])",
+        "cc_binary(name='_objs/x/conflict/foo.pic.o', srcs=['bar.cc'])");
+    update(defaultFlags().with(Flag.KEEP_GOING),
+        "//conflict:x", "//conflict:_objs/x/conflict/foo.pic.o");
+    assertNoEvents();
+  }
+
+  /**
+   * The current action conflict detection code will only mark one of the targets as having an
+   * error, and with multi-threaded analysis it is not deterministic which one that will be.
+   */
+  public void testActionConflictMarksTargetInvalid() throws Exception {
+    useConfiguration("--force_pic");
+    scratch.file("conflict/BUILD",
+        "cc_library(name='x', srcs=['foo.cc'])",
+        "cc_binary(name='_objs/x/conflict/foo.pic.o', srcs=['bar.cc'])");
+    reporter.removeHandler(failFastHandler); // expect errors
+    update(defaultFlags().with(Flag.KEEP_GOING),
+        "//conflict:x", "//conflict:_objs/x/conflict/foo.pic.o");
+    ConfiguredTarget a = getConfiguredTarget("//conflict:x");
+    ConfiguredTarget b = getConfiguredTarget("//conflict:_objs/x/conflict/foo.pic.o");
+    assertTrue(hasTopLevelAnalysisError(a) ^ hasTopLevelAnalysisError(b));
+  }
+
+  /**
+   *  BUILD file involved in BUILD-file cycle is changed
+   */
+  public void testBuildFileInCycleChanged() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'],",
+        "          deps = ['//java/b'])");
+    scratch.file("java/b/BUILD",
+        "java_library(name = 'b',",
+        "          srcs = ['B.java'],",
+        "          deps = ['//java/c'])");
+    scratch.file("java/c/BUILD",
+        "java_library(name = 'c',",
+        "          srcs = ['C.java'],",
+        "          deps = ['//java/b'])");
+    // expect error
+    reporter.removeHandler(failFastHandler);
+    update(defaultFlags().with(Flag.KEEP_GOING), "//java/a:A");
+    ConfiguredTarget old = getConfiguredTarget("//java/a:A");
+    // drop dependency on from b to c
+    scratch.overwriteFile("java/b/BUILD",
+        "java_library(name = 'b',",
+        "             srcs = ['B.java'])");
+    eventCollector.clear();
+    reporter.addHandler(failFastHandler);
+    update("//java/a:A");
+    ConfiguredTarget current = getConfiguredTarget("//java/a:A");
+    assertNotSame(old, current);
+  }
+
+  private void assertNoTargetsVisited() {
+    Set<?> analyzedTargets = getSkyframeEvaluatedTargetKeys();
+    assertEquals(analyzedTargets.toString(), 0, analyzedTargets.size());
+  }
+
+  public void testSecondRunAllCacheHits() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'])");
+    update("//java/a:A");
+    update("//java/a:A");
+    assertNoTargetsVisited();
+  }
+
+  public void testDependencyAllCacheHits() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_library(name = 'x', srcs = ['A.java'], deps = ['y'])",
+        "java_library(name = 'y', srcs = ['B.java'])");
+    update("//java/a:x");
+    Set<?> oldAnalyzedTargets = getSkyframeEvaluatedTargetKeys();
+    assertTrue(oldAnalyzedTargets.size() >= 2);  // could be greater due to implicit deps
+    assertEquals(1, countObjectsPartiallyMatchingRegex(oldAnalyzedTargets, "//java/a:x"));
+    assertEquals(1, countObjectsPartiallyMatchingRegex(oldAnalyzedTargets, "//java/a:y"));
+    update("//java/a:y");
+    assertNoTargetsVisited();
+  }
+
+  public void testSupersetNotAllCacheHits() throws Exception {
+    scratch.file("java/a/BUILD",
+        // It's important that all targets are of the same rule class, otherwise the second update
+        // call might analyze more than one extra target because of potential implicit dependencies.
+        "java_library(name = 'x', srcs = ['A.java'], deps = ['y'])",
+        "java_library(name = 'y', srcs = ['B.java'], deps = ['z'])",
+        "java_library(name = 'z', srcs = ['C.java'])");
+    update("//java/a:y");
+    Set<?> oldAnalyzedTargets = getSkyframeEvaluatedTargetKeys();
+    assertTrue(oldAnalyzedTargets.size() >= 3);  // could be greater due to implicit deps
+    assertEquals(0, countObjectsPartiallyMatchingRegex(oldAnalyzedTargets, "//java/a:x"));
+    assertEquals(1, countObjectsPartiallyMatchingRegex(oldAnalyzedTargets, "//java/a:y"));
+    update("//java/a:x");
+    Set<?> newAnalyzedTargets = getSkyframeEvaluatedTargetKeys();
+    assertTrue(newAnalyzedTargets.size() >= 1);  // could be greater due to implicit deps
+    assertEquals(1, countObjectsPartiallyMatchingRegex(newAnalyzedTargets, "//java/a:x"));
+    assertEquals(0, countObjectsPartiallyMatchingRegex(newAnalyzedTargets, "//java/a:y"));
+  }
+
+  public void testExtraActions() throws Exception {
+    scratch.file("java/com/google/a/BUILD", "java_library(name='a', srcs=['A.java'])");
+    scratch.file("java/com/google/b/BUILD", "java_library(name='b', srcs=['B.java'])");
+    scratch.file("extra/BUILD",
+        "extra_action(name = 'extra',",
+        "             out_templates = ['$(OWNER_LABEL_DIGEST)_$(ACTION_ID).tst'],",
+        "             cmd = '')",
+        "action_listener(name = 'listener',",
+        "                mnemonics = ['Javac'],",
+        "                extra_actions = [':extra'])");
+
+    useConfiguration("--experimental_action_listener=//extra:listener");
+    update("//java/com/google/a:a");
+    update("//java/com/google/b:b");
+  }
+
+  public void testExtraActionsCaching() throws Exception {
+    scratch.file("java/a/BUILD", "java_library(name='a', srcs=['A.java'])");
+    scratch.file("extra/BUILD",
+        "extra_action(name = 'extra',",
+        "             out_templates = ['$(OWNER_LABEL_DIGEST)_$(ACTION_ID).tst'],",
+        "             cmd = 'echo $(EXTRA_ACTION_FILE)')",
+        "action_listener(name = 'listener',",
+        "                mnemonics = ['Javac'],",
+        "                extra_actions = [':extra'])");
+    useConfiguration("--experimental_action_listener=//extra:listener");
+
+    update("//java/a:a");
+    getConfiguredTarget("//java/a:a");
+
+    scratch.overwriteFile("extra/BUILD",
+        "extra_action(name = 'extra',",
+        "             out_templates = ['$(OWNER_LABEL_DIGEST)_$(ACTION_ID).tst'],",
+        "             cmd = 'echo $(BUG)')", // <-- change here
+        "action_listener(name = 'listener',",
+        "                mnemonics = ['Javac'],",
+        "                extra_actions = [':extra'])");
+    reporter.removeHandler(failFastHandler);
+    try {
+      update("//java/a:a");
+      fail();
+    } catch (ViewCreationFailedException e) {
+      assertThat(e.getMessage()).contains("Analysis of target '//java/a:a' failed");
+      assertContainsEvent("Unable to expand make variables: $(BUG)");
+    }
+  }
+
+  public void testConfigurationCachingWithWarningReplay() throws Exception {
+    useConfiguration("--test_sharding_strategy=experimental_heuristic");
+    update();
+    assertContainsEvent("Heuristic sharding is intended as a one-off experimentation tool");
+    eventCollector.clear();
+    update();
+    assertContainsEvent("Heuristic sharding is intended as a one-off experimentation tool");
+  }
+  
+  public void testWorkspaceStatusCommandIsNotCachedForNullBuild() throws Exception {
+    update();
+    WorkspaceStatusAction actionA = getView().getLastWorkspaceBuildInfoActionForTesting();
+    assertEquals("DummyBuildInfoAction", actionA.getMnemonic());
+
+    workspaceStatusActionFactory.setKey("Second");
+    update();
+    WorkspaceStatusAction actionB = getView().getLastWorkspaceBuildInfoActionForTesting();
+    assertEquals("DummyBuildInfoActionSecond", actionB.getMnemonic());
+  }
+
+  public void testSkyframeCacheInvalidationBuildFileChange() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'])");
+    String aTarget = "//java/a:A";
+    update(aTarget);
+    ConfiguredTarget firstCT = getConfiguredTarget(aTarget);
+
+    scratch.overwriteFile("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['B.java'])");
+
+    update(aTarget);
+    ConfiguredTarget updatedCT = getConfiguredTarget(aTarget);
+    assertNotSame(firstCT, updatedCT);
+
+    update(aTarget);
+    ConfiguredTarget updated2CT = getConfiguredTarget(aTarget);
+    assertSame(updatedCT, updated2CT);
+  }
+
+  public void testSkyframeDifferentPackagesInvalidation() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_test(name = 'A',",
+        "          srcs = ['A.java'])");
+
+    scratch.file("java/b/BUILD",
+        "java_test(name = 'B',",
+        "          srcs = ['B.java'])");
+
+    String aTarget = "//java/a:A";
+    update(aTarget);
+    ConfiguredTarget oldAConfTarget = getConfiguredTarget(aTarget);
+    String bTarget = "//java/b:B";
+    update(bTarget);
+    ConfiguredTarget oldBConfTarget = getConfiguredTarget(bTarget);
+
+    scratch.overwriteFile("java/b/BUILD",
+        "java_test(name = 'B',",
+        "          srcs = ['C.java'])");
+
+    update(aTarget);
+    // Check that 'A' was not invalidated because 'B' was modified and invalidated.
+    ConfiguredTarget newAConfTarget = getConfiguredTarget(aTarget);
+    ConfiguredTarget newBConfTarget = getConfiguredTarget(bTarget);
+
+    assertSame(oldAConfTarget, newAConfTarget);
+    assertNotSame(oldBConfTarget, newBConfTarget);
+  }
+
+  private int countObjectsPartiallyMatchingRegex(Iterable<? extends Object> elements,
+      String toStringMatching) {
+    toStringMatching = ".*" + toStringMatching + ".*";
+    int result = 0;
+    for (Object o : elements) {
+      if (o.toString().matches(toStringMatching)) {
+        ++result;
+      }
+    }
+    return result;
+  }
+
+  public void testGetSkyframeEvaluatedTargetKeysOmitsCachedTargets() throws Exception {
+    scratch.file("java/a/BUILD",
+        "java_library(name = 'x', srcs = ['A.java'], deps = ['z', 'w'])",
+        "java_library(name = 'y', srcs = ['B.java'], deps = ['z', 'w'])",
+        "java_library(name = 'z', srcs = ['C.java'])",
+        "java_library(name = 'w', srcs = ['D.java'])");
+
+    update("//java/a:x");
+    Set<?> oldAnalyzedTargets = getSkyframeEvaluatedTargetKeys();
+    assertTrue(oldAnalyzedTargets.size() >= 2);  // could be greater due to implicit deps
+    assertEquals(1, countObjectsPartiallyMatchingRegex(oldAnalyzedTargets, "//java/a:x"));
+    assertEquals(0, countObjectsPartiallyMatchingRegex(oldAnalyzedTargets, "//java/a:y"));
+    assertEquals(1, countObjectsPartiallyMatchingRegex(oldAnalyzedTargets, "//java/a:z"));
+    assertEquals(1, countObjectsPartiallyMatchingRegex(oldAnalyzedTargets, "//java/a:w"));
+
+    // Unless the build is not fully cached, we get notified about newly evaluated targets, as well
+    // as cached top-level targets. For the two tests above to work correctly, we need to ensure
+    // that getSkyframeEvaluatedTargetKeys() doesn't return these.
+    update("//java/a:x", "//java/a:y", "//java/a:z");
+    Set<?> newAnalyzedTargets = getSkyframeEvaluatedTargetKeys();
+    assertThat(newAnalyzedTargets).hasSize(2);
+    assertEquals(1, countObjectsPartiallyMatchingRegex(newAnalyzedTargets, "//java/a:B.java"));
+    assertEquals(1, countObjectsPartiallyMatchingRegex(newAnalyzedTargets, "//java/a:y"));
+  }
+
+  /**
+   * {link AnalysisCachingTest} without loading phase.
+   */
+  public static class AnalysisCachingTestWithoutLoading extends AnalysisCachingTest {
+    @Override
+    public void setUp() throws Exception {
+      disableLoading();
+      super.setUp();
+    }
+
+    // Error processing without loading phase is not working properly yet.
+    @Override
+    public void testBuildFileInCycleChanged() {}
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisCachingTestBase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisCachingTestBase.java
new file mode 100644
index 0000000..6db4202
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisCachingTestBase.java
@@ -0,0 +1,41 @@
+// Copyright 2015 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.analysis.util;
+
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+
+/**
+ * Base class for analysis caching tests.
+ */
+public abstract class AnalysisCachingTestBase extends AnalysisTestCase {
+
+  protected static final String CONFLICT_MSG = "is generated by these conflicting actions:";
+
+  // In skyframe if configuredTarget contains error then null is returned.
+  protected boolean hasTopLevelAnalysisError(ConfiguredTarget configuredTarget) {
+    return !getAnalysisResult().getTargetsToBuild().contains(configuredTarget);
+  }
+
+  protected void assertEventCached(String target, String expectedWarning) throws Exception {
+    reporter.removeHandler(failFastHandler);
+    // Run with keep_going, so this method can also be used for errors (which otherwise throw an
+    // exception).
+    update(defaultFlags().with(Flag.KEEP_GOING), target);
+    assertContainsEvent(expectedWarning);
+    eventCollector.clear();
+    update(defaultFlags().with(Flag.KEEP_GOING), target);
+    assertContainsEvent(expectedWarning);
+  }
+}