// Copyright 2017 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.packages.Attribute.attr;
import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
import static org.junit.Assert.assertThrows;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.analysis.util.BuildViewTestBase.AnalysisFailureRecorder;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.analysis.util.DummyTestFragment;
import com.google.devtools.build.lib.analysis.util.MockRule;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.Attribute.ComputedDefault;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.NoSuchTargetException;
import com.google.devtools.build.lib.packages.RuleClass.ToolchainResolutionMode;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
import com.google.devtools.build.lib.util.FileTypeSet;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Integration tests for configurable attributes.
 */
@RunWith(JUnit4.class)
public class ConfigurableAttributesTest extends BuildViewTestCase {

  private void writeConfigRules() throws Exception {
    scratch.file(
        "conditions/BUILD",
        "config_setting(",
        "    name = 'a',",
        "    values = {'foo': 'a'})",
        "config_setting(",
        "    name = 'b',",
        "    values = {'foo': 'b'})");
  }

  private void writeHelloRules(boolean includeDefaultCondition) throws IOException {
    scratch.file("java/hello/BUILD",
        "java_binary(",
        "    name = 'hello',",
        "    srcs = ['hello.java'],",
        "    deps = select({",
        "        '//conditions:a': [':adep'],",
        "        '//conditions:b': [':bdep'],",
        includeDefaultCondition
            ? "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': [':defaultdep'],"
            : "",
        "    }))",
        "",
        "java_library(",
        "    name = 'adep',",
        "    srcs = ['adep.java'])",
        "java_library(",
        "    name = 'bdep',",
        "    srcs = ['bdep.java'])",
        "java_library(",
        "    name = 'defaultdep',",
        "    srcs = ['defaultdep.java'])");
  }

  private static final String ADEP_INPUT = "bin java/hello/libadep.jar";
  private static final String BDEP_INPUT = "bin java/hello/libbdep.jar";
  private static final String CDEP_INPUT = "bin java/hello/libcdep.jar";
  private static final String DEFAULTDEP_INPUT = "bin java/hello/libdefaultdep.jar";

  /**
   * Checks that, given the specified configuration parameters, the input rule *has* the expected
   * attribute values and *doesn't have* the unexpected attribute values.
   */
  private void checkRule(
      String ruleLabel,
      String attributeName,
      Collection<String> options,
      Iterable<String> expected,
      Iterable<String> notExpected)
      throws Exception {
    useConfiguration(options.toArray(new String[options.size()]));
    ConfiguredTarget binary = getConfiguredTarget(ruleLabel);
    assertThat(binary).isNotNull();
    Set<String> actualDeps = artifactsToStrings(getPrerequisiteArtifacts(binary, attributeName));
    expected.forEach(expectedInput -> assertThat(actualDeps).contains(expectedInput));
    notExpected.forEach(unexpectedInput -> assertThat(actualDeps).doesNotContain(unexpectedInput));
  }

  private void checkRule(String ruleLabel, String option,
      Iterable<String> expected, Iterable<String> notExpected) throws Exception {
    checkRule(ruleLabel, ImmutableList.of(option), expected, notExpected);
  }

  private void checkRule(
      String ruleLabel,
      Collection<String> options,
      Iterable<String> expected,
      Iterable<String> notExpected)
      throws Exception {
    checkRule(ruleLabel, "deps", options, expected, notExpected);
  }

  private static final MockRule RULE_WITH_OUTPUT_ATTR =
      () -> MockRule.define("rule_with_output_attr", attr("out", BuildType.OUTPUT));

  private static final MockRule RULE_WITH_COMPUTED_DEFAULT =
      () -> MockRule.define(
          "rule_with_computed_default",
          attr("string_attr", Type.STRING),
          attr("$computed_attr", Type.STRING).value(
              new ComputedDefault("string_attr") {
                @Override
                public Object getDefault(AttributeMap rule) {
                  return rule.get("string_attr", Type.STRING) + "2";
                }
            }));

  private static final MockRule RULE_WITH_BOOLEAN_ATTR =
      () -> MockRule.define("rule_with_boolean_attr", attr("boolean_attr", Type.BOOLEAN));

  private static final MockRule RULE_WITH_ALLOWED_VALUES =
      () -> MockRule.define(
          "rule_with_allowed_values",
          attr("one_two", Type.STRING)
              .allowedValues(new Attribute.AllowedValueSet("one", "two")));

  private static final MockRule RULE_WITH_LABEL_DEFAULT =
      () ->
          MockRule.define(
              "rule_with_label_default",
              (builder, env) ->
                  builder.add(
                      attr("dep", BuildType.LABEL)
                          .value(Label.parseAbsoluteUnchecked("//foo:default"))
                          .allowedFileTypes(FileTypeSet.ANY_FILE)));

  private static final MockRule RULE_WITH_NO_PLATFORM =
      () ->
          MockRule.define(
              "rule_with_no_platform",
              (builder, env) ->
                  builder
                      .add(attr("deps", LABEL_LIST).allowedFileTypes())
                      .useToolchainResolution(ToolchainResolutionMode.DISABLED));

  @Override
  protected ConfiguredRuleClassProvider createRuleClassProvider() {
    ConfiguredRuleClassProvider.Builder builder =
        new ConfiguredRuleClassProvider.Builder()
            .addRuleDefinition(RULE_WITH_OUTPUT_ATTR)
            .addRuleDefinition(RULE_WITH_COMPUTED_DEFAULT)
            .addRuleDefinition(RULE_WITH_BOOLEAN_ATTR)
            .addRuleDefinition(RULE_WITH_ALLOWED_VALUES)
            .addRuleDefinition(RULE_WITH_LABEL_DEFAULT)
            .addRuleDefinition(RULE_WITH_NO_PLATFORM);
    TestRuleClassProvider.addStandardRules(builder);
    // Allow use of --foo as a dummy flag
    builder.addConfigurationFragment(DummyTestFragment.class);
    return builder.build();
  }

  @Before
  public void setupStarlarkJavaBinary() throws Exception {
    setBuildLanguageOptions("--experimental_google_legacy_api");
  }

  @Test
  public void basicConfigurability() throws Exception {
    writeHelloRules(/*includeDefaultCondition=*/true);
    writeConfigRules();
    checkRule(
        "//java/hello:hello",
        "--foo=a",
        /*expected:*/ ImmutableList.of(ADEP_INPUT),
        /*not expected:*/ ImmutableList.of(BDEP_INPUT, DEFAULTDEP_INPUT));
    checkRule(
        "//java/hello:hello",
        "--foo=b",
        /*expected:*/ ImmutableList.of(BDEP_INPUT),
        /*not expected:*/ ImmutableList.of(ADEP_INPUT, DEFAULTDEP_INPUT));
  }

  @Test
  public void configurabilityDefaults() throws Exception {
    writeHelloRules(/*includeDefaultCondition=*/true);
    writeConfigRules();
    checkRule(
        "//java/hello:hello",
        "--foo=something_random",
        /*expected:*/ ImmutableList.of(DEFAULTDEP_INPUT),
        /*not expected:*/ ImmutableList.of(ADEP_INPUT, BDEP_INPUT));
    checkRule("//java/hello:hello", "",
        /*expected:*/ ImmutableList.of(DEFAULTDEP_INPUT),
        /*not expected:*/ ImmutableList.of(ADEP_INPUT, BDEP_INPUT));
  }

