// Copyright 2021 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.analysis.test;

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

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.EventBus;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.AnalysisResult;
import com.google.devtools.build.lib.analysis.ConfiguredAspect;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.Depset;
import com.google.devtools.build.lib.packages.StarlarkProvider;
import com.google.devtools.build.lib.packages.StructImpl;
import com.google.devtools.build.lib.packages.TestTimeout;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** {@link com.google.devtools.build.lib.analysis.test.TestActionBuilder} tests. */
@RunWith(JUnit4.class)
public class TestActionBuilderTest extends BuildViewTestCase {

  @Before
  public final void createBuildFile() throws Exception {
    analysisMock.pySupport().setup(mockToolsConfig);

    scratch.file(
        "tests/BUILD",
        "py_test(name = 'small_test_1',",
        "        srcs = ['small_test_1.py'],",
        "        data = [':xUnit'],",
        "        size = 'small',",
        "        tags = ['tag1'])",
        "",
        "sh_test(name = 'small_test_2',",
        "        srcs = ['small_test_2.sh'],",
        "        data = ['//testing/shbase:googletest.sh'],",
        "        size = 'small',",
        "        tags = ['tag2'])",
        "",
        "sh_test(name = 'large_test_1',",
        "        srcs = ['large_test_1.sh'],",
        "        data = ['//testing/shbase:googletest.sh', ':xUnit'],",
        "        size = 'large',",
        "        tags = ['tag1'])",
        "",
        "py_binary(name = 'notest',",
        "        srcs = ['notest.py'])",
        "cc_library(name = 'xUnit')",
        "",
        "test_suite(name = 'smallTests', tags=['small'])");
  }

  @Test
  public void testFlakyAttributeValidation() throws Exception {
    scratch.file("flaky/BUILD",
        "sh_test(name = 'good_test',",
        "        srcs = ['a.sh'])",
        "sh_test(name = 'flaky_test',",
        "        srcs = ['a.sh'],",
        "        flaky = 1)");
    Artifact testStatus = Iterables.getOnlyElement(getTestStatusArtifacts("//flaky:good_test"));
    assertThat(testStatus).isNotNull();
    TestRunnerAction action = (TestRunnerAction) getGeneratingAction(testStatus);
    assertThat(action.getTestProperties().isFlaky()).isFalse();

    testStatus = Iterables.getOnlyElement(getTestStatusArtifacts("//flaky:flaky_test"));
    assertThat(testStatus).isNotNull();
    action = (TestRunnerAction) getGeneratingAction(testStatus);
    assertThat(action.getTestProperties().isFlaky()).isTrue();
  }

  @Test
  public void testIllegalBooleanFlakySetting() throws Exception {
    checkError("flaky", "bad_test",
        "boolean is not one of [0, 1]",
        "sh_test(name = 'bad_test',",
        "        srcs = ['a.sh'],",
        "        flaky = 2)");
  }

  @Test
  public void testRunsPerTest() throws Exception {
    useConfiguration("--runs_per_test=2");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//tests:small_test_1");
    assertThat(testStatusList).hasSize(2);
    Artifact testStatus1 = testStatusList.get(0);
    Artifact testStatus2 = testStatusList.get(1);
    assertThat(testStatus1).isNotNull();
    assertThat(testStatus2).isNotNull();
    assertThat(testStatus2).isNotSameInstanceAs(testStatus1);
    assertThat(getGeneratingAction(testStatus2))
        .isNotSameInstanceAs(getGeneratingAction(testStatus1));
    assertThat(testStatus1.getRootRelativePath().getPathString())
        .contains("tests/small_test_1/run_1_of_2/test");
    assertThat(testStatus2.getRootRelativePath().getPathString())
        .contains("tests/small_test_1/run_2_of_2/test");
  }

  @Test
  public void testRunsPerTestCanBeOverridden() throws Exception {
    useConfiguration("--runs_per_test=1", "--runs_per_test=2");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//tests:small_test_1");
    assertThat(testStatusList).hasSize(2);
    Artifact testStatus1 = testStatusList.get(0);
    Artifact testStatus2 = testStatusList.get(1);
    assertThat(testStatus1).isNotNull();
    assertThat(testStatus2).isNotNull();
    assertThat(testStatus2).isNotSameInstanceAs(testStatus1);
    assertThat(getGeneratingAction(testStatus2))
        .isNotSameInstanceAs(getGeneratingAction(testStatus1));
    assertThat(testStatus1.getRootRelativePath().getPathString())
        .contains("tests/small_test_1/run_1_of_2/test");
    assertThat(testStatus2.getRootRelativePath().getPathString())
        .contains("tests/small_test_1/run_2_of_2/test");
  }

  /**
   * Test that test rules always construct with a standard timeout, either
   * inferred from size or explicitly set by attribute.
   */
  @Test
  public void testTestTimeoutFlagOverridesTimeoutDefaultsValues() throws Exception {
    scratch.file("javatests/timeouts/BUILD",
        "java_test(name = 'small_no_timeout',",
        "          srcs = [],",
        "          size = 'small')",
        "java_test(name = 'small_with_timeout',",
        "          srcs = [],",
        "          size = 'small',",
        "          timeout = 'long')");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//javatests/timeouts:small_no_timeout");
    TestRunnerAction testAction = (TestRunnerAction)
        getGeneratingAction(Iterables.get(testStatusList, 0));
    Integer timeout = testAction.getTestProperties().getTimeout().getTimeoutSeconds();
    assertThat(timeout).isEqualTo(TestTimeout.SHORT.getTimeoutSeconds());

    testStatusList = getTestStatusArtifacts("//javatests/timeouts:small_with_timeout");
    testAction = (TestRunnerAction) getGeneratingAction(Iterables.get(testStatusList, 0));
    timeout = testAction.getTestProperties().getTimeout().getTimeoutSeconds();
    assertThat(timeout).isEqualTo(TestTimeout.LONG.getTimeoutSeconds());
  }

  @Test
  public void testRunsPerTestWithSharding() throws Exception {
    useConfiguration("--runs_per_test=2");
    scratch.file(
        "javatests/jt/BUILD",
        "java_test(name = 'RGT',",
        "          shard_count = 10,",
        "          srcs = ['RGT.java'])");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//javatests/jt:RGT");
    assertThat(testStatusList).hasSize(20);
    Artifact testStatus1 = testStatusList.get(0);
    Artifact testStatus10 = testStatusList.get(9);
    Artifact testStatus11 = testStatusList.get(10);
    assertThat(testStatus1).isNotNull();
    assertThat(testStatus10).isNotNull();
    assertThat(testStatus11).isNotNull();
    assertThat(testStatus1.getRootRelativePath().getPathString())
        .contains("javatests/jt/RGT/shard_1_of_10_run_1_of_2/test");
    assertThat(testStatus10.getRootRelativePath().getPathString())
        .contains("javatests/jt/RGT/shard_5_of_10_run_2_of_2/test");
    assertThat(testStatus11.getRootRelativePath().getPathString())
        .contains("javatests/jt/RGT/shard_6_of_10_run_1_of_2/test");
  }

  @Test
  public void testAspectOverNonExpandingTestSuitesVisitsImplicitTests() throws Exception {
    scratch.file(
        "BUILD",
        "sh_test(name = 'test_a',",
        "        srcs = [':a.sh'])",
        "",
        "sh_test(name = 'test_b',",
        "        srcs = [':b.sh'])",
        "",
        "test_suite(name = 'suite'",
        ")");
    writeLabelCollectionAspect();

    useLoadingOptions("--noexpand_test_suites");
    AnalysisResult analysisResult =
        update(
            ImmutableList.of("//:suite"),
            ImmutableList.of("//:aspect.bzl%a"),
            /* keepGoing= */ false,
            /* loadingPhaseThreads= */ 1,
            /* doAnalysis= */ true,
            new EventBus());
    ConfiguredAspect aspectValue =
        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseAbsoluteUnchecked("//:aspect.bzl"), "StructImpl");
    StructImpl info = (StructImpl) aspectValue.get(key);
    assertThat(((Depset) info.getValue("labels")).getSet(String.class).toList())
        .containsExactly("@//:suite", "@//:test_a", "@//:test_b");
  }

  @Test
  public void testAspectOverNonExpandingTestSuitesVisitsExplicitTests() throws Exception {
    scratch.file(
        "BUILD",
        "sh_test(name = 'test_a',",
        "        srcs = [':a.sh'])",
        "",
        "sh_test(name = 'test_b',",
        "        srcs = [':b.sh'])",
        "",
        "test_suite(name = 'suite',",
        "           tests = [':test_b']",
        ")");
    writeLabelCollectionAspect();

    useLoadingOptions("--noexpand_test_suites");
    AnalysisResult analysisResult =
        update(
            ImmutableList.of("//:suite"),
            ImmutableList.of("//:aspect.bzl%a"),
            /* keepGoing= */ false,
            /* loadingPhaseThreads= */ 1,
            /* doAnalysis= */ true,
            new EventBus());
    ConfiguredAspect aspectValue =
        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseAbsoluteUnchecked("//:aspect.bzl"), "StructImpl");
    StructImpl info = (StructImpl) aspectValue.get(key);
    assertThat(((Depset) info.getValue("labels")).getSet(String.class).toList())
        .containsExactly("@//:suite", "@//:test_b");
  }

  @Test
  public void testAspectOverExpandingTestSuitesDoesNotVisitSuite() throws Exception {
    scratch.file(
        "BUILD",
        "sh_test(name = 'test_a',",
        "        srcs = [':a.sh'])",
        "",
        "sh_test(name = 'test_b',",
        "        srcs = [':b.sh'])",
        "",
        "test_suite(name = 'suite',",
        ")");
    writeLabelCollectionAspect();

    AnalysisResult analysisResult =
        update(
            ImmutableList.of("//:suite"),
            ImmutableList.of("//:aspect.bzl%a"),
            /* keepGoing= */ false,
            /* loadingPhaseThreads= */ 1,
            /* doAnalysis= */ true,
            new EventBus());
    final StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseAbsoluteUnchecked("//:aspect.bzl"), "StructImpl");

    List<String> labels = new ArrayList<>();
    for (ConfiguredAspect a : analysisResult.getAspectsMap().values()) {
      StructImpl info = (StructImpl) a.get(key);
      labels.addAll(((Depset) info.getValue("labels")).getSet(String.class).toList());
    }
    assertThat(labels).containsExactly("@//:test_a", "@//:test_b");
  }

  private void writeLabelCollectionAspect() throws IOException {
    scratch.file(
        "aspect.bzl",
        "StructImpl = provider(fields = ['labels'])",
        "def _impl(target,ctx):",
        "    print(target.label)",
        "    transitive = []",
        "    if hasattr(ctx.rule.attr, 'tests'):",
        "      transitive += [dep[StructImpl].labels for dep in ctx.rule.attr.tests]",
        "    if hasattr(ctx.rule.attr, '_implicit_tests'):",
        "      transitive += [dep[StructImpl].labels for dep in ctx.rule.attr._implicit_tests]",
        "    return [StructImpl(labels = depset([str(target.label)], transitive = transitive))]",
        "",
        "a = aspect(_impl, attr_aspects = ['tests', '_implicit_tests'])");
  }

  /**
   * Regression test for bug {@link "http://b/2644860"}.
   */
  @Test
  public void testIllegalTestSizeAttributeDoesNotCrashTestSuite() throws Exception {
    checkError("bad_size", "illegal_size_test",
        "In rule 'illegal_size_test', size 'bad' is not a valid size",
        "sh_test(name = 'illegal_size_test',",
        "        srcs = ['illegal.sh'],",
        "        size = 'bad')",
        "test_suite(name = 'everything')");
  }

  /**
   * Regression test for bug {@link "http://b/2644860"} but with an illegal Timeout.
   */
  @Test
  public void testIllegalTestTimeoutAttributeDoesNotCrashTestSuite() throws Exception {
    checkError("bad_timeout", "illegal_timeout_test",
        "In rule 'illegal_timeout_test', timeout 'unreasonable' is not a valid timeout",
        "sh_test(name = 'illegal_timeout_test',",
        "        srcs = ['illegal.sh'],",
        "        timeout = 'unreasonable')",
        "test_suite(name = 'everything')");
  }

  /**
   * Overriding the exec group from within the test affects the way exec properties are selected.
   */
  @Test
  public void testOverrideExecGroup() throws Exception {
    scratch.file(
        "some_test.bzl",
        "def _some_test_impl(ctx):",
        "    script = ctx.actions.declare_file(ctx.attr.name + '.sh')",
        "    ctx.actions.write(script, 'shell script goes here', is_executable = True)",
        "    return [",
        "        DefaultInfo(executable = script),",
        "        testing.ExecutionInfo({}, exec_group = 'custom_group'),",
        "    ]",
        "",
        "some_test = rule(",
        "    implementation = _some_test_impl,",
        "    exec_groups = {'custom_group': exec_group()},",
        "    test = True,",
        ")");
    scratch.file(
        "BUILD",
        "load(':some_test.bzl', 'some_test')",
        "some_test(",
        "    name = 'custom_exec_group_test',",
        "    exec_properties = {'test.key': 'bad', 'custom_group.key': 'good'},",
        ")");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//:custom_exec_group_test");
    TestRunnerAction testAction = (TestRunnerAction) getGeneratingAction(testStatusList.get(0));
    ImmutableMap<String, String> executionInfo = testAction.getExecutionInfo();
    assertThat(executionInfo).containsExactly("key", "good");
  }

  private ImmutableList<Artifact.DerivedArtifact> getTestStatusArtifacts(String label)
      throws Exception {
    ConfiguredTarget target = getConfiguredTarget(label);
    return target.getProvider(TestProvider.class).getTestParams().getTestStatusArtifacts();
  }

}
