| // Copyright 2014 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.skyframe; | 
 |  | 
 | import static com.google.common.truth.Truth.assertThat; | 
 | import static com.google.devtools.build.skyframe.GraphTester.CONCATENATE; | 
 | import static com.google.devtools.build.skyframe.GraphTester.NODE_TYPE; | 
 | import static org.junit.Assert.fail; | 
 |  | 
 | import com.google.common.base.Preconditions; | 
 | import com.google.common.collect.ImmutableList; | 
 | import com.google.common.collect.Iterables; | 
 | import com.google.common.collect.Sets; | 
 | import com.google.common.eventbus.EventBus; | 
 | import com.google.common.testing.GcFinalization; | 
 | import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor; | 
 | import com.google.devtools.build.lib.events.Reporter; | 
 | import com.google.devtools.build.lib.testutil.TestUtils; | 
 | import com.google.devtools.build.lib.util.Pair; | 
 | import com.google.devtools.build.skyframe.GraphTester.StringValue; | 
 | import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.DeletingNodeVisitor; | 
 | import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.DirtyingInvalidationState; | 
 | import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.DirtyingNodeVisitor; | 
 | import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.InvalidationState; | 
 | import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.InvalidationType; | 
 | import com.google.devtools.build.skyframe.QueryableGraph.Reason; | 
 | import java.lang.ref.WeakReference; | 
 | import java.util.HashSet; | 
 | import java.util.Random; | 
 | import java.util.Set; | 
 | import java.util.concurrent.CountDownLatch; | 
 | import java.util.concurrent.TimeUnit; | 
 | import java.util.concurrent.atomic.AtomicReference; | 
 | import org.junit.After; | 
 | import org.junit.Before; | 
 | import org.junit.Test; | 
 | import org.junit.experimental.runners.Enclosed; | 
 | import org.junit.runner.RunWith; | 
 | import org.junit.runners.JUnit4; | 
 |  | 
 | /** | 
 |  * Tests for {@link InvalidatingNodeVisitor}. | 
 |  */ | 
 | @RunWith(Enclosed.class) | 
 | public class EagerInvalidatorTest { | 
 |   protected InMemoryGraphImpl graph; | 
 |   protected GraphTester tester = new GraphTester(); | 
 |   protected InvalidationState state = newInvalidationState(); | 
 |   protected AtomicReference<InvalidatingNodeVisitor<?>> visitor = new AtomicReference<>(); | 
 |   protected DirtyTrackingProgressReceiver progressReceiver; | 
 |   private IntVersion graphVersion = IntVersion.of(0); | 
 |  | 
 |   @After | 
 |   public void assertNoTrackedErrors() { | 
 |     TrackingAwaiter.INSTANCE.assertNoErrors(); | 
 |   } | 
 |  | 
 |   // The following three methods should be abstract, but junit4 does not allow us to run inner | 
 |   // classes in an abstract outer class. Thus, we provide implementations. These methods will never | 
 |   // be run because only the inner classes, annotated with @RunWith, will actually be executed. | 
 |   EvaluationProgressReceiver.InvalidationState expectedState() { | 
 |     throw new UnsupportedOperationException(); | 
 |   } | 
 |  | 
 |   @SuppressWarnings("unused") // Overridden by subclasses. | 
 |   void invalidate( | 
 |       InMemoryGraph graph, DirtyTrackingProgressReceiver progressReceiver, SkyKey... keys) | 
 |       throws InterruptedException { | 
 |     throw new UnsupportedOperationException(); | 
 |   } | 
 |  | 
 |   boolean gcExpected() { throw new UnsupportedOperationException(); } | 
 |  | 
 |   private boolean isInvalidated(SkyKey key) { | 
 |     NodeEntry entry = graph.get(null, Reason.OTHER, key); | 
 |     if (gcExpected()) { | 
 |       return entry == null; | 
 |     } else { | 
 |       return entry == null || entry.isDirty(); | 
 |     } | 
 |   } | 
 |  | 
 |   private void assertChanged(SkyKey key) { | 
 |     NodeEntry entry = graph.get(null, Reason.OTHER, key); | 
 |     if (gcExpected()) { | 
 |       assertThat(entry).isNull(); | 
 |     } else { | 
 |       assertThat(entry.isChanged()).isTrue(); | 
 |     } | 
 |   } | 
 |  | 
 |   private void assertDirtyAndNotChanged(SkyKey key) { | 
 |     NodeEntry entry = graph.get(null, Reason.OTHER, key); | 
 |     if (gcExpected()) { | 
 |       assertThat(entry).isNull(); | 
 |     } else { | 
 |       assertThat(entry.isDirty()).isTrue(); | 
 |       assertThat(entry.isChanged()).isFalse(); | 
 |     } | 
 |  | 
 |   } | 
 |  | 
 |   protected InvalidationState newInvalidationState() { | 
 |     throw new UnsupportedOperationException("Sublcasses must override"); | 
 |   } | 
 |  | 
 |   protected InvalidationType defaultInvalidationType() { | 
 |     throw new UnsupportedOperationException("Sublcasses must override"); | 
 |   } | 
 |  | 
 |   protected boolean reverseDepsPresent() { | 
 |     throw new UnsupportedOperationException("Subclasses must override"); | 
 |   } | 
 |  | 
 |   // Convenience method for eval-ing a single value. | 
 |   protected SkyValue eval(boolean keepGoing, SkyKey key) throws InterruptedException { | 
 |     SkyKey[] keys = { key }; | 
 |     return eval(keepGoing, keys).get(key); | 
 |   } | 
 |  | 
 |   protected <T extends SkyValue> EvaluationResult<T> eval(boolean keepGoing, SkyKey... keys) | 
 |     throws InterruptedException { | 
 |     Reporter reporter = new Reporter(new EventBus()); | 
 |     ParallelEvaluator evaluator = | 
 |         new ParallelEvaluator( | 
 |             graph, | 
 |             graphVersion, | 
 |             tester.getSkyFunctionMap(), | 
 |             reporter, | 
 |             new MemoizingEvaluator.EmittedEventState(), | 
 |             InMemoryMemoizingEvaluator.DEFAULT_STORED_EVENT_FILTER, | 
 |             ErrorInfoManager.UseChildErrorInfoIfNecessary.INSTANCE, | 
 |             keepGoing, | 
 |             200, | 
 |             new DirtyTrackingProgressReceiver(null)); | 
 |     graphVersion = graphVersion.next(); | 
 |     return evaluator.eval(ImmutableList.copyOf(keys)); | 
 |   } | 
 |  | 
 |   protected void invalidateWithoutError(DirtyTrackingProgressReceiver progressReceiver, | 
 |       SkyKey... keys) throws InterruptedException { | 
 |     invalidate(graph, progressReceiver, keys); | 
 |     assertThat(state.isEmpty()).isTrue(); | 
 |   } | 
 |  | 
 |   protected void set(String name, String value) { | 
 |     tester.set(name, new StringValue(value)); | 
 |   } | 
 |  | 
 |   protected SkyKey skyKey(String name) { | 
 |     return GraphTester.toSkyKeys(name).get(0); | 
 |   } | 
 |  | 
 |   protected void assertValueValue(String name, String expectedValue) throws InterruptedException { | 
 |     StringValue value = (StringValue) eval(false, skyKey(name)); | 
 |     assertThat(value.getValue()).isEqualTo(expectedValue); | 
 |   } | 
 |  | 
 |   @Before | 
 |   public void setUp() throws Exception { | 
 |     progressReceiver = new DirtyTrackingProgressReceiver(null); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void receiverWorks() throws Exception { | 
 |     final Set<SkyKey> invalidated = Sets.newConcurrentHashSet(); | 
 |     DirtyTrackingProgressReceiver receiver = new DirtyTrackingProgressReceiver( | 
 |         new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { | 
 |       @Override | 
 |       public void invalidated(SkyKey skyKey, InvalidationState state) { | 
 |         Preconditions.checkState(state == expectedState()); | 
 |         invalidated.add(skyKey); | 
 |       } | 
 |     }); | 
 |     graph = new InMemoryGraphImpl(); | 
 |     set("a", "a"); | 
 |     set("b", "b"); | 
 |     tester.getOrCreate("ab").addDependency("a").addDependency("b") | 
 |         .setComputedValue(CONCATENATE); | 
 |     assertValueValue("ab", "ab"); | 
 |  | 
 |     set("a", "c"); | 
 |     invalidateWithoutError(receiver, skyKey("a")); | 
 |     assertThat(invalidated).containsExactly(skyKey("a"), skyKey("ab")); | 
 |     assertValueValue("ab", "cb"); | 
 |     set("b", "d"); | 
 |     invalidateWithoutError(receiver, skyKey("b")); | 
 |     assertThat(invalidated).containsExactly(skyKey("a"), skyKey("ab"), skyKey("b")); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void receiverIsNotifiedAboutNodesInError() throws Exception { | 
 |     final Set<SkyKey> invalidated = Sets.newConcurrentHashSet(); | 
 |     DirtyTrackingProgressReceiver receiver = new DirtyTrackingProgressReceiver( | 
 |         new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { | 
 |       @Override | 
 |       public void invalidated(SkyKey skyKey, InvalidationState state) { | 
 |         Preconditions.checkState(state == expectedState()); | 
 |         invalidated.add(skyKey); | 
 |       } | 
 |     }); | 
 |  | 
 |     // Given a graph consisting of two nodes, "a" and "ab" such that "ab" depends on "a", | 
 |     // And given "ab" is in error, | 
 |     graph = new InMemoryGraphImpl(); | 
 |     set("a", "a"); | 
 |     tester.getOrCreate("ab").addDependency("a").setHasError(true); | 
 |     eval(false, skyKey("ab")); | 
 |  | 
 |     // When "a" is invalidated, | 
 |     invalidateWithoutError(receiver, skyKey("a")); | 
 |  | 
 |     // Then the invalidation receiver is notified of both "a" and "ab"'s invalidations. | 
 |     assertThat(invalidated).containsExactly(skyKey("a"), skyKey("ab")); | 
 |  | 
 |     // Note that this behavior isn't strictly required for correctness. This test is | 
 |     // meant to document current behavior and protect against programming error. | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void invalidateValuesNotInGraph() throws Exception { | 
 |     final Set<SkyKey> invalidated = Sets.newConcurrentHashSet(); | 
 |     DirtyTrackingProgressReceiver receiver = new DirtyTrackingProgressReceiver( | 
 |         new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { | 
 |       @Override | 
 |       public void invalidated(SkyKey skyKey, InvalidationState state) { | 
 |         Preconditions.checkState(state == InvalidationState.DIRTY); | 
 |         invalidated.add(skyKey); | 
 |       } | 
 |     }); | 
 |     graph = new InMemoryGraphImpl(); | 
 |     invalidateWithoutError(receiver, skyKey("a")); | 
 |     assertThat(invalidated).isEmpty(); | 
 |     set("a", "a"); | 
 |     assertValueValue("a", "a"); | 
 |     invalidateWithoutError(receiver, skyKey("b")); | 
 |     assertThat(invalidated).isEmpty(); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void invalidatedValuesAreGCedAsExpected() throws Exception { | 
 |     SkyKey key = GraphTester.skyKey("a"); | 
 |     HeavyValue heavyValue = new HeavyValue(); | 
 |     WeakReference<HeavyValue> weakRef = new WeakReference<>(heavyValue); | 
 |     tester.set("a", heavyValue); | 
 |  | 
 |     graph = new InMemoryGraphImpl(); | 
 |     eval(false, key); | 
 |     invalidate(graph, new DirtyTrackingProgressReceiver(null), key); | 
 |  | 
 |     tester = null; | 
 |     heavyValue = null; | 
 |     if (gcExpected()) { | 
 |       GcFinalization.awaitClear(weakRef); | 
 |     } else { | 
 |       // Not a reliable check, but better than nothing. | 
 |       System.gc(); | 
 |       Thread.sleep(300); | 
 |       assertThat(weakRef.get()).isNotNull(); | 
 |     } | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void reverseDepsConsistent() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     set("a", "a"); | 
 |     set("b", "b"); | 
 |     set("c", "c"); | 
 |     tester.getOrCreate("ab").addDependency("a").addDependency("b").setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate("bc").addDependency("b").addDependency("c").setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate("ab_c").addDependency("ab").addDependency("c") | 
 |         .setComputedValue(CONCATENATE); | 
 |     eval(false, skyKey("ab_c"), skyKey("bc")); | 
 |  | 
 |     assertThat(graph.get(null, Reason.OTHER, skyKey("a")).getReverseDepsForDoneEntry()) | 
 |         .containsExactly(skyKey("ab")); | 
 |     assertThat(graph.get(null, Reason.OTHER, skyKey("b")).getReverseDepsForDoneEntry()) | 
 |         .containsExactly(skyKey("ab"), skyKey("bc")); | 
 |     assertThat(graph.get(null, Reason.OTHER, skyKey("c")).getReverseDepsForDoneEntry()) | 
 |         .containsExactly(skyKey("ab_c"), skyKey("bc")); | 
 |  | 
 |     invalidateWithoutError(new DirtyTrackingProgressReceiver(null), skyKey("ab")); | 
 |     eval(false); | 
 |  | 
 |     // The graph values should be gone. | 
 |     assertThat(isInvalidated(skyKey("ab"))).isTrue(); | 
 |     assertThat(isInvalidated(skyKey("abc"))).isTrue(); | 
 |  | 
 |     // The reverse deps to ab and ab_c should have been removed if reverse deps are cleared. | 
 |     Set<SkyKey> reverseDeps = new HashSet<>(); | 
 |     if (reverseDepsPresent()) { | 
 |       reverseDeps.add(skyKey("ab")); | 
 |     } | 
 |     assertThat(graph.get(null, Reason.OTHER, skyKey("a")).getReverseDepsForDoneEntry()) | 
 |         .containsExactlyElementsIn(reverseDeps); | 
 |     reverseDeps.add(skyKey("bc")); | 
 |     assertThat(graph.get(null, Reason.OTHER, skyKey("b")).getReverseDepsForDoneEntry()) | 
 |         .containsExactlyElementsIn(reverseDeps); | 
 |     reverseDeps.clear(); | 
 |     if (reverseDepsPresent()) { | 
 |       reverseDeps.add(skyKey("ab_c")); | 
 |     } | 
 |     reverseDeps.add(skyKey("bc")); | 
 |     assertThat(graph.get(null, Reason.OTHER, skyKey("c")).getReverseDepsForDoneEntry()) | 
 |         .containsExactlyElementsIn(reverseDeps); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void interruptChild() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     int numValues = 50; // More values than the invalidator has threads. | 
 |     final SkyKey[] family = new SkyKey[numValues]; | 
 |     final SkyKey child = GraphTester.skyKey("child"); | 
 |     final StringValue childValue = new StringValue("child"); | 
 |     tester.set(child, childValue); | 
 |     family[0] = child; | 
 |     for (int i = 1; i < numValues; i++) { | 
 |       SkyKey member = skyKey(Integer.toString(i)); | 
 |       tester.getOrCreate(member).addDependency(family[i - 1]).setComputedValue(CONCATENATE); | 
 |       family[i] = member; | 
 |     } | 
 |     SkyKey parent = GraphTester.skyKey("parent"); | 
 |     tester.getOrCreate(parent).addDependency(family[numValues - 1]).setComputedValue(CONCATENATE); | 
 |     eval(/*keepGoing=*/false, parent); | 
 |     final Thread mainThread = Thread.currentThread(); | 
 |     final AtomicReference<SkyKey> badKey = new AtomicReference<>(); | 
 |     DirtyTrackingProgressReceiver receiver = | 
 |         new DirtyTrackingProgressReceiver( | 
 |             new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { | 
 |               @Override | 
 |               public void invalidated(SkyKey skyKey, InvalidationState state) { | 
 |                 if (skyKey.equals(child)) { | 
 |                   // Interrupt on the very first invalidate | 
 |                   mainThread.interrupt(); | 
 |                 } else if (!skyKey.functionName().equals(NODE_TYPE)) { | 
 |                   // All other invalidations should have the GraphTester's key type. | 
 |                   // Exceptions thrown here may be silently dropped, so keep track of errors | 
 |                   // ourselves. | 
 |                   badKey.set(skyKey); | 
 |                 } | 
 |                 try { | 
 |                   assertThat( | 
 |                           visitor | 
 |                               .get() | 
 |                               .getInterruptionLatchForTestingOnly() | 
 |                               .await(2, TimeUnit.HOURS)) | 
 |                       .isTrue(); | 
 |                 } catch (InterruptedException e) { | 
 |                   // We may well have thrown here because by the time we try to await, the main | 
 |                   // thread is already interrupted. | 
 |                 } | 
 |               } | 
 |             }); | 
 |     try { | 
 |       invalidateWithoutError(receiver, child); | 
 |       fail(); | 
 |     } catch (InterruptedException e) { | 
 |       // Expected. | 
 |     } | 
 |     assertThat(badKey.get()).isNull(); | 
 |     assertThat(state.isEmpty()).isFalse(); | 
 |     final Set<SkyKey> invalidated = Sets.newConcurrentHashSet(); | 
 |     assertThat(isInvalidated(parent)).isFalse(); | 
 |     assertThat(graph.get(null, Reason.OTHER, parent).getValue()).isNotNull(); | 
 |     receiver = new DirtyTrackingProgressReceiver( | 
 |         new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { | 
 |       @Override | 
 |       public void invalidated(SkyKey skyKey, InvalidationState state) { | 
 |         invalidated.add(skyKey); | 
 |       } | 
 |     }); | 
 |     invalidateWithoutError(receiver); | 
 |     assertThat(invalidated).contains(parent); | 
 |     assertThat(state.getInvalidationsForTesting()).isEmpty(); | 
 |  | 
 |     // Regression test coverage: | 
 |     // "all pending values are marked changed on interrupt". | 
 |     assertThat(isInvalidated(child)).isTrue(); | 
 |     assertChanged(child); | 
 |     for (int i = 1; i < numValues; i++) { | 
 |       assertDirtyAndNotChanged(family[i]); | 
 |     } | 
 |     assertDirtyAndNotChanged(parent); | 
 |   } | 
 |  | 
 |   private SkyKey[] constructLargeGraph(int size) { | 
 |     Random random = new Random(TestUtils.getRandomSeed()); | 
 |     SkyKey[] values = new SkyKey[size]; | 
 |     for (int i = 0; i < size; i++) { | 
 |       String iString = Integer.toString(i); | 
 |       SkyKey iKey = GraphTester.toSkyKey(iString); | 
 |       set(iString, iString); | 
 |       for (int j = 0; j < i; j++) { | 
 |         if (random.nextInt(3) == 0) { | 
 |           tester.getOrCreate(iKey).addDependency(Integer.toString(j)); | 
 |         } | 
 |       } | 
 |       values[i] = iKey; | 
 |     } | 
 |     return values; | 
 |   } | 
 |  | 
 |   /** Returns a subset of {@code nodes} that are still valid and so can be invalidated. */ | 
 |   private Set<Pair<SkyKey, InvalidationType>> getValuesToInvalidate(SkyKey[] nodes) { | 
 |     Set<Pair<SkyKey, InvalidationType>> result = new HashSet<>(); | 
 |     Random random = new Random(TestUtils.getRandomSeed()); | 
 |     for (SkyKey node : nodes) { | 
 |       if (!isInvalidated(node)) { | 
 |         if (result.isEmpty() || random.nextInt(3) == 0) { | 
 |           // Add at least one node, if we can. | 
 |           result.add(Pair.of(node, defaultInvalidationType())); | 
 |         } | 
 |       } | 
 |     } | 
 |     return result; | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void interruptThreadInReceiver() throws Exception { | 
 |     Random random = new Random(TestUtils.getRandomSeed()); | 
 |     int graphSize = 1000; | 
 |     int tries = 5; | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey[] values = constructLargeGraph(graphSize); | 
 |     eval(/*keepGoing=*/false, values); | 
 |     final Thread mainThread = Thread.currentThread(); | 
 |     for (int run = 0; run < tries; run++) { | 
 |       Set<Pair<SkyKey, InvalidationType>> valuesToInvalidate = getValuesToInvalidate(values); | 
 |       // Find how many invalidations will actually be enqueued for invalidation in the first round, | 
 |       // so that we can interrupt before all of them are done. | 
 |       int validValuesToDo = | 
 |           Sets.difference(valuesToInvalidate, state.getInvalidationsForTesting()).size(); | 
 |       for (Pair<SkyKey, InvalidationType> pair : state.getInvalidationsForTesting()) { | 
 |         if (!isInvalidated(pair.first)) { | 
 |           validValuesToDo++; | 
 |         } | 
 |       } | 
 |       int countDownStart = validValuesToDo > 0 ? random.nextInt(validValuesToDo) : 0; | 
 |       final CountDownLatch countDownToInterrupt = new CountDownLatch(countDownStart); | 
 |       final DirtyTrackingProgressReceiver receiver = | 
 |           new DirtyTrackingProgressReceiver( | 
 |               new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { | 
 |             @Override | 
 |             public void invalidated(SkyKey skyKey, InvalidationState state) { | 
 |               countDownToInterrupt.countDown(); | 
 |               if (countDownToInterrupt.getCount() == 0) { | 
 |                 mainThread.interrupt(); | 
 |                 // Wait for the main thread to be interrupted uninterruptibly, because the main | 
 |                 // thread is going to interrupt us, and we don't want to get into an interrupt | 
 |                 // fight. Only if we get interrupted without the main thread also being interrupted | 
 |                 // will this throw an InterruptedException. | 
 |                 TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions( | 
 |                     visitor.get().getInterruptionLatchForTestingOnly(), | 
 |                     "Main thread was not interrupted"); | 
 |               } | 
 |             } | 
 |           }); | 
 |       try { | 
 |         invalidate( | 
 |             graph, | 
 |             receiver, | 
 |             Sets.newHashSet(Iterables.transform(valuesToInvalidate, pair -> pair.first)) | 
 |                 .toArray(new SkyKey[0])); | 
 |         assertThat(state.getInvalidationsForTesting()).isEmpty(); | 
 |       } catch (InterruptedException e) { | 
 |         // Expected. | 
 |       } | 
 |       if (state.isEmpty()) { | 
 |         // Ran out of values to invalidate. | 
 |         break; | 
 |       } | 
 |     } | 
 |  | 
 |     eval(/*keepGoing=*/false, values); | 
 |   } | 
 |  | 
 |   protected void setupInvalidatableGraph() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     set("a", "a"); | 
 |     set("b", "b"); | 
 |     tester.getOrCreate("ab").addDependency("a").addDependency("b").setComputedValue(CONCATENATE); | 
 |     assertValueValue("ab", "ab"); | 
 |     set("a", "c"); | 
 |   } | 
 |  | 
 |   private static class HeavyValue implements SkyValue { | 
 |   } | 
 |  | 
 |   /** | 
 |    * Test suite for the deleting invalidator. | 
 |    */ | 
 |   @RunWith(JUnit4.class) | 
 |   public static class DeletingInvalidatorTest extends EagerInvalidatorTest { | 
 |     @Override | 
 |     protected void invalidate( | 
 |         InMemoryGraph graph, DirtyTrackingProgressReceiver progressReceiver, SkyKey... keys) | 
 |         throws InterruptedException { | 
 |       Iterable<SkyKey> diff = ImmutableList.copyOf(keys); | 
 |       DeletingNodeVisitor deletingNodeVisitor = | 
 |           EagerInvalidator.createDeletingVisitorIfNeeded( | 
 |               graph, diff, new DirtyTrackingProgressReceiver(progressReceiver), state, true); | 
 |       if (deletingNodeVisitor != null) { | 
 |         visitor.set(deletingNodeVisitor); | 
 |         deletingNodeVisitor.run(); | 
 |       } | 
 |     } | 
 |  | 
 |     @Override | 
 |     EvaluationProgressReceiver.InvalidationState expectedState() { | 
 |       return EvaluationProgressReceiver.InvalidationState.DELETED; | 
 |     } | 
 |  | 
 |     @Override | 
 |     boolean gcExpected() { | 
 |       return true; | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected InvalidationState newInvalidationState() { | 
 |       return new InvalidatingNodeVisitor.DeletingInvalidationState(); | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected InvalidationType defaultInvalidationType() { | 
 |       return InvalidationType.DELETED; | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected boolean reverseDepsPresent() { | 
 |       return false; | 
 |     } | 
 |  | 
 |     @Test | 
 |     public void dirtyTrackingProgressReceiverWorksWithDeletingInvalidator() throws Exception { | 
 |       setupInvalidatableGraph(); | 
 |       DirtyTrackingProgressReceiver receiver = new DirtyTrackingProgressReceiver( | 
 |           new EvaluationProgressReceiver.NullEvaluationProgressReceiver()); | 
 |  | 
 |       // Dirty the node, and ensure that the tracker is aware of it: | 
 |       Iterable<SkyKey> diff1 = ImmutableList.of(skyKey("a")); | 
 |       InvalidationState state1 = new DirtyingInvalidationState(); | 
 |       Preconditions.checkNotNull( | 
 |               EagerInvalidator.createInvalidatingVisitorIfNeeded( | 
 |                   graph, | 
 |                   diff1, | 
 |                   receiver, | 
 |                   state1, | 
 |                   AbstractQueueVisitor.EXECUTOR_FACTORY)) | 
 |           .run(); | 
 |       assertThat(receiver.getUnenqueuedDirtyKeys()).containsExactly(skyKey("a"), skyKey("ab")); | 
 |  | 
 |       // Delete the node, and ensure that the tracker is no longer tracking it: | 
 |       Iterable<SkyKey> diff = ImmutableList.of(skyKey("a")); | 
 |       Preconditions.checkNotNull(EagerInvalidator.createDeletingVisitorIfNeeded(graph, diff, | 
 |           receiver, state, true)).run(); | 
 |       assertThat(receiver.getUnenqueuedDirtyKeys()).isEmpty(); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * Test suite for the dirtying invalidator. | 
 |    */ | 
 |   @RunWith(JUnit4.class) | 
 |   public static class DirtyingInvalidatorTest extends EagerInvalidatorTest { | 
 |     @Override | 
 |     protected void invalidate( | 
 |         InMemoryGraph graph, DirtyTrackingProgressReceiver progressReceiver, SkyKey... keys) | 
 |         throws InterruptedException { | 
 |       Iterable<SkyKey> diff = ImmutableList.copyOf(keys); | 
 |       DirtyingNodeVisitor dirtyingNodeVisitor = | 
 |           EagerInvalidator.createInvalidatingVisitorIfNeeded( | 
 |               graph, | 
 |               diff, | 
 |               progressReceiver, | 
 |               state, | 
 |               AbstractQueueVisitor.EXECUTOR_FACTORY); | 
 |       if (dirtyingNodeVisitor != null) { | 
 |         visitor.set(dirtyingNodeVisitor); | 
 |         dirtyingNodeVisitor.run(); | 
 |       } | 
 |     } | 
 |  | 
 |     @Override | 
 |     EvaluationProgressReceiver.InvalidationState expectedState() { | 
 |       return EvaluationProgressReceiver.InvalidationState.DIRTY; | 
 |     } | 
 |  | 
 |     @Override | 
 |     boolean gcExpected() { | 
 |       return false; | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected InvalidationState newInvalidationState() { | 
 |       return new DirtyingInvalidationState(); | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected InvalidationType defaultInvalidationType() { | 
 |       return InvalidationType.CHANGED; | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected boolean reverseDepsPresent() { | 
 |       return true; | 
 |     } | 
 |  | 
 |     @Test | 
 |     public void dirtyTrackingProgressReceiverWorksWithDirtyingInvalidator() throws Exception { | 
 |       setupInvalidatableGraph(); | 
 |       DirtyTrackingProgressReceiver receiver = new DirtyTrackingProgressReceiver( | 
 |           new EvaluationProgressReceiver.NullEvaluationProgressReceiver()); | 
 |  | 
 |       // Dirty the node, and ensure that the tracker is aware of it: | 
 |       invalidate(graph, receiver, skyKey("a")); | 
 |       assertThat(receiver.getUnenqueuedDirtyKeys()).hasSize(2); | 
 |     } | 
 |   } | 
 | } |