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

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

import com.google.common.collect.ImmutableList;
import com.google.common.truth.Correspondence;
import com.google.devtools.build.lib.analysis.test.AnalysisFailure;
import com.google.devtools.build.lib.analysis.test.AnalysisFailureInfo;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.analysis.util.MockRule;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.Depset;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import javax.annotation.Nullable;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkSemantics;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Tests verifying analysis failure propagation via {@link AnalysisFailureInfo} when {@code
 * --allow_analysis_failures=true}.
 */
@RunWith(TestParameterInjector.class)
public final class AnalysisFailureInfoTest extends BuildViewTestCase {

  @Before
  public void setUp() throws Exception {
    useConfiguration("--allow_analysis_failures=true");
  }

  @Test
  public void analysisFailureInfoStarlarkApi() throws Exception {
    Label label = Label.create("test", "test");
    AnalysisFailure failure = new AnalysisFailure(label, "ErrorMessage");
    assertThat(getattr(failure, "label")).isSameInstanceAs(label);
    assertThat(getattr(failure, "message")).isEqualTo("ErrorMessage");

    AnalysisFailureInfo info = AnalysisFailureInfo.forAnalysisFailures(ImmutableList.of(failure));
    // info.causes.to_list()[0] == failure
    NestedSet<AnalysisFailure> causes =
        Depset.cast(getattr(info, "causes"), AnalysisFailure.class, "causes");
    assertThat(causes.toList().get(0)).isSameInstanceAs(failure);
  }

  private static Object getattr(Object x, String name) throws Exception {
    return Starlark.getattr(/*mu=*/ null, StarlarkSemantics.DEFAULT, x, name, null);
  }

  /** Regression test for b/154007057 (rule name) and b/186685477 (output file). */
  @Test
  public void nativeRuleExpanderFailure(
      @TestParameter({"//test:bad_variable", "//test:bad_variable.out"}) String targetToRequest)
      throws Exception {
    scratch.file(
        "test/BUILD",
        "genrule(",
        "    name = 'bad_variable',",
        "    outs = ['bad_variable.out'],",
        "    cmd = 'cp $< $@',  # Error to use $< with no srcs",
        ")");

    ConfiguredTarget target = getConfiguredTarget(targetToRequest);
    AnalysisFailureInfo info =
        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
    AnalysisFailure failure = info.getCauses().getSet(AnalysisFailure.class).toList().get(0);
    assertThat(failure.getMessage()).contains("variable '$<' : no input file");
    assertThat(failure.getLabel()).isEqualTo(Label.parseAbsoluteUnchecked("//test:bad_variable"));
  }

  /** Regression test for b/154007057. */
  @Test
  public void nativeRuleConfiguredTargetFactoryCreateReturningNull() throws Exception {
    scratch.file(
        "test/BUILD",
        "native_rule_with_failing_configured_target_factory(",
        "    name = 'bad_factory',",
        ")");

    ConfiguredTarget target = getConfiguredTarget("//test:bad_factory");
    AnalysisFailureInfo info =
        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
    AnalysisFailure failure = info.getCauses().getSet(AnalysisFailure.class).toList().get(0);
    assertThat(failure.getMessage()).contains("FailingRuleConfiguredTargetFactory.create() fails");
    assertThat(failure.getLabel()).isEqualTo(Label.parseAbsoluteUnchecked("//test:bad_factory"));
  }

  /** Dummy factory whose {@code create()} method always returns {@code null}. */
  public static final class FailingRuleConfiguredTargetFactory
      implements RuleConfiguredTargetFactory {
    @Override
    @Nullable
    public ConfiguredTarget create(RuleContext ruleContext) {
      ruleContext.ruleError("FailingRuleConfiguredTargetFactory.create() fails");
      return null;
    }
  }

  @Test
  public void analysisTestNotReturningAnalysisTestResultInfo_cannotPropagate() throws Exception {
    scratch.file(
        "test/BUILD", //
        "providerless_analysis_test(name = 'providerless')");

    reporter.removeHandler(failFastHandler);
    getConfiguredTarget("//test:providerless");
    assertContainsEvent(
        "Error while collecting analysis-phase failure information for '//test:providerless': rules"
            + " with analysis_test=true must return an instance of AnalysisTestResultInfo");
  }

  /** Regression test for b/233890545 */
  @Test
  public void analysisTestExpectingFailureDependedOnByAnalysisTest_cannotPropagate()
      throws Exception {
    useConfiguration("--allow_analysis_failures=false");
    scratch.file(
        "test/extension.bzl",
        "def bad_rule_impl(ctx):",
        "   fail('Bad rule fails')",
        "",
        "bad_rule = rule(",
        "  implementation = bad_rule_impl,",
        "  attrs = {'dep': attr.label()}",
        ")",
        "",
        "def analysis_test_impl(ctx):",
        "  return [AnalysisTestResultInfo(success = False, message = 'Expect failure')]",
        "",
        "_transition = analysis_test_transition(",
        "  settings = {'//command_line_option:allow_analysis_failures': 'True'}",
        ")",
        "",
        "analysis_test = rule(",
        "  implementation = analysis_test_impl,",
        "  analysis_test = True,",
        "  attrs = {'dep': attr.label(cfg = _transition)}",
        ")");

    scratch.file(
        "test/BUILD",
        "load('//test:extension.bzl', 'bad_rule', 'analysis_test')",
        "",
        "analysis_test(name = 'outer', dep = ':inner')",
        "analysis_test(name = 'inner', dep = ':tested_by_inner')",
        "bad_rule(name = 'tested_by_inner')");

    reporter.removeHandler(failFastHandler);
    getConfiguredTarget("//test:outer");
    assertContainsEvent(
        "Error while collecting analysis-phase failure information for '//test:inner':"
            + " analysis_test rule '//test:inner' cannot be transitively depended on by another"
            + " analysis test rule");
  }

  @Override
  protected ConfiguredRuleClassProvider createRuleClassProvider() {
    ConfiguredRuleClassProvider.Builder builder =
        new ConfiguredRuleClassProvider.Builder()
            .addRuleDefinition(
                ((MockRule)
                    () ->
                        MockRule.factory(FailingRuleConfiguredTargetFactory.class)
                            .define("native_rule_with_failing_configured_target_factory")))
            .addRuleDefinition(
                (MockRule)
                    () ->
                        MockRule.ancestor(
                                BaseRuleClasses.TestBaseRule.class,
                                BaseRuleClasses.NativeBuildRule.class)
                            .type(RuleClassType.TEST)
                            .define(
                                "providerless_analysis_test",
                                (ruleClassBuilder, env) -> ruleClassBuilder.setIsAnalysisTest()));
    TestRuleClassProvider.addStandardRules(builder);
    return builder.build();
  }

  private static final Correspondence<AnalysisFailure, AnalysisFailure>
      analysisFailureCorrespondence =
          Correspondence.from(
              (actual, expected) ->
                  actual.getLabel().equals(expected.getLabel())
                      && actual.getMessage().contains(expected.getMessage()),
              "is equivalent to");

  @Test
  public void starlarkRuleFailure() throws Exception {
    scratch.file(
        "test/extension.bzl",
        "def custom_rule_impl(ctx):",
        "   fail('This Is My Failure Message')",
        "",
        "custom_rule = rule(implementation = custom_rule_impl)");

    scratch.file(
        "test/BUILD", "load('//test:extension.bzl', 'custom_rule')", "", "custom_rule(name = 'r')");

    ConfiguredTarget target = getConfiguredTarget("//test:r");
    AnalysisFailureInfo info =
        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
    AnalysisFailure failure = info.getCauses().getSet(AnalysisFailure.class).toList().get(0);
    assertThat(failure.getMessage()).contains("This Is My Failure Message");
    assertThat(failure.getLabel()).isEqualTo(Label.parseAbsoluteUnchecked("//test:r"));
  }

  @Test
  public void starlarkRuleFailure_forTest() throws Exception {
    scratch.file(
        "test/extension.bzl",
        "def custom_rule_impl(ctx):",
        "   fail('This Is My Failure Message')",
        "",
        "custom_test = rule(implementation = custom_rule_impl,",
        "    test = True)");

    scratch.file(
        "test/BUILD", "load('//test:extension.bzl', 'custom_test')", "", "custom_test(name = 'r')");

    ConfiguredTarget target = getConfiguredTarget("//test:r");
    AnalysisFailureInfo info =
        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
    AnalysisFailure failure = info.getCauses().getSet(AnalysisFailure.class).toList().get(0);
    assertThat(failure.getMessage()).contains("This Is My Failure Message");
    assertThat(failure.getLabel()).isEqualTo(Label.parseAbsoluteUnchecked("//test:r"));
  }

  @Test
  public void starlarkRuleFailure_withOutput() throws Exception {
    scratch.file(
        "test/extension.bzl",
        "def custom_rule_impl(ctx):",
        "   fail('This Is My Failure Message')",
        "",
        "custom_rule = rule(implementation = custom_rule_impl,",
        "    outputs = {'my_output': '%{name}.txt'})");

    scratch.file(
        "test/BUILD", "load('//test:extension.bzl', 'custom_rule')", "", "custom_rule(name = 'r')");

    ConfiguredTarget target = getConfiguredTarget("//test:r");
    AnalysisFailureInfo info =
        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
    AnalysisFailure failure = info.getCauses().getSet(AnalysisFailure.class).toList().get(0);
    assertThat(failure.getMessage()).contains("This Is My Failure Message");
    assertThat(failure.getLabel()).isEqualTo(Label.parseAbsoluteUnchecked("//test:r"));
  }

  @Test
  public void transitiveStarlarkRuleFailure() throws Exception {
    scratch.file(
        "test/extension.bzl",
        "def custom_rule_impl(ctx):",
        "   fail('This Is My Failure Message')",
        "",
        "custom_rule = rule(implementation = custom_rule_impl)",
        "",
        "def depending_rule_impl(ctx):",
        "   return []",
        "",
        "depending_rule = rule(implementation = depending_rule_impl,",
        "     attrs = {'deps' : attr.label_list()})");

    scratch.file(
        "test/BUILD",
        "load('//test:extension.bzl', 'custom_rule', 'depending_rule')",
        "",
        "custom_rule(name = 'one')",
        "custom_rule(name = 'two')",
        "depending_rule(name = 'failures_are_direct_deps',",
        "    deps = [':one', ':two'])",
        "depending_rule(name = 'failures_are_indirect_deps',",
        "    deps = [':failures_are_direct_deps'])");

    ConfiguredTarget target = getConfiguredTarget("//test:failures_are_indirect_deps");
    AnalysisFailureInfo info =
        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());

    AnalysisFailure expectedOne =
        new AnalysisFailure(
            Label.parseAbsoluteUnchecked("//test:one"), "This Is My Failure Message");
    AnalysisFailure expectedTwo =
        new AnalysisFailure(
            Label.parseAbsoluteUnchecked("//test:two"), "This Is My Failure Message");

    assertThat(info.getCausesNestedSet().toList())
        .comparingElementsUsing(analysisFailureCorrespondence)
        .containsExactly(expectedOne, expectedTwo);
  }

  @Test
  public void starlarkAspectFailure() throws Exception {
    scratch.file(
        "test/extension.bzl",
        "def custom_aspect_impl(target, ctx):",
        "   fail('This Is My Aspect Failure Message')",
        "",
        "custom_aspect = aspect(implementation = custom_aspect_impl, attr_aspects = ['deps'])",
        "",
        "def custom_rule_impl(ctx):",
        "   return []",
        "",
        "custom_rule = rule(implementation = custom_rule_impl,",
        "     attrs = {'deps' : attr.label_list(aspects = [custom_aspect])})");
    scratch.file(
        "test/BUILD",
        "load('//test:extension.bzl', 'custom_rule')",
        "",
        "custom_rule(name = 'one')",
        "custom_rule(name = 'two', deps = [':one'])");

    ConfiguredTarget target = getConfiguredTarget("//test:two");
    AnalysisFailureInfo info =
        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
    AnalysisFailure expectedOne =
        new AnalysisFailure(
            Label.parseAbsoluteUnchecked("//test:one"), "This Is My Aspect Failure Message");

    assertThat(info.getCausesNestedSet().toList())
        .comparingElementsUsing(analysisFailureCorrespondence)
        .containsExactly(expectedOne);
  }

  @Test
  public void transitiveStarlarkAspectFailure() throws Exception {
    scratch.file(
        "test/extension.bzl",
        "def custom_aspect_impl(target, ctx):",
        "   if hasattr(ctx.rule.attr, 'kaboom') and ctx.rule.attr.kaboom:",
        "       fail('This Is My Aspect Failure Message')",
        "   return []",
        "",
        "custom_aspect = aspect(implementation = custom_aspect_impl, attr_aspects = ['deps'])",
        "",
        "def custom_rule_impl(ctx):",
        "   return []",
        "",
        "custom_rule = rule(implementation = custom_rule_impl,",
        "     attrs = {'deps' : attr.label_list(aspects = [custom_aspect]),",
        "              'kaboom' : attr.bool()})");
    scratch.file(
        "test/BUILD",
        "load('//test:extension.bzl', 'custom_rule')",
        "",
        "custom_rule(name = 'one', kaboom = True)",
        "custom_rule(name = 'two', deps = [':one'])",
        "custom_rule(name = 'three', deps = [':two'])");

    ConfiguredTarget target = getConfiguredTarget("//test:three");
    AnalysisFailureInfo info =
        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
    AnalysisFailure expectedOne =
        new AnalysisFailure(
            Label.parseAbsoluteUnchecked("//test:one"), "This Is My Aspect Failure Message");

    assertThat(info.getCausesNestedSet().toList())
        .comparingElementsUsing(analysisFailureCorrespondence)
        .containsExactly(expectedOne);
  }

  @Test
  public void starlarkAspectAndRuleFailure_analysisFailureInfoPropagatesOnlyFromRuleFailure()
      throws Exception {
    scratch.file(
        "test/extension.bzl",
        "def custom_aspect_impl(target, ctx):",
        "   fail('This Is My Aspect Failure Message')",
        "",
        "custom_aspect = aspect(implementation = custom_aspect_impl, attr_aspects = ['deps'])",
        "",
        "def custom_rule_impl(ctx):",
        "   fail('This Is My Rule Failure Message')",
        "",
        "custom_rule = rule(implementation = custom_rule_impl,",
        "     attrs = {'deps' : attr.label_list(aspects = [custom_aspect])})");
    scratch.file(
        "test/BUILD",
        "load('//test:extension.bzl', 'custom_rule')",
        "",
        "custom_rule(name = 'one')",
        "custom_rule(name = 'two', deps = [':one'])");

    ConfiguredTarget target = getConfiguredTarget("//test:two");
    AnalysisFailureInfo info =
        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
    AnalysisFailure expectedRuleFailure =
        new AnalysisFailure(
            Label.parseAbsoluteUnchecked("//test:one"), "This Is My Rule Failure Message");

    assertThat(info.getCausesNestedSet().toList())
        .comparingElementsUsing(analysisFailureCorrespondence)
        .containsExactly(expectedRuleFailure);
  }
}
