// 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.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.graph.ImmutableGraph;
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.ActionKeyContext;
import com.google.devtools.build.lib.actions.ActionLookupData;
import com.google.devtools.build.lib.actions.ActionResult;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Executor;
import com.google.devtools.build.lib.actions.FileStateValue;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.actions.util.DummyExecutor;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.testutil.TimestampGranularityUtils;
import com.google.devtools.build.lib.util.CrashFailureDetails;
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.Root;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.ErrorInfo;
import com.google.devtools.build.skyframe.EvaluationProgressReceiver;
import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState;
import com.google.devtools.build.skyframe.GraphInconsistencyReceiver;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
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 java.util.function.Supplier;
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,
            GraphInconsistencyReceiver.THROWING);
  }

  @Before
  public final void createExecutor() throws Exception {
    executor = new DummyExecutor(fileSystem, rootDirectory);
  }

  private static final class TrackingEvaluationProgressReceiver
      implements EvaluationProgressReceiver {

    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);
      }
    }

    private static final class EvaluatedEntry {
      public final SkyKey skyKey;
      final EvaluationSuccessState successState;
      public final EvaluationState state;

      EvaluatedEntry(SkyKey skyKey, EvaluationSuccessState successState, EvaluationState state) {
        this.skyKey = skyKey;
        this.successState = successState;
        this.state = state;
      }

      @Override
      public boolean equals(Object obj) {
        return obj instanceof EvaluatedEntry
            && this.skyKey.equals(((EvaluatedEntry) obj).skyKey)
            && this.successState.equals(((EvaluatedEntry) obj).successState)
            && this.state.equals(((EvaluatedEntry) obj).state);
      }

      @Override
      public int hashCode() {
        return Objects.hashCode(skyKey, successState, 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,
        @Nullable SkyValue value,
        @Nullable ErrorInfo error,
        Supplier<EvaluationSuccessState> evaluationSuccessState,
        EvaluationState state) {
      evaluated.add(new EvaluatedEntry(skyKey, evaluationSuccessState.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,
          NestedSetBuilder.create(Order.STABLE_ORDER, input),
          ImmutableSet.of(output));
      this.executionCounter = executionCounter;
    }

    @Override
    public ActionResult 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 = getInputs().getSingleton().getPath().getInputStream()) {
        inputLen = in.read(input);
      } catch (IOException e) {
        throw new ActionExecutionException(
            e, this, false, CrashFailureDetails.detailedExitCodeForThrowable(e));
      }

      // 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, CrashFailureDetails.detailedExitCodeForThrowable(e));
      }
      return ActionResult.EMPTY;
    }

    @Override
    public String getMnemonic() {
      return "ExecutionCountingAction";
    }

    @Override
    protected void computeKey(
        ActionKeyContext actionKeyContext,
        @Nullable Artifact.ArtifactExpander artifactExpander,
        Fingerprint fp) {
      fp.addString(getPrimaryOutput().getExecPathString());
      fp.addInt(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 final 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 Object processSkyframeValues(
        ImmutableList<? extends SkyKey> keys, SkyframeLookupResult values, boolean valuesMissing) {
      assertThat(keys).containsExactly(actionDepKey);
      return null;
    }

    @Override
    public ImmutableList<SkyKey> getDirectSkyframeDependencies() {
      return ImmutableList.of(actionDepKey);
    }

    @Override
    public ImmutableGraph<SkyKey> getSkyframeDependenciesForRewinding(SkyKey self) {
      throw new UnsupportedOperationException();
    }
  }

  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;
    }
  }

  /** Ensure that a file's ctime was updated from an older value. */
  private static void checkCtimeUpdated(Path path, long oldCtime) throws IOException {
    if (oldCtime >= path.stat().getLastChangeTime()) {
      throw new IllegalStateException(String.format("path=(%s), ctime=(%d)", path, oldCtime));
    }
  }

  private void maybeChangeFile(Artifact file, ChangeArtifact changeRequest) throws Exception {
    if (changeRequest == ChangeArtifact.DONT_CHANGE) {
      return;
    }

    Path path = file.getPath();

    if (changeRequest.changeMtime()) {
      long ctime = path.stat().getLastChangeTime();
      // Ensure enough time elapsed for file updates to have a visible effect on the file's ctime.
      TimestampGranularityUtils.waitForTimestampGranularity(ctime, reporter.getOutErr());
      // waitForTimestampGranularity waits long enough for System.currentTimeMillis() to be greater
      // than the time at the setCommandStartTime() call. Therefore setting
      // System.currentTimeMillis() is guaranteed to advance the file's ctime.
      path.setLastModifiedTime(System.currentTimeMillis());
      checkCtimeUpdated(path, ctime);
    }

    if (changeRequest.changeContent()) {
      long ctime = path.stat().getLastChangeTime();
      // Ensure enough time elapsed for file updates to have a visible effect on the file's ctime.
      TimestampGranularityUtils.waitForTimestampGranularity(ctime, reporter.getOutErr());
      appendToFile(path);
      checkCtimeUpdated(path, ctime);
    }

    // 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().getRoot(), 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,
        null,
        executor,
        options,
        null,
        null,
        /* trustRemoteArtifacts= */ false);

    // Check that our invalidation receiver is working correctly. We'll rely on it again.
    SkyKey actionKey = ActionLookupData.create(ACTION_LOOKUP_KEY, 0);
    TrackingEvaluationProgressReceiver.EvaluatedEntry evaluatedAction =
        progressReceiver.getEvalutedEntry(actionKey);
    assertThat(evaluatedAction).isNotNull();

    // 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,
        null,
        executor,
        options,
        null,
        null,
        /* trustRemoteArtifacts= */ false);

    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);
      } 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);
      }
    } 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(Root.fromPath(rootDirectory), PathFragment.create("action.dep"));
  }

  private static 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),
        unconditionalExecution
            ? ExpectActionIs.REEXECUTED
            : ExpectActionIs.DIRTIED_BUT_VERIFIED_CLEAN);
  }

  @Test
  public void testCacheCheckingActionWithNonChangingInput() throws Exception {
    assertActionWithNonChangingInput(/* unconditionalExecution */ false);
  }

  @Test
  public void testCacheBypassingActionWithNonChangingInput() throws Exception {
    assertActionWithNonChangingInput(/* unconditionalExecution */ true);
  }

  private void assertActionWithNonChangingInput(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
              ? NestedSetBuilder.emptySet(Order.STABLE_ORDER)
              : NestedSetBuilder.create(Order.STABLE_ORDER, input),
          ImmutableSet.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, CrashFailureDetails.detailedExitCodeForThrowable(e));
      }
      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, CrashFailureDetails.detailedExitCodeForThrowable(e));
      }
    }

    @Override
    public String getMnemonic() {
      return "MockActionMnemonic";
    }

    @Override
    protected void computeKey(
        ActionKeyContext actionKeyContext,
        @Nullable Artifact.ArtifactExpander artifactExpander,
        Fingerprint fp) {
      fp.addInt(42);
    }
  }

  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 ActionResult execute(ActionExecutionContext actionExecutionContext)
              throws ActionExecutionException, InterruptedException {
            writeOutput(null, "gen1");
            return ActionResult.EMPTY;
          }
        });

    registerAction(
        new SingleOutputSkyframeAwareAction(genFile1, genFile2) {

          @Override
          public ImmutableList<SkyKey> getDirectSkyframeDependencies() {
            return ImmutableList.of();
          }

          @Override
          public Object processSkyframeValues(
              ImmutableList<? extends SkyKey> keys,
              SkyframeLookupResult values,
              boolean valuesMissing) {
            assertThat(keys).isEmpty();
            assertThat(valuesMissing).isFalse();
            return null;
          }

          @Override
          public ImmutableGraph<SkyKey> getSkyframeDependenciesForRewinding(SkyKey self) {
            throw new UnsupportedOperationException();
          }

          @Override
          public ActionResult execute(ActionExecutionContext actionExecutionContext)
              throws ActionExecutionException {
            writeOutput(readInput(), "gen2");
            return ActionResult.EMPTY;
          }
        });

    builder.buildArtifacts(
        reporter,
        ImmutableSet.of(genFile2),
        null,
        null,
        null,
        null,
        null,
        executor,
        options,
        null,
        null,
        /* trustRemoteArtifacts= */ false);
  }
}
