// Copyright 2022 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.rewinding;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;

import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.util.AnalysisMock;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.includescanning.IncludeScanningModule;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.WorkspaceBuilder;
import com.google.devtools.build.lib.testutil.ActionEventRecorder;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Integration tests for action rewinding.
 *
 * <p>Uses {@link TestParameter}s to run tests with all four combinations of {@code
 * --track_incremental_state} and {@code --keep_going}.
 */
// TODO(b/228090759): Consider asserting on graph structure to improve coverage for incrementality.
// TODO(b/228090759): Add back actionFromPreviousBuildReevaluated.
@RunWith(TestParameterInjector.class)
public final class RewindingTest extends BuildIntegrationTestCase {

  @TestParameter private boolean trackIncrementalState;
  @TestParameter private boolean keepGoing;

  private final ActionEventRecorder actionEventRecorder = new ActionEventRecorder();
  private final RewindingTestsHelper helper = new RewindingTestsHelper(this, actionEventRecorder);

  @Override
  protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
    return super.getRuntimeBuilder()
        .addBlazeModule(new IncludeScanningModule())
        .addBlazeModule(helper.makeControllableActionStrategyModule("standalone"))
        .addBlazeModule(
            new BlazeModule() {
              @Override
              public void workspaceInit(
                  BlazeRuntime runtime, BlazeDirectories directories, WorkspaceBuilder builder) {
                // Null out RepositoryHelpersHolder so that we don't trigger
                // RepoMappingManifestAction. This preserves action graph structure between blaze
                // and bazel, which is important for this test's assertions.
                builder.setSkyframeExecutorRepositoryHelpersHolder(null);
              }
            });
  }

  @Override
  protected void setupOptions() throws Exception {
    super.setupOptions();
    addOptions(
        "--spawn_strategy=standalone",
        "--noexperimental_merged_skyframe_analysis_execution",
        "--rewind_lost_inputs",
        "--features=cc_include_scanning",
        "--experimental_remote_include_extraction_size_threshold=0",
        "--track_incremental_state=" + trackIncrementalState,
        "--keep_going=" + keepGoing);
    runtimeWrapper.registerSubscriber(actionEventRecorder);
  }

  /**
   * Skips test cases that cannot run with bazel.
   *
   * <p>{@link BuildIntegrationTestCase} currently does not support CPP compilation on bazel.
   */
  // TODO(b/195425240): Remove once CPP compilation on bazel is supported. Assumptions that
  // generated headers are always under k8-opt will need to be relaxed to support other platforms.
  private static void skipIfBazel() {
    assume().that(AnalysisMock.get().isThisBazel()).isFalse();
  }

  @Test
  public void noLossSmokeTest() throws Exception {
    helper.runNoLossSmokeTest();
  }

  @Test
  public void buildingParentFoundUndoneChildNotToleratedWithoutRewinding() throws Exception {
    helper.runBuildingParentFoundUndoneChildNotToleratedWithoutRewinding();
  }

  @Test
  public void dependentActionsReevaluated() throws Exception {
    helper.runDependentActionsReevaluated_spawnFailed();
  }

  @Test
  public void multipleLostInputsForRewindPlan() throws Exception {
    helper.runMultipleLostInputsForRewindPlan();
  }

  @Test
  public void multiplyLosingInputsFails() throws Exception {
    helper.runMultiplyLosingInputsFails();
    assertOutputForRule2NotCreated();
  }

  @Test
  public void interruptedDuringRewindStopsNormally() throws Exception {
    helper.runInterruptedDuringRewindStopsNormally();
    assertOutputForRule2NotCreated();
  }

  @Test
  public void failureDuringRewindStopsNormally() throws Exception {
    helper.runFailureDuringRewindStopsNormally();
    assertOutputForRule2NotCreated();
  }

  /**
   * Because this test infrastructure allows builds to write outputs to the filesystem, these
   * "fail"/"stops normally" tests can assert that the build's output file was not written.
   */
  private void assertOutputForRule2NotCreated() throws Exception {
    Artifact output =
        Iterables.getOnlyElement(
            getFilesToBuild(getExistingConfiguredTarget("//test:rule2")).toList());
    assertThat(output.getPath().exists()).isFalse();
  }

  @Test
  public void intermediateActionRewound() throws Exception {
    helper.runIntermediateActionRewound();
  }

  @Test
  public void chainOfActionsRewound() throws Exception {
    helper.runChainOfActionsRewound();
  }

  @Test
  public void nondeterministicActionRewound() throws Exception {
    helper.runNondeterministicActionRewound();
  }

  @Test
  public void parallelTrackSharedActionsRewound() throws Exception {
    helper.runParallelTrackSharedActionsRewound();
  }

  @Test
  public void treeFileArtifactRewound() throws Exception {
    skipIfBazel();
    helper.runTreeFileArtifactRewound_spawnFailed();
  }

  @Test
  public void treeArtifactRewound_allFilesLost() throws Exception {
    skipIfBazel();
    helper.runTreeArtifactRewound_allFilesLost_spawnFailed();
  }

  @Test
  public void treeArtifactRewound_oneFileLost() throws Exception {
    skipIfBazel();
    helper.runTreeArtifactRewound_oneFileLost_spawnFailed();
  }

  @Test
  public void generatedRunfilesRewound_allFilesLost() throws Exception {
    helper.runGeneratedRunfilesRewound_allFilesLost_spawnFailed();
  }

  @Test
  public void generatedRunfilesRewound_oneFileLost() throws Exception {
    helper.runGeneratedRunfilesRewound_oneFileLost_spawnFailed();
  }

  @Test
  public void dupeDirectAndRunfilesDependencyRewound() throws Exception {
    helper.runDupeDirectAndRunfilesDependencyRewound_spawnFailed();
  }

  @Test
  public void treeInRunfilesRewound() throws Exception {
    helper.runTreeInRunfilesRewound_spawnFailed();
  }

  @Test
  public void inputsFromSameGeneratingActionSplitAmongNestedSetChildren() throws Exception {
    helper.runInputsFromSameGeneratingActionSplitAmongNestedSetChildren();
  }

  @Test
  public void generatedHeaderRewound_lostInInputDiscovery() throws Exception {
    skipIfBazel();
    helper.runGeneratedHeaderRewound_lostInInputDiscovery_spawnFailed();
  }

  @Test
  public void generatedHeaderRewound_lostInActionExecution() throws Exception {
    skipIfBazel();
    helper.runGeneratedHeaderRewound_lostInActionExecution_spawnFailed();
  }

  @Test
  public void generatedTransitiveHeaderRewound_lostInInputDiscovery() throws Exception {
    skipIfBazel();
    helper.runGeneratedTransitiveHeaderRewound_lostInInputDiscovery_spawnFailed();
  }

  @Test
  public void generatedTransitiveHeaderRewound_lostInActionExecution() throws Exception {
    skipIfBazel();
    helper.runGeneratedTransitiveHeaderRewound_lostInActionExecution_spawnFailed();
  }

  @Test
  public void doneToDirtyDepForNodeInError() throws Exception {
    helper.runDoneToDirtyDepForNodeInError();
  }
}