  /**
   * Duplicate label definitions are fine as long as they're in different selection branches.
   */
  @Test
  public void depsWithDuplicatesInDifferentBranches() throws Exception {
    writeConfigRules();
    scratch.file("java/hello/BUILD",
        "java_binary(",
        "    name = 'hello',",
        "    srcs = ['hello.java'],",
        "    deps = select({",
        "        '//conditions:a': [':adep', ':cdep'],",
        "        '//conditions:b': [':bdep', ':cdep'],",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': [':defaultdep'],",
        "    }))",
        "",
        "java_library(",
        "    name = 'adep',",
        "    srcs = ['adep.java'])",
        "java_library(",
        "    name = 'bdep',",
        "    srcs = ['bdep.java'])",
        "java_library(",
        "    name = 'cdep',",
        "    srcs = ['cdep.java'])");
    checkRule(
        "//java/hello:hello",
        "--foo=a",
        /*expected:*/ ImmutableList.of(ADEP_INPUT, CDEP_INPUT),
        /*not expected:*/ ImmutableList.of(BDEP_INPUT, DEFAULTDEP_INPUT));
  }

  /**
   * Duplicate label definitions are *not* fine within the same branch.
   */
  @Test
  public void depsWithDuplicatesInSameBranch() throws Exception {
    writeConfigRules();
    scratch.file("java/hello/BUILD",
        "java_binary(",
        "    name = 'hello',",
        "    srcs = ['hello.java'],",
        "    deps = select({",
        "        '//conditions:a': [':adep', ':cdep', ':adep'],",
        "        '//conditions:b': [':bdep', ':cdep'],",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': [':defaultdep'],",
        "    }))",
        "",
        "java_library(",
        "    name = 'adep',",
        "    srcs = ['adep.java'])",
        "java_library(",
        "    name = 'bdep',",
        "    srcs = ['bdep.java'])",
        "java_library(",
        "    name = 'cdep',",
        "    srcs = ['cdep.java'])");

    reporter.removeHandler(failFastHandler); // Expect errors.
    useConfiguration("--foo=a");
    getConfiguredTarget("//java/hello:hello");
    assertContainsEvent(
        "Label '//java/hello:adep' is duplicated in the 'deps' attribute of rule 'hello'");
  }

  /**
   * When an attribute includes multiple selects, we don't allow duplicates even across
   * selects (this saves us from having to do possibly expensive value iteration since the
   * number of values can grow exponentially with respect to the number of selects).
   */
  @Test
  public void duplicatesAcrossMultipleSelects() throws Exception {
    writeConfigRules();
    scratch.file(
        "java/hello/BUILD",
        "java_binary(",
        "    name = 'hello',",
        "    srcs = select({",
        "        '//conditions:a': ['a.java'],",
        "        '//conditions:b': ['b.java'],",
        "        })",
        "        + select({",
        "        '//conditions:a': ['a.java'],",
        "        '//conditions:b': ['c.java'],",
        "    }))");

    reporter.removeHandler(failFastHandler); // Expect errors.
    useConfiguration("--foo=a");
    getConfiguredTarget("//java/hello:hello");
    assertContainsEvent(
        "in srcs attribute of java_binary rule //java/hello:hello: Label '//java/hello:a.java' is"
            + " duplicated");
  }

  /**
   * Even with multiple selects, duplicates are allowed within a *single* select as long as
   * they're in different branches (and thus mutually exclusive).
   */
  @Test
  public void duplicatesInDifferentBranchesMultipleSelects() throws Exception {
    writeConfigRules();
    scratch.file("java/hello/BUILD",
        "java_binary(",
        "    name = 'hello',",
        "    srcs = select({",
        "        '//conditions:a': ['a.java'],",
        "        '//conditions:b': ['a.java'],",
        "        })",
        "        + select({",
        "        '//conditions:a': ['b.java'],",
        "        '//conditions:b': ['b.java'],",
        "    }))");

    useConfiguration("--foo=a");
    getConfiguredTarget("//java/hello:hello");
    assertNoEvents();
  }

  /**
   * With multiple selects, a single select still can't duplicate labels within the same branch.
   */
  @Test
  public void duplicatesInSameBranchMultipleSelects() throws Exception {
    writeConfigRules();
    scratch.file("java/hello/BUILD",
        "java_binary(",
        "    name = 'hello',",
        "    srcs = select({",
        "        '//conditions:a': ['a.java', 'a.java'],",
        "        '//conditions:b': ['b.java'],",
        "        })",
        "        + select({",
        "        '//conditions:a': ['c.java'],",
        "        '//conditions:b': ['d.java'],",
        "    }))");

    reporter.removeHandler(failFastHandler); // Expect errors.
    useConfiguration("--foo=a");
    getConfiguredTarget("//java/hello:hello");
    assertContainsEvent(
        "Label '//java/hello:a.java' is duplicated in the 'srcs' attribute of rule 'hello'");
  }

  /**
   * Attributes of type {@link BuildType#OUTPUT} are not configurable.
   */
  @Test
  public void outputTypeNotConfigurable() throws Exception {
    writeConfigRules();
    scratch.file("foo/BUILD",
        "rule_with_output_attr(",
        "    name = 'has_an_out',",
        "    out = select({",
        "        '//conditions:a': 'a.out',",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': 'default.out'})",
        ")");

    reporter.removeHandler(failFastHandler); // Expect errors.
    getConfiguredTarget("//foo:has_an_out");
    assertContainsEvent("attribute \"out\" is not configurable");
  }

  /**
   * Attributes of type {@link BuildType#OUTPUT_LIST} are not configurable.
   */
  @Test
  public void outputListTypeNotConfigurable() throws Exception {
    writeConfigRules();
    scratch.file("foo/BUILD",
        "genrule(",
        "    name = 'generator',",
        "    srcs = [],",
        "    outs = select({",
        "        '//conditions:a': ['a.out'],",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': ['default.out']})",
        ")");

    reporter.removeHandler(failFastHandler); // Expect errors.
    getConfiguredTarget("//foo:generator");
    assertContainsEvent("attribute \"outs\" is not configurable");
  }

  /**
   * Tests that computed defaults faithfully reflect the values of the attributes they depend on.
   */
  @Test
  public void computedDefaults() throws Exception {
    writeConfigRules();
    scratch.file("test/BUILD",
        "rule_with_computed_default(",
        "    name = 'the_rule',",
        "    string_attr = select({",
        "        '//conditions:a': 'a',",
        "        '//conditions:b': 'b',",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': 'default',",
        "    }))");

    // Configuration a:
    useConfiguration("--foo=a");
    ConfiguredTargetAndData binary = getConfiguredTargetAndData("//test:the_rule");
    AttributeMap attributes = getMapperFromConfiguredTargetAndTarget(binary);
    assertThat(attributes.get("$computed_attr", Type.STRING)).isEqualTo("a2");

    // configuration b:
    useConfiguration("--foo=b");
    binary = getConfiguredTargetAndData("//test:the_rule");
    attributes = getMapperFromConfiguredTargetAndTarget(binary);
    assertThat(attributes.get("$computed_attr", Type.STRING)).isEqualTo("b2");
  }

  @Test
  public void configKeyTypeChecking_Int() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    scratch.file(
        "java/foo/BUILD",
        "java_library(",
        "    name = 'int_key',",
        "    srcs = select({123: ['a.java']})",
        ")");
    assertTargetError(
        "//java/foo:int_key", "select: got int for dict key, want a Label or label string");
  }

  @Test
  public void configKeyTypeChecking_Bool() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    scratch.file(
        "java/foo/BUILD",
        "java_library(",
        "    name = 'bool_key',",
        "    srcs = select({True: ['a.java']})",
        ")");
    assertTargetError(
        "//java/foo:bool_key", "select: got bool for dict key, want a Label or label string");
  }

  @Test
  public void configKeyTypeChecking_None() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    scratch.file(
        "java/foo/BUILD",
        "java_library(",
        "    name = 'none_key',",
        "    srcs = select({None: ['a.java']})",
        ")");
    assertTargetError(
        "//java/foo:none_key", "select: got NoneType for dict key, want a Label or label string");
  }

  @Test
  public void selectWithoutConditionsMakesNoSense() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    scratch.file(
        "foo/BUILD",
        "genrule(",
        "    name = 'nothing',",
        "    srcs = [],",
        "    outs = ['notmuch'],",
        "    cmd = select({})",
        ")");
    assertTargetError(
        "//foo:nothing",
        "select({}) with an empty dictionary can never resolve because it includes no conditions "
            + "to match");
  }

  /**
   * Tests that config keys must resolve to existent targets.
   */
  @Test
  public void missingConfigKey() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    // Only create one of two necessary configurability rules:
    scratch.file(
        "conditions/BUILD", "config_setting(", "    name = 'a',", "    values = {'foo': 'a'})");
    writeHelloRules(/*includeDefaultCondition=*/true);
    getConfiguredTarget("//java/hello:hello");
    assertContainsEvent("no such target '//conditions:b'");
  }

  /**
   * Tests that config keys must resolve to config_setting targets.
   */
  @Test
  public void invalidConfigKey() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    scratch.file(
        "conditions/BUILD",
        "config_setting(",
        "    name = 'a',",
        "    values = {'foo': 'a'})",
        "rule_with_output_attr(",
        "    name = 'b',",
        "    out = 'b.out')");
    writeHelloRules(/*includeDefaultCondition=*/true);
    assertThat(getConfiguredTarget("//java/hello:hello")).isNull();
    assertContainsEvent("//conditions:b is not a valid select() condition for //java/hello:hello");
    assertDoesNotContainEvent("//conditions:a"); // This one is legitimate..
  }

  @Test
  public void configKeyNonexistentTarget() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    scratch.file("foo/BUILD",
        "genrule(",
        "    name = 'g',",
        "    outs = ['g.out'],",
        "    cmd = select({':fake': ''})",
        ")");
    assertThat(getConfiguredTarget("//foo:g")).isNull();
    assertContainsEvent("//foo:fake is not a valid select() condition for //foo:g");
  }

  @Test
  public void configKeyNonexistentTarget_otherPackage() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    scratch.file(
        "conditions/BUILD", "config_setting(", "    name = 'a',", "    values = {'foo': 'a'})");
    scratch.file("bar/BUILD");
    scratch.file(
        "foo/BUILD",
        "genrule(",
        "    name = 'g',",
        "    outs = ['g.out'],",
        // With an invalid target and a real target, validate skyframe error handling.
        // See http://b/162021059 for details.
        "    cmd = select({'//bar:fake': '', '//conditions:a': ''})",
        ")");
    assertThat(getConfiguredTarget("//foo:g")).isNull();
    assertContainsEvent("bar/BUILD: no such target '//bar:fake'");
    assertContainsEvent("foo/BUILD:1:8: errors encountered resolving select() keys for //foo:g");
  }

  /**
   * Tests config keys with multiple requirements.
   */
  @Test
  public void multiConditionConfigKeys() throws Exception {
    writeHelloRules(/*includeDefaultCondition=*/true);
    scratch.file(
        "conditions/BUILD",
        "config_setting(",
        "    name = 'a',",
        "    values = {",
        "        'foo': 'a',",
        "        'compilation_mode': 'dbg'",
        "    })",
        "config_setting(",
        "    name = 'b',",
        "    values = {'foo': 'b'})");
    checkRule(
        "//java/hello:hello",
        "--foo=a",
        /*expected:*/ ImmutableList.of(DEFAULTDEP_INPUT),
        /*not expected:*/ ImmutableList.of(ADEP_INPUT, BDEP_INPUT));
    checkRule(
        "//java/hello:hello",
        ImmutableList.of("--foo=a", "--compilation_mode=dbg"),
        /*expected:*/ ImmutableList.of(ADEP_INPUT),
        /*not expected:*/ ImmutableList.of(BDEP_INPUT, DEFAULTDEP_INPUT));
  }

  /**
   * Tests that changing a config_setting invalidates the rule that uses it.
   */
  @Test
  public void configKeyInvalidation() throws Exception {
    writeHelloRules(/*includeDefaultCondition=*/true);
    writeConfigRules();

    // Iteration 1: --test_args=a should apply //conditions:a.
    useConfiguration("--foo=a");
    checkRule(
        "//java/hello:hello",
        "--foo=a",
        /*expected:*/ ImmutableList.of(ADEP_INPUT),
        /*not expected:*/ ImmutableList.of(BDEP_INPUT, DEFAULTDEP_INPUT));

    // Rewrite the condition for //conditions:a.
    scratch.overwriteFile(
        "conditions/BUILD",
        "config_setting(",
        "    name = 'a',",
        "    values = {'foo': 'c'})",
        "config_setting(",
        "    name = 'b',",
        "    values = {'foo': 'b'})");

    // Iteration 2: same exact analysis should now apply the default condition.
    invalidatePackages();
    checkRule(
        "//java/hello:hello",
        "--foo=a",
        /*expected:*/ ImmutableList.of(DEFAULTDEP_INPUT),
        /*not expected:*/ ImmutableList.of(ADEP_INPUT, BDEP_INPUT));
  }

  /**
   * Tests that multiple matches are not allowed for conditions where one is not a specialization
   * of the other.
   */
  @Test
  public void multipleMatches() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    scratch.file("conditions/BUILD",
        "config_setting(",
        "    name = 'dup1',",
        "    values = {'compilation_mode': 'opt'})",
        "config_setting(",
        "    name = 'dup2',",
        "    values = {'define': 'foo=bar'})");
    scratch.file("a/BUILD",
        "genrule(",
        "    name = 'gen',",
        "    cmd = '',",
        "    outs = ['gen.out'],",
        "    srcs = select({",
        "        '//conditions:dup1': ['a.in'],",
        "        '//conditions:dup2': ['b.in'],",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': [':default.in'],",
        "    }))");
    useConfiguration("-c", "opt", "--define", "foo=bar");
    assertThat(getConfiguredTarget("//a:gen")).isNull();
    assertContainsEvent(
        "Illegal ambiguous match on configurable attribute \"srcs\" in //a:gen:\n"
            + "//conditions:dup1\n"
            + "//conditions:dup2\n"
            + "Multiple matches are not allowed unless one is unambiguously more specialized "
            + "or they resolve to the same value.");
  }

  /**
   * Tests that when multiple conditions match and for every matching pair, one is
   * a specialization of the other, the most specialized match is chosen.
   */
  @Test
  public void multipleMatchesConditionAndSubcondition() throws Exception {
    scratch.file(
        "conditions/BUILD",
        "config_setting(",
        "    name = 'generic',",
        "    values = {'compilation_mode': 'opt'})",
        "config_setting(",
        "    name = 'precise',",
        "    values = {'compilation_mode': 'opt', 'define': 'foo=bar'})",
        "config_setting(",
        "    name = 'most_precise',",
        "    values = {'compilation_mode': 'opt', 'define': 'foo=bar', 'foo': 'baz'})");
        scratch.file("java/a/BUILD",
            "java_binary(",
            "    name = 'binary',",
            "    srcs = ['binary.java'],",
            "    deps = select({",
            "        '//conditions:generic': [':generic'],",
            "        '//conditions:precise': [':precise'],",
            "        '//conditions:most_precise': [':most_precise'],",
            "    }))",
            "java_library(",
            "    name = 'generic',",
            "    srcs = ['generic.java'])",
            "java_library(",
            "    name = 'precise',",
            "    srcs = ['precise.java'])",
            "java_library(",
            "    name = 'most_precise',",
            "    srcs = ['most_precise.java'])");
    checkRule(
        "//java/a:binary",
        ImmutableList.of("-c", "opt", "--define", "foo=bar", "--foo", "baz"),
        /*expected:*/ ImmutableList.of("bin java/a/libmost_precise.jar"),
        /*not expected:*/ ImmutableList.of(
            "bin java/a/libgeneric.jar", "bin java/a/libprecise.jar"));
  }

  /** Tests that multiple matches are allowed for conditions where the value is the same. */
  @Test
  public void multipleMatchesSameValue() throws Exception {
    reporter.removeHandler(failFastHandler); // Expect errors.
    scratch.file(
        "conditions/BUILD",
        "config_setting(",
        "    name = 'dup1',",
        "    values = {'compilation_mode': 'opt'})",
        "config_setting(",
        "    name = 'dup2',",
        "    values = {'define': 'foo=bar'})");
    scratch.file(
        "a/BUILD",
        "genrule(",
        "    name = 'gen',",
        "    cmd = '',",
        "    outs = ['gen.out'],",
        "    srcs = select({",
        "        '//conditions:dup1': ['a.in'],",
        "        '//conditions:dup2': ['a.in'],",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': [':default.in'],",
        "    }))");
    checkRule(
        "//a:gen",
        "srcs",
        ImmutableList.of("-c", "opt", "--define", "foo=bar"),
        /*expected:*/ ImmutableList.of("src a/a.in"),
        /*not expected:*/ ImmutableList.of("src a/default.in"));
  }

  /**
   * Tests that when multiple conditions match but one condition is more specialized than the
   * others, it is chosen and there is no error.
   */
  @Test
  public void multipleMatchesUnambiguous() throws Exception {
    scratch.file(
        "conditions/BUILD",
        "config_setting(",
        "    name = 'a',",
        "    values = {'define': 'a=1'})",
        "config_setting(",
        "    name = 'b',",
        "    values = {'compilation_mode': 'opt'})",
        "config_setting(",
        "    name = 'c',",
        "    values = {'foo': 'baz'})",
        "config_setting(",
        "    name = 'b_a_c',", // Named to come alphabetically after a and b but before c.
        "    values = {'define': 'a=1', 'foo': 'baz', 'compilation_mode': 'opt'})");
    scratch.file("java/a/BUILD",
        "java_binary(",
        "    name = 'binary',",
        "    srcs = ['binary.java'],",
        "    deps = select({",
        "        '//conditions:a': [':a'],",
        "        '//conditions:b': [':b'],",
        "        '//conditions:c': [':c'],",
        "        '//conditions:b_a_c': [':b_a_c'],",
        "    }))",
        "java_library(",
        "    name = 'a',",
        "    srcs = ['a.java'])",
        "java_library(",
        "    name = 'b',",
        "    srcs = ['b.java'])",
        "java_library(",
        "    name = 'c',",
        "    srcs = ['c.java'])",
        "java_library(",
        "    name = 'b_a_c',",
        "    srcs = ['b_a_c.java'])");
    checkRule(
        "//java/a:binary",
        ImmutableList.of("--define", "a=1", "--compilation_mode", "opt", "--foo", "baz"),
        /*expected:*/ ImmutableList.of("bin java/a/libb_a_c.jar"),
        /*not expected:*/ ImmutableList.of(
            "bin java/a/liba.jar", "bin java/a/libb.jar", "bin java/a/libc.jar"));
  }

  /** Tests that default conditions are only required when no main condition matches. */
  @Test
  public void noDefaultCondition() throws Exception {
    writeHelloRules(/*includeDefaultCondition=*/false);
    writeConfigRules();

    // An explicit configuration matches: all is well.
    checkRule(
        "//java/hello:hello",
        "--foo=a",
        /*expected:*/ ImmutableList.of(ADEP_INPUT),
        /*not expected:*/ ImmutableList.of(BDEP_INPUT, DEFAULTDEP_INPUT));

    // Nothing matches: expect an error.
    reporter.removeHandler(failFastHandler);
    useConfiguration("");
    assertThat(getConfiguredTarget("//java/hello:hello")).isNull();
    assertContainsEvent(
        "configurable attribute \"deps\" in //java/hello:hello doesn't match this configuration");
  }

  @Test
  public void noMatchCustomErrorMessage() throws Exception {
    writeConfigRules();
    scratch.file("java/hello/BUILD",
        "java_binary(",
        "    name = 'hello_default_no_match_error',",
        "    srcs = select({",
        "        '//conditions:a': ['not_chosen.java'],",
        "    }))",
        "java_binary(",
        "    name = 'hello_custom_no_match_error',",
        "    srcs = select({",
        "        '//conditions:a': ['not_chosen.java'],",
        "    },",
        "    no_match_error = 'You always have to choose condition a!'",
        "))");

    reporter.removeHandler(failFastHandler);
    AnalysisFailureRecorder analysisFailureRecorder = new AnalysisFailureRecorder();
    eventBus.register(analysisFailureRecorder);
    useConfiguration("");

    assertThat(getConfiguredTarget("//java/hello:hello_default_no_match_error")).isNull();
    assertContainsEvent(
        "configurable attribute \"srcs\" in //java/hello:hello_default_no_match_error doesn't"
            + " match this configuration. Would a default condition help?\n"
            + "\n"
            + "Conditions checked:");
    // Verify a Root Cause is reported when a target cannot be configured due to no matching config.
    assertThat(analysisFailureRecorder.causes).hasSize(1);
    AnalysisRootCauseEvent rootCause = analysisFailureRecorder.causes.get(0);
    assertThat(rootCause.getLabel())
        .isEqualTo(Label.parseCanonical("//java/hello:hello_default_no_match_error"));

    eventBus.unregister(analysisFailureRecorder);
    analysisFailureRecorder = new AnalysisFailureRecorder();
    eventBus.register(analysisFailureRecorder);
    eventCollector.clear();

    assertThat(getConfiguredTarget("//java/hello:hello_custom_no_match_error")).isNull();
    assertContainsEvent(
        "configurable attribute \"srcs\" in //java/hello:hello_custom_no_match_error doesn't match "
            + "this configuration: You always have to choose condition a!");
    // Verify a Root Cause is reported when a target cannot be configured due to no matching config.
    assertThat(analysisFailureRecorder.causes).hasSize(1);
    rootCause = analysisFailureRecorder.causes.get(0);
    assertThat(rootCause.getLabel())
        .isEqualTo(Label.parseCanonical("//java/hello:hello_custom_no_match_error"));
  }

  @Test
  public void nativeTypeConcatenatedWithSelect() throws Exception {
    writeConfigRules();
    scratch.file(
        "java/foo/rule.bzl",
        "def _rule_impl(ctx):",
        "    return []",
        "myrule = rule(",
        "    implementation = _rule_impl,",
        "    attrs = {",
        "        'deps': attr.label_keyed_string_dict()",
        "    },",
        ")");
    scratch.file(
        "java/foo/BUILD",
        "load(':rule.bzl', 'myrule')",
        "myrule(",
        "    name = 'mytarget',",
        "    deps = {':always': 'always'} | select({",
        "        '//conditions:a': {':a': 'a'},",
        "        '//conditions:b': {':b': 'b'},",
        "    })",
        ")",
        "java_binary(",
        "    name = 'binary',",
        "    srcs = ['binary.java'],",
        "    deps = [':always'] + select({",
        "        '//conditions:a': [':a'],",
        "        '//conditions:b': [':b'],",
        "    })",
        ")",
        "java_library(",
        "    name = 'always',",
        "    srcs = ['always.java'])",
        "java_library(",
        "    name = 'a',",
        "    srcs = ['a.java'])",
        "java_library(",
        "    name = 'b',",
        "    srcs = ['b.java'])");

    checkRule(
        "//java/foo:binary",
        "--foo=b",
        /*expected:*/ ImmutableList.of("bin java/foo/libalways.jar", "bin java/foo/libb.jar"),
        /*not expected:*/ ImmutableList.of("bin java/foo/liba.jar"));

    checkRule(
        "//java/foo:mytarget",
        "--foo=b",
        /*expected:*/ ImmutableList.of("bin java/foo/libalways.jar", "bin java/foo/libb.jar"),
        /*not expected:*/ ImmutableList.of("bin java/foo/liba.jar"));
  }

  @Test
  public void selectConcatenatedWithNativeType() throws Exception {
    writeConfigRules();
    scratch.file(
        "java/foo/rule.bzl",
        "def _rule_impl(ctx):",
        "    return []",
        "myrule = rule(",
        "    implementation = _rule_impl,",
        "    attrs = {",
        "        'deps': attr.label_keyed_string_dict()",
        "    },",
        ")");
    scratch.file(
        "java/foo/BUILD",
        "load(':rule.bzl', 'myrule')",
        "myrule(",
        "    name = 'mytarget',",
        "    deps = select({",
        "        '//conditions:a': {':a': 'a'},",
        "        '//conditions:b': {':b': 'b'},",
        "    }) | {':always': 'always'}",
        ")",
        "java_binary(",
        "    name = 'binary',",
        "    srcs = ['binary.java'],",
        "    deps = select({",
        "        '//conditions:a': [':a'],",
        "        '//conditions:b': [':b'],",
        "    }) + [':always'])",
        "java_library(",
        "    name = 'always',",
        "    srcs = ['always.java'])",
        "java_library(",
        "    name = 'a',",
        "    srcs = ['a.java'])",
        "java_library(",
        "    name = 'b',",
        "    srcs = ['b.java'])");

    checkRule(
        "//java/foo:binary",
        "--foo=b",
        /*expected:*/ ImmutableList.of("bin java/foo/libalways.jar", "bin java/foo/libb.jar"),
        /*not expected:*/ ImmutableList.of("bin java/foo/liba.jar"));

    checkRule(
        "//java/foo:mytarget",
        "--foo=b",
        /*expected:*/ ImmutableList.of("bin java/foo/libalways.jar", "bin java/foo/libb.jar"),
        /*not expected:*/ ImmutableList.of("bin java/foo/liba.jar"));
  }

  @Test
  public void selectConcatenatedWithSelect() throws Exception {
    writeConfigRules();
    scratch.file(
        "java/foo/rule.bzl",
        "def _rule_impl(ctx):",
        "    return []",
        "myrule = rule(",
        "    implementation = _rule_impl,",
        "    attrs = {",
        "        'deps': attr.label_keyed_string_dict()",
        "    },",
        ")");
    scratch.file(
        "java/foo/BUILD",
        "load(':rule.bzl', 'myrule')",
        "myrule(",
        "    name = 'mytarget',",
        "    deps = select({",
        "        '//conditions:a': {':a': 'a'},",
        "        '//conditions:b': {':b': 'b'},",
        "    }) | select({",
        "        '//conditions:a': {':a2': 'a2'},",
        "        '//conditions:b': {':b2': 'b2'},",
        "    })",
        ")",
        "java_binary(",
        "    name = 'binary',",
        "    srcs = ['binary.java'],",
        "    deps = select({",
        "        '//conditions:a': [':a'],",
        "        '//conditions:b': [':b'],",
        "    }) + select({",
        "        '//conditions:a': [':a2'],",
        "        '//conditions:b': [':b2'],",
        "    })",
        ")",
        "java_library(",
        "    name = 'a',",
        "    srcs = ['a.java'])",
        "java_library(",
        "    name = 'b',",
        "    srcs = ['b.java'])",
        "java_library(",
        "    name = 'a2',",
        "    srcs = ['a2.java'])",
        "java_library(",
        "    name = 'b2',",
        "    srcs = ['b2.java'])");

    checkRule(
        "//java/foo:binary",
        "--foo=b",
        /*expected:*/ ImmutableList.of("bin java/foo/libb.jar", "bin java/foo/libb2.jar"),
        /*not expected:*/ ImmutableList.of("bin java/foo/liba.jar", "bin java/foo/liba2.jar"));

    checkRule(
        "//java/foo:mytarget",
        "--foo=b",
        /*expected:*/ ImmutableList.of("bin java/foo/libb.jar", "bin java/foo/libb2.jar"),
        /*not expected:*/ ImmutableList.of("bin java/foo/liba.jar", "bin java/foo/liba2.jar"));
  }

  @Test
  public void dictsWithSameKey() throws Exception {
    writeConfigRules();
    scratch.file(
        "java/foo/rule.bzl",
        "def _rule_impl(ctx):",
        "    outputs = []",
        "    for target, value in ctx.attr.deps.items():",
        "        output = ctx.actions.declare_file(target.label.name + value)",
        "        ctx.actions.write(content = value, output = output)",
        "        outputs.append(output)",
        "    return [DefaultInfo(files=depset(outputs))]",
        "myrule = rule(",
        "    implementation = _rule_impl,",
        "    attrs = {",
        "        'deps': attr.label_keyed_string_dict()",
        "    },",
        ")");
    scratch.file(
        "java/foo/BUILD",
        "load(':rule.bzl', 'myrule')",
        "myrule(",
        "    name = 'mytarget',",
        "    deps = select({",
        "        '//conditions:a': {':a': 'a'},",
        "    }) | select({",
        "        '//conditions:a': {':a': 'a2'},",
        "    })",
        ")",
        "java_library(",
        "    name = 'a',",
        "    srcs = ['a.java']",
        ")",
        "filegroup(",
        "    name = 'group',",
        "    srcs = [':mytarget'],",
        ")");

    checkRule(
        "//java/foo:group",
        "srcs",
        ImmutableList.of("--foo=a"),
        /*expected:*/ ImmutableList.of("bin java/foo/aa2"),
        /*not expected:*/ ImmutableList.of("bin java/foo/aa"));
  }

  @Test
  public void selectConcatenatedWithNonSupportingType() throws Exception {
    writeConfigRules();
    scratch.file(
        "foo/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = 0 + select({",
        "        '//conditions:a': 0,",
        "        '//conditions:b': 1,",
        "    }))");

    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//foo:binary")).isNull();
    assertContainsEvent("type 'boolean' doesn't support select concatenation");
  }

  @Test
  public void concatenationWithDifferentTypes() throws Exception {
    writeConfigRules();
    scratch.file("java/foo/BUILD",
        "java_binary(",
        "    name = 'binary',",
        "    srcs = select({",
        "        '//conditions:a': ['a.java'],",
        "        '//conditions:b': ['b.java'],",
        "    }) + 'always.java'",
        ")");

    reporter.removeHandler(failFastHandler);
    assertThrows(NoSuchTargetException.class, () -> getTarget("//java/foo:binary"));
    assertContainsEvent("Cannot combine incompatible types");
  }

  @Test
  public void selectsWithGlobs() throws Exception {
    writeConfigRules();
    scratch.file("java/foo/globbed/ceecee.java");
    scratch.file("java/foo/BUILD",
        "java_binary(",
        "    name = 'binary',",
        "    srcs = glob(['globbed/*.java']) + select({",
        "        '//conditions:a': ['a.java'],",
        "        '//conditions:b': ['b.java'],",
        "    }))");

    useConfiguration("--foo=b");
    ConfiguredTarget binary = getConfiguredTarget("//java/foo:binary");
    assertThat(binary).isNotNull();
    Set<String> sources = artifactsToStrings(getPrerequisiteArtifacts(binary, "srcs"));
    assertThat(sources).contains("src java/foo/b.java");
    assertThat(sources).contains("src java/foo/globbed/ceecee.java");
    assertThat(sources).doesNotContain("src java/foo/a.java");
  }

  @Test
  public void selectsWithGlobsWrongType() throws Exception {
    writeConfigRules();
    scratch.file(
        "foo/BUILD",
        "genrule(",
        "    name = 'gen',",
        "    srcs = [],",
        "    outs = ['gen.out'],",
        "    cmd = 'echo' + select({",
        "        '//conditions:a': 'a',",
        "        '//conditions:b': 'b',",
        "    }) + glob(['globbed.java'], allow_empty = True))");

    reporter.removeHandler(failFastHandler);
    assertThrows(NoSuchTargetException.class, () -> getTarget("//foo:binary"));
    assertContainsEvent("Cannot combine incompatible types");
  }

  @Test
  public void globsInSelect() throws Exception {
    writeConfigRules();
    scratch.file("java/foo/globbed/ceecee.java");
    scratch.file("java/foo/BUILD",
        "java_binary(",
        "    name = 'binary',",
        "    srcs = ['binary.java'] + select({",
        "        '//conditions:a': glob(['globbed/*.java']),",
        "        '//conditions:b': ['b.java'],",
        "    }))");

    useConfiguration("--foo=a");
    ConfiguredTarget binary = getConfiguredTarget("//java/foo:binary");
    assertThat(binary).isNotNull();
    Set<String> sources = artifactsToStrings(getPrerequisiteArtifacts(binary, "srcs"));
    assertThat(sources).contains("src java/foo/binary.java");
    assertThat(sources).contains("src java/foo/globbed/ceecee.java");
    assertThat(sources).doesNotContain("src java/foo/b.java");
  }

  @Test
  public void selectAcceptedInAttributeWithAllowedValues() throws Exception {
    scratch.file("foo/BUILD",
        "rule_with_allowed_values(",
        "    name = 'rule',",
        "    one_two = select({",
        "        '//conditions:default': 'one',",
        "    }))");
    assertThat(getConfiguredTarget("//foo:rule")).isNotNull();
  }

  @Test
  public void selectWithNonAllowedValueCausesError() throws Exception {
    scratch.file("foo/BUILD",
        "rule_with_allowed_values(",
        "    name = 'rule',",
        "    one_two = select({",
        "        '//conditions:default': 'TOTALLY_ILLEGAL_VALUE',",
        "    }))");
    reporter.removeHandler(failFastHandler); // Expect errors.
    getConfiguredTarget("//foo:rule");
    assertContainsEvent(
        "invalid value in 'one_two' attribute: "
        + "has to be one of 'one' or 'two' instead of 'TOTALLY_ILLEGAL_VALUE'");
  }

  @Test
  public void selectWithMultipleNonAllowedValuesCausesMultipleErrors() throws Exception {
    scratch.file("foo/BUILD",
        "rule_with_allowed_values(",
        "    name = 'rule',",
        "    one_two = select({",
        "        '//conditions:a': 'TOTALLY_ILLEGAL_VALUE',",
        "        '//conditions:default': 'DIFFERENT_BUT_STILL_ILLEGAL',",
        "    }))");
    reporter.removeHandler(failFastHandler); // Expect errors.
    getConfiguredTarget("//foo:rule");
    assertContainsEvent(
        "invalid value in 'one_two' attribute: "
        + "has to be one of 'one' or 'two' instead of 'TOTALLY_ILLEGAL_VALUE'");
    assertContainsEvent(
        "invalid value in 'one_two' attribute: "
        + "has to be one of 'one' or 'two' instead of 'DIFFERENT_BUT_STILL_ILLEGAL'");
  }

  @Test
  public void selectConcatenationWithAllowedValues() throws Exception {
    scratch.file("foo/BUILD",
           "rule_with_allowed_values(",
           "    name = 'rule',",
           "    one_two = 'on' + select({",
           "        '//conditions:default': 'e',",
           "    }))");
    assertThat(getConfiguredTarget("//foo:rule")).isNotNull();
  }

  @Test
  public void selectConcatenationWithNonAllowedValues() throws Exception {
    scratch.file("foo/BUILD",
           "rule_with_allowed_values(",
           "    name = 'rule',",
           "    one_two = 'on' + select({",
           "        '//conditions:default': 'o',",
           "    }))");
    reporter.removeHandler(failFastHandler); // Expect errors.
    getConfiguredTarget("//foo:binary");
    assertContainsEvent(
        "invalid value in 'one_two' attribute: "
        + "has to be one of 'one' or 'two' instead of 'ono'");
  }

  @Test
  public void computedDefaultAttributesCanReferenceConfigurableAttributes() throws Exception {
    scratch.file(
        "test/selector_rules.bzl",
        "def _impl(ctx):",
        "  ctx.actions.write(",
        "      output=ctx.outputs.out_file,",
        "      content=ctx.attr.string_value,",
        "  )",
        "  return []",
        "",
        "def _derived_value(string_value):",
        "  return Label(\"//test:%s\" % string_value)",
        "",
        "selector_rule = rule(",
        "  attrs = {",
        "      \"string_value\": attr.string(default = \"\"),",
        "      \"out_file\": attr.output(),",
        "      \"_derived\": attr.label(default = _derived_value),",
        "  },",
        "implementation = _impl,",
        ")");
    scratch.file("test/BUILD",
        "genrule(name = \"foo\", srcs = [], outs = [\"foo.out\"], cmd = \"\")");
    scratch.file(
        "foo/BUILD",
        "load('//test:selector_rules.bzl', \"selector_rule\")",
        "selector_rule(",
        "    name = \"rule\",",
        "    out_file = \"rule.out\",",
        "    string_value = select({\"//conditions:default\": \"foo\"}),",
        ")");
    getConfiguredTarget("//foo:rule");
    assertNoEvents();
  }

  @Test
  public void selectableDefaultValueWithTypeDefault() throws Exception {
    writeConfigRules();
    scratch.file("srctest/BUILD",
        "genrule(",
        "    name = 'gen',",
        "    cmd = '',",
        "    outs = ['gen.out'],",
        "    srcs = select({",
        "        '//conditions:a': None,",
        "    }))");

    useConfiguration("--foo=a");
    ConfiguredTargetAndData ctad = getConfiguredTargetAndData("//srctest:gen");
    AttributeMap attributes = getMapperFromConfiguredTargetAndTarget(ctad);
    assertThat(attributes.get("srcs", LABEL_LIST)).isEmpty();
  }

  @Test
  public void selectableDefaultValueWithRuleDefault() throws Exception {
    writeConfigRules();
    scratch.file("foo/BUILD",
        "rule_with_label_default(",
        "    name = 'rule',",
        "    dep = select({",
        "        '//conditions:a': None,",
        "    }))",
        "rule_with_boolean_attr(",
        "    name = 'default',",
        "    boolean_attr = 1)");

    useConfiguration("--foo=a");
    ConfiguredTargetAndData ctad = getConfiguredTargetAndData("//foo:rule");
    AttributeMap attributes = getMapperFromConfiguredTargetAndTarget(ctad);
    assertThat(attributes.get("dep", BuildType.LABEL))
        .isEqualTo(Label.parseCanonical("//foo:default"));
  }

  @Test
  public void noneValuesWithMultipleSelectsMixedValues() throws Exception {
    writeConfigRules();
    scratch.file("a/BUILD",
        "genrule(",
        "    name = 'gen',",
        "    srcs = [],",
        "    outs = ['out'],",
        "    cmd = '',",
        "    message = select({",
        "        '//conditions:a': 'defined message 1',",
        "        '//conditions:b': None,",
        "    }) + select({",
        "        '//conditions:a': None,",
        "        '//conditions:b': 'defined message 2',",
        "    }),",
        ")");

    reporter.removeHandler(failFastHandler);
    useConfiguration("--define", "mode=a");
    assertThat(getConfiguredTarget("//a:gen")).isNull();
    assertContainsEvent("Cannot combine incompatible types (select of string, select of NoneType)");
  }

  @Test
  public void emptySelectCannotBeConcatenated() throws Exception {
    scratch.file("a/BUILD",
        "genrule(",
        "    name = 'gen',",
        "    srcs = [],",
        "    outs = ['out'],",
        "    cmd = select({}) + ' always include'",
        ")");

    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:gen")).isNull();
    assertContainsEvent(
        "select({}) with an empty dictionary can never resolve because it includes no conditions "
            + "to match");
  }

  @Test
  public void selectOnConstraints() throws Exception {
    // create some useful constraints and platforms.
    scratch.file(
        "conditions/BUILD",
        "constraint_setting(name = 'fruit')",
        "constraint_value(name = 'apple', constraint_setting = 'fruit')",
        "constraint_value(name = 'banana', constraint_setting = 'fruit')",
        "platform(",
        "    name = 'apple_platform',",
        "    constraint_values = [':apple'],",
        ")",
        "platform(",
        "    name = 'banana_platform',",
        "    constraint_values = [':banana'],",
        ")",
        "config_setting(",
        "    name = 'a',",
        "    constraint_values = [':apple']",
        ")",
        "config_setting(",
        "    name = 'b',",
        "    constraint_values = [':banana']",
        ")");
    scratch.file(
        "check/BUILD",
        "filegroup(name = 'adep', srcs = ['afile'])",
        "filegroup(name = 'bdep', srcs = ['bfile'])",
        "filegroup(name = 'defaultdep', srcs = ['defaultfile'])",
        "filegroup(name = 'hello',",
        "    srcs = select({",
        "        '//conditions:a': [':adep'],",
        "        '//conditions:b': [':bdep'],",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': [':defaultdep'],",
        "    }))");
    checkRule(
        "//check:hello",
        "srcs",
        ImmutableList.of("--experimental_platforms=//conditions:apple_platform"),
        /*expected:*/ ImmutableList.of("src check/afile"),
        /*not expected:*/ ImmutableList.of("src check/bfile", "src check/defaultfile"));
  }

  @Test
  public void selectDirectlyOnConstraints() throws Exception {
    // Tests select()ing directly on a constraint_value (with no intermediate config_setting).
    scratch.file(
        "conditions/BUILD",
        "constraint_setting(name = 'fruit')",
        "constraint_value(name = 'apple', constraint_setting = 'fruit')",
        "constraint_value(name = 'banana', constraint_setting = 'fruit')",
        "platform(",
        "    name = 'apple_platform',",
        "    constraint_values = [':apple'],",
        ")",
        "platform(",
        "    name = 'banana_platform',",
        "    constraint_values = [':banana'],",
        ")");
    scratch.file(
        "check/defs.bzl",
        "def _impl(ctx):",
        "  pass",
        "simple_rule = rule(",
        "  implementation = _impl,",
        "  attrs = {'srcs': attr.label_list(allow_files = True)}",
        ")");
    scratch.file(
        "check/BUILD",
        "load('//check:defs.bzl', 'simple_rule')",
        "filegroup(name = 'adep', srcs = ['afile'])",
        "filegroup(name = 'bdep', srcs = ['bfile'])",
        "simple_rule(name = 'hello',",
        "    srcs = select({",
        "        '//conditions:apple': [':adep'],",
        "        '//conditions:banana': [':bdep'],",
        "    }))");
    checkRule(
        "//check:hello",
        "srcs",
        ImmutableList.of("--platforms=//conditions:apple_platform"),
        /*expected:*/ ImmutableList.of("src check/afile"),
        /*not expected:*/ ImmutableList.of("src check/bfile", "src check/defaultfile"));
    checkRule(
        "//check:hello",
        "srcs",
        ImmutableList.of("--platforms=//conditions:banana_platform"),
        /*expected:*/ ImmutableList.of("src check/bfile"),
        /*not expected:*/ ImmutableList.of("src check/afile", "src check/defaultfile"));
  }

  @Test
  public void nonToolchainResolvingTargetsCantSelectDirectlyOnConstraints() throws Exception {
    // Tests select()ing directly on a constraint_value (with no intermediate config_setting).
    scratch.file(
        "conditions/BUILD",
        "constraint_setting(name = 'fruit')",
        "constraint_value(name = 'apple', constraint_setting = 'fruit')",
        "platform(",
        "    name = 'apple_platform',",
        "    constraint_values = [':apple'],",
        ")");
    scratch.file(
        "check/BUILD",
        "filegroup(name = 'adep', srcs = ['afile'])",
        "rule_with_no_platform(name = 'hello',",
        "    deps = select({",
        "        '//conditions:apple': [':adep'],",
        "    })",
        ")");
    reporter.removeHandler(failFastHandler);
    useConfiguration("--platforms=//conditions:apple_platform");
    assertThat(getConfiguredTarget("//check:hello")).isNull();
    assertContainsEvent("//conditions:apple is not a valid select() condition for //check:hello");
  }

  @Test
  public void selectOnlyToolchainResolvingTargetsCanSelectDirectlyOnConstraints() throws Exception {
    // Tests select()ing directly on a constraint_value when the rule uses toolchain resolution
    // *only if it has a select()*. As of this test, alias() is the only rule that supports that
    // (see Alias#useToolchainResolution(ToolchainResolutionMode.HAS_SELECT).
    scratch.file(
        "conditions/BUILD",
        "constraint_setting(name = 'fruit')",
        "constraint_value(name = 'apple', constraint_setting = 'fruit')",
        "constraint_value(name = 'banana', constraint_setting = 'fruit')",
        "platform(",
        "    name = 'apple_platform',",
        "    constraint_values = [':apple'],",
        ")");
    scratch.file(
        "check/defs.bzl",
        "def _impl(ctx):",
        "  pass",
        "simple_rule = rule(",
        "  implementation = _impl,",
        "  attrs = {}",
        ")");
    scratch.file(
        "check/BUILD",
        "load('//check:defs.bzl', 'simple_rule')",
        "filegroup(name = 'bdep', srcs = ['bfile'])",
        "simple_rule(name = 'hello')",
        "simple_rule(name = 'tere')",
        "alias(",
        "    name = 'selectable_alias',",
        "    actual = select({",
        "        '//conditions:apple': ':hello',",
        "        '//conditions:banana': ':tere',",
        "    }))");
    useConfiguration("--platforms=//conditions:apple_platform");
    assertThat(
            getConfiguredTarget("//check:selectable_alias")
                .getActual()
                .getLabel()
                .getCanonicalForm())
        .isEqualTo("//check:hello");
  }

  @Test
  public void multipleMatchErrorWhenAliasResolvesToSameSetting() throws Exception {
    scratch.file(
        "a/BUILD",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' })",
        "alias(",
        "    name = 'alias_to_foo',",
        "    actual = ':foo')",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        ':foo': 0,",
        "        'alias_to_foo': 1,",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNull();
    assertContainsEvent(
        "configurable attribute \"boolean_attr\" in //a:binary doesn't match this configuration. "
            + "Would a default condition help?\n\n"
            + "Conditions checked:\n"
            + " //a:foo\n"
            + " //a:alias_to_foo");
  }

  @Test
  public void defaultVisibilityConfigSetting_noVisibilityEnforcement() throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions("--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=false");
    scratch.file("c/BUILD", "config_setting(name = 'foo', define_values = { 'foo': '1' })");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNotNull();
    assertNoEvents();
  }

  @Test
  public void privateVisibilityConfigSetting_noVisibilityEnforcement() throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions("--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=false");
    scratch.file(
        "c/BUILD",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        "    visibility = ['//visibility:private']",
        ")");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNotNull();
    assertNoEvents();
  }

  @Test
  public void publicVisibilityConfigSetting__noVisibilityEnforcement() throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions("--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=false");
    scratch.file(
        "c/BUILD",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        "    visibility = ['//visibility:public']",
        ")");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNotNull();
    assertNoEvents();
  }
  @Test
  public void defaultVisibilityConfigSetting_defaultIsPublic() throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions("--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=false");
    scratch.file("c/BUILD", "config_setting(name = 'foo', define_values = { 'foo': '1' })");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNotNull();
    assertNoEvents();
  }

  @Test
  public void privateVisibilityConfigSetting_defaultIsPublic() throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions("--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=false");
    scratch.file(
        "c/BUILD",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        "    visibility = ['//visibility:private']",
        ")");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNull();
    assertContainsEvent("'//c:foo' is not visible from target '//a:binary'");
  }

  @Test
  public void publicVisibilityConfigSetting_defaultIsPublic() throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions("--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=false");
    scratch.file(
        "c/BUILD",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        "    visibility = ['//visibility:public']",
        ")");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNotNull();
    assertNoEvents();
  }

  @Test
  public void defaultPublicVisibility_aliasVisibilityIgnored_aliasVisibilityIsDefault()
      throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions(
        "--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=false");
    scratch.file(
        "c/BUILD",
        "alias(",
        "    name = 'foo_alias',",
        "    actual = ':foo')",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        ")");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo_alias': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNotNull();
    assertNoEvents();
  }

  @Test
  public void defaultPublicVisibility_aliasVisibilityIgnored_aliasVisibilityIsExplicit()
      throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions(
        "--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=false");
    scratch.file(
        "c/BUILD",
        "alias(",
        "    name = 'foo_alias',",
        "    actual = ':foo',",
        // Current flag combo skips this and directly checks the config_setting's visibility.
        "    visibility = ['//visibility:private']",
        ")",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        ")");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo_alias': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNotNull();
    assertNoEvents();
  }

  @Test
  public void defaultPublicVisibility_aliasVisibilityIgnored_configSettingVisibilityIsExplicit()
      throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions(
        "--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=false");
    scratch.file(
        "c/BUILD",
        "alias(",
        "    name = 'foo_alias',",
        "    actual = ':foo',",
        // Current flag combo skips this and directly checks the config_setting's visibility.
        "    visibility = ['//visibility:public']",
        ")",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        "    visibility = ['//visibility:private']",
        ")");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo_alias': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNull();
    assertContainsEvent("'//c:foo' is not visible from target '//a:binary'");
  }

  @Test
  public void defaultPublicVisibility_trimmedConfigsDontCrash() throws Exception {
    // When enforcing config_setting visibility with
    // --incompatible_config_setting_private_default_visibility=false, the alias
    // ConfiguredTargetAndData clones itself with the ConfiguredTargetAndData of the config_setting
    // it refers to. ConfiguredTargetAndData.fromConfiguredTarget has a safety check that both
    // configs are the same. When the target with a select() is a test and --trim_test_configuration
    // is on, the alias takes the parent's config (with TestOptions) but the config_setting has it
    // stripped. This is a regression test that Blaze doesn't crash expecting those configs to be
    // equal.
    setPackageOptions(
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=false");
    useConfiguration("--trim_test_configuration=true");
    scratch.file(
        "c/defs.bzl",
        "def _impl(ctx):",
        "    output = ctx.outputs.out",
        "    ctx.actions.write(output = output, content = 'hi', is_executable = True)",
        "    return [DefaultInfo(executable = output)]",
        "",
        "fake_test = rule(",
        "    attrs = {",
        "        'msg': attr.string(),",
        "    },",
        "    test = True,",
        "    outputs = {'out': 'foo.out'},",
        "    implementation = _impl,",
        ")");
    scratch.file(
        "c/BUILD",
        "load(':defs.bzl', 'fake_test')",
        "alias(",
        "    name = 'foo_alias',",
        "    actual = ':foo',",
        ")",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        ")",
        "fake_test(",
        "    name = 'foo_test',",
        "    msg = select({",
        "        ':foo_alias': 'hi',",
        "        '//conditions:default': 'there'",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//c:foo_test")).isNotNull();
    assertNoEvents();
  }

  @Test
  public void defaultVisibilityConfigSetting_defaultIsPrivate() throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions("--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=true");
    scratch.file("c/BUILD", "config_setting(name = 'foo', define_values = { 'foo': '1' })");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNull();
    assertContainsEvent("'//c:foo' is not visible from target '//a:binary'");
  }

  @Test
  public void privateVisibilityConfigSetting_defaultIsPrivate() throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions("--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=true");
    scratch.file(
        "c/BUILD",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        "    visibility = ['//visibility:private']",
        ")");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNull();
    assertContainsEvent("'//c:foo' is not visible from target '//a:binary'");
  }

  @Test
  public void publicVisibilityConfigSetting_defaultIsPrivate() throws Exception {
    // Production builds default to private visibility, but BuildViewTestCase defaults to public.
    setPackageOptions("--default_visibility=private",
        "--incompatible_enforce_config_setting_visibility=true",
        "--incompatible_config_setting_private_default_visibility=true");
    scratch.file(
        "c/BUILD",
        "config_setting(",
        "    name = 'foo',",
        "    define_values = { 'foo': '1' },",
        "    visibility = ['//visibility:public']",
        ")");
    scratch.file(
        "a/BUILD",
        "rule_with_boolean_attr(",
        "    name = 'binary',",
        "    boolean_attr = select({",
        "        '//c:foo': 0,",
        "        '//conditions:default': 1",
        "    }))");
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//a:binary")).isNotNull();
    assertNoEvents();
  }

  @Test
  public void selectWithLabelKeysInMacro() throws Exception {
    writeConfigRules();
    scratch.file("java/BUILD");
    scratch.file(
        "java/macros.bzl",
        "def my_java_binary(name, deps = [], **kwargs):",
        "    native.java_binary(",
        "        name = name,",
        "        deps = select({",
        "            Label('//conditions:a'): [Label('//java/foo:a')],",
        "            '//conditions:b': [Label('//java/foo:b')],",
        "        }) + select({",
        "            '//conditions:a': [Label('//java/foo:a2')],",
        "            Label('//conditions:b'): [Label('//java/foo:b2')],",
        "        }),",
        "        **kwargs,",
        "    )");
    scratch.file(
        "java/foo/BUILD",
        "load('//java:macros.bzl', 'my_java_binary')",
        "my_java_binary(",
        "    name = 'binary',",
        "    srcs = ['binary.java'],",
        ")",
        "java_library(",
        "    name = 'a',",
        "    srcs = ['a.java'])",
        "java_library(",
        "    name = 'b',",
        "    srcs = ['b.java'])",
        "java_library(",
        "    name = 'a2',",
        "    srcs = ['a2.java'])",
        "java_library(",
        "    name = 'b2',",
        "    srcs = ['b2.java'])");

    checkRule(
        "//java/foo:binary",
        "--foo=b",
        /*expected:*/ ImmutableList.of("bin java/foo/libb.jar", "bin java/foo/libb2.jar"),
        /*not expected:*/ ImmutableList.of("bin java/foo/liba.jar", "bin java/foo/liba2.jar"));
  }
}
