| // Copyright 2015 The Bazel Authors. All rights reserved. | 
 | // | 
 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | // you may not use this file except in compliance with the License. | 
 | // You may obtain a copy of the License at | 
 | // | 
 | //    http://www.apache.org/licenses/LICENSE-2.0 | 
 | // | 
 | // Unless required by applicable law or agreed to in writing, software | 
 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | // See the License for the specific language governing permissions and | 
 | // limitations under the License. | 
 | package com.google.devtools.build.lib.skyframe; | 
 |  | 
 | import static com.google.common.truth.Truth.assertThat; | 
 |  | 
 | import com.google.common.base.Objects; | 
 | import com.google.common.base.Supplier; | 
 | import com.google.common.collect.ImmutableList; | 
 | import com.google.common.collect.ImmutableSet; | 
 | import com.google.common.collect.Iterables; | 
 | import com.google.common.collect.Sets; | 
 | import com.google.common.util.concurrent.Callables; | 
 | import com.google.devtools.build.lib.actions.AbstractAction; | 
 | import com.google.devtools.build.lib.actions.Action; | 
 | import com.google.devtools.build.lib.actions.ActionExecutionContext; | 
 | import com.google.devtools.build.lib.actions.ActionExecutionException; | 
 | import com.google.devtools.build.lib.actions.Artifact; | 
 | import com.google.devtools.build.lib.actions.Executor; | 
 | import com.google.devtools.build.lib.actions.util.ActionsTestUtil; | 
 | import com.google.devtools.build.lib.actions.util.DummyExecutor; | 
 | import com.google.devtools.build.lib.util.Fingerprint; | 
 | import com.google.devtools.build.lib.vfs.Path; | 
 | import com.google.devtools.build.lib.vfs.PathFragment; | 
 | import com.google.devtools.build.lib.vfs.RootedPath; | 
 | import com.google.devtools.build.skyframe.EvaluationProgressReceiver; | 
 | import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState; | 
 | import com.google.devtools.build.skyframe.SkyFunction.Environment; | 
 | import com.google.devtools.build.skyframe.SkyKey; | 
 | import com.google.devtools.build.skyframe.SkyValue; | 
 | import java.io.IOException; | 
 | import java.io.InputStream; | 
 | import java.io.OutputStream; | 
 | import java.nio.charset.StandardCharsets; | 
 | import java.util.Set; | 
 | import java.util.concurrent.Callable; | 
 | import java.util.concurrent.atomic.AtomicInteger; | 
 | import javax.annotation.Nullable; | 
 | import org.junit.Before; | 
 | import org.junit.Test; | 
 | import org.junit.runner.RunWith; | 
 | import org.junit.runners.JUnit4; | 
 |  | 
 | /** Tests for {@link SkyframeAwareAction}. */ | 
 | @RunWith(JUnit4.class) | 
 | public class SkyframeAwareActionTest extends TimestampBuilderTestCase { | 
 |   private Builder builder; | 
 |   private Executor executor; | 
 |   private TrackingEvaluationProgressReceiver progressReceiver; | 
 |  | 
 |   @Before | 
 |   public final void createBuilder() throws Exception { | 
 |     progressReceiver = new TrackingEvaluationProgressReceiver(); | 
 |     builder = createBuilder(inMemoryCache, 1, /*keepGoing=*/ false, progressReceiver); | 
 |   } | 
 |  | 
 |   @Before | 
 |   public final void createExecutor() throws Exception { | 
 |     executor = new DummyExecutor(rootDirectory); | 
 |   } | 
 |  | 
 |   private static final class TrackingEvaluationProgressReceiver | 
 |       extends EvaluationProgressReceiver.NullEvaluationProgressReceiver { | 
 |  | 
 |     public static final class InvalidatedKey { | 
 |       public final SkyKey skyKey; | 
 |       public final InvalidationState state; | 
 |  | 
 |       InvalidatedKey(SkyKey skyKey, InvalidationState state) { | 
 |         this.skyKey = skyKey; | 
 |         this.state = state; | 
 |       } | 
 |  | 
 |       @Override | 
 |       public boolean equals(Object obj) { | 
 |         return obj instanceof InvalidatedKey | 
 |             && this.skyKey.equals(((InvalidatedKey) obj).skyKey) | 
 |             && this.state.equals(((InvalidatedKey) obj).state); | 
 |       } | 
 |  | 
 |       @Override | 
 |       public int hashCode() { | 
 |         return Objects.hashCode(skyKey, state); | 
 |       } | 
 |     } | 
 |  | 
 |     public static final class EvaluatedEntry { | 
 |       public final SkyKey skyKey; | 
 |       public final SkyValue value; | 
 |       public final EvaluationState state; | 
 |  | 
 |       EvaluatedEntry(SkyKey skyKey, SkyValue value, EvaluationState state) { | 
 |         this.skyKey = skyKey; | 
 |         this.value = value; | 
 |         this.state = state; | 
 |       } | 
 |  | 
 |       @Override | 
 |       public boolean equals(Object obj) { | 
 |         return obj instanceof EvaluatedEntry | 
 |             && this.skyKey.equals(((EvaluatedEntry) obj).skyKey) | 
 |             && this.value.equals(((EvaluatedEntry) obj).value) | 
 |             && this.state.equals(((EvaluatedEntry) obj).state); | 
 |       } | 
 |  | 
 |       @Override | 
 |       public int hashCode() { | 
 |         return Objects.hashCode(skyKey, value, state); | 
 |       } | 
 |     } | 
 |  | 
 |     public final Set<InvalidatedKey> invalidated = Sets.newConcurrentHashSet(); | 
 |     public final Set<SkyKey> enqueued = Sets.newConcurrentHashSet(); | 
 |     public final Set<EvaluatedEntry> evaluated = Sets.newConcurrentHashSet(); | 
 |  | 
 |     public void reset() { | 
 |       invalidated.clear(); | 
 |       enqueued.clear(); | 
 |       evaluated.clear(); | 
 |     } | 
 |  | 
 |     public boolean wasInvalidated(SkyKey skyKey) { | 
 |       for (InvalidatedKey e : invalidated) { | 
 |         if (e.skyKey.equals(skyKey)) { | 
 |           return true; | 
 |         } | 
 |       } | 
 |       return false; | 
 |     } | 
 |  | 
 |     public EvaluatedEntry getEvalutedEntry(SkyKey forKey) { | 
 |       for (EvaluatedEntry e : evaluated) { | 
 |         if (e.skyKey.equals(forKey)) { | 
 |           return e; | 
 |         } | 
 |       } | 
 |       return null; | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void invalidated(SkyKey skyKey, InvalidationState state) { | 
 |       invalidated.add(new InvalidatedKey(skyKey, state)); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void enqueueing(SkyKey skyKey) { | 
 |       enqueued.add(skyKey); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void evaluated( | 
 |         SkyKey skyKey, Supplier<SkyValue> skyValueSupplier, EvaluationState state) { | 
 |       evaluated.add(new EvaluatedEntry(skyKey, skyValueSupplier.get(), state)); | 
 |     } | 
 |   } | 
 |  | 
 |   /** A mock action that counts how many times it was executed. */ | 
 |   private static class ExecutionCountingAction extends AbstractAction { | 
 |     private final AtomicInteger executionCounter; | 
 |  | 
 |     ExecutionCountingAction(Artifact input, Artifact output, AtomicInteger executionCounter) { | 
 |       super(ActionsTestUtil.NULL_ACTION_OWNER, ImmutableList.of(input), ImmutableList.of(output)); | 
 |       this.executionCounter = executionCounter; | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void execute(ActionExecutionContext actionExecutionContext) | 
 |         throws ActionExecutionException, InterruptedException { | 
 |       executionCounter.incrementAndGet(); | 
 |  | 
 |       // This action first reads its input file (there can be only one). For the purpose of these | 
 |       // tests we assume that the input file is short, maybe just 10 bytes long. | 
 |       byte[] input = new byte[10]; | 
 |       int inputLen = 0; | 
 |       try (InputStream in = Iterables.getOnlyElement(getInputs()).getPath().getInputStream()) { | 
 |         inputLen = in.read(input); | 
 |       } catch (IOException e) { | 
 |         throw new ActionExecutionException(e, this, false); | 
 |       } | 
 |  | 
 |       // This action then writes the contents of the input to the (only) output file, and appends an | 
 |       // extra "x" character too. | 
 |       try (OutputStream out = getPrimaryOutput().getPath().getOutputStream()) { | 
 |         out.write(input, 0, inputLen); | 
 |         out.write('x'); | 
 |       } catch (IOException e) { | 
 |         throw new ActionExecutionException(e, this, false); | 
 |       } | 
 |     } | 
 |  | 
 |     @Override | 
 |     public String getMnemonic() { | 
 |       return null; | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected String computeKey() { | 
 |       return getPrimaryOutput().getExecPathString() + executionCounter.get(); | 
 |     } | 
 |   } | 
 |  | 
 |   private static class ExecutionCountingCacheBypassingAction extends ExecutionCountingAction { | 
 |     ExecutionCountingCacheBypassingAction( | 
 |         Artifact input, Artifact output, AtomicInteger executionCounter) { | 
 |       super(input, output, executionCounter); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public boolean executeUnconditionally() { | 
 |       return true; | 
 |     } | 
 |  | 
 |     @Override | 
 |     public boolean isVolatile() { | 
 |       return true; | 
 |     } | 
 |   } | 
 |  | 
 |   /** A mock skyframe-aware action that counts how many times it was executed. */ | 
 |   private static class SkyframeAwareExecutionCountingAction | 
 |       extends ExecutionCountingCacheBypassingAction implements SkyframeAwareAction { | 
 |     private final SkyKey actionDepKey; | 
 |  | 
 |     SkyframeAwareExecutionCountingAction( | 
 |         Artifact input, Artifact output, AtomicInteger executionCounter, SkyKey actionDepKey) { | 
 |       super(input, output, executionCounter); | 
 |       this.actionDepKey = actionDepKey; | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void establishSkyframeDependencies(Environment env) | 
 |         throws ExceptionBase, InterruptedException { | 
 |       // Establish some Skyframe dependency. A real action would then use this to compute and | 
 |       // cache data for the execute(...) method. | 
 |       env.getValue(actionDepKey); | 
 |     } | 
 |   } | 
 |  | 
 |   private interface ExecutionCountingActionFactory { | 
 |     ExecutionCountingAction create(Artifact input, Artifact output, AtomicInteger executionCounter); | 
 |   } | 
 |  | 
 |   private enum ChangeArtifact { | 
 |     DONT_CHANGE, | 
 |     CHANGE_MTIME { | 
 |       @Override | 
 |       boolean changeMtime() { | 
 |         return true; | 
 |       } | 
 |     }, | 
 |     CHANGE_MTIME_AND_CONTENT { | 
 |       @Override | 
 |       boolean changeMtime() { | 
 |         return true; | 
 |       } | 
 |  | 
 |       @Override | 
 |       boolean changeContent() { | 
 |         return true; | 
 |       } | 
 |     }; | 
 |  | 
 |     boolean changeMtime() { | 
 |       return false; | 
 |     } | 
 |  | 
 |     boolean changeContent() { | 
 |       return false; | 
 |     } | 
 |   } | 
 |  | 
 |   private enum ExpectActionIs { | 
 |     NOT_DIRTIED { | 
 |       @Override | 
 |       boolean actuallyClean() { | 
 |         return true; | 
 |       } | 
 |     }, | 
 |     DIRTIED_BUT_VERIFIED_CLEAN { | 
 |       @Override | 
 |       boolean dirtied() { | 
 |         return true; | 
 |       } | 
 |  | 
 |       @Override | 
 |       boolean actuallyClean() { | 
 |         return true; | 
 |       } | 
 |     }, | 
 |  | 
 |     // REBUILT_BUT_ACTION_CACHE_HIT, | 
 |     // This would be a bug, symptom of a skyframe-aware action that doesn't bypass the action cache | 
 |     // and is incorrectly regarded as an action cache hit when its inputs stayed the same but its | 
 |     // "skyframe dependencies" changed. | 
 |  | 
 |     REEXECUTED { | 
 |       @Override | 
 |       boolean dirtied() { | 
 |         return true; | 
 |       } | 
 |  | 
 |       @Override | 
 |       boolean reexecuted() { | 
 |         return true; | 
 |       } | 
 |     }; | 
 |  | 
 |     boolean dirtied() { | 
 |       return false; | 
 |     } | 
 |  | 
 |     boolean actuallyClean() { | 
 |       return false; | 
 |     } | 
 |  | 
 |     boolean reexecuted() { | 
 |       return false; | 
 |     } | 
 |   } | 
 |  | 
 |   private void maybeChangeFile(Artifact file, ChangeArtifact changeRequest) throws Exception { | 
 |     if (changeRequest == ChangeArtifact.DONT_CHANGE) { | 
 |       return; | 
 |     } | 
 |  | 
 |     if (changeRequest.changeMtime()) { | 
 |       // 1000000 should be larger than the filesystem timestamp granularity. | 
 |       file.getPath().setLastModifiedTime(file.getPath().getLastModifiedTime() + 1000000); | 
 |       tsgm.waitForTimestampGranularity(reporter.getOutErr()); | 
 |     } | 
 |  | 
 |     if (changeRequest.changeContent()) { | 
 |       appendToFile(file.getPath()); | 
 |     } | 
 |  | 
 |     // Invalidate the file state value to inform Skyframe that the file may have changed. | 
 |     // This will also invalidate the action execution value. | 
 |     differencer.invalidate( | 
 |         ImmutableList.of( | 
 |             FileStateValue.key( | 
 |                 RootedPath.toRootedPath(file.getRoot().getPath(), file.getRootRelativePath())))); | 
 |   } | 
 |  | 
 |   private void assertActionExecutions( | 
 |       ExecutionCountingActionFactory actionFactory, | 
 |       ChangeArtifact changeActionInput, | 
 |       Callable<Void> betweenBuilds, | 
 |       ExpectActionIs expectActionIs) | 
 |       throws Exception { | 
 |     // Set up the action's input, output, owner and most importantly the execution counter. | 
 |     Artifact actionInput = createSourceArtifact("foo/action-input.txt"); | 
 |     Artifact actionOutput = createDerivedArtifact("foo/action-output.txt"); | 
 |     AtomicInteger executionCounter = new AtomicInteger(0); | 
 |  | 
 |     scratch.file(actionInput.getPath().getPathString(), "foo"); | 
 |  | 
 |     // Generating actions of artifacts are found by looking them up in the graph. The lookup value | 
 |     // must be present in the graph before execution. | 
 |     Action action = actionFactory.create(actionInput, actionOutput, executionCounter); | 
 |     registerAction(action); | 
 |  | 
 |     // Build the output for the first time. | 
 |     builder.buildArtifacts( | 
 |         reporter, | 
 |         ImmutableSet.of(actionOutput), | 
 |         null, | 
 |         null, | 
 |         null, | 
 |         null, | 
 |         executor, | 
 |         null, | 
 |         false, | 
 |         null, | 
 |         null); | 
 |  | 
 |     // Sanity check that our invalidation receiver is working correctly. We'll rely on it again. | 
 |     SkyKey actionKey = ActionExecutionValue.key(OWNER_KEY, 0); | 
 |     TrackingEvaluationProgressReceiver.EvaluatedEntry evaluatedAction = | 
 |         progressReceiver.getEvalutedEntry(actionKey); | 
 |     assertThat(evaluatedAction).isNotNull(); | 
 |     SkyValue actionValue = evaluatedAction.value; | 
 |  | 
 |     // Mutate the action input if requested. | 
 |     maybeChangeFile(actionInput, changeActionInput); | 
 |  | 
 |     // Execute user code before next build. | 
 |     betweenBuilds.call(); | 
 |  | 
 |     // Rebuild the output. | 
 |     progressReceiver.reset(); | 
 |     builder.buildArtifacts( | 
 |         reporter, | 
 |         ImmutableSet.of(actionOutput), | 
 |         null, | 
 |         null, | 
 |         null, | 
 |         null, | 
 |         executor, | 
 |         null, | 
 |         false, | 
 |         null, | 
 |         null); | 
 |  | 
 |     if (expectActionIs.dirtied()) { | 
 |       assertThat(progressReceiver.wasInvalidated(actionKey)).isTrue(); | 
 |  | 
 |       TrackingEvaluationProgressReceiver.EvaluatedEntry newEntry = | 
 |           progressReceiver.getEvalutedEntry(actionKey); | 
 |       assertThat(newEntry).isNotNull(); | 
 |       if (expectActionIs.actuallyClean()) { | 
 |         // Action was dirtied but verified clean. | 
 |         assertThat(newEntry.state).isEqualTo(EvaluationState.CLEAN); | 
 |         assertThat(newEntry.value).isEqualTo(actionValue); | 
 |       } else { | 
 |         // Action was dirtied and rebuilt. It was either reexecuted or was an action cache hit, | 
 |         // doesn't matter here. | 
 |         assertThat(newEntry.state).isEqualTo(EvaluationState.BUILT); | 
 |         assertThat(newEntry.value).isNotEqualTo(actionValue); | 
 |       } | 
 |     } else { | 
 |       // Action was not dirtied. | 
 |       assertThat(progressReceiver.wasInvalidated(actionKey)).isFalse(); | 
 |     } | 
 |  | 
 |     // Assert that the action was executed the right number of times. Whether the action execution | 
 |     // function was called again is up for the test method to verify. | 
 |     assertThat(executionCounter.get()).isEqualTo(expectActionIs.reexecuted() ? 2 : 1); | 
 |   } | 
 |  | 
 |   private RootedPath createSkyframeDepOfAction() throws Exception { | 
 |     scratch.file(rootDirectory.getRelative("action.dep").getPathString(), "blah"); | 
 |     return RootedPath.toRootedPath(rootDirectory, PathFragment.create("action.dep")); | 
 |   } | 
 |  | 
 |   private void appendToFile(Path path) throws Exception { | 
 |     try (OutputStream stm = path.getOutputStream(/*append=*/ true)) { | 
 |       stm.write("blah".getBytes(StandardCharsets.UTF_8)); | 
 |     } | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testCacheCheckingActionWithContentChangingInput() throws Exception { | 
 |     assertActionWithContentChangingInput(/* unconditionalExecution */ false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testCacheBypassingActionWithContentChangingInput() throws Exception { | 
 |     assertActionWithContentChangingInput(/* unconditionalExecution */ true); | 
 |   } | 
 |  | 
 |   private void assertActionWithContentChangingInput(final boolean unconditionalExecution) | 
 |       throws Exception { | 
 |     // Assert that a simple, non-skyframe-aware action is executed twice | 
 |     // if its input's content changes between builds. | 
 |     assertActionExecutions( | 
 |         new ExecutionCountingActionFactory() { | 
 |           @Override | 
 |           public ExecutionCountingAction create( | 
 |               Artifact input, Artifact output, AtomicInteger executionCounter) { | 
 |             return unconditionalExecution | 
 |                 ? new ExecutionCountingCacheBypassingAction(input, output, executionCounter) | 
 |                 : new ExecutionCountingAction(input, output, executionCounter); | 
 |           } | 
 |         }, | 
 |         ChangeArtifact.CHANGE_MTIME_AND_CONTENT, | 
 |         Callables.<Void>returning(null), | 
 |         ExpectActionIs.REEXECUTED); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testCacheCheckingActionWithMtimeChangingInput() throws Exception { | 
 |     assertActionWithMtimeChangingInput(/* unconditionalExecution */ false); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testCacheBypassingActionWithMtimeChangingInput() throws Exception { | 
 |     assertActionWithMtimeChangingInput(/* unconditionalExecution */ true); | 
 |   } | 
 |  | 
 |   private void assertActionWithMtimeChangingInput(final boolean unconditionalExecution) | 
 |       throws Exception { | 
 |     // Assert that a simple, non-skyframe-aware action is executed only once | 
 |     // if its input's mtime changes but its contents stay the same between builds. | 
 |     assertActionExecutions( | 
 |         new ExecutionCountingActionFactory() { | 
 |           @Override | 
 |           public ExecutionCountingAction create( | 
 |               Artifact input, Artifact output, AtomicInteger executionCounter) { | 
 |             return unconditionalExecution | 
 |                 ? new ExecutionCountingCacheBypassingAction(input, output, executionCounter) | 
 |                 : new ExecutionCountingAction(input, output, executionCounter); | 
 |           } | 
 |         }, | 
 |         ChangeArtifact.CHANGE_MTIME, | 
 |         Callables.<Void>returning(null), | 
 |         ExpectActionIs.DIRTIED_BUT_VERIFIED_CLEAN); | 
 |   } | 
 |  | 
 |   public void testActionWithNonChangingInput(final boolean unconditionalExecution) | 
 |       throws Exception { | 
 |     // Assert that a simple, non-skyframe-aware action is executed only once | 
 |     // if its input does not change at all between builds. | 
 |     assertActionExecutions( | 
 |         new ExecutionCountingActionFactory() { | 
 |           @Override | 
 |           public ExecutionCountingAction create( | 
 |               Artifact input, Artifact output, AtomicInteger executionCounter) { | 
 |             return unconditionalExecution | 
 |                 ? new ExecutionCountingCacheBypassingAction(input, output, executionCounter) | 
 |                 : new ExecutionCountingAction(input, output, executionCounter); | 
 |           } | 
 |         }, | 
 |         ChangeArtifact.DONT_CHANGE, | 
 |         Callables.<Void>returning(null), | 
 |         ExpectActionIs.NOT_DIRTIED); | 
 |   } | 
 |  | 
 |   private void assertActionWithMaybeChangingInputAndChangingSkyframeDeps( | 
 |       ChangeArtifact changeInputFile) throws Exception { | 
 |     final RootedPath depPath = createSkyframeDepOfAction(); | 
 |     final SkyKey skyframeDep = FileStateValue.key(depPath); | 
 |  | 
 |     // Assert that an action-cache-check-bypassing action is executed twice if its skyframe deps | 
 |     // change while its input does not. The skyframe dependency is established by making the action | 
 |     // skyframe-aware and updating the value between builds. | 
 |     assertActionExecutions( | 
 |         new ExecutionCountingActionFactory() { | 
 |           @Override | 
 |           public ExecutionCountingAction create( | 
 |               Artifact input, Artifact output, AtomicInteger executionCounter) { | 
 |             return new SkyframeAwareExecutionCountingAction( | 
 |                 input, output, executionCounter, skyframeDep); | 
 |           } | 
 |         }, | 
 |         changeInputFile, | 
 |         new Callable<Void>() { | 
 |           @Override | 
 |           public Void call() throws Exception { | 
 |             // Invalidate the dependency and change what its value will be in the next build. This | 
 |             // should enforce rebuilding of the action. | 
 |             appendToFile(depPath.asPath()); | 
 |             differencer.invalidate(ImmutableList.of(skyframeDep)); | 
 |             return null; | 
 |           } | 
 |         }, | 
 |         ExpectActionIs.REEXECUTED); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testActionWithNonChangingInputButChangingSkyframeDeps() throws Exception { | 
 |     assertActionWithMaybeChangingInputAndChangingSkyframeDeps(ChangeArtifact.DONT_CHANGE); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testActionWithChangingInputMtimeAndChangingSkyframeDeps() throws Exception { | 
 |     assertActionWithMaybeChangingInputAndChangingSkyframeDeps(ChangeArtifact.CHANGE_MTIME); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testActionWithChangingInputAndChangingSkyframeDeps() throws Exception { | 
 |     assertActionWithMaybeChangingInputAndChangingSkyframeDeps( | 
 |         ChangeArtifact.CHANGE_MTIME_AND_CONTENT); | 
 |   } | 
 |  | 
 |   @Test | 
 |   public void testActionWithNonChangingInputAndNonChangingSkyframeDeps() throws Exception { | 
 |     final SkyKey skyframeDep = FileStateValue.key(createSkyframeDepOfAction()); | 
 |  | 
 |     // Assert that an action-cache-check-bypassing action is executed only once if neither its input | 
 |     // nor its Skyframe dependency changes between builds. | 
 |     assertActionExecutions( | 
 |         new ExecutionCountingActionFactory() { | 
 |           @Override | 
 |           public ExecutionCountingAction create( | 
 |               Artifact input, Artifact output, AtomicInteger executionCounter) { | 
 |             return new SkyframeAwareExecutionCountingAction( | 
 |                 input, output, executionCounter, skyframeDep); | 
 |           } | 
 |         }, | 
 |         ChangeArtifact.DONT_CHANGE, | 
 |         new Callable<Void>() { | 
 |           @Override | 
 |           public Void call() throws Exception { | 
 |             // Invalidate the dependency but leave its value up-to-date, so the action should not | 
 |             // be rebuilt. | 
 |             differencer.invalidate(ImmutableList.of(skyframeDep)); | 
 |             return null; | 
 |           } | 
 |         }, | 
 |         ExpectActionIs.DIRTIED_BUT_VERIFIED_CLEAN); | 
 |   } | 
 |  | 
 |   private abstract static class SingleOutputAction extends AbstractAction { | 
 |     SingleOutputAction(@Nullable Artifact input, Artifact output) { | 
 |       super( | 
 |           ActionsTestUtil.NULL_ACTION_OWNER, | 
 |           input == null ? ImmutableList.<Artifact>of() : ImmutableList.of(input), | 
 |           ImmutableList.of(output)); | 
 |     } | 
 |  | 
 |     protected static final class Buffer { | 
 |       final int size; | 
 |       final byte[] data; | 
 |  | 
 |       Buffer(byte[] data, int size) { | 
 |         this.data = data; | 
 |         this.size = size; | 
 |       } | 
 |     } | 
 |  | 
 |     protected Buffer readInput() throws ActionExecutionException { | 
 |       byte[] input = new byte[100]; | 
 |       int inputLen = 0; | 
 |       try (InputStream in = getPrimaryInput().getPath().getInputStream()) { | 
 |         inputLen = in.read(input, 0, input.length); | 
 |       } catch (IOException e) { | 
 |         throw new ActionExecutionException(e, this, false); | 
 |       } | 
 |       return new Buffer(input, inputLen); | 
 |     } | 
 |  | 
 |     protected void writeOutput(@Nullable Buffer buf, String data) throws ActionExecutionException { | 
 |       try (OutputStream out = getPrimaryOutput().getPath().getOutputStream()) { | 
 |         if (buf != null) { | 
 |           out.write(buf.data, 0, buf.size); | 
 |         } | 
 |         out.write(data.getBytes(StandardCharsets.UTF_8), 0, data.length()); | 
 |       } catch (IOException e) { | 
 |         throw new ActionExecutionException(e, this, false); | 
 |       } | 
 |     } | 
 |  | 
 |     @Override | 
 |     public String getMnemonic() { | 
 |       return "MockActionMnemonic"; | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected String computeKey() { | 
 |       return new Fingerprint().addInt(42).hexDigestAndReset(); | 
 |     } | 
 |   } | 
 |  | 
 |   private abstract static class SingleOutputSkyframeAwareAction extends SingleOutputAction | 
 |       implements SkyframeAwareAction { | 
 |     SingleOutputSkyframeAwareAction(@Nullable Artifact input, Artifact output) { | 
 |       super(input, output); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public boolean executeUnconditionally() { | 
 |       return true; | 
 |     } | 
 |  | 
 |     @Override | 
 |     public boolean isVolatile() { | 
 |       return true; | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * Regression test to avoid a potential race condition in {@link ActionExecutionFunction}. | 
 |    * | 
 |    * <p>The test ensures that when ActionExecutionFunction executes a Skyframe-aware action | 
 |    * (implementor of {@link SkyframeAwareAction}), ActionExecutionFunction first requests the inputs | 
 |    * of the action and ensures they are built before requesting any of its Skyframe dependencies. | 
 |    * | 
 |    * <p>This strict ordering is very important to avoid the race condition, which could arise if the | 
 |    * compute method were too eager to request all dependencies: request input files but even if some | 
 |    * are missing, request also the skyframe-dependencies. The race is described in this method's | 
 |    * body. | 
 |    */ | 
 |   @Test | 
 |   public void testRaceConditionBetweenInputAcquisitionAndSkyframeDeps() throws Exception { | 
 |     // Sequence of events on threads A and B, showing SkyFunctions and requested SkyKeys, leading | 
 |     // to an InconsistentFilesystemException: | 
 |     // | 
 |     // _______________[Thread A]_________________|_______________[Thread B]_________________ | 
 |     // ActionExecutionFunction(gen2_action:      | idle | 
 |     //   genfiles/gen1 -> genfiles/foo/bar/gen2) | | 
 |     // ARTIFACT:genfiles/gen1                    | | 
 |     // MOCK_VALUE:dummy_argument                 | | 
 |     // env.valuesMissing():yes ==> return        | | 
 |     //                                           | | 
 |     // ArtifactFunction(genfiles/gen1)           | MockFunction() | 
 |     // CONFIGURED_TARGET://foo:gen1              | FILE:genfiles/foo | 
 |     // ACTION_EXECUTION:gen1_action              | env.valuesMissing():yes ==> return | 
 |     // env.valuesMissing():yes ==> return        | | 
 |     //                                           | FileFunction(genfiles/foo) | 
 |     // ActionExecutionFunction(gen1_action)      | FILE:genfiles | 
 |     // ARTIFACT:genfiles/gen0                    | env.valuesMissing():yes ==> return | 
 |     // env.valuesMissing():yes ==> return        | | 
 |     //                                           | FileFunction(genfiles) | 
 |     // ArtifactFunction(genfiles/gen0)           | FILE_STATE:genfiles | 
 |     // CONFIGURED_TARGET://foo:gen0              | env.valuesMissing():yes ==> return | 
 |     // ACTION_EXECUTION:gen0_action              | | 
 |     // env.valuesMissing():yes ==> return        | FileStateFunction(genfiles) | 
 |     //                                           | stat genfiles | 
 |     // ActionExecutionFunction(gen0_action)      | return FileStateValue:non-existent | 
 |     // create output directory: genfiles         | | 
 |     // working                                   | FileFunction(genfiles/foo) | 
 |     //                                           | FILE:genfiles | 
 |     //                                           | FILE_STATE:genfiles/foo | 
 |     //                                           | env.valuesMissing():yes ==> return | 
 |     //                                           | | 
 |     //                                           | FileStateFunction(genfiles/foo) | 
 |     //                                           | stat genfiles/foo | 
 |     //                                           | return FileStateValue:non-existent | 
 |     //                                           | | 
 |     // done, created genfiles/gen0               | FileFunction(genfiles/foo) | 
 |     // return ActionExecutionValue(gen0_action)  | FILE:genfiles | 
 |     //                                           | FILE_STATE:genfiles/foo | 
 |     // ArtifactFunction(genfiles/gen0)           | return FileValue(genfiles/foo:non-existent) | 
 |     // CONFIGURED_TARGET://foo:gen0              | | 
 |     // ACTION_EXECUTION:gen0_action              | MockFunction() | 
 |     // return ArtifactSkyKey(genfiles/gen0)      | FILE:genfiles/foo | 
 |     //                                           | FILE:genfiles/foo/bar/gen1 | 
 |     // ActionExecutionFunction(gen1_action)      | env.valuesMissing():yes ==> return | 
 |     // ARTIFACT:genfiles/gen0                    | | 
 |     // create output directory: genfiles/foo/bar | FileFunction(genfiles/foo/bar/gen1) | 
 |     // done, created genfiles/foo/bar/gen1       | FILE:genfiles/foo/bar | 
 |     // return ActionExecutionValue(gen1_action)  | env.valuesMissing():yes ==> return | 
 |     //                                           | | 
 |     // idle                                      | FileFunction(genfiles/foo/bar) | 
 |     //                                           | FILE:genfiles/foo | 
 |     //                                           | FILE_STATE:genfiles/foo/bar | 
 |     //                                           | env.valuesMissing():yes ==> return | 
 |     //                                           | | 
 |     //                                           | FileStateFunction(genfiles/foo/bar) | 
 |     //                                           | stat genfiles/foo/bar | 
 |     //                                           | return FileStateValue:directory | 
 |     //                                           | | 
 |     //                                           | FileFunction(genfiles/foo/bar) | 
 |     //                                           | FILE:genfiles/foo | 
 |     //                                           | FILE_STATE:genfiles/foo/bar | 
 |     //                                           | throw InconsistentFilesystemException: | 
 |     //                                           |     genfiles/foo doesn't exist but | 
 |     //                                           |     genfiles/foo/bar does! | 
 |  | 
 |     Artifact genFile1 = createDerivedArtifact("foo/bar/gen1.txt"); | 
 |     Artifact genFile2 = createDerivedArtifact("gen2.txt"); | 
 |  | 
 |     registerAction( | 
 |         new SingleOutputAction(null, genFile1) { | 
 |           @Override | 
 |           public void execute(ActionExecutionContext actionExecutionContext) | 
 |               throws ActionExecutionException, InterruptedException { | 
 |             writeOutput(null, "gen1"); | 
 |           } | 
 |         }); | 
 |  | 
 |     registerAction( | 
 |         new SingleOutputSkyframeAwareAction(genFile1, genFile2) { | 
 |           @Override | 
 |           public void establishSkyframeDependencies(Environment env) throws ExceptionBase { | 
 |             assertThat(env.valuesMissing()).isFalse(); | 
 |           } | 
 |  | 
 |           @Override | 
 |           public void execute(ActionExecutionContext actionExecutionContext) | 
 |               throws ActionExecutionException, InterruptedException { | 
 |             writeOutput(readInput(), "gen2"); | 
 |           } | 
 |         }); | 
 |  | 
 |     builder.buildArtifacts( | 
 |         reporter, | 
 |         ImmutableSet.of(genFile2), | 
 |         null, | 
 |         null, | 
 |         null, | 
 |         null, | 
 |         executor, | 
 |         null, | 
 |         false, | 
 |         null, | 
 |         null); | 
 |   } | 
 | } |