// 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 org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment; import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment.TargetProviderEnvironment; import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException; import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; import com.google.devtools.build.lib.analysis.util.MockRule; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.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.skyframe.ConfiguredTargetAndData; import com.google.devtools.build.lib.syntax.Type; 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.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 = {'test_arg': 'a'})", "config_setting(", " name = 'b',", " values = {'test_arg': '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 options, Iterable expected, Iterable notExpected) throws Exception { useConfiguration(options.toArray(new String[options.size()])); ConfiguredTarget binary = getConfiguredTarget(ruleLabel); assertThat(binary).isNotNull(); Set 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 expected, Iterable notExpected) throws Exception { checkRule(ruleLabel, ImmutableList.of(option), expected, notExpected); } private void checkRule( String ruleLabel, Collection options, Iterable expected, Iterable 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))); @Override protected ConfiguredRuleClassProvider getRuleClassProvider() { 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); TestRuleClassProvider.addStandardRules(builder); return builder.build(); } @Test public void basicConfigurability() throws Exception { writeHelloRules(/*includeDefaultCondition=*/true); writeConfigRules(); checkRule("//java/hello:hello", "--test_arg=a", /*expected:*/ ImmutableList.of(ADEP_INPUT), /*not expected:*/ ImmutableList.of(BDEP_INPUT, DEFAULTDEP_INPUT)); checkRule("//java/hello:hello", "--test_arg=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", "--test_arg=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", "--test_arg=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("--test_arg=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:c': ['c.java'],", " '//conditions:d': ['a.java'],", " }))"); reporter.removeHandler(failFastHandler); // Expect errors. useConfiguration("--test_arg=a"); getConfiguredTarget("//java/hello:hello"); assertContainsEvent( "Label '//java/hello:a.java' is duplicated in the 'srcs' attribute of rule 'hello'"); } /** * 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("--test_arg=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("--test_arg=a"); getConfiguredTarget("//java/hello:hello"); assertContainsEvent( "Label '//java/hello:a.java' is duplicated in the 'srcs' attribute of rule 'hello'"); } /** * Tests that {@link RedirectChaser} doesn't support configured attribute instances, and * triggers an appropriate error upon finding them. */ @Test public void redirectChaser() throws Exception { writeConfigRules(); useConfiguration("--test_arg=a"); scratch.file("java/hello/BUILD", "alias(", "name = 'good_base',", "actual = ':good_redirect')", "alias(", "name = 'good_redirect',", "actual = ':actual_content')", "filegroup(", "name = 'actual_content',", "srcs = ['a.txt', 'b.txt'])", "alias(", "name = 'bad_base',", "actual = ':bad_redirect')", "alias(", "name = 'bad_redirect',", "actual = select({", " '//conditions:a': ':actual_content',", " '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': ':actual_content',", " }))", "genrule(", " name = 'non_filegroup_target',", " srcs = [ 'whatever' ],", " outs = [ 'whateverelse' ],", " cmd = 'true')", "alias(", " name = 'base_non_filegroup_target',", " actual = ':non_filegroup_target')" ); ConfigurationEnvironment env = new TargetProviderEnvironment(getSkyframeExecutor().getPackageManager(), reporter); // Legal case: assertThat( RedirectChaser.followRedirects( env, Label.parseAbsolute("//java/hello:good_base", ImmutableMap.of()), "srcs") .toString()) .isEqualTo("//java/hello:actual_content"); // Legal case: assertThat( RedirectChaser.followRedirects( env, Label.parseAbsolute( "//java/hello:base_non_filegroup_target", ImmutableMap.of()), "srcs") .toString()) .isEqualTo("//java/hello:non_filegroup_target"); // Illegal case: try { RedirectChaser.followRedirects( env, Label.parseAbsolute("//java/hello:bad_base", ImmutableMap.of()), "srcs"); fail("Expected RedirectChaser to fail on a sequence with configurable 'srcs' values"); } catch (InvalidConfigurationException e) { // Expected failure.. assertThat(e) .hasMessageThat() .isEqualTo("The value of 'actual' cannot be configuration-dependent"); } } /** * 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("--test_arg=a"); ConfiguredTargetAndData binary = getConfiguredTargetAndData("//test:the_rule"); AttributeMap attributes = getMapperFromConfiguredTargetAndTarget(binary); assertThat(attributes.get("$computed_attr", Type.STRING)).isEqualTo("a2"); // configuration b: useConfiguration("--test_arg=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", "int_key", "java_library(", " name = 'int_key',", " srcs = select({123: ['a.java']})", ")"); assertTargetError("//java/foo:int_key", "Invalid key: 123. select keys must be label references"); } @Test public void configKeyTypeChecking_Bool() throws Exception { reporter.removeHandler(failFastHandler); // Expect errors. scratch.file("java/foo/BUILD", "bool_key", "java_library(", " name = 'bool_key',", " srcs = select({True: ['a.java']})", ")"); assertTargetError("//java/foo:bool_key", "Invalid key: true. select keys must be label references"); } @Test public void configKeyTypeChecking_None() throws Exception { reporter.removeHandler(failFastHandler); // Expect errors. scratch.file("java/foo/BUILD", "none_key", "java_library(", " name = 'none_key',", " srcs = select({None: ['a.java']})", ")"); assertTargetError("//java/foo:none_key", "Invalid key: None. select keys must be label references"); } @Test public void configKeyTypeChecking_Dict() throws Exception { reporter.removeHandler(failFastHandler); // Expect errors. // If we embed a {} literal directly into the select, it fails with a Skylark error before // we even get to select's type checking (since {} isn't a valid hashable type for the // dictionary Skylark passes to the select function's invoke method). We can get around that // by freezing the dict from an external .bzl file. scratch.file("java/foo/external_dict.bzl", "m = {}", "def external_dict():", " return m"); scratch.file("java/foo/BUILD", "dict_key", "load('//java/foo:external_dict.bzl', 'external_dict')", "java_library(", " name = 'dict_key',", " srcs = select({external_dict(): ['a.java']})", ")"); assertTargetError("//java/foo:dict_key", "Invalid key: {}. select keys must be label references"); } /** * 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 = {'test_arg': 'a'})"); writeHelloRules(/*includeDefaultCondition=*/true); getConfiguredTarget("//java/hello:hello"); assertContainsEvent("no such target '//conditions:b': target 'b' not declared in package"); } /** * 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 = {'test_arg': '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 configuration key 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 configuration key 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 = {", " 'test_arg': 'a',", " 'compilation_mode': 'dbg'", " })", "config_setting(", " name = 'b',", " values = {'test_arg': 'b'})"); checkRule("//java/hello:hello", "--test_arg=a", /*expected:*/ ImmutableList.of(DEFAULTDEP_INPUT), /*not expected:*/ ImmutableList.of(ADEP_INPUT, BDEP_INPUT)); checkRule("//java/hello:hello", ImmutableList.of("--test_arg=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("--test_arg=a"); checkRule("//java/hello:hello", "--test_arg=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 = {'test_arg': 'c'})", "config_setting(", " name = 'b',", " values = {'test_arg': 'b'})"); // Iteration 2: same exact analysis should now apply the default condition. invalidatePackages(); checkRule("//java/hello:hello", "--test_arg=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."); } /** * 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', 'test_arg': '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", "--test_arg", "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 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 = {'test_arg': 'baz'})", "config_setting(", " name = 'b_a_c',", // Named to come alphabetically after a and b but before c. " values = {'define': 'a=1', 'test_arg': '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", "--test_arg", "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", "--test_arg=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\" 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); useConfiguration(""); assertThat(getConfiguredTarget("//java/hello:hello_default_no_match_error")).isNull(); String commonPrefix = "Configurable attribute \"srcs\" doesn't match this configuration"; assertContainsEvent(commonPrefix + " (would a default condition help?).\nConditions checked:"); eventCollector.clear(); assertThat(getConfiguredTarget("//java/hello:hello_custom_no_match_error")).isNull(); assertContainsEvent(commonPrefix + ": You always have to choose condition a!"); } @Test public void nativeTypeConcatenatedWithSelect() throws Exception { writeConfigRules(); scratch.file("java/foo/BUILD", "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", "--test_arg=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/BUILD", "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", "--test_arg=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/BUILD", "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", "--test_arg=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 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); try { getTarget("//java/foo:binary"); fail(); } catch (NoSuchTargetException e) { assertContainsEvent("'+' operator applied to 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("--test_arg=b"); ConfiguredTarget binary = getConfiguredTarget("//java/foo:binary"); assertThat(binary).isNotNull(); Set 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']))"); reporter.removeHandler(failFastHandler); try { getTarget("//foo:binary"); fail(); } catch (NoSuchTargetException e) { assertContainsEvent("'+' operator applied to 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("--test_arg=a"); ConfiguredTarget binary = getConfiguredTarget("//java/foo:binary"); assertThat(binary).isNotNull(); Set 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 struct()", "", "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("--test_arg=a"); ConfiguredTargetAndData ctad = getConfiguredTargetAndData("//srctest:gen"); AttributeMap attributes = getMapperFromConfiguredTargetAndTarget(ctad); assertThat(attributes.get("srcs", BuildType.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("--test_arg=a"); ConfiguredTargetAndData ctad = getConfiguredTargetAndData("//foo:rule"); AttributeMap attributes = getMapperFromConfiguredTargetAndTarget(ctad); assertThat(attributes.get("dep", BuildType.LABEL)) .isEqualTo(Label.parseAbsolute("//foo:default", ImmutableMap.of())); } @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( "'+' operator applied to 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( "'+' operator applied to incompatible types (select of unknown, string)"); } @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("afile", "acontents"); scratch.file("bfile", "bcontents"); scratch.file("defaultfile", "defaultcontents"); 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")); } }