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

import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.analysis.BaseRuleClasses.ACTION_LISTENER;
import static com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode.TARGET;
import static com.google.devtools.build.lib.analysis.util.TestAspects.EMPTY_LATE_BOUND_LABEL;
import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
import static com.google.devtools.build.lib.packages.Attribute.attr;
import static com.google.devtools.build.lib.packages.BuildType.LABEL;
import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
import static org.junit.Assert.fail;

import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil.NullAction;
import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
import com.google.devtools.build.lib.analysis.util.TestAspects;
import com.google.devtools.build.lib.analysis.util.TestAspects.AspectInfo;
import com.google.devtools.build.lib.analysis.util.TestAspects.AspectRequiringRule;
import com.google.devtools.build.lib.analysis.util.TestAspects.BaseRule;
import com.google.devtools.build.lib.analysis.util.TestAspects.DummyRuleFactory;
import com.google.devtools.build.lib.analysis.util.TestAspects.RuleInfo;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.events.OutputFilter.RegexOutputFilter;
import com.google.devtools.build.lib.packages.AspectDefinition;
import com.google.devtools.build.lib.packages.AspectParameters;
import com.google.devtools.build.lib.packages.NativeAspectClass;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.vfs.ModifiedFileSet;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests for aspect creation and merging with configured targets.
 *
 * <p>Uses the complete analysis machinery and depends on custom rules so that behaviors related to
 * aspects can be tested even if they aren't used by regular rules.
 */
@RunWith(JUnit4.class)
public class AspectTest extends AnalysisTestCase {

  private void pkg(String name, String... contents) throws Exception {
    scratch.file("" + name + "/BUILD", contents);
  }

  @Test
  public void testAspectAppliedToAliasWithSelect() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new AspectRequiringRule());
    pkg("a",
        "aspect(name='a', foo=[':b'])",
        "alias(name='b', actual=select({'//conditions:default': ':c'}))",
        "base(name='c')");
    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("aspect //a:c", "rule //a:a");
  }

  @Test
  public void testAspectAppliedToChainedAliases() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new AspectRequiringRule());
    pkg("a",
        "aspect(name='a', foo=[':b'])",
        "alias(name='b', actual=':c')",
        "alias(name='c', actual=':d')",
        "alias(name='d', actual=':e')",
        "base(name='e')");

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("aspect //a:e", "rule //a:a");
  }

  @Test
  public void testAspectAppliedToChainedAliasesAndSelect() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new AspectRequiringRule());
    pkg("a",
        "aspect(name='a', foo=[':b'])",
        "alias(name='b', actual=select({'//conditions:default': ':c'}))",
        "alias(name='c', actual=select({'//conditions:default': ':d'}))",
        "base(name='d')");
    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("aspect //a:d", "rule //a:a");
  }

  @Test
  public void providersOfAspectAreMergedIntoDependency() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new AspectRequiringRule());
    pkg("a",
        "aspect(name='a', foo=[':b'])",
        "aspect(name='b', foo=[])");

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("aspect //a:b", "rule //a:a");
  }

  @Test
  public void aspectIsNotCreatedIfAdvertisedProviderIsNotPresent() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.LiarRule(),
        new TestAspects.AspectRequiringProviderRule());

    pkg("a",
        "aspect_requiring_provider(name='a', foo=[':b'])",
        "liar(name='b', foo=[])");

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData()).containsExactly("rule //a:a");
  }

  @Test
  public void aspectCreationWorksThroughBind() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.HonestRule(),
        new TestAspects.AspectRequiringProviderRule());

    pkg("a",
        "aspect_requiring_provider(name='a', foo=['//external:b'])",
        "honest(name='b', foo=[])");

    scratch.overwriteFile("WORKSPACE",
        "bind(name='b', actual='//a:b')");

    skyframeExecutor.invalidateFilesUnderPathForTesting(reporter,
        ModifiedFileSet.EVERYTHING_MODIFIED, rootDirectory);

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("rule //a:a", "aspect //a:b");
  }


  @Test
  public void aspectCreatedIfAdvertisedProviderIsPresent() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.HonestRule(),
        new TestAspects.AspectRequiringProviderRule());

    pkg("a",
        "aspect_requiring_provider(name='a', foo=[':b'])",
        "honest(name='b', foo=[])");

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("rule //a:a", "aspect //a:b");
  }

  @Test
  public void aspectCreatedIfAtLeastOneSetOfAdvertisedProvidersArePresent() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.HonestRule(),
        new TestAspects.HonestRule2(), new TestAspects.AspectRequiringProviderSetsRule());

    pkg("a",
        "aspect_requiring_provider_sets(name='a', foo=[':b', ':c'])",
        "honest(name='b', foo=[])",
        "honest2(name='c', foo=[])");

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("rule //a:a", "aspect //a:b", "aspect //a:c");
  }

  @Test
  public void aspectWithParametrizedDefinition() throws Exception {
    setRulesAvailableInTests(
        new TestAspects.BaseRule(),
        new TestAspects.HonestRule(),
        new TestAspects.ParametrizedDefinitionAspectRule());

    pkg(
        "a",
        "honest(name='q', foo=[])",
        "parametrized_definition_aspect(name='a', foo=[':b'], baz='//a:q')",
        "honest(name='c', foo=[])",
        "honest(name='b', foo=[':c'])");

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(TestAspects.RuleInfo.class).getData())
        .containsExactly(
            "rule //a:a",
            "aspect //a:b data //a:q $dep:[ //a:q]",
            "aspect //a:c data //a:q $dep:[ //a:q]");
  }

  @Test
  public void aspectInError() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.ErrorAspectRule(),
        new TestAspects.SimpleRule());

    pkg("a",
        "simple(name='a', foo=[':b'])",
        "error_aspect(name='b', foo=[':c'])",
        "simple(name='c')");

    reporter.removeHandler(failFastHandler);
    // getConfiguredTarget() uses a separate code path that does not hit
    // SkyframeBuildView#configureTargets
    try {
      update("//a:a");
      fail();
    } catch (ViewCreationFailedException e) {
      // expected
    }
    assertContainsEvent("Aspect error");
  }

  @Test
  public void transitiveAspectInError() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.ErrorAspectRule(),
        new TestAspects.SimpleRule());

    pkg("a",
        "error_aspect(name='a', foo=[':b'])",
        "error_aspect(name='b', bar=[':c'])",
        "error_aspect(name='c', bar=[':d'])",
        "error_aspect(name='d')");

    reporter.removeHandler(failFastHandler);
    // getConfiguredTarget() uses a separate code path that does not hit
    // SkyframeBuildView#configureTargets
    try {
      update("//a:a");
      fail();
    } catch (ViewCreationFailedException e) {
      // expected
    }
    assertContainsEvent("Aspect error");
  }

  @Test
  public void aspectDependenciesDontShowDeprecationWarnings() throws Exception {
    setRulesAvailableInTests(
        new TestAspects.BaseRule(), new TestAspects.ExtraAttributeAspectRule());

    pkg("extra", "base(name='extra', deprecation='bad aspect')");

    pkg("a",
        "rule_with_extra_deps_aspect(name='a', foo=[':b'])",
        "base(name='b')");

    getConfiguredTarget("//a:a");
    assertContainsEventWithFrequency("bad aspect", 0);
  }

  @Test
  public void ruleDependencyDeprecationWarningsAbsentDuringAspectEvaluations() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.AspectRequiringRule());

    pkg("a", "aspect(name='a', foo=['//b:b'])");
    pkg("b", "aspect(name='b', bar=['//d:d'])");
    pkg("d", "base(name='d', deprecation='bad rule')");

    getConfiguredTarget("//a:a");
    assertContainsEventWithFrequency("bad rule", 1);
  }

  @Test
  public void aspectWarningsFilteredByOutputFiltersForAssociatedRules() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.WarningAspectRule());
    pkg("a", "warning_aspect(name='a', foo=['//b:b', '//c:c'])");
    pkg("b", "base(name='b')");
    pkg("c", "base(name='c')");

    reporter.setOutputFilter(RegexOutputFilter.forPattern(Pattern.compile("^//b:")));

    getConfiguredTarget("//a:a");
    assertContainsEventWithFrequency("Aspect warning on //b:b", 1);
    assertContainsEventWithFrequency("Aspect warning on //c:c", 0);
  }

  @Test
  public void sameTargetInDifferentAttributes() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.AspectRequiringRule(),
        new TestAspects.SimpleRule());
    pkg("a",
        "aspect(name='a', foo=[':b'], bar=[':b'])",
        "aspect(name='b', foo=[])");

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("aspect //a:b", "rule //a:a");
  }

  @Test
  public void informationFromBaseRulePassedToAspect() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(), new TestAspects.HonestRule(),
        new TestAspects.AspectRequiringProviderRule());

    pkg("a",
        "aspect_requiring_provider(name='a', foo=[':b'], baz='hello')",
        "honest(name='b', foo=[])");

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("rule //a:a", "aspect //a:b data hello");
  }

  /**
   * Rule definitions to be used in emptyAspectAttributesAreAvailableInRuleContext().
   */
  public static class EmptyAspectAttributesAreAvailableInRuleContext {
    public static class TestRule implements RuleDefinition {
      @Override
      public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) {
        return builder
            .add(attr("foo", LABEL_LIST).legacyAllowAnyFileType()
                .aspect(ASPECT_WITH_EMPTY_LATE_BOUND_ATTRIBUTE))
            .build();
      }

      @Override
      public Metadata getMetadata() {
        return RuleDefinition.Metadata.builder().name("testrule")
            .factoryClass(DummyRuleFactory.class).ancestors(BaseRule.class).build();
      }
    }

    public static class AspectWithEmptyLateBoundAttribute extends NativeAspectClass
      implements ConfiguredAspectFactory {
      @Override
      public AspectDefinition getDefinition(AspectParameters params) {
        return new AspectDefinition.Builder(this)
            .add(attr(":late", LABEL).value(EMPTY_LATE_BOUND_LABEL)).build();
      }

      @Override
      public ConfiguredAspect create(
          ConfiguredTarget base, RuleContext ruleContext, AspectParameters parameters)
          throws InterruptedException {
        Object lateBoundPrereq = ruleContext.getPrerequisite(":late", TARGET);
        return new ConfiguredAspect.Builder(this, parameters, ruleContext)
            .addProvider(
                AspectInfo.class,
                new AspectInfo(
                    NestedSetBuilder.create(
                        Order.STABLE_ORDER, lateBoundPrereq != null ? "non-empty" : "empty")))
            .build();
      }
    }
    public static final AspectWithEmptyLateBoundAttribute ASPECT_WITH_EMPTY_LATE_BOUND_ATTRIBUTE =
        new AspectWithEmptyLateBoundAttribute();
  }

  /**
   * An Aspect has a late-bound attribute with no value (that is, a LateBoundLabel whose
   * getDefault() returns `null`).
   * Test that this attribute is available in the RuleContext which is provided to the Aspect's
   * `create()` method.
   */
  @Test
  public void emptyAspectAttributesAreAvailableInRuleContext() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(),
        new EmptyAspectAttributesAreAvailableInRuleContext.TestRule());
    pkg("a",
        "testrule(name='a', foo=[':b'])",
        "testrule(name='b')");
    ConfiguredTarget a = getConfiguredTarget("//a:a");
    assertThat(a.getProvider(RuleInfo.class).getData()).contains("empty");
  }

  /**
   * Rule definitions to be used in extraActionsAreEmitted().
   */
  public static class ExtraActionsAreEmitted {
    public static class TestRule implements RuleDefinition {
      @Override
      public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) {
        return builder
            .add(attr("foo", LABEL_LIST).legacyAllowAnyFileType()
                .aspect(ASPECT_THAT_REGISTERS_ACTION))
            .add(attr(":action_listener", LABEL_LIST).cfg(HOST).value(ACTION_LISTENER))
            .build();
      }

      @Override
      public Metadata getMetadata() {
        return RuleDefinition.Metadata.builder().name("testrule")
            .factoryClass(DummyRuleFactory.class).ancestors(BaseRule.class).build();
      }
    }

    public static class AspectThatRegistersAction extends NativeAspectClass
      implements ConfiguredAspectFactory {
      @Override
      public AspectDefinition getDefinition(AspectParameters params) {
        return new AspectDefinition.Builder(this).build();
      }



      @Override
      public ConfiguredAspect create(
          ConfiguredTarget base, RuleContext ruleContext, AspectParameters parameters)
              throws InterruptedException {
        ruleContext.registerAction(new NullAction(ruleContext.createOutputArtifact()));
        return new ConfiguredAspect.Builder(this, parameters, ruleContext).build();
      }
    }
    private static final AspectThatRegistersAction ASPECT_THAT_REGISTERS_ACTION =
        new AspectThatRegistersAction();
  }

  /**
   * Test that actions registered in an Aspect are reported as extra-actions on the attached rule.
   * AspectThatRegistersAction registers a NullAction, whose mnemonic is "Null". We have an
   * action_listener that targets that mnemonic, which makes sure the Aspect machinery will expose
   * an ExtraActionArtifactsProvider.
   * The rule //a:a doesn't have an aspect, so the only action we get is the one on //a:b
   * (which does have an aspect).
   */
  @Test
  public void extraActionsAreEmitted() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(),
        new ExtraActionsAreEmitted.TestRule());
    useConfiguration("--experimental_action_listener=//extra_actions:listener");
    scratch.file(
        "extra_actions/BUILD",
        "extra_action(name='xa', cmd='echo dont-care')",
        "action_listener(name='listener', mnemonics=['Null'], extra_actions=[':xa'])");
    pkg("a",
        "testrule(name='a', foo=[':b'])",
        "testrule(name='b')");
    update();

    ConfiguredTarget a = getConfiguredTarget("//a:a");
    NestedSet<Artifact> extraActionArtifacts =
        a.getProvider(ExtraActionArtifactsProvider.class).getTransitiveExtraActionArtifacts();
    for (Artifact artifact : extraActionArtifacts) {
      assertThat(artifact.getOwnerLabel()).isEqualTo(Label.create("@//a", "b"));
    }
  }

  @Test
  public void aspectPropagatesToAllAttributes() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(),
        new TestAspects.SimpleRule(),
        new TestAspects.AllAttributesAspectRule());
    pkg("a",
        "simple(name='a', foo=[':b'], foo1=':c', txt='some text')",
        "simple(name='b', foo=[], txt='some text')",
        "simple(name='c', foo=[], txt='more text')",
        "all_attributes_aspect(name='x', foo=[':a'])");

    ConfiguredTarget a = getConfiguredTarget("//a:x");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("aspect //a:a",  "aspect //a:b", "aspect //a:c", "rule //a:x");
  }

  /**
   * Tests that when --experimental_extra_action_top_level_only, Blaze reports extra-actions for
   * actions registered by Aspects injected by a top-level rule. Because we can't know whether an
   * aspect was injected by a top-level target or one of its children, we approximate it by only
   * reporting extra-actions from Aspects that the top-level target could have injected.
   *
   * <p>Here, injector1() and injector2() inject aspects into their children. null_rule() just
   * passes the aspects to its children. The test makes sure that actions registered by aspect1
   * (injected by injector1()) are reported to the extra-action mechanism. Actions registered by
   * aspect2 (from injector2) are not reported, because the target under test (//x:a) doesn't inject
   * aspect2.
   */
  @Test
  public void extraActionsAreEmitted_topLevel() throws Exception {
    useConfiguration(
        "--experimental_action_listener=//pkg1:listener",
        "--experimental_extra_action_top_level_only");

    scratch.file(
        "x/BUILD",
        "load(':extension.bzl', 'injector1', 'injector2', 'null_rule')",
        "injector1(name='a', deps=[':b'])",
        "null_rule(name='b', deps=[':c'])",
        "null_rule(name='c', deps=[':d'])",
        "injector2(name = 'd', extra_deps=[':e'])",
        "null_rule(name = 'e')");

    scratch.file(
        "x/extension.bzl",
        "def _aspect_impl(target, ctx):",
        "  ctx.empty_action(mnemonic='Mnemonic')",
        "  return struct()",
        "aspect1 = aspect(_aspect_impl, attr_aspects=['deps'])",
        "aspect2 = aspect(_aspect_impl, attr_aspects=['extra_deps'])",
        "def _rule_impl(ctx):",
        "  return struct()",
        "injector1 = rule(_rule_impl, attrs = { 'deps' : attr.label_list(aspects = [aspect1]) })",
        "null_rule = rule(_rule_impl, attrs = { 'deps' : attr.label_list() })",
        "injector2 = rule(",
        "  _rule_impl, attrs = { 'extra_deps' : attr.label_list(aspects = [aspect2]) })");

    scratch.file(
        "pkg1/BUILD",
        "extra_action(name='xa', cmd='echo dont-care')",
        "action_listener(name='listener', mnemonics=['Mnemonic'], extra_actions=[':xa'])");

    // Sanity check: //x:d injects an aspect which produces some extra-action.
    {
      BuildView.AnalysisResult analysisResult = update("//x:d");

      // Get owners of all extra-action artifacts.
      List<Label> extraArtifactOwners = new ArrayList<>();
      for (Artifact artifact : analysisResult.getAdditionalArtifactsToBuild()) {
        if (artifact.getRootRelativePathString().endsWith(".xa")) {
          extraArtifactOwners.add(artifact.getOwnerLabel());
        }
      }
      assertThat(extraArtifactOwners).containsExactly(Label.create("@//x", "e"));
    }

    // Actual test: //x:a reports actions registered by the aspect it injects.
    {
      BuildView.AnalysisResult analysisResult = update("//x:a");

      // Get owners of all extra-action artifacts.
      List<Label> extraArtifactOwners = new ArrayList<>();
      for (Artifact artifact : analysisResult.getAdditionalArtifactsToBuild()) {
        if (artifact.getRootRelativePathString().endsWith(".xa")) {
          extraArtifactOwners.add(artifact.getOwnerLabel());
        }
      }
      assertThat(extraArtifactOwners)
          .containsExactly(
              Label.create("@//x", "b"), Label.create("@//x", "c"), Label.create("@//x", "d"));
    }
  }

  @Test
  public void extraActionsFromDifferentAspectsDontConflict() throws Exception {
    useConfiguration(
        "--experimental_action_listener=//pkg1:listener",
        "--experimental_extra_action_top_level_only");

    scratch.file(
        "x/BUILD",
        "load(':extension.bzl', 'injector1', 'injector2', 'null_rule')",
        "injector2(name='i2_a', deps = [':i1_a'])",
        "injector1(name='i1_a', deps=[':n'], param = 'a')",
        "injector1(name='i1_b', deps=[':n'], param = 'b')",
        "injector2(name='i2', deps=[':n'])",
        "null_rule(name = 'n')");

    scratch.file(
        "x/extension.bzl",
        "def _aspect_impl(target, ctx):",
        "  ctx.empty_action(mnemonic='Mnemonic')",
        "  return struct()",
        "aspect1 = aspect(_aspect_impl, attr_aspects=['deps'], attrs =",
        "    {'param': attr.string(values = ['a', 'b'])})",
        "aspect2 = aspect(_aspect_impl, attr_aspects=['deps'])",
        "def _rule_impl(ctx):",
        "  return struct()",
        "injector1 = rule(_rule_impl, attrs =",
        "    { 'deps' : attr.label_list(aspects = [aspect1]), 'param' : attr.string() })",
        "injector2 = rule(_rule_impl, attrs = { 'deps' : attr.label_list(aspects = [aspect2]) })",
        "null_rule = rule(_rule_impl, attrs = { 'deps' : attr.label_list() })"
    );

    scratch.file(
        "pkg1/BUILD",
        "extra_action(name='xa', cmd='echo dont-care')",
        "action_listener(name='listener', mnemonics=['Mnemonic'], extra_actions=[':xa'])");

    update("//x:i1_a", "//x:i1_b", "//x:i2", "//x:i2_a");

    // Implicitly check that update() didn't throw an exception because of two actions producing
    // the same outputs.
  }

  @Test
  public void aspectPropagatesToAllAttributesImplicit() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(),
        new TestAspects.SimpleRule(),
        new TestAspects.ImplicitDepRule(),
        new TestAspects.AllAttributesAspectRule());

    scratch.file(
        "extra/BUILD",
        "simple(name ='extra')"
    );
    pkg("a",
        "simple(name='a', foo=[':b'], foo1=':c', txt='some text')",
        "simple(name='b', foo=[], txt='some text')",
        "implicit_dep(name='c')",
        "all_attributes_aspect(name='x', foo=[':a'])");
    update();

    ConfiguredTarget a = getConfiguredTarget("//a:x");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly(
            "aspect //a:a",
            "aspect //a:b",
            "aspect //a:c",
            "aspect //extra:extra",
            "rule //a:x");
  }


  @Test
  public void aspectPropagatesToAllAttributesLateBound() throws Exception {
    setRulesAvailableInTests(new TestAspects.BaseRule(),
        new TestAspects.SimpleRule(),
        new TestAspects.LateBoundDepRule(),
        new TestAspects.AllAttributesAspectRule());

    scratch.file(
        "extra/BUILD",
        "simple(name ='extra')"
    );
    pkg("a",
        "simple(name='a', foo=[':b'], foo1=':c', txt='some text')",
        "simple(name='b', foo=[], txt='some text')",
        "late_bound_dep(name='c')",
        "all_attributes_aspect(name='x', foo=[':a'])");
    useConfiguration("--plugin=//extra:extra");
    update();

    ConfiguredTarget a = getConfiguredTarget("//a:x");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly(
            "aspect //a:a",
            "aspect //a:b",
            "aspect //a:c",
            "aspect //extra:extra",
            "rule //a:x");
  }

  /**
   * Ensures an aspect with attr = '*' doesn't try to propagate to its own implicit attributes.
   * Doing so leads to a dependency cycle.
   */
  @Test
  public void aspectWithAllAttributesDoesNotPropagateToOwnImplicitAttributes() throws Exception {
    setRulesAvailableInTests(
        new TestAspects.BaseRule(),
        new TestAspects.SimpleRule(),
        new TestAspects.AllAttributesWithToolAspectRule());
    pkg(
        "a",
        "simple(name='tool')",
        "simple(name='a')",
        "all_attributes_with_tool_aspect(name='x', foo=[':a'])");

    ConfiguredTarget a = getConfiguredTarget("//a:x");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly("aspect //a:a", "rule //a:x");
  }

  /**
   * Makes sure the aspect *will* propagate to its implicit attributes if there is a "regular"
   * dependency path to it (i.e. not through its own implicit attributes).
   */
  @Test
  public void aspectWithAllAttributesPropagatesToItsToolIfThereIsPath() throws Exception {
    setRulesAvailableInTests(
        new TestAspects.BaseRule(),
        new TestAspects.SimpleRule(),
        new TestAspects.AllAttributesWithToolAspectRule());
    pkg(
        "a",
        "simple(name='tool')",
        "simple(name='a', foo=[':b'], foo1=':c', txt='some text')",
        "simple(name='b', foo=[], txt='some text')",
        "simple(name='c', foo=[':tool'], txt='more text')",
        "all_attributes_with_tool_aspect(name='x', foo=[':a'])");

    ConfiguredTarget a = getConfiguredTarget("//a:x");
    assertThat(a.getProvider(RuleInfo.class).getData())
        .containsExactly(
            "aspect //a:a", "aspect //a:b", "aspect //a:c", "aspect //a:tool", "rule //a:x");
  }
}
