| // 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.collect.Iterables.getOnlyElement; | 
 | import static com.google.common.truth.Truth.assertThat; | 
 | import static com.google.common.truth.Truth.assertWithMessage; | 
 | import static com.google.devtools.build.lib.testutil.EventIterableSubjectFactory.assertThatEvents; | 
 | import static com.google.devtools.build.skyframe.EvaluationResultSubjectFactory.assertThatEvaluationResult; | 
 | import static com.google.devtools.build.skyframe.GraphTester.CONCATENATE; | 
 | import static org.junit.Assert.fail; | 
 |  | 
 | import com.google.common.base.Supplier; | 
 | import com.google.common.collect.ImmutableList; | 
 | import com.google.common.collect.ImmutableMap; | 
 | import com.google.common.collect.ImmutableSet; | 
 | import com.google.common.collect.Iterables; | 
 | import com.google.common.collect.Lists; | 
 | import com.google.common.collect.Sets; | 
 | import com.google.common.eventbus.EventBus; | 
 | import com.google.common.util.concurrent.Uninterruptibles; | 
 | import com.google.devtools.build.lib.events.Event; | 
 | import com.google.devtools.build.lib.events.EventHandler; | 
 | import com.google.devtools.build.lib.events.EventKind; | 
 | import com.google.devtools.build.lib.events.ExtendedEventHandler; | 
 | import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable; | 
 | import com.google.devtools.build.lib.events.Reporter; | 
 | import com.google.devtools.build.lib.events.StoredEventHandler; | 
 | import com.google.devtools.build.lib.testutil.TestThread; | 
 | import com.google.devtools.build.lib.testutil.TestUtils; | 
 | import com.google.devtools.build.skyframe.GraphTester.StringValue; | 
 | import com.google.devtools.build.skyframe.NotifyingHelper.EventType; | 
 | import com.google.devtools.build.skyframe.NotifyingHelper.Listener; | 
 | import com.google.devtools.build.skyframe.NotifyingHelper.Order; | 
 | import com.google.devtools.build.skyframe.SkyFunctionException.Transience; | 
 | import java.util.ArrayList; | 
 | import java.util.List; | 
 | import java.util.Map; | 
 | import java.util.Set; | 
 | import java.util.concurrent.CountDownLatch; | 
 | import java.util.concurrent.Semaphore; | 
 | import java.util.concurrent.TimeUnit; | 
 | import java.util.concurrent.atomic.AtomicBoolean; | 
 | import java.util.concurrent.atomic.AtomicInteger; | 
 | import javax.annotation.Nullable; | 
 | import org.junit.After; | 
 | import org.junit.Before; | 
 | import org.junit.Test; | 
 | import org.junit.runner.RunWith; | 
 | import org.junit.runners.JUnit4; | 
 |  | 
 | /** | 
 |  * Tests for {@link ParallelEvaluator}. | 
 |  */ | 
 | @RunWith(JUnit4.class) | 
 | public class ParallelEvaluatorTest { | 
 |   protected ProcessableGraph graph; | 
 |   protected IntVersion graphVersion = IntVersion.of(0); | 
 |   protected GraphTester tester = new GraphTester(); | 
 |  | 
 |   private StoredEventHandler storedEventHandler; | 
 |  | 
 |   private DirtyTrackingProgressReceiver revalidationReceiver = | 
 |       new DirtyTrackingProgressReceiver(null); | 
 |  | 
 |   @Before | 
 |   public void initializeReporter() { | 
 |     storedEventHandler = new StoredEventHandler(); | 
 |   } | 
 |  | 
 |   @After | 
 |   public void assertNoTrackedErrors() { | 
 |     TrackingAwaiter.INSTANCE.assertNoErrors(); | 
 |   } | 
 |  | 
 |   private ParallelEvaluator makeEvaluator( | 
 |       ProcessableGraph graph, | 
 |       ImmutableMap<SkyFunctionName, ? extends SkyFunction> builders, | 
 |       boolean keepGoing, | 
 |       EventFilter storedEventFilter) { | 
 |     Version oldGraphVersion = graphVersion; | 
 |     graphVersion = graphVersion.next(); | 
 |     return new ParallelEvaluator( | 
 |         graph, | 
 |         oldGraphVersion, | 
 |         builders, | 
 |         storedEventHandler, | 
 |         new MemoizingEvaluator.EmittedEventState(), | 
 |         storedEventFilter, | 
 |         ErrorInfoManager.UseChildErrorInfoIfNecessary.INSTANCE, | 
 |         keepGoing, | 
 |         150, | 
 |         revalidationReceiver); | 
 |   } | 
 |  | 
 |   private ParallelEvaluator makeEvaluator(ProcessableGraph graph, | 
 |       ImmutableMap<SkyFunctionName, ? extends SkyFunction> builders, boolean keepGoing) { | 
 |     return makeEvaluator(graph, builders, keepGoing, | 
 |         InMemoryMemoizingEvaluator.DEFAULT_STORED_EVENT_FILTER); | 
 |   } | 
 |  | 
 |   /** Convenience method for eval-ing a single value. */ | 
 |   protected SkyValue eval(boolean keepGoing, SkyKey key) throws InterruptedException { | 
 |     return eval(keepGoing, ImmutableList.of(key)).get(key); | 
 |   } | 
 |  | 
 |   protected ErrorInfo evalValueInError(SkyKey key) throws InterruptedException { | 
 |     return eval(true, ImmutableList.of(key)).getError(key); | 
 |   } | 
 |  | 
 |   protected <T extends SkyValue> EvaluationResult<T> eval(boolean keepGoing, SkyKey... keys) | 
 |       throws InterruptedException { | 
 |     return eval(keepGoing, ImmutableList.copyOf(keys)); | 
 |   } | 
 |  | 
 |   protected <T extends SkyValue> EvaluationResult<T> eval(boolean keepGoing, Iterable<SkyKey> keys) | 
 |       throws InterruptedException { | 
 |     ParallelEvaluator evaluator = makeEvaluator(graph, tester.getSkyFunctionMap(), keepGoing); | 
 |     return evaluator.eval(keys); | 
 |   } | 
 |  | 
 |   protected GraphTester.TestFunction set(String name, String value) { | 
 |     return tester.set(name, new StringValue(value)); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void smoke() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     set("a", "a"); | 
 |     set("b", "b"); | 
 |     tester.getOrCreate("ab").addDependency("a").addDependency("b").setComputedValue(CONCATENATE); | 
 |     StringValue value = (StringValue) eval(false, GraphTester.toSkyKey("ab")); | 
 |     assertThat(value.getValue()).isEqualTo("ab"); | 
 |     assertThat(storedEventHandler.getEvents()).isEmpty(); | 
 |     assertThat(storedEventHandler.getPosts()).isEmpty(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Test interruption handling when a long-running SkyFunction gets interrupted. | 
 |    */ | 
 |   @Test | 
 |   public void interruptedFunction() throws Exception { | 
 |     runInterruptionTest(new SkyFunctionFactory() { | 
 |       @Override | 
 |       public SkyFunction create(final Semaphore threadStarted, final String[] errorMessage) { | 
 |         return new SkyFunction() { | 
 |           @Override | 
 |           public SkyValue compute(SkyKey key, Environment env) throws InterruptedException { | 
 |             // Signal the waiting test thread that the evaluator thread has really started. | 
 |             threadStarted.release(); | 
 |  | 
 |             // Simulate a SkyFunction that runs for 10 seconds (this number was chosen arbitrarily). | 
 |             // The main thread should interrupt it shortly after it got started. | 
 |             Thread.sleep(10 * 1000); | 
 |  | 
 |             // Set an error message to indicate that the expected interruption didn't happen. | 
 |             // We can't use Assert.fail(String) on an async thread. | 
 |             errorMessage[0] = "SkyFunction should have been interrupted"; | 
 |             return null; | 
 |           } | 
 |  | 
 |           @Nullable | 
 |           @Override | 
 |           public String extractTag(SkyKey skyKey) { | 
 |             return null; | 
 |           } | 
 |         }; | 
 |       } | 
 |     }); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Test interruption handling when the Evaluator is in-between running SkyFunctions. | 
 |    * | 
 |    * <p>This is the point in time after a SkyFunction requested a dependency which is not yet built | 
 |    * so the builder returned null to the Evaluator, and the latter is about to schedule evaluation | 
 |    * of the missing dependency but gets interrupted before the dependency's SkyFunction could start. | 
 |    */ | 
 |   @Test | 
 |   public void interruptedEvaluatorThread() throws Exception { | 
 |     runInterruptionTest( | 
 |         new SkyFunctionFactory() { | 
 |           @Override | 
 |           public SkyFunction create(final Semaphore threadStarted, final String[] errorMessage) { | 
 |             return new SkyFunction() { | 
 |               // No need to synchronize access to this field; we always request just one more | 
 |               // dependency, so it's only one SkyFunction running at any time. | 
 |               private int valueIdCounter = 0; | 
 |  | 
 |               @Override | 
 |               public SkyValue compute(SkyKey key, Environment env) throws InterruptedException { | 
 |                 // Signal the waiting test thread that the Evaluator thread has really started. | 
 |                 threadStarted.release(); | 
 |  | 
 |                 // Keep the evaluator busy until the test's thread gets scheduled and can | 
 |                 // interrupt the Evaluator's thread. | 
 |                 env.getValue(GraphTester.toSkyKey("a" + valueIdCounter++)); | 
 |  | 
 |                 // This method never throws InterruptedException, therefore it's the responsibility | 
 |                 // of the Evaluator to detect the interrupt and avoid calling subsequent | 
 |                 // SkyFunctions. | 
 |                 return null; | 
 |               } | 
 |  | 
 |               @Nullable | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }; | 
 |           } | 
 |         }); | 
 |   } | 
 |  | 
 |   private void runPartialResultOnInterruption(boolean buildFastFirst) throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     // Two runs for fastKey's builder and one for the start of waitKey's builder. | 
 |     final CountDownLatch allValuesReady = new CountDownLatch(3); | 
 |     final SkyKey waitKey = GraphTester.toSkyKey("wait"); | 
 |     final SkyKey fastKey = GraphTester.toSkyKey("fast"); | 
 |     SkyKey leafKey = GraphTester.toSkyKey("leaf"); | 
 |     tester.getOrCreate(waitKey).setBuilder(new SkyFunction() { | 
 |           @Override | 
 |           public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { | 
 |             allValuesReady.countDown(); | 
 |             Thread.sleep(10000); | 
 |             throw new AssertionError("Should have been interrupted"); | 
 |           } | 
 |  | 
 |           @Override | 
 |           public String extractTag(SkyKey skyKey) { | 
 |             return null; | 
 |           } | 
 |         }); | 
 |     tester.getOrCreate(fastKey).setBuilder(new ChainedFunction(null, null, allValuesReady, false, | 
 |         new StringValue("fast"), ImmutableList.of(leafKey))); | 
 |     tester.set(leafKey, new StringValue("leaf")); | 
 |     if (buildFastFirst) { | 
 |       eval(/*keepGoing=*/false, fastKey); | 
 |     } | 
 |     final Set<SkyKey> receivedValues = Sets.newConcurrentHashSet(); | 
 |     revalidationReceiver = new DirtyTrackingProgressReceiver( | 
 |         new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { | 
 |       @Override | 
 |       public void evaluated(SkyKey skyKey, Supplier<SkyValue> skyValueSupplier, | 
 |           EvaluationState state) { | 
 |         receivedValues.add(skyKey); | 
 |       } | 
 |     }); | 
 |     TestThread evalThread = new TestThread() { | 
 |       @Override | 
 |       public void runTest() throws Exception { | 
 |         try { | 
 |           eval(/*keepGoing=*/true, waitKey, fastKey); | 
 |           fail(); | 
 |         } catch (InterruptedException e) { | 
 |           // Expected. | 
 |         } | 
 |       } | 
 |     }; | 
 |     evalThread.start(); | 
 |     assertThat(allValuesReady.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue(); | 
 |     evalThread.interrupt(); | 
 |     evalThread.join(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | 
 |     assertThat(evalThread.isAlive()).isFalse(); | 
 |     if (buildFastFirst) { | 
 |       // If leafKey was already built, it is not reported to the receiver. | 
 |       assertThat(receivedValues).containsExactly(fastKey); | 
 |     } else { | 
 |       // On first time being built, leafKey is registered too. | 
 |       assertThat(receivedValues).containsExactly(fastKey, leafKey); | 
 |     } | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void partialResultOnInterruption() throws Exception { | 
 |     runPartialResultOnInterruption(/*buildFastFirst=*/false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void partialCachedResultOnInterruption() throws Exception { | 
 |     runPartialResultOnInterruption(/*buildFastFirst=*/true); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Factory for SkyFunctions for interruption testing (see {@link #runInterruptionTest}). | 
 |    */ | 
 |   private interface SkyFunctionFactory { | 
 |     /** | 
 |      * Creates a SkyFunction suitable for a specific test scenario. | 
 |      * | 
 |      * @param threadStarted a latch which the returned SkyFunction must | 
 |      *     {@link Semaphore#release() release} once it started (otherwise the test won't work) | 
 |      * @param errorMessage a single-element array; the SkyFunction can put a error message in it | 
 |      *     to indicate that an assertion failed (calling {@code fail} from async thread doesn't | 
 |      *     work) | 
 |      */ | 
 |     SkyFunction create(final Semaphore threadStarted, final String[] errorMessage); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Test that we can handle the Evaluator getting interrupted at various points. | 
 |    * | 
 |    * <p>This method creates an Evaluator with the specified SkyFunction for GraphTested.NODE_TYPE, | 
 |    * then starts a thread, requests evaluation and asserts that evaluation started. It then | 
 |    * interrupts the Evaluator thread and asserts that it acknowledged the interruption. | 
 |    * | 
 |    * @param valueBuilderFactory creates a SkyFunction which may or may not handle interruptions | 
 |    *     (depending on the test) | 
 |    */ | 
 |   private void runInterruptionTest(SkyFunctionFactory valueBuilderFactory) throws Exception { | 
 |     final Semaphore threadStarted = new Semaphore(0); | 
 |     final Semaphore threadInterrupted = new Semaphore(0); | 
 |     final String[] wasError = new String[] { null }; | 
 |     final ParallelEvaluator evaluator = | 
 |         makeEvaluator( | 
 |             new InMemoryGraphImpl(), | 
 |             ImmutableMap.of( | 
 |                 GraphTester.NODE_TYPE, valueBuilderFactory.create(threadStarted, wasError)), | 
 |             false); | 
 |  | 
 |     Thread t = new Thread(new Runnable() { | 
 |         @Override | 
 |         public void run() { | 
 |           try { | 
 |             evaluator.eval(ImmutableList.of(GraphTester.toSkyKey("a"))); | 
 |  | 
 |             // There's no real need to set an error here. If the thread is not interrupted then | 
 |             // threadInterrupted is not released and the test thread will fail to acquire it. | 
 |             wasError[0] = "evaluation should have been interrupted"; | 
 |           } catch (InterruptedException e) { | 
 |             // This is the interrupt we are waiting for. It should come straight from the | 
 |             // evaluator (more precisely, the AbstractQueueVisitor). | 
 |             // Signal the waiting test thread that the interrupt was acknowledged. | 
 |             threadInterrupted.release(); | 
 |           } | 
 |         } | 
 |     }); | 
 |  | 
 |     // Start the thread and wait for a semaphore. This ensures that the thread was really started. | 
 |     t.start(); | 
 |     assertThat(threadStarted.tryAcquire(TestUtils.WAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) | 
 |         .isTrue(); | 
 |  | 
 |     // Interrupt the thread and wait for a semaphore. This ensures that the thread was really | 
 |     // interrupted and this fact was acknowledged. | 
 |     t.interrupt(); | 
 |     assertThat( | 
 |             threadInterrupted.tryAcquire( | 
 |                 TestUtils.WAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) | 
 |         .isTrue(); | 
 |  | 
 |     // The SkyFunction may have reported an error. | 
 |     if (wasError[0] != null) { | 
 |       fail(wasError[0]); | 
 |     } | 
 |  | 
 |     // Wait for the thread to finish. | 
 |     t.join(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void unrecoverableError() throws Exception { | 
 |     class CustomRuntimeException extends RuntimeException {} | 
 |     final CustomRuntimeException expected = new CustomRuntimeException(); | 
 |  | 
 |     final SkyFunction builder = new SkyFunction() { | 
 |       @Override | 
 |       @Nullable | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) | 
 |           throws SkyFunctionException, InterruptedException { | 
 |         throw expected; | 
 |       } | 
 |  | 
 |       @Override | 
 |       @Nullable | 
 |       public String extractTag(SkyKey skyKey) { | 
 |         return null; | 
 |       } | 
 |     }; | 
 |  | 
 |     final ParallelEvaluator evaluator = | 
 |         makeEvaluator( | 
 |             new InMemoryGraphImpl(), ImmutableMap.of(GraphTester.NODE_TYPE, builder), false); | 
 |  | 
 |     SkyKey valueToEval = GraphTester.toSkyKey("a"); | 
 |     try { | 
 |       evaluator.eval(ImmutableList.of(valueToEval)); | 
 |       fail("Expected RuntimeException"); | 
 |     } catch (RuntimeException re) { | 
 |       assertThat(re) | 
 |           .hasMessageThat() | 
 |           .contains("Unrecoverable error while evaluating node '" + valueToEval.toString() + "'"); | 
 |       assertThat(re).hasCauseThat().isInstanceOf(CustomRuntimeException.class); | 
 |     } | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void simpleWarning() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     set("a", "a").setWarning("warning on 'a'"); | 
 |     StringValue value = (StringValue) eval(false, GraphTester.toSkyKey("a")); | 
 |     assertThat(value.getValue()).isEqualTo("a"); | 
 |     assertThatEvents(storedEventHandler.getEvents()).containsExactly("warning on 'a'"); | 
 |   } | 
 |  | 
 |   /** Regression test: events from already-done value not replayed. */ | 
 |   @Test | 
 |   public void eventFromDoneChildRecorded() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     set("a", "a").setWarning("warning on 'a'"); | 
 |     SkyKey a = GraphTester.toSkyKey("a"); | 
 |     SkyKey top = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(top).addDependency(a).setComputedValue(CONCATENATE); | 
 |     // Build a so that it is already in the graph. | 
 |     eval(false, a); | 
 |     assertThat(storedEventHandler.getEvents()).hasSize(1); | 
 |     storedEventHandler.clear(); | 
 |     // Build top. The warning from a should be reprinted. | 
 |     eval(false, top); | 
 |     assertThat(storedEventHandler.getEvents()).hasSize(1); | 
 |     storedEventHandler.clear(); | 
 |     // Build top again. The warning should have been stored in the value. | 
 |     eval(false, top); | 
 |     assertThat(storedEventHandler.getEvents()).hasSize(1); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void postableFromDoneChildRecorded() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     Postable post = new Postable() {}; | 
 |     set("a", "a").setPostable(post); | 
 |     SkyKey a = GraphTester.toSkyKey("a"); | 
 |     SkyKey top = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(top).addDependency(a).setComputedValue(CONCATENATE); | 
 |     // Build a so that it is already in the graph. | 
 |     eval(false, a); | 
 |     assertThat(storedEventHandler.getPosts()).containsExactly(post); | 
 |     storedEventHandler.clear(); | 
 |     // Build top. The warning from a should be reprinted. | 
 |     eval(false, top); | 
 |     assertThat(storedEventHandler.getPosts()).containsExactly(post); | 
 |     storedEventHandler.clear(); | 
 |     // Build top again. The warning should have been stored in the value. | 
 |     eval(false, top); | 
 |     assertThat(storedEventHandler.getPosts()).containsExactly(post); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void storedEventFilter() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey a = GraphTester.toSkyKey("a"); | 
 |     final AtomicBoolean evaluated = new AtomicBoolean(false); | 
 |     tester.getOrCreate(a).setBuilder(new SkyFunction() { | 
 |       @Nullable | 
 |       @Override | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) { | 
 |         evaluated.set(true); | 
 |         env.getListener().handle(Event.error(null, "boop")); | 
 |         env.getListener().handle(Event.warn(null, "beep")); | 
 |         return new StringValue("a"); | 
 |       } | 
 |  | 
 |       @Nullable | 
 |       @Override | 
 |       public String extractTag(SkyKey skyKey) { | 
 |         return null; | 
 |       } | 
 |     }); | 
 |     ParallelEvaluator evaluator = | 
 |         makeEvaluator( | 
 |             graph, | 
 |             tester.getSkyFunctionMap(), | 
 |             /*keepGoing=*/ false, | 
 |             new EventFilter() { | 
 |               @Override | 
 |               public boolean apply(Event event) { | 
 |                 return event.getKind() == EventKind.ERROR; | 
 |               } | 
 |  | 
 |               @Override | 
 |               public boolean storeEvents() { | 
 |                 return true; | 
 |               } | 
 |             }); | 
 |     evaluator.eval(ImmutableList.of(a)); | 
 |     assertThat(evaluated.get()).isTrue(); | 
 |     assertThat(storedEventHandler.getEvents()).hasSize(2); | 
 |     assertThatEvents(storedEventHandler.getEvents()).containsExactly("boop", "beep"); | 
 |     storedEventHandler.clear(); | 
 |     evaluator = makeEvaluator(graph, tester.getSkyFunctionMap(), /*keepGoing=*/ false); | 
 |     evaluated.set(false); | 
 |     evaluator.eval(ImmutableList.of(a)); | 
 |     assertThat(evaluated.get()).isFalse(); | 
 |     assertThatEvents(storedEventHandler.getEvents()).containsExactly("boop"); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void shouldCreateErrorValueWithRootCause() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     set("a", "a"); | 
 |     SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     tester.getOrCreate(parentErrorKey).addDependency("a").addDependency(errorKey) | 
 |     .setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     ErrorInfo error = evalValueInError(parentErrorKey); | 
 |     assertThat(error.getRootCauses()).containsExactly(errorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void shouldBuildOneTarget() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     set("a", "a"); | 
 |     set("b", "b"); | 
 |     SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); | 
 |     SkyKey errorFreeKey = GraphTester.toSkyKey("ab"); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     tester.getOrCreate(parentErrorKey).addDependency(errorKey).addDependency("a") | 
 |     .setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     tester.getOrCreate(errorFreeKey).addDependency("a").addDependency("b") | 
 |     .setComputedValue(CONCATENATE); | 
 |     EvaluationResult<StringValue> result = eval(true, parentErrorKey, errorFreeKey); | 
 |     ErrorInfo error = result.getError(parentErrorKey); | 
 |     assertThat(error.getRootCauses()).containsExactly(errorKey); | 
 |     StringValue abValue = result.get(errorFreeKey); | 
 |     assertThat(abValue.getValue()).isEqualTo("ab"); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void catastropheHaltsBuild_KeepGoing_KeepEdges() throws Exception { | 
 |     catastrophicBuild(true, true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void catastropheHaltsBuild_KeepGoing_NoKeepEdges() throws Exception { | 
 |     catastrophicBuild(true, false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void catastropheInBuild_NoKeepGoing_KeepEdges() throws Exception { | 
 |     catastrophicBuild(false, true); | 
 |   } | 
 |  | 
 |   private void catastrophicBuild(boolean keepGoing, boolean keepEdges) throws Exception { | 
 |     graph = new InMemoryGraphImpl(keepEdges); | 
 |  | 
 |     SkyKey catastropheKey = GraphTester.toSkyKey("catastrophe"); | 
 |     SkyKey otherKey = GraphTester.toSkyKey("someKey"); | 
 |  | 
 |     final Exception catastrophe = new SomeErrorException("bad"); | 
 |     tester | 
 |         .getOrCreate(catastropheKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Nullable | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { | 
 |                 throw new SkyFunctionException(catastrophe, Transience.PERSISTENT) { | 
 |                   @Override | 
 |                   public boolean isCatastrophic() { | 
 |                     return true; | 
 |                   } | 
 |                 }; | 
 |               } | 
 |  | 
 |               @Nullable | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |  | 
 |     tester.getOrCreate(otherKey).setBuilder(new SkyFunction() { | 
 |       @Nullable | 
 |       @Override | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { | 
 |         new CountDownLatch(1).await(); | 
 |         throw new RuntimeException("can't get here"); | 
 |       } | 
 |  | 
 |       @Nullable | 
 |       @Override | 
 |       public String extractTag(SkyKey skyKey) { | 
 |         return null; | 
 |       } | 
 |     }); | 
 |  | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(catastropheKey).setComputedValue(CONCATENATE); | 
 |     EvaluationResult<StringValue> result = eval(keepGoing, topKey, otherKey); | 
 |     ErrorInfo error = result.getError(topKey); | 
 |     assertThat(error.getRootCauses()).containsExactly(catastropheKey); | 
 |     if (keepGoing) { | 
 |       assertThat(result.getCatastrophe()).isSameAs(catastrophe); | 
 |     } | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void parentFailureDoesntAffectChild() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     tester.getOrCreate(parentKey).setHasError(true); | 
 |     SkyKey childKey = GraphTester.toSkyKey("child"); | 
 |     set("child", "onions"); | 
 |     tester.getOrCreate(parentKey).addDependency(childKey).setComputedValue(CONCATENATE); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, parentKey, childKey); | 
 |     // Child is guaranteed to complete successfully before parent can run (and fail), | 
 |     // since parent depends on it. | 
 |     StringValue childValue = result.get(childKey); | 
 |     assertThat(childValue).isNotNull(); | 
 |     assertThat(childValue.getValue()).isEqualTo("onions"); | 
 |     ErrorInfo error = result.getError(parentKey); | 
 |     assertThat(error).isNotNull(); | 
 |     assertThat(error.getRootCauses()).containsExactly(parentKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void newParentOfErrorShouldHaveError() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     ErrorInfo error = evalValueInError(errorKey); | 
 |     assertThat(error.getRootCauses()).containsExactly(errorKey); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     tester.getOrCreate(parentKey).addDependency("error").setComputedValue(CONCATENATE); | 
 |     error = evalValueInError(parentKey); | 
 |     assertThat(error.getRootCauses()).containsExactly(errorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void errorTwoLevelsDeep() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     tester.getOrCreate("mid").addDependency(errorKey).setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(parentKey).addDependency("mid").setComputedValue(CONCATENATE); | 
 |     ErrorInfo error = evalValueInError(parentKey); | 
 |     assertThat(error.getRootCauses()).containsExactly(errorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void valueNotUsedInFailFastErrorRecovery() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     SkyKey recoveryKey = GraphTester.toSkyKey("midRecovery"); | 
 |     SkyKey badKey = GraphTester.toSkyKey("bad"); | 
 |  | 
 |     tester.getOrCreate(topKey).addDependency(recoveryKey).setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(recoveryKey).addErrorDependency(badKey, new StringValue("i recovered")) | 
 |         .setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(badKey).setHasError(true); | 
 |  | 
 |     EvaluationResult<SkyValue> result = eval(/*keepGoing=*/true, ImmutableList.of(recoveryKey)); | 
 |     assertThat(result.errorMap()).isEmpty(); | 
 |     assertThatEvaluationResult(result).hasNoError(); | 
 |     assertThat(result.get(recoveryKey)).isEqualTo(new StringValue("i recovered")); | 
 |  | 
 |     result = eval(/*keepGoing=*/false, ImmutableList.of(topKey)); | 
 |     assertThatEvaluationResult(result).hasError(); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     assertThat(result.errorMap()).hasSize(1); | 
 |     assertThat(result.getError(topKey).getException()).isNotNull(); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void multipleRootCauses() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     SkyKey errorKey2 = GraphTester.toSkyKey("error2"); | 
 |     SkyKey errorKey3 = GraphTester.toSkyKey("error3"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     tester.getOrCreate(errorKey2).setHasError(true); | 
 |     tester.getOrCreate(errorKey3).setHasError(true); | 
 |     tester.getOrCreate("mid").addDependency(errorKey).addDependency(errorKey2) | 
 |       .setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(parentKey) | 
 |       .addDependency("mid").addDependency(errorKey2).addDependency(errorKey3) | 
 |       .setComputedValue(CONCATENATE); | 
 |     ErrorInfo error = evalValueInError(parentKey); | 
 |     assertThat(error.getRootCauses()).containsExactly(errorKey, errorKey2, errorKey3); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void rootCauseWithNoKeepGoing() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     tester.getOrCreate("mid").addDependency(errorKey).setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(parentKey).addDependency("mid").setComputedValue(CONCATENATE); | 
 |     EvaluationResult<StringValue> result = eval(false, ImmutableList.of(parentKey)); | 
 |     Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); | 
 |     assertThat(error.getKey()).isEqualTo(parentKey); | 
 |     assertThat(error.getValue().getRootCauses()).containsExactly(errorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void errorBubblesToParentsOfTopLevelValue() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     final SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     final CountDownLatch latch = new CountDownLatch(1); | 
 |     tester.getOrCreate(errorKey).setBuilder(new ChainedFunction(null, /*waitToFinish=*/latch, null, | 
 |         false, /*value=*/null, ImmutableList.<SkyKey>of())); | 
 |     tester.getOrCreate(parentKey).setBuilder(new ChainedFunction(/*notifyStart=*/latch, null, null, | 
 |         false, new StringValue("unused"), ImmutableList.of(errorKey))); | 
 |     EvaluationResult<StringValue> result = eval( /*keepGoing=*/false, | 
 |         ImmutableList.of(parentKey, errorKey)); | 
 |     assertWithMessage(result.toString()).that(result.errorMap().size()).isEqualTo(2); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void noKeepGoingAfterKeepGoingFails() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     tester.getOrCreate(parentKey).addDependency(errorKey); | 
 |     ErrorInfo error = evalValueInError(parentKey); | 
 |     assertThat(error.getRootCauses()).containsExactly(errorKey); | 
 |     SkyKey[] list = { parentKey }; | 
 |     EvaluationResult<StringValue> result = eval(false, list); | 
 |     ErrorInfo errorInfo = result.getError(); | 
 |     assertThat(errorInfo.getRootCauses()).containsExactly(errorKey); | 
 |     assertThat(errorInfo.getException()).hasMessageThat().isEqualTo(errorKey.toString()); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void twoErrors() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey firstError = GraphTester.toSkyKey("error1"); | 
 |     SkyKey secondError = GraphTester.toSkyKey("error2"); | 
 |     CountDownLatch firstStart = new CountDownLatch(1); | 
 |     CountDownLatch secondStart = new CountDownLatch(1); | 
 |     tester.getOrCreate(firstError).setBuilder(new ChainedFunction(firstStart, secondStart, | 
 |         /*notifyFinish=*/null, /*waitForException=*/false, /*value=*/null, | 
 |         ImmutableList.<SkyKey>of())); | 
 |     tester.getOrCreate(secondError).setBuilder(new ChainedFunction(secondStart, firstStart, | 
 |         /*notifyFinish=*/null, /*waitForException=*/false, /*value=*/null, | 
 |         ImmutableList.<SkyKey>of())); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/false, firstError, secondError); | 
 |     assertWithMessage(result.toString()).that(result.hasError()).isTrue(); | 
 |     // With keepGoing=false, the eval call will terminate with exactly one error (the first one | 
 |     // thrown). But the first one thrown here is non-deterministic since we synchronize the | 
 |     // builders so that they run at roughly the same time. | 
 |     assertThat(ImmutableSet.of(firstError, secondError)).contains( | 
 |         Iterables.getOnlyElement(result.errorMap().keySet())); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void simpleCycle() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     tester.getOrCreate(aKey).addDependency(bKey); | 
 |     tester.getOrCreate(bKey).addDependency(aKey); | 
 |     ErrorInfo errorInfo = eval(false, ImmutableList.of(aKey)).getError(); | 
 |     assertThat(errorInfo.getException()).isNull(); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder(); | 
 |     assertThat(cycleInfo.getPathToCycle()).isEmpty(); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void cycleWithHead() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     SkyKey midKey = GraphTester.toSkyKey("mid"); | 
 |     tester.getOrCreate(topKey).addDependency(midKey); | 
 |     tester.getOrCreate(midKey).addDependency(aKey); | 
 |     tester.getOrCreate(aKey).addDependency(bKey); | 
 |     tester.getOrCreate(bKey).addDependency(aKey); | 
 |     ErrorInfo errorInfo = eval(false, ImmutableList.of(topKey)).getError(); | 
 |     assertThat(errorInfo.getException()).isNull(); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder(); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey, midKey).inOrder(); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void selfEdgeWithHead() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     SkyKey midKey = GraphTester.toSkyKey("mid"); | 
 |     tester.getOrCreate(topKey).addDependency(midKey); | 
 |     tester.getOrCreate(midKey).addDependency(aKey); | 
 |     tester.getOrCreate(aKey).addDependency(aKey); | 
 |     ErrorInfo errorInfo = eval(false, ImmutableList.of(topKey)).getError(); | 
 |     assertThat(errorInfo.getException()).isNull(); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(aKey).inOrder(); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey, midKey).inOrder(); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void cycleWithKeepGoing() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     SkyKey midKey = GraphTester.toSkyKey("mid"); | 
 |     SkyKey goodKey = GraphTester.toSkyKey("good"); | 
 |     StringValue goodValue = new StringValue("good"); | 
 |     tester.set(goodKey, goodValue); | 
 |     tester.getOrCreate(topKey).addDependency(midKey); | 
 |     tester.getOrCreate(midKey).addDependency(aKey); | 
 |     tester.getOrCreate(aKey).addDependency(bKey); | 
 |     tester.getOrCreate(bKey).addDependency(aKey); | 
 |     EvaluationResult<StringValue> result = eval(true, topKey, goodKey); | 
 |     assertThat(result.get(goodKey)).isEqualTo(goodValue); | 
 |     assertThat(result.get(topKey)).isNull(); | 
 |     ErrorInfo errorInfo = result.getError(topKey); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder(); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey, midKey).inOrder(); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void twoCycles() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     SkyKey cKey = GraphTester.toSkyKey("c"); | 
 |     SkyKey dKey = GraphTester.toSkyKey("d"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(aKey).addDependency(cKey); | 
 |     tester.getOrCreate(aKey).addDependency(bKey); | 
 |     tester.getOrCreate(bKey).addDependency(aKey); | 
 |     tester.getOrCreate(cKey).addDependency(dKey); | 
 |     tester.getOrCreate(dKey).addDependency(cKey); | 
 |     EvaluationResult<StringValue> result = eval(false, ImmutableList.of(topKey)); | 
 |     assertThat(result.get(topKey)).isNull(); | 
 |     ErrorInfo errorInfo = result.getError(topKey); | 
 |     Iterable<CycleInfo> cycles = CycleInfo.prepareCycles(topKey, | 
 |         ImmutableList.of(new CycleInfo(ImmutableList.of(aKey, bKey)), | 
 |         new CycleInfo(ImmutableList.of(cKey, dKey)))); | 
 |     assertThat(cycles).contains(getOnlyElement(errorInfo.getCycleInfo())); | 
 |   } | 
 |  | 
 |  | 
 |   @Test | 
 |   public void twoCyclesKeepGoing() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     SkyKey cKey = GraphTester.toSkyKey("c"); | 
 |     SkyKey dKey = GraphTester.toSkyKey("d"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(aKey).addDependency(cKey); | 
 |     tester.getOrCreate(aKey).addDependency(bKey); | 
 |     tester.getOrCreate(bKey).addDependency(aKey); | 
 |     tester.getOrCreate(cKey).addDependency(dKey); | 
 |     tester.getOrCreate(dKey).addDependency(cKey); | 
 |     EvaluationResult<StringValue> result = eval(true, ImmutableList.of(topKey)); | 
 |     assertThat(result.get(topKey)).isNull(); | 
 |     ErrorInfo errorInfo = result.getError(topKey); | 
 |     CycleInfo aCycle = new CycleInfo(ImmutableList.of(topKey), ImmutableList.of(aKey, bKey)); | 
 |     CycleInfo cCycle = new CycleInfo(ImmutableList.of(topKey), ImmutableList.of(cKey, dKey)); | 
 |     assertThat(errorInfo.getCycleInfo()).containsExactly(aCycle, cCycle); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void triangleBelowHeadCycle() throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     SkyKey cKey = GraphTester.toSkyKey("c"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(aKey); | 
 |     tester.getOrCreate(aKey).addDependency(bKey).addDependency(cKey); | 
 |     tester.getOrCreate(bKey).addDependency(cKey); | 
 |     tester.getOrCreate(cKey).addDependency(topKey); | 
 |     EvaluationResult<StringValue> result = eval(true, ImmutableList.of(topKey)); | 
 |     assertThat(result.get(topKey)).isNull(); | 
 |     ErrorInfo errorInfo = result.getError(topKey); | 
 |     CycleInfo topCycle = new CycleInfo(ImmutableList.of(topKey, aKey, cKey)); | 
 |     assertThat(errorInfo.getCycleInfo()).containsExactly(topCycle); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void longCycle() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     SkyKey cKey = GraphTester.toSkyKey("c"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(aKey); | 
 |     tester.getOrCreate(aKey).addDependency(bKey); | 
 |     tester.getOrCreate(bKey).addDependency(cKey); | 
 |     tester.getOrCreate(cKey).addDependency(topKey); | 
 |     EvaluationResult<StringValue> result = eval(true, ImmutableList.of(topKey)); | 
 |     assertThat(result.get(topKey)).isNull(); | 
 |     ErrorInfo errorInfo = result.getError(topKey); | 
 |     CycleInfo topCycle = new CycleInfo(ImmutableList.of(topKey, aKey, bKey, cKey)); | 
 |     assertThat(errorInfo.getCycleInfo()).containsExactly(topCycle); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void cycleWithTail() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     SkyKey cKey = GraphTester.toSkyKey("c"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(aKey).addDependency(cKey); | 
 |     tester.getOrCreate(aKey).addDependency(bKey); | 
 |     tester.getOrCreate(bKey).addDependency(aKey).addDependency(cKey); | 
 |     tester.getOrCreate(cKey); | 
 |     tester.set(cKey, new StringValue("cValue")); | 
 |     EvaluationResult<StringValue> result = eval(false, ImmutableList.of(topKey)); | 
 |     assertThat(result.get(topKey)).isNull(); | 
 |     ErrorInfo errorInfo = result.getError(topKey); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder(); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey).inOrder(); | 
 |   } | 
 |  | 
 |   /** Regression test: "value cannot be ready in a cycle". */ | 
 |   @Test | 
 |   public void selfEdgeWithExtraChildrenUnderCycle() throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey zKey = GraphTester.toSkyKey("z"); | 
 |     SkyKey cKey = GraphTester.toSkyKey("c"); | 
 |     tester.getOrCreate(aKey).addDependency(zKey); | 
 |     tester.getOrCreate(zKey).addDependency(cKey).addDependency(zKey); | 
 |     tester.getOrCreate(cKey).addDependency(aKey); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(aKey)); | 
 |     assertThat(result.get(aKey)).isNull(); | 
 |     ErrorInfo errorInfo = result.getError(aKey); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(zKey).inOrder(); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(aKey).inOrder(); | 
 |   } | 
 |  | 
 |   /** Regression test: "value cannot be ready in a cycle". */ | 
 |   @Test | 
 |   public void cycleWithExtraChildrenUnderCycle() throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     SkyKey cKey = GraphTester.toSkyKey("c"); | 
 |     SkyKey dKey = GraphTester.toSkyKey("d"); | 
 |     tester.getOrCreate(aKey).addDependency(bKey); | 
 |     tester.getOrCreate(bKey).addDependency(cKey).addDependency(dKey); | 
 |     tester.getOrCreate(cKey).addDependency(aKey); | 
 |     tester.getOrCreate(dKey).addDependency(bKey); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(aKey)); | 
 |     assertThat(result.get(aKey)).isNull(); | 
 |     ErrorInfo errorInfo = result.getError(aKey); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(bKey, dKey).inOrder(); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(aKey).inOrder(); | 
 |   } | 
 |  | 
 |   /** Regression test: "value cannot be ready in a cycle". */ | 
 |   @Test | 
 |   public void cycleAboveIndependentCycle() throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     SkyKey cKey = GraphTester.toSkyKey("c"); | 
 |     tester.getOrCreate(aKey).addDependency(bKey); | 
 |     tester.getOrCreate(bKey).addDependency(cKey); | 
 |     tester.getOrCreate(cKey).addDependency(aKey).addDependency(bKey); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(aKey)); | 
 |     assertThat(result.get(aKey)).isNull(); | 
 |     assertThat(result.getError(aKey).getCycleInfo()).containsExactly( | 
 |         new CycleInfo(ImmutableList.of(aKey, bKey, cKey)), | 
 |         new CycleInfo(ImmutableList.of(aKey), ImmutableList.of(bKey, cKey))); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void valueAboveCycleAndExceptionReportsException() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey aKey = GraphTester.toSkyKey("a"); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     SkyKey bKey = GraphTester.toSkyKey("b"); | 
 |     tester.getOrCreate(aKey).addDependency(bKey).addDependency(errorKey); | 
 |     tester.getOrCreate(bKey).addDependency(bKey); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(aKey)); | 
 |     assertThat(result.get(aKey)).isNull(); | 
 |     assertThat(result.getError(aKey).getException()).isNotNull(); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(result.getError(aKey).getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(bKey).inOrder(); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(aKey).inOrder(); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void errorValueStored() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     EvaluationResult<StringValue> result = eval(false, ImmutableList.of(errorKey)); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     assertThat(result.errorMap().keySet()).containsExactly(errorKey); | 
 |     ErrorInfo errorInfo = result.getError(); | 
 |     assertThat(errorInfo.getRootCauses()).containsExactly(errorKey); | 
 |     // Update value. But builder won't rebuild it. | 
 |     tester.getOrCreate(errorKey).setHasError(false); | 
 |     tester.set(errorKey, new StringValue("no error?")); | 
 |     result = eval(false, ImmutableList.of(errorKey)); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     assertThat(result.errorMap().keySet()).containsExactly(errorKey); | 
 |     errorInfo = result.getError(); | 
 |     assertThat(errorInfo.getRootCauses()).containsExactly(errorKey); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Regression test: "OOM in Skyframe cycle detection". | 
 |    * We only store the first 20 cycles found below any given root value. | 
 |    */ | 
 |   @Test | 
 |   public void manyCycles() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     for (int i = 0; i < 100; i++) { | 
 |       SkyKey dep = GraphTester.toSkyKey(Integer.toString(i)); | 
 |       tester.getOrCreate(topKey).addDependency(dep); | 
 |       tester.getOrCreate(dep).addDependency(dep); | 
 |     } | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(topKey)); | 
 |     assertThat(result.get(topKey)).isNull(); | 
 |     assertManyCycles(result.getError(topKey), topKey, /*selfEdge=*/false); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Regression test: "OOM in Skyframe cycle detection". | 
 |    * We filter out multiple paths to a cycle that go through the same child value. | 
 |    */ | 
 |   @Test | 
 |   public void manyPathsToCycle() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     SkyKey midKey = GraphTester.toSkyKey("mid"); | 
 |     SkyKey cycleKey = GraphTester.toSkyKey("cycle"); | 
 |     tester.getOrCreate(topKey).addDependency(midKey); | 
 |     tester.getOrCreate(cycleKey).addDependency(cycleKey); | 
 |     for (int i = 0; i < 100; i++) { | 
 |       SkyKey dep = GraphTester.toSkyKey(Integer.toString(i)); | 
 |       tester.getOrCreate(midKey).addDependency(dep); | 
 |       tester.getOrCreate(dep).addDependency(cycleKey); | 
 |     } | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(topKey)); | 
 |     assertThat(result.get(topKey)).isNull(); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(result.getError(topKey).getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).hasSize(1); | 
 |     assertThat(cycleInfo.getPathToCycle()).hasSize(3); | 
 |     assertThat(cycleInfo.getPathToCycle().subList(0, 2)).containsExactly(topKey, midKey).inOrder(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Checks that errorInfo has many self-edge cycles, and that one of them is a self-edge of | 
 |    * topKey, if {@code selfEdge} is true. | 
 |    */ | 
 |   private static void assertManyCycles(ErrorInfo errorInfo, SkyKey topKey, boolean selfEdge) { | 
 |     assertThat(Iterables.size(errorInfo.getCycleInfo())).isGreaterThan(1); | 
 |     assertThat(Iterables.size(errorInfo.getCycleInfo())).isLessThan(50); | 
 |     boolean foundSelfEdge = false; | 
 |     for (CycleInfo cycle : errorInfo.getCycleInfo()) { | 
 |       assertThat(cycle.getCycle()).hasSize(1); // Self-edge. | 
 |       if (!Iterables.isEmpty(cycle.getPathToCycle())) { | 
 |         assertThat(cycle.getPathToCycle()).containsExactly(topKey).inOrder(); | 
 |       } else { | 
 |         assertThat(cycle.getCycle()).containsExactly(topKey).inOrder(); | 
 |         foundSelfEdge = true; | 
 |       } | 
 |     } | 
 |     assertWithMessage(errorInfo + ", " + topKey).that(foundSelfEdge).isEqualTo(selfEdge); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void manyUnprocessedValuesInCycle() throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     SkyKey lastSelfKey = GraphTester.toSkyKey("zlastSelf"); | 
 |     SkyKey firstSelfKey = GraphTester.toSkyKey("afirstSelf"); | 
 |     SkyKey midSelfKey = GraphTester.toSkyKey("midSelf9"); | 
 |     // We add firstSelf first so that it is processed last in cycle detection (LIFO), meaning that | 
 |     // none of the dep values have to be cleared from firstSelf. | 
 |     tester.getOrCreate(firstSelfKey).addDependency(firstSelfKey); | 
 |     for (int i = 0; i < 100; i++) { | 
 |       SkyKey firstDep = GraphTester.toSkyKey("first" + i); | 
 |       SkyKey midDep = GraphTester.toSkyKey("midSelf" + i + "dep"); | 
 |       SkyKey lastDep = GraphTester.toSkyKey("last" + i); | 
 |       tester.getOrCreate(firstSelfKey).addDependency(firstDep); | 
 |       tester.getOrCreate(midSelfKey).addDependency(midDep); | 
 |       tester.getOrCreate(lastSelfKey).addDependency(lastDep); | 
 |       if (i == 90) { | 
 |         // Most of the deps will be cleared from midSelf. | 
 |         tester.getOrCreate(midSelfKey).addDependency(midSelfKey); | 
 |       } | 
 |       tester.getOrCreate(firstDep).addDependency(firstDep); | 
 |       tester.getOrCreate(midDep).addDependency(midDep); | 
 |       tester.getOrCreate(lastDep).addDependency(lastDep); | 
 |     } | 
 |     // All the deps will be cleared from lastSelf. | 
 |     tester.getOrCreate(lastSelfKey).addDependency(lastSelfKey); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, | 
 |         ImmutableList.of(lastSelfKey, firstSelfKey, midSelfKey)); | 
 |     assertWithMessage(result.toString()).that(result.keyNames()).isEmpty(); | 
 |     assertThat(result.errorMap().keySet()).containsExactly(lastSelfKey, firstSelfKey, midSelfKey); | 
 |  | 
 |     // Check lastSelfKey. | 
 |     ErrorInfo errorInfo = result.getError(lastSelfKey); | 
 |     assertWithMessage(errorInfo.toString()) | 
 |         .that(Iterables.size(errorInfo.getCycleInfo())) | 
 |         .isEqualTo(1); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(lastSelfKey); | 
 |     assertThat(cycleInfo.getPathToCycle()).isEmpty(); | 
 |  | 
 |     // Check firstSelfKey. It should not have discovered its own self-edge, because there were too | 
 |     // many other values before it in the queue. | 
 |     assertManyCycles(result.getError(firstSelfKey), firstSelfKey, /*selfEdge=*/false); | 
 |  | 
 |     // Check midSelfKey. It should have discovered its own self-edge. | 
 |     assertManyCycles(result.getError(midSelfKey), midSelfKey, /*selfEdge=*/true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void errorValueStoredWithKeepGoing() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     EvaluationResult<StringValue> result = eval(true, ImmutableList.of(errorKey)); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     assertThat(result.errorMap().keySet()).containsExactly(errorKey); | 
 |     ErrorInfo errorInfo = result.getError(); | 
 |     assertThat(errorInfo.getRootCauses()).containsExactly(errorKey); | 
 |     // Update value. But builder won't rebuild it. | 
 |     tester.getOrCreate(errorKey).setHasError(false); | 
 |     tester.set(errorKey, new StringValue("no error?")); | 
 |     result = eval(true, ImmutableList.of(errorKey)); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     assertThat(result.errorMap().keySet()).containsExactly(errorKey); | 
 |     errorInfo = result.getError(); | 
 |     assertThat(errorInfo.getRootCauses()).containsExactly(errorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void continueWithErrorDep() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     tester.set("after", new StringValue("after")); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     tester.getOrCreate(parentKey).addErrorDependency(errorKey, new StringValue("recovered")) | 
 |         .setComputedValue(CONCATENATE).addDependency("after"); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(parentKey)); | 
 |     assertThat(result.errorMap()).isEmpty(); | 
 |     assertThat(result.get(parentKey).getValue()).isEqualTo("recoveredafter"); | 
 |     result = eval(/*keepGoing=*/false, ImmutableList.of(parentKey)); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); | 
 |     assertThat(error.getKey()).isEqualTo(parentKey); | 
 |     assertThat(error.getValue().getRootCauses()).containsExactly(errorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void transformErrorDep() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); | 
 |     tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")) | 
 |         .setHasError(true); | 
 |     EvaluationResult<StringValue> result = eval( | 
 |         /*keepGoing=*/false, ImmutableList.of(parentErrorKey)); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); | 
 |     assertThat(error.getKey()).isEqualTo(parentErrorKey); | 
 |     assertThat(error.getValue().getRootCauses()).containsExactly(parentErrorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void transformErrorDepKeepGoing() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); | 
 |     tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")) | 
 |         .setHasError(true); | 
 |     EvaluationResult<StringValue> result = eval( | 
 |         /*keepGoing=*/true, ImmutableList.of(parentErrorKey)); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); | 
 |     assertThat(error.getKey()).isEqualTo(parentErrorKey); | 
 |     assertThat(error.getValue().getRootCauses()).containsExactly(parentErrorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void transformErrorDepOneLevelDownKeepGoing() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     tester.set("after", new StringValue("after")); | 
 |     SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); | 
 |     tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")); | 
 |     tester.set(parentErrorKey, new StringValue("parent value")); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(parentErrorKey).addDependency("after") | 
 |         .setComputedValue(CONCATENATE); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(topKey)); | 
 |     assertThat(ImmutableList.<String>copyOf(result.<String>keyNames())).containsExactly("top"); | 
 |     assertThat(result.get(topKey).getValue()).isEqualTo("parent valueafter"); | 
 |     assertThat(result.errorMap()).isEmpty(); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void transformErrorDepOneLevelDownNoKeepGoing() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     tester.set("after", new StringValue("after")); | 
 |     SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); | 
 |     tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")); | 
 |     tester.set(parentErrorKey, new StringValue("parent value")); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(parentErrorKey).addDependency("after") | 
 |         .setComputedValue(CONCATENATE); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/false, ImmutableList.of(topKey)); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); | 
 |     assertThat(error.getKey()).isEqualTo(topKey); | 
 |     assertThat(error.getValue().getRootCauses()).containsExactly(errorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void errorDepDoesntStopOtherDep() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     final SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     EvaluationResult<StringValue> result1 = eval(/*keepGoing=*/ true, ImmutableList.of(errorKey)); | 
 |     assertThatEvaluationResult(result1).hasError(); | 
 |     assertThatEvaluationResult(result1) | 
 |         .hasErrorEntryForKeyThat(errorKey) | 
 |         .hasExceptionThat() | 
 |         .isNotNull(); | 
 |     final SkyKey otherKey = GraphTester.toSkyKey("other"); | 
 |     tester.getOrCreate(otherKey).setConstantValue(new StringValue("other")); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     final Exception topException = new SomeErrorException("top exception"); | 
 |     final AtomicInteger numComputes = new AtomicInteger(0); | 
 |     tester | 
 |         .getOrCreate(topKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Nullable | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) | 
 |                   throws SkyFunctionException, InterruptedException { | 
 |                 Map<SkyKey, ValueOrException<SomeErrorException>> values = | 
 |                     env.getValuesOrThrow( | 
 |                         ImmutableList.of(errorKey, otherKey), SomeErrorException.class); | 
 |                 if (numComputes.incrementAndGet() == 1) { | 
 |                   assertThat(env.valuesMissing()).isTrue(); | 
 |                 } else { | 
 |                   assertThat(numComputes.get()).isEqualTo(2); | 
 |                   assertThat(env.valuesMissing()).isFalse(); | 
 |                 } | 
 |                 try { | 
 |                   values.get(errorKey).get(); | 
 |                   throw new AssertionError("Should have thrown"); | 
 |                 } catch (SomeErrorException e) { | 
 |                   throw new SkyFunctionException(topException, Transience.PERSISTENT) {}; | 
 |                 } | 
 |               } | 
 |  | 
 |               @Nullable | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |     EvaluationResult<StringValue> result2 = eval(/*keepGoing=*/ true, ImmutableList.of(topKey)); | 
 |     assertThatEvaluationResult(result2).hasError(); | 
 |     assertThatEvaluationResult(result2) | 
 |         .hasErrorEntryForKeyThat(topKey) | 
 |         .hasExceptionThat() | 
 |         .isSameAs(topException); | 
 |     assertThat(numComputes.get()).isEqualTo(2); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Make sure that multiple unfinished children can be cleared from a cycle value. | 
 |    */ | 
 |   @Test | 
 |   public void cycleWithMultipleUnfinishedChildren() throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     SkyKey cycleKey = GraphTester.toSkyKey("zcycle"); | 
 |     SkyKey midKey = GraphTester.toSkyKey("mid"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     SkyKey selfEdge1 = GraphTester.toSkyKey("selfEdge1"); | 
 |     SkyKey selfEdge2 = GraphTester.toSkyKey("selfEdge2"); | 
 |     tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE); | 
 |     // selfEdge* come before cycleKey, so cycleKey's path will be checked first (LIFO), and the | 
 |     // cycle with mid will be detected before the selfEdge* cycles are. | 
 |     tester.getOrCreate(midKey).addDependency(selfEdge1).addDependency(selfEdge2) | 
 |         .addDependency(cycleKey) | 
 |     .setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(cycleKey).addDependency(midKey); | 
 |     tester.getOrCreate(selfEdge1).addDependency(selfEdge1); | 
 |     tester.getOrCreate(selfEdge2).addDependency(selfEdge2); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableSet.of(topKey)); | 
 |     assertThat(result.errorMap().keySet()).containsExactly(topKey); | 
 |     Iterable<CycleInfo> cycleInfos = result.getError(topKey).getCycleInfo(); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(cycleInfos); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(midKey, cycleKey); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Regression test: "value in cycle depends on error". | 
 |    * The mid value will have two parents -- top and cycle. Error bubbles up from mid to cycle, and | 
 |    * we should detect cycle. | 
 |    */ | 
 |   private void cycleAndErrorInBubbleUp(boolean keepGoing) throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     tester = new GraphTester(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     SkyKey cycleKey = GraphTester.toSkyKey("cycle"); | 
 |     SkyKey midKey = GraphTester.toSkyKey("mid"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(midKey).addDependency(errorKey).addDependency(cycleKey) | 
 |         .setComputedValue(CONCATENATE); | 
 |  | 
 |     // We need to ensure that cycle value has finished his work, and we have recorded dependencies | 
 |     CountDownLatch cycleFinish = new CountDownLatch(1); | 
 |     tester.getOrCreate(cycleKey).setBuilder(new ChainedFunction(null, | 
 |         null, cycleFinish, false, new StringValue(""), ImmutableSet.<SkyKey>of(midKey))); | 
 |     tester.getOrCreate(errorKey).setBuilder(new ChainedFunction(null, cycleFinish, | 
 |         null, /*waitForException=*/false, null, ImmutableSet.<SkyKey>of())); | 
 |  | 
 |     EvaluationResult<StringValue> result = eval(keepGoing, ImmutableSet.of(topKey)); | 
 |     assertThat(result.errorMap().keySet()).containsExactly(topKey); | 
 |     Iterable<CycleInfo> cycleInfos = result.getError(topKey).getCycleInfo(); | 
 |     if (keepGoing) { | 
 |       // The error thrown will only be recorded in keep_going mode. | 
 |       assertThat(result.getError().getRootCauses()).containsExactly(errorKey); | 
 |     } | 
 |     assertThat(cycleInfos).isNotEmpty(); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(cycleInfos); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(midKey, cycleKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void cycleAndErrorInBubbleUpNoKeepGoing() throws Exception { | 
 |     cycleAndErrorInBubbleUp(false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void cycleAndErrorInBubbleUpKeepGoing() throws Exception { | 
 |     cycleAndErrorInBubbleUp(true); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Regression test: "value in cycle depends on error". | 
 |    * We add another value that won't finish building before the threadpool shuts down, to check that | 
 |    * the cycle detection can handle unfinished values. | 
 |    */ | 
 |   @Test | 
 |   public void cycleAndErrorAndOtherInBubbleUp() throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     tester = new GraphTester(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     SkyKey cycleKey = GraphTester.toSkyKey("cycle"); | 
 |     SkyKey midKey = GraphTester.toSkyKey("mid"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE); | 
 |     // We should add cycleKey first and errorKey afterwards. Otherwise there is a chance that | 
 |     // during error propagation cycleKey will not be processed, and we will not detect the cycle. | 
 |     tester.getOrCreate(midKey).addDependency(errorKey).addDependency(cycleKey) | 
 |         .setComputedValue(CONCATENATE); | 
 |     SkyKey otherTop = GraphTester.toSkyKey("otherTop"); | 
 |     CountDownLatch topStartAndCycleFinish = new CountDownLatch(2); | 
 |     // In nokeep_going mode, otherTop will wait until the threadpool has received an exception, | 
 |     // then request its own dep. This guarantees that there is a value that is not finished when | 
 |     // cycle detection happens. | 
 |     tester.getOrCreate(otherTop).setBuilder(new ChainedFunction(topStartAndCycleFinish, | 
 |         new CountDownLatch(0), null, /*waitForException=*/true, new StringValue("never returned"), | 
 |         ImmutableSet.<SkyKey>of(GraphTester.toSkyKey("dep that never builds")))); | 
 |  | 
 |     tester.getOrCreate(cycleKey).setBuilder(new ChainedFunction(null, null, | 
 |         topStartAndCycleFinish, /*waitForException=*/false, new StringValue(""), | 
 |         ImmutableSet.<SkyKey>of(midKey))); | 
 |     // error waits until otherTop starts and cycle finishes, to make sure otherTop will request | 
 |     // its dep before the threadpool shuts down. | 
 |     tester.getOrCreate(errorKey).setBuilder(new ChainedFunction(null, topStartAndCycleFinish, | 
 |         null, /*waitForException=*/false, null, | 
 |         ImmutableSet.<SkyKey>of())); | 
 |     EvaluationResult<StringValue> result = | 
 |         eval(/*keepGoing=*/false, ImmutableSet.of(topKey, otherTop)); | 
 |     assertThat(result.errorMap().keySet()).containsExactly(topKey); | 
 |     Iterable<CycleInfo> cycleInfos = result.getError(topKey).getCycleInfo(); | 
 |     assertThat(cycleInfos).isNotEmpty(); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(cycleInfos); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(midKey, cycleKey); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Regression test: "value in cycle depends on error". | 
 |    * Here, we add an additional top-level key in error, just to mix it up. | 
 |    */ | 
 |   private void cycleAndErrorAndError(boolean keepGoing) throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     tester = new GraphTester(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     SkyKey cycleKey = GraphTester.toSkyKey("cycle"); | 
 |     SkyKey midKey = GraphTester.toSkyKey("mid"); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE); | 
 |     tester.getOrCreate(midKey).addDependency(errorKey).addDependency(cycleKey) | 
 |         .setComputedValue(CONCATENATE); | 
 |     SkyKey otherTop = GraphTester.toSkyKey("otherTop"); | 
 |     CountDownLatch topStartAndCycleFinish = new CountDownLatch(2); | 
 |     // In nokeep_going mode, otherTop will wait until the threadpool has received an exception, | 
 |     // then throw its own exception. This guarantees that its exception will not be the one | 
 |     // bubbling up, but that there is a top-level value with an exception by the time the bubbling | 
 |     // up starts. | 
 |     tester.getOrCreate(otherTop).setBuilder(new ChainedFunction(topStartAndCycleFinish, | 
 |         new CountDownLatch(0), null, /*waitForException=*/!keepGoing, null, | 
 |         ImmutableSet.<SkyKey>of())); | 
 |     // error waits until otherTop starts and cycle finishes, to make sure otherTop will request | 
 |     // its dep before the threadpool shuts down. | 
 |     tester.getOrCreate(errorKey).setBuilder(new ChainedFunction(null, topStartAndCycleFinish, | 
 |         null, /*waitForException=*/false, null, | 
 |         ImmutableSet.<SkyKey>of())); | 
 |     tester.getOrCreate(cycleKey).setBuilder(new ChainedFunction(null, null, | 
 |         topStartAndCycleFinish, /*waitForException=*/false, new StringValue(""), | 
 |         ImmutableSet.<SkyKey>of(midKey))); | 
 |     EvaluationResult<StringValue> result = | 
 |         eval(keepGoing, ImmutableSet.of(topKey, otherTop)); | 
 |     if (keepGoing) { | 
 |       assertThat(result.errorMap().keySet()).containsExactly(otherTop, topKey); | 
 |       assertThat(result.getError(otherTop).getRootCauses()).containsExactly(otherTop); | 
 |       // The error thrown will only be recorded in keep_going mode. | 
 |       assertThat(result.getError(topKey).getRootCauses()).containsExactly(errorKey); | 
 |     } | 
 |     Iterable<CycleInfo> cycleInfos = result.getError(topKey).getCycleInfo(); | 
 |     assertThat(cycleInfos).isNotEmpty(); | 
 |     CycleInfo cycleInfo = Iterables.getOnlyElement(cycleInfos); | 
 |     assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey); | 
 |     assertThat(cycleInfo.getCycle()).containsExactly(midKey, cycleKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void cycleAndErrorAndErrorNoKeepGoing() throws Exception { | 
 |     cycleAndErrorAndError(false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void cycleAndErrorAndErrorKeepGoing() throws Exception { | 
 |     cycleAndErrorAndError(true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testFunctionCrashTrace() throws Exception { | 
 |     final SkyFunctionName childType = SkyFunctionName.create("child"); | 
 |     final SkyFunctionName parentType = SkyFunctionName.create("parent"); | 
 |  | 
 |     class ChildFunction implements SkyFunction { | 
 |       @Override | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) { | 
 |         throw new IllegalStateException("I WANT A PONY!!!"); | 
 |       } | 
 |  | 
 |       @Override public String extractTag(SkyKey skyKey) { return null; } | 
 |     } | 
 |  | 
 |     class ParentFunction implements SkyFunction { | 
 |       @Override | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { | 
 |         SkyValue dep = env.getValue(LegacySkyKey.create(childType, "billy the kid")); | 
 |         if (dep == null) { | 
 |           return null; | 
 |         } | 
 |         throw new IllegalStateException();  // Should never get here. | 
 |       } | 
 |  | 
 |       @Override public String extractTag(SkyKey skyKey) { return null; } | 
 |     } | 
 |  | 
 |     ImmutableMap<SkyFunctionName, SkyFunction> skyFunctions = ImmutableMap.of( | 
 |         childType, new ChildFunction(), | 
 |         parentType, new ParentFunction()); | 
 |     ParallelEvaluator evaluator = makeEvaluator(new InMemoryGraphImpl(), skyFunctions, false); | 
 |  | 
 |     try { | 
 |       evaluator.eval(ImmutableList.of(LegacySkyKey.create(parentType, "octodad"))); | 
 |       fail(); | 
 |     } catch (RuntimeException e) { | 
 |       assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("I WANT A PONY!!!"); | 
 |       assertThat(e) | 
 |           .hasMessageThat() | 
 |           .isEqualTo( | 
 |               "Unrecoverable error while evaluating node 'child:billy the kid' " | 
 |                   + "(requested by nodes 'parent:octodad')"); | 
 |     } | 
 |   } | 
 |  | 
 |   private static class SomeOtherErrorException extends Exception { | 
 |     public SomeOtherErrorException(String msg) { | 
 |       super(msg); | 
 |     } | 
 |   } | 
 |  | 
 |   private void unexpectedErrorDep(boolean keepGoing) throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     final SomeOtherErrorException exception = new SomeOtherErrorException("error exception"); | 
 |     tester.getOrCreate(errorKey).setBuilder(new SkyFunction() { | 
 |       @Override | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { | 
 |         throw new SkyFunctionException(exception, Transience.PERSISTENT) {}; | 
 |       } | 
 |  | 
 |       @Override | 
 |       public String extractTag(SkyKey skyKey) { | 
 |         throw new UnsupportedOperationException(); | 
 |       } | 
 |     }); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     tester.getOrCreate(topKey).addErrorDependency(errorKey, new StringValue("recovered")) | 
 |         .setComputedValue(CONCATENATE); | 
 |     EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(topKey)); | 
 |     assertThat(result.keyNames()).isEmpty(); | 
 |     assertThat(result.getError(topKey).getException()).isSameAs(exception); | 
 |     assertThat(result.getError(topKey).getRootCauses()).containsExactly(errorKey); | 
 |   } | 
 |  | 
 |   /** | 
 |    * This and the following three tests are in response a bug: "Skyframe error propagation model is | 
 |    * problematic". They ensure that exceptions a child throws that a value does not specify it can | 
 |    * handle in getValueOrThrow do not cause a crash. | 
 |    */ | 
 |   @Test | 
 |   public void unexpectedErrorDepKeepGoing() throws Exception { | 
 |     unexpectedErrorDep(true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void unexpectedErrorDepNoKeepGoing() throws Exception { | 
 |     unexpectedErrorDep(false); | 
 |   } | 
 |  | 
 |   private void unexpectedErrorDepOneLevelDown(final boolean keepGoing) throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); | 
 |     final SomeErrorException exception = new SomeErrorException("error exception"); | 
 |     final SomeErrorException topException = new SomeErrorException("top exception"); | 
 |     final StringValue topValue = new StringValue("top"); | 
 |     tester.getOrCreate(errorKey).setBuilder(new SkyFunction() { | 
 |       @Override | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) throws GenericFunctionException { | 
 |         throw new GenericFunctionException(exception, Transience.PERSISTENT); | 
 |       } | 
 |  | 
 |       @Override | 
 |       public String extractTag(SkyKey skyKey) { | 
 |         throw new UnsupportedOperationException(); | 
 |       } | 
 |     }); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     final SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     tester.getOrCreate(parentKey).addDependency(errorKey).setComputedValue(CONCATENATE); | 
 |     tester | 
 |         .getOrCreate(topKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) | 
 |                   throws GenericFunctionException, InterruptedException { | 
 |                 try { | 
 |                   if (env.getValueOrThrow(parentKey, SomeErrorException.class) == null) { | 
 |                     return null; | 
 |                   } | 
 |                 } catch (SomeErrorException e) { | 
 |                   assertWithMessage(e.toString()).that(e).isEqualTo(exception); | 
 |                 } | 
 |                 if (keepGoing) { | 
 |                   return topValue; | 
 |                 } else { | 
 |                   throw new GenericFunctionException(topException, Transience.PERSISTENT); | 
 |                 } | 
 |               } | 
 |  | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 throw new UnsupportedOperationException(); | 
 |               } | 
 |             }); | 
 |     tester.getOrCreate(topKey).addErrorDependency(errorKey, new StringValue("recovered")) | 
 |         .setComputedValue(CONCATENATE); | 
 |     EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(topKey)); | 
 |     if (!keepGoing) { | 
 |       assertThat(result.keyNames()).isEmpty(); | 
 |       assertThat(result.getError(topKey).getException()).isEqualTo(topException); | 
 |       assertThat(result.getError(topKey).getRootCauses()).containsExactly(topKey); | 
 |       assertThatEvaluationResult(result).hasError(); | 
 |     } else { | 
 |       assertThatEvaluationResult(result).hasNoError(); | 
 |       assertThat(result.get(topKey)).isSameAs(topValue); | 
 |     } | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void unexpectedErrorDepOneLevelDownKeepGoing() throws Exception { | 
 |     unexpectedErrorDepOneLevelDown(true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void unexpectedErrorDepOneLevelDownNoKeepGoing() throws Exception { | 
 |     unexpectedErrorDepOneLevelDown(false); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Exercises various situations involving groups of deps that overlap -- request one group, then | 
 |    * request another group that has a dep in common with the first group. | 
 |    * | 
 |    * @param sameFirst whether the dep in common in the two groups should be the first dep. | 
 |    * @param twoCalls whether the two groups should be requested in two different builder calls. | 
 |    * @param valuesOrThrow whether the deps should be requested using getValuesOrThrow. | 
 |    */ | 
 |   private void sameDepInTwoGroups(final boolean sameFirst, final boolean twoCalls, | 
 |       final boolean valuesOrThrow) throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey topKey = GraphTester.toSkyKey("top"); | 
 |     final List<SkyKey> leaves = new ArrayList<>(); | 
 |     for (int i = 1; i <= 3; i++) { | 
 |       SkyKey leaf = GraphTester.toSkyKey("leaf" + i); | 
 |       leaves.add(leaf); | 
 |       tester.set(leaf, new StringValue("leaf" + i)); | 
 |     } | 
 |     final SkyKey leaf4 = GraphTester.toSkyKey("leaf4"); | 
 |     tester.set(leaf4, new StringValue("leaf" + 4)); | 
 |     tester.getOrCreate(topKey).setBuilder(new SkyFunction() { | 
 |       @Override | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException, | 
 |           InterruptedException { | 
 |         if (valuesOrThrow) { | 
 |           env.getValuesOrThrow(leaves, SomeErrorException.class); | 
 |         } else { | 
 |           env.getValues(leaves); | 
 |         } | 
 |         if (twoCalls && env.valuesMissing()) { | 
 |           return null; | 
 |         } | 
 |         SkyKey first = sameFirst ? leaves.get(0) : leaf4; | 
 |         SkyKey second = sameFirst ? leaf4 : leaves.get(2); | 
 |         List<SkyKey> secondRequest = ImmutableList.of(first, second); | 
 |         if (valuesOrThrow) { | 
 |           env.getValuesOrThrow(secondRequest, SomeErrorException.class); | 
 |         } else { | 
 |           env.getValues(secondRequest); | 
 |         } | 
 |         if (env.valuesMissing()) { | 
 |           return null; | 
 |         } | 
 |         return new StringValue("top"); | 
 |       } | 
 |  | 
 |       @Override | 
 |       public String extractTag(SkyKey skyKey) { | 
 |         return null; | 
 |       } | 
 |     }); | 
 |     eval(/*keepGoing=*/false, topKey); | 
 |     assertThat(eval(/*keepGoing=*/false, topKey)).isEqualTo(new StringValue("top")); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void sameDepInTwoGroups_Same_Two_Throw() throws Exception { | 
 |     sameDepInTwoGroups(/*sameFirst=*/true, /*twoCalls=*/true, /*valuesOrThrow=*/true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void sameDepInTwoGroups_Same_Two_Deps() throws Exception { | 
 |     sameDepInTwoGroups(/*sameFirst=*/true, /*twoCalls=*/true, /*valuesOrThrow=*/false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void sameDepInTwoGroups_Same_One_Throw() throws Exception { | 
 |     sameDepInTwoGroups(/*sameFirst=*/true, /*twoCalls=*/false, /*valuesOrThrow=*/true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void sameDepInTwoGroups_Same_One_Deps() throws Exception { | 
 |     sameDepInTwoGroups(/*sameFirst=*/true, /*twoCalls=*/false, /*valuesOrThrow=*/false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void sameDepInTwoGroups_Different_Two_Throw() throws Exception { | 
 |     sameDepInTwoGroups(/*sameFirst=*/false, /*twoCalls=*/true, /*valuesOrThrow=*/true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void sameDepInTwoGroups_Different_Two_Deps() throws Exception { | 
 |     sameDepInTwoGroups(/*sameFirst=*/false, /*twoCalls=*/true, /*valuesOrThrow=*/false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void sameDepInTwoGroups_Different_One_Throw() throws Exception { | 
 |     sameDepInTwoGroups(/*sameFirst=*/false, /*twoCalls=*/false, /*valuesOrThrow=*/true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void sameDepInTwoGroups_Different_One_Deps() throws Exception { | 
 |     sameDepInTwoGroups(/*sameFirst=*/false, /*twoCalls=*/false, /*valuesOrThrow=*/false); | 
 |   } | 
 |  | 
 |   private void getValuesOrThrowWithErrors(boolean keepGoing) throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     final SkyKey errorDep = GraphTester.toSkyKey("errorChild"); | 
 |     final SomeErrorException childExn = new SomeErrorException("child error"); | 
 |     tester.getOrCreate(errorDep).setBuilder(new SkyFunction() { | 
 |       @Override | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { | 
 |         throw new GenericFunctionException(childExn, Transience.PERSISTENT); | 
 |       } | 
 |  | 
 |       @Override | 
 |       public String extractTag(SkyKey skyKey) { | 
 |         return null; | 
 |       } | 
 |     }); | 
 |     final List<SkyKey> deps = new ArrayList<>(); | 
 |     for (int i = 1; i <= 3; i++) { | 
 |       SkyKey dep = GraphTester.toSkyKey("child" + i); | 
 |       deps.add(dep); | 
 |       tester.set(dep, new StringValue("child" + i)); | 
 |     } | 
 |     final SomeErrorException parentExn = new SomeErrorException("parent error"); | 
 |     tester | 
 |         .getOrCreate(parentKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) | 
 |                   throws SkyFunctionException, InterruptedException { | 
 |                 try { | 
 |                   SkyValue value = env.getValueOrThrow(errorDep, SomeErrorException.class); | 
 |                   if (value == null) { | 
 |                     return null; | 
 |                   } | 
 |                 } catch (SomeErrorException e) { | 
 |                   // Recover from the child error. | 
 |                 } | 
 |                 env.getValues(deps); | 
 |                 if (env.valuesMissing()) { | 
 |                   return null; | 
 |                 } | 
 |                 throw new GenericFunctionException(parentExn, Transience.PERSISTENT); | 
 |               } | 
 |  | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |     EvaluationResult<StringValue> evaluationResult = eval(keepGoing, ImmutableList.of(parentKey)); | 
 |     assertThat(evaluationResult.hasError()).isTrue(); | 
 |     assertThat(evaluationResult.getError().getException()) | 
 |         .isEqualTo(keepGoing ? parentExn : childExn); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void getValuesOrThrowWithErrors_NoKeepGoing() throws Exception { | 
 |     getValuesOrThrowWithErrors(/*keepGoing=*/false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void getValuesOrThrowWithErrors_KeepGoing() throws Exception { | 
 |     getValuesOrThrowWithErrors(/*keepGoing=*/true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void duplicateCycles() throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey grandparentKey = GraphTester.toSkyKey("grandparent"); | 
 |     SkyKey parentKey1 = GraphTester.toSkyKey("parent1"); | 
 |     SkyKey parentKey2 = GraphTester.toSkyKey("parent2"); | 
 |     SkyKey loopKey1 = GraphTester.toSkyKey("loop1"); | 
 |     SkyKey loopKey2 = GraphTester.toSkyKey("loop2"); | 
 |     tester.getOrCreate(loopKey1).addDependency(loopKey2); | 
 |     tester.getOrCreate(loopKey2).addDependency(loopKey1); | 
 |     tester.getOrCreate(parentKey1).addDependency(loopKey1); | 
 |     tester.getOrCreate(parentKey2).addDependency(loopKey2); | 
 |     tester.getOrCreate(grandparentKey).addDependency(parentKey1); | 
 |     tester.getOrCreate(grandparentKey).addDependency(parentKey2); | 
 |  | 
 |     ErrorInfo errorInfo = evalValueInError(grandparentKey); | 
 |     List<ImmutableList<SkyKey>> cycles = Lists.newArrayList(); | 
 |     for (CycleInfo cycleInfo : errorInfo.getCycleInfo()) { | 
 |       cycles.add(cycleInfo.getCycle()); | 
 |     } | 
 |     // Skyframe doesn't automatically dedupe cycles that are the same except for entry point. | 
 |     assertThat(cycles).hasSize(2); | 
 |     int numUniqueCycles = 0; | 
 |     CycleDeduper<SkyKey> cycleDeduper = new CycleDeduper<SkyKey>(); | 
 |     for (ImmutableList<SkyKey> cycle : cycles) { | 
 |       if (cycleDeduper.seen(cycle)) { | 
 |         numUniqueCycles++; | 
 |       } | 
 |     } | 
 |     assertThat(numUniqueCycles).isEqualTo(1); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void signalValueEnqueuedAndEvaluated() throws Exception { | 
 |     final Set<SkyKey> enqueuedValues = Sets.newConcurrentHashSet(); | 
 |     final Set<SkyKey> evaluatedValues = Sets.newConcurrentHashSet(); | 
 |     EvaluationProgressReceiver progressReceiver = | 
 |         new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { | 
 |       @Override | 
 |       public void enqueueing(SkyKey skyKey) { | 
 |         enqueuedValues.add(skyKey); | 
 |       } | 
 |  | 
 |       @Override | 
 |       public void evaluated(SkyKey skyKey, Supplier<SkyValue> skyValueSupplier, | 
 |           EvaluationState state) { | 
 |         evaluatedValues.add(skyKey); | 
 |       } | 
 |     }; | 
 |  | 
 |     ExtendedEventHandler reporter = | 
 |         new Reporter( | 
 |             new EventBus(), | 
 |             new EventHandler() { | 
 |               @Override | 
 |               public void handle(Event e) { | 
 |                 throw new IllegalStateException(); | 
 |               } | 
 |             }); | 
 |  | 
 |     MemoizingEvaluator aug = | 
 |         new InMemoryMemoizingEvaluator( | 
 |             ImmutableMap.of(GraphTester.NODE_TYPE, tester.getFunction()), | 
 |             new SequencedRecordingDifferencer(), | 
 |             progressReceiver); | 
 |     SequentialBuildDriver driver = new SequentialBuildDriver(aug); | 
 |  | 
 |     tester.getOrCreate("top1").setComputedValue(CONCATENATE) | 
 |         .addDependency("d1").addDependency("d2"); | 
 |     tester.getOrCreate("top2").setComputedValue(CONCATENATE).addDependency("d3"); | 
 |     tester.getOrCreate("top3"); | 
 |     assertThat(enqueuedValues).isEmpty(); | 
 |     assertThat(evaluatedValues).isEmpty(); | 
 |  | 
 |     tester.set("d1", new StringValue("1")); | 
 |     tester.set("d2", new StringValue("2")); | 
 |     tester.set("d3", new StringValue("3")); | 
 |  | 
 |     driver.evaluate(ImmutableList.of(GraphTester.toSkyKey("top1")), false, 200, reporter); | 
 |     assertThat(enqueuedValues).containsExactlyElementsIn( | 
 |         GraphTester.toSkyKeys("top1", "d1", "d2")); | 
 |     assertThat(evaluatedValues).containsExactlyElementsIn( | 
 |         GraphTester.toSkyKeys("top1", "d1", "d2")); | 
 |     enqueuedValues.clear(); | 
 |     evaluatedValues.clear(); | 
 |  | 
 |     driver.evaluate(ImmutableList.of(GraphTester.toSkyKey("top2")), false, 200, reporter); | 
 |     assertThat(enqueuedValues).containsExactlyElementsIn(GraphTester.toSkyKeys("top2", "d3")); | 
 |     assertThat(evaluatedValues).containsExactlyElementsIn(GraphTester.toSkyKeys("top2", "d3")); | 
 |     enqueuedValues.clear(); | 
 |     evaluatedValues.clear(); | 
 |  | 
 |     driver.evaluate(ImmutableList.of(GraphTester.toSkyKey("top1")), false, 200, reporter); | 
 |     assertThat(enqueuedValues).isEmpty(); | 
 |     assertThat(evaluatedValues).containsExactlyElementsIn(GraphTester.toSkyKeys("top1")); | 
 |   } | 
 |  | 
 |   public void runDepOnErrorHaltsNoKeepGoingBuildEagerly(boolean childErrorCached, | 
 |       final boolean handleChildError) throws Exception { | 
 |     graph = new InMemoryGraphImpl(); | 
 |     SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     final SkyKey childKey = GraphTester.toSkyKey("child"); | 
 |     tester.getOrCreate(childKey).setHasError(/*hasError=*/true); | 
 |     // The parent should be built exactly twice: once during normal evaluation and once | 
 |     // during error bubbling. | 
 |     final AtomicInteger numParentInvocations = new AtomicInteger(0); | 
 |     tester | 
 |         .getOrCreate(parentKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) | 
 |                   throws SkyFunctionException, InterruptedException { | 
 |                 int invocations = numParentInvocations.incrementAndGet(); | 
 |                 if (handleChildError) { | 
 |                   try { | 
 |                     SkyValue value = env.getValueOrThrow(childKey, SomeErrorException.class); | 
 |                     // On the first invocation, either the child error should already be cached and | 
 |                     // not propagated, or it should be computed freshly and not propagated. On the | 
 |                     // second build (error bubbling), the child error should be propagated. | 
 |                     assertWithMessage("bogus non-null value " + value).that(value == null).isTrue(); | 
 |                     assertWithMessage("parent incorrectly re-computed during normal evaluation") | 
 |                         .that(invocations) | 
 |                         .isEqualTo(1); | 
 |                     assertWithMessage("child error not propagated during error bubbling") | 
 |                         .that(env.inErrorBubblingForTesting()) | 
 |                         .isFalse(); | 
 |                     return value; | 
 |                   } catch (SomeErrorException e) { | 
 |                     assertWithMessage("child error propagated during normal evaluation") | 
 |                         .that(env.inErrorBubblingForTesting()) | 
 |                         .isTrue(); | 
 |                     assertThat(invocations).isEqualTo(2); | 
 |                     return null; | 
 |                   } | 
 |                 } else { | 
 |                   if (invocations == 1) { | 
 |                     assertWithMessage( | 
 |                             "parent's first computation should be during normal evaluation") | 
 |                         .that(env.inErrorBubblingForTesting()) | 
 |                         .isFalse(); | 
 |                     return env.getValue(childKey); | 
 |                   } else { | 
 |                     assertThat(invocations).isEqualTo(2); | 
 |                     assertWithMessage("parent incorrectly re-computed during normal evaluation") | 
 |                         .that(env.inErrorBubblingForTesting()) | 
 |                         .isTrue(); | 
 |                     return env.getValue(childKey); | 
 |                   } | 
 |                 } | 
 |               } | 
 |  | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |     if (childErrorCached) { | 
 |       // Ensure that the child is already in the graph. | 
 |       evalValueInError(childKey); | 
 |     } | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/false, ImmutableList.of(parentKey)); | 
 |     assertThat(numParentInvocations.get()).isEqualTo(2); | 
 |     assertThat(result.hasError()).isTrue(); | 
 |     assertThat(result.getError().getRootCauseOfException()).isEqualTo(childKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void depOnErrorHaltsNoKeepGoingBuildEagerly_ChildErrorCachedAndHandled() | 
 |       throws Exception { | 
 |     runDepOnErrorHaltsNoKeepGoingBuildEagerly(/*childErrorCached=*/true, | 
 |         /*handleChildError=*/true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void depOnErrorHaltsNoKeepGoingBuildEagerly_ChildErrorCachedAndNotHandled() | 
 |       throws Exception { | 
 |     runDepOnErrorHaltsNoKeepGoingBuildEagerly(/*childErrorCached=*/true, | 
 |         /*handleChildError=*/false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void depOnErrorHaltsNoKeepGoingBuildEagerly_ChildErrorFreshAndHandled() throws Exception { | 
 |     runDepOnErrorHaltsNoKeepGoingBuildEagerly(/*childErrorCached=*/false, | 
 |         /*handleChildError=*/true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void depOnErrorHaltsNoKeepGoingBuildEagerly_ChildErrorFreshAndNotHandled() | 
 |       throws Exception { | 
 |     runDepOnErrorHaltsNoKeepGoingBuildEagerly(/*childErrorCached=*/false, | 
 |         /*handleChildError=*/false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void raceConditionWithNoKeepGoingErrors_FutureError() throws Exception { | 
 |     final CountDownLatch errorCommitted = new CountDownLatch(1); | 
 |     final CountDownLatch otherStarted = new CountDownLatch(1); | 
 |     final CountDownLatch otherParentSignaled = new CountDownLatch(1); | 
 |     final SkyKey errorParentKey = GraphTester.toSkyKey("errorParentKey"); | 
 |     final SkyKey errorKey = GraphTester.toSkyKey("errorKey"); | 
 |     final SkyKey otherParentKey = GraphTester.toSkyKey("otherParentKey"); | 
 |     final SkyKey otherKey = GraphTester.toSkyKey("otherKey"); | 
 |     final AtomicInteger numOtherParentInvocations = new AtomicInteger(0); | 
 |     final AtomicInteger numErrorParentInvocations = new AtomicInteger(0); | 
 |     tester | 
 |         .getOrCreate(otherParentKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) | 
 |                   throws SkyFunctionException, InterruptedException { | 
 |                 int invocations = numOtherParentInvocations.incrementAndGet(); | 
 |                 assertWithMessage("otherParentKey should not be restarted") | 
 |                     .that(invocations) | 
 |                     .isEqualTo(1); | 
 |                 return env.getValue(otherKey); | 
 |               } | 
 |  | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |     tester | 
 |         .getOrCreate(otherKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { | 
 |                 otherStarted.countDown(); | 
 |                 TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions( | 
 |                     errorCommitted, "error didn't get committed to the graph in time"); | 
 |                 return new StringValue("other"); | 
 |               } | 
 |  | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |     tester | 
 |         .getOrCreate(errorKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { | 
 |                 TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions( | 
 |                     otherStarted, "other didn't start in time"); | 
 |                 throw new GenericFunctionException( | 
 |                     new SomeErrorException("error"), Transience.PERSISTENT); | 
 |               } | 
 |  | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |     tester | 
 |         .getOrCreate(errorParentKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) | 
 |                   throws SkyFunctionException, InterruptedException { | 
 |                 int invocations = numErrorParentInvocations.incrementAndGet(); | 
 |                 try { | 
 |                   SkyValue value = env.getValueOrThrow(errorKey, SomeErrorException.class); | 
 |                   assertWithMessage("bogus non-null value " + value).that(value == null).isTrue(); | 
 |                   if (invocations == 1) { | 
 |                     return null; | 
 |                   } else { | 
 |                     assertThat(env.inErrorBubblingForTesting()).isFalse(); | 
 |                     fail("RACE CONDITION: errorParentKey was restarted!"); | 
 |                     return null; | 
 |                   } | 
 |                 } catch (SomeErrorException e) { | 
 |                   assertWithMessage("child error propagated during normal evaluation") | 
 |                       .that(env.inErrorBubblingForTesting()) | 
 |                       .isTrue(); | 
 |                   assertThat(invocations).isEqualTo(2); | 
 |                   return null; | 
 |                 } | 
 |               } | 
 |  | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |     graph = | 
 |         new NotifyingHelper.NotifyingProcessableGraph( | 
 |             new InMemoryGraphImpl(), | 
 |             new Listener() { | 
 |               @Override | 
 |               public void accept(SkyKey key, EventType type, Order order, Object context) { | 
 |                 if (key.equals(errorKey) && type == EventType.SET_VALUE && order == Order.AFTER) { | 
 |                   errorCommitted.countDown(); | 
 |                   TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions( | 
 |                       otherParentSignaled, "otherParent didn't get signaled in time"); | 
 |                   // We try to give some time for ParallelEvaluator to incorrectly re-evaluate | 
 |                   // 'otherParentKey'. This test case is testing for a real race condition and the | 
 |                   // 10ms time was chosen experimentally to give a true positive rate of 99.8% | 
 |                   // (without a sleep it has a 1% true positive rate). There's no good way to do | 
 |                   // this without sleeping. We *could* introspect ParallelEvaulator's | 
 |                   // AbstractQueueVisitor to see if the re-evaluation has been enqueued, but that's | 
 |                   // relying on pretty low-level implementation details. | 
 |                   Uninterruptibles.sleepUninterruptibly(10, TimeUnit.MILLISECONDS); | 
 |                 } | 
 |                 if (key.equals(otherParentKey) | 
 |                     && type == EventType.SIGNAL | 
 |                     && order == Order.AFTER) { | 
 |                   otherParentSignaled.countDown(); | 
 |                 } | 
 |               } | 
 |             }); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/false, | 
 |         ImmutableList.of(otherParentKey, errorParentKey)); | 
 |     assertThat(result.hasError()).isTrue(); | 
 |     assertThat(result.getError().getRootCauseOfException()).isEqualTo(errorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void cachedErrorsFromKeepGoingUsedOnNoKeepGoing() throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     tester = new GraphTester(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     SkyKey parent1Key = GraphTester.toSkyKey("parent1"); | 
 |     SkyKey parent2Key = GraphTester.toSkyKey("parent2"); | 
 |     tester.getOrCreate(parent1Key).addDependency(errorKey).setConstantValue( | 
 |         new StringValue("parent1")); | 
 |     tester.getOrCreate(parent2Key).addDependency(errorKey).setConstantValue( | 
 |         new StringValue("parent2")); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(parent1Key)); | 
 |     assertThat(result.hasError()).isTrue(); | 
 |     assertThat(result.getError().getRootCauseOfException()).isEqualTo(errorKey); | 
 |     result = eval(/*keepGoing=*/false, ImmutableList.of(parent2Key)); | 
 |     assertThat(result.hasError()).isTrue(); | 
 |     assertThat(result.getError(parent2Key).getRootCauseOfException()).isEqualTo(errorKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void cachedTopLevelErrorsShouldHaltNoKeepGoingBuildEarly() throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     tester = new GraphTester(); | 
 |     SkyKey errorKey = GraphTester.toSkyKey("error"); | 
 |     tester.getOrCreate(errorKey).setHasError(true); | 
 |     EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(errorKey)); | 
 |     assertThat(result.hasError()).isTrue(); | 
 |     assertThat(result.getError().getRootCauseOfException()).isEqualTo(errorKey); | 
 |     SkyKey rogueKey = GraphTester.toSkyKey("rogue"); | 
 |     tester.getOrCreate(rogueKey).setBuilder(new SkyFunction() { | 
 |       @Override | 
 |       public SkyValue compute(SkyKey skyKey, Environment env) { | 
 |         // This SkyFunction could do an arbitrarily bad computation, e.g. loop-forever. So we want | 
 |         // to make sure that it is never run when we want to fail-fast anyway. | 
 |         fail("eval call should have already terminated"); | 
 |         return null; | 
 |       } | 
 |  | 
 |       @Override | 
 |       public String extractTag(SkyKey skyKey) { | 
 |         return null; | 
 |       } | 
 |     }); | 
 |     result = eval(/*keepGoing=*/false, ImmutableList.of(errorKey, rogueKey)); | 
 |     assertThat(result.hasError()).isTrue(); | 
 |     assertThat(result.getError(errorKey).getRootCauseOfException()).isEqualTo(errorKey); | 
 |     assertThat(result.errorMap()).doesNotContainKey(rogueKey); | 
 |   } | 
 |  | 
 |   private void runUnhandledTransitiveErrors(boolean keepGoing, | 
 |       final boolean explicitlyPropagateError) throws Exception { | 
 |     graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); | 
 |     tester = new GraphTester(); | 
 |     SkyKey grandparentKey = GraphTester.toSkyKey("grandparent"); | 
 |     final SkyKey parentKey = GraphTester.toSkyKey("parent"); | 
 |     final SkyKey childKey = GraphTester.toSkyKey("child"); | 
 |     final AtomicBoolean errorPropagated = new AtomicBoolean(false); | 
 |     tester | 
 |         .getOrCreate(grandparentKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) | 
 |                   throws SkyFunctionException, InterruptedException { | 
 |                 try { | 
 |                   return env.getValueOrThrow(parentKey, SomeErrorException.class); | 
 |                 } catch (SomeErrorException e) { | 
 |                   errorPropagated.set(true); | 
 |                   throw new GenericFunctionException(e, Transience.PERSISTENT); | 
 |                 } | 
 |               } | 
 |  | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |     tester | 
 |         .getOrCreate(parentKey) | 
 |         .setBuilder( | 
 |             new SkyFunction() { | 
 |               @Override | 
 |               public SkyValue compute(SkyKey skyKey, Environment env) | 
 |                   throws SkyFunctionException, InterruptedException { | 
 |                 if (explicitlyPropagateError) { | 
 |                   try { | 
 |                     return env.getValueOrThrow(childKey, SomeErrorException.class); | 
 |                   } catch (SomeErrorException e) { | 
 |                     throw new GenericFunctionException(e, childKey); | 
 |                   } | 
 |                 } else { | 
 |                   return env.getValue(childKey); | 
 |                 } | 
 |               } | 
 |  | 
 |               @Override | 
 |               public String extractTag(SkyKey skyKey) { | 
 |                 return null; | 
 |               } | 
 |             }); | 
 |     tester.getOrCreate(childKey).setHasError(/*hasError=*/true); | 
 |     EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(grandparentKey)); | 
 |     assertThat(result.hasError()).isTrue(); | 
 |     assertThat(errorPropagated.get()).isTrue(); | 
 |     assertThat(result.getError().getRootCauseOfException()).isEqualTo(grandparentKey); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void unhandledTransitiveErrorsDuringErrorBubbling_ImplicitPropagation() throws Exception { | 
 |     runUnhandledTransitiveErrors(/*keepGoing=*/false, /*explicitlyPropagateError=*/false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void unhandledTransitiveErrorsDuringErrorBubbling_ExplicitPropagation() throws Exception { | 
 |     runUnhandledTransitiveErrors(/*keepGoing=*/false, /*explicitlyPropagateError=*/true); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void unhandledTransitiveErrorsDuringNormalEvaluation_ImplicitPropagation() | 
 |       throws Exception { | 
 |     runUnhandledTransitiveErrors(/*keepGoing=*/true, /*explicitlyPropagateError=*/false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void unhandledTransitiveErrorsDuringNormalEvaluation_ExplicitPropagation() | 
 |       throws Exception { | 
 |     runUnhandledTransitiveErrors(/*keepGoing=*/true, /*explicitlyPropagateError=*/true); | 
 |   } | 
 | } |