| // Copyright 2015 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.devtools.build.lib.analysis.select; |
| |
| 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.Type.STRING; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.devtools.build.lib.analysis.BaseRuleClasses; |
| import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory; |
| import com.google.devtools.build.lib.analysis.RuleContext; |
| import com.google.devtools.build.lib.analysis.RuleDefinition; |
| import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.packages.AggregatingAttributeMapper; |
| import com.google.devtools.build.lib.packages.Attribute; |
| import com.google.devtools.build.lib.packages.AttributeMap; |
| import com.google.devtools.build.lib.packages.BuildType; |
| import com.google.devtools.build.lib.packages.Rule; |
| import com.google.devtools.build.lib.packages.RuleClass; |
| import com.google.devtools.build.lib.packages.Type; |
| import com.google.devtools.build.lib.testutil.TestRuleClassProvider; |
| import com.google.devtools.build.lib.testutil.UnknownRuleConfiguredTarget; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * Unit tests for {@link AggregatingAttributeMapper}. |
| */ |
| @RunWith(JUnit4.class) |
| public class AggregatingAttributeMapperTest extends AbstractAttributeMapperTest { |
| |
| @Before |
| public final void createMapper() throws Exception { |
| // Run AbstractAttributeMapper tests through an AggregatingAttributeMapper. |
| mapper = AggregatingAttributeMapper.of(rule); |
| } |
| |
| @Override |
| protected ConfiguredRuleClassProvider createRuleClassProvider() { |
| ConfiguredRuleClassProvider.Builder builder = |
| new ConfiguredRuleClassProvider.Builder() |
| .addRuleDefinition(new RuleWithDefaults()) |
| .addRuleDefinition(new RuleWithComputedDefaults()); |
| TestRuleClassProvider.addStandardRules(builder); |
| return builder.build(); |
| } |
| |
| /** |
| * Tests that {@link AggregatingAttributeMapper#visitAttribute} returns an |
| * attribute's sole value when declared directly (i.e. not as a configurable dict). |
| */ |
| @Test |
| public void testGetPossibleValuesDirectAttribute() throws Exception { |
| Rule rule = scratchRule("a", "myrule", |
| "sh_binary(name = 'myrule',", |
| " srcs = ['a.sh'])"); |
| assertThat(AggregatingAttributeMapper.of(rule).visitAttribute("srcs", BuildType.LABEL_LIST)) |
| .containsExactly(ImmutableList.of(label("//a:a.sh"))); |
| } |
| |
| /** |
| * Tests that {@link AggregatingAttributeMapper#visitAttribute} returns |
| * every possible value that a configurable attribute can resolve to. |
| */ |
| @Test |
| public void testGetPossibleValuesConfigurableAttribute() throws Exception { |
| Rule rule = scratchRule("a", "myrule", |
| "sh_binary(name = 'myrule',", |
| " srcs = select({", |
| " '//conditions:a': ['a.sh'],", |
| " '//conditions:b': ['b.sh'],", |
| " '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': ['default.sh'],", |
| " }))"); |
| assertThat(AggregatingAttributeMapper.of(rule).visitAttribute("srcs", BuildType.LABEL_LIST)) |
| .containsExactly( |
| ImmutableList.of(label("//a:a.sh")), |
| ImmutableList.of(label("//a:b.sh")), |
| ImmutableList.of(label("//a:default.sh"))); |
| } |
| |
| @Test |
| public void testGetPossibleValuesWithConcatenatedSelects() throws Exception { |
| Rule rule = scratchRule("a", "myrule", |
| "sh_binary(name = 'myrule',", |
| " srcs = select({", |
| " '//conditions:a1': ['a1.sh'],", |
| " '//conditions:b1': ['b1.sh']})", |
| " + select({", |
| " '//conditions:a2': ['a2.sh'],", |
| " '//conditions:b2': ['b2.sh']})", |
| " )"); |
| assertThat(AggregatingAttributeMapper.of(rule).visitAttribute("srcs", BuildType.LABEL_LIST)) |
| .containsExactly( |
| ImmutableList.of(label("//a:a1.sh"), label("//a:a2.sh")), |
| ImmutableList.of(label("//a:a1.sh"), label("//a:b2.sh")), |
| ImmutableList.of(label("//a:b1.sh"), label("//a:a2.sh")), |
| ImmutableList.of(label("//a:b1.sh"), label("//a:b2.sh"))); |
| } |
| |
| /** |
| * Given a large number of selects, we expect better than the naive |
| * exponential performance from evaluating select1 x select2 x select3 x ... |
| */ |
| @Test |
| public void testGetPossibleValuesWithManySelects() throws Exception { |
| String pattern = " + select({'//conditions:a1': '%c', '//conditions:a2': '%s'})"; |
| StringBuilder ruleDef = new StringBuilder(); |
| ruleDef.append("genrule(name = 'gen', srcs = [], outs = ['gen.out'], cmd = ''"); |
| for (char c : "abcdefghijklmnopqrstuvwxyz".toCharArray()) { |
| ruleDef.append(String.format(pattern, c, Character.toUpperCase(c))); |
| } |
| ruleDef.append(")"); |
| Rule rule = scratchRule("a", "gen", ruleDef.toString()); |
| // Naive evaluation would visit 2^26 cases and either overflow memory or timeout the test. |
| assertThat(AggregatingAttributeMapper.of(rule).visitAttribute("cmd", Type.STRING)) |
| .containsExactly("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); |
| } |
| |
| @Test |
| public void testGetPossibleValuesWithMultipleSelectsWithOverlappingConditions() throws Exception { |
| Rule rule = |
| scratchRule( |
| "a", |
| "myrule", |
| "sh_binary(name = 'myrule',", |
| // Even though this combination seems invalid it's allowed due to select specialization. |
| " srcs = select({'//conditions:x': ['x1.sh']})", |
| " + select({'//conditions:y': ['y1.sh']})", |
| " + select({", |
| " '//conditions:x': ['x2.sh'],", |
| " '//conditions:y': ['y2.sh'],", |
| " '//conditions:z': ['z2.sh']})", |
| " )"); |
| assertThat(AggregatingAttributeMapper.of(rule).visitAttribute("srcs", BuildType.LABEL_LIST)) |
| .containsExactly( |
| ImmutableList.of(label("//a:x1.sh"), label("//a:y1.sh"), label("//a:x2.sh")), |
| ImmutableList.of(label("//a:x1.sh"), label("//a:y1.sh"), label("//a:y2.sh")), |
| ImmutableList.of(label("//a:x1.sh"), label("//a:y1.sh"), label("//a:z2.sh"))); |
| } |
| |
| /** |
| * Tests that, on rule visitation, {@link AggregatingAttributeMapper} visits *every* possible |
| * value in a configurable attribute (including configuration key labels). |
| */ |
| @Test |
| public void testVisitationConfigurableAttribute() throws Exception { |
| Rule rule = scratchRule("a", "myrule", |
| "sh_binary(name = 'myrule',", |
| " srcs = select({", |
| " '//conditions:a': ['a.sh'],", |
| " '//conditions:b': ['b.sh'],", |
| " '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': ['default.sh'],", |
| " }))"); |
| |
| assertThat(getLabelsForAttribute(AggregatingAttributeMapper.of(rule), "srcs")) |
| .containsExactlyElementsIn( |
| ImmutableList.of( |
| "//a:a.sh", "//a:b.sh", "//a:default.sh", "//conditions:a", "//conditions:b")); |
| } |
| |
| @Test |
| public void testGetReachableLabels() throws Exception { |
| Rule rule = scratchRule("x", "main", |
| "cc_binary(", |
| " name = 'main',", |
| " srcs = select({", |
| " '//conditions:a': ['a.cc'],", |
| " '//conditions:b': ['b.cc']})", |
| " + ", |
| " ['always.cc']", |
| " + ", |
| " select({", |
| " '//conditions:c': ['c.cc'],", |
| " '//conditions:d': ['d.cc'],", |
| " '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "': ['default.cc'],", |
| " }))"); |
| |
| ImmutableList<Label> valueLabels = |
| ImmutableList.of( |
| label("//x:a.cc"), label("@//x:b.cc"), |
| label("//x:always.cc"), label("@//x:c.cc"), |
| label("//x:d.cc"), label("@//x:default.cc")); |
| ImmutableList<Label> keyLabels = |
| ImmutableList.of( |
| label("@//conditions:a"), label("@//conditions:b"), |
| label("@//conditions:c"), label("@//conditions:d")); |
| |
| AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule); |
| assertThat(mapper.getReachableLabels("srcs", true)) |
| .containsExactlyElementsIn(Iterables.concat(valueLabels, keyLabels)); |
| assertThat(mapper.getReachableLabels("srcs", false)).containsExactlyElementsIn(valueLabels); |
| } |
| |
| /** Custom rule to support testing over default values. */ |
| public static final class RuleWithDefaults |
| implements RuleDefinition, RuleConfiguredTargetFactory { |
| @Override |
| public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) { |
| return builder |
| .add( |
| attr("attribute", BuildType.LABEL).value(label("//default:value")).allowedFileTypes()) |
| .build(); |
| } |
| |
| @Override |
| public Metadata getMetadata() { |
| return RuleDefinition.Metadata.builder() |
| .name("rule_with_default") |
| .ancestors(BaseRuleClasses.NativeBuildRule.class) |
| .factoryClass(UnknownRuleConfiguredTarget.class) |
| .build(); |
| } |
| |
| @Override |
| public ConfiguredTarget create(RuleContext ruleContext) { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| |
| @Test |
| public void testVisitationWithDefaultValues() throws Exception { |
| Rule rule = |
| scratchRule( |
| "a", |
| "myrule", |
| "rule_with_default(name = 'myrule',", |
| " attribute = select({", |
| " '//conditions:a': None,", |
| " }))"); |
| |
| assertThat(getLabelsForAttribute(AggregatingAttributeMapper.of(rule), "attribute")) |
| .containsExactly("//conditions:a", "//default:value"); |
| } |
| |
| @Test |
| public void testGetReachableLabelsWithDefaultValues() throws Exception { |
| Rule rule = |
| scratchRule( |
| "a", |
| "myrule", |
| "rule_with_default(name = 'myrule',", |
| " attribute = select({", |
| " '//conditions:a': None,", |
| " }))"); |
| |
| AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule); |
| assertThat(mapper.getReachableLabels("attribute", true)) |
| .containsExactly(label("//default:value"), label("//conditions:a")); |
| } |
| |
| /** |
| * Custom rule to support testing over computed defaults. |
| */ |
| public static final class RuleWithComputedDefaults |
| implements RuleDefinition, RuleConfiguredTargetFactory { |
| @Override |
| public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) { |
| return builder |
| .add(attr("configurable1", STRING)) |
| .add(attr("configurable2", STRING)) |
| .add(attr("nonconfigurable", STRING).nonconfigurable("that's the point")) |
| .add(attr("$computed_default_with_configurable_deps", STRING).value( |
| new Attribute.ComputedDefault("configurable1", "configurable2") { |
| @Override |
| public Object getDefault(AttributeMap rule) { |
| return Joiner.on(" ").join( |
| rule.get("configurable1", STRING), |
| rule.get("configurable2", STRING), |
| rule.get("nonconfigurable", STRING) |
| ); |
| } |
| })) |
| .add(attr("$computed_default_without_configurable_deps", STRING).value( |
| new Attribute.ComputedDefault() { |
| @Override |
| public Object getDefault(AttributeMap rule) { |
| return rule.get("nonconfigurable", STRING); |
| } |
| })) |
| .build(); |
| } |
| |
| @Override |
| public Metadata getMetadata() { |
| return RuleDefinition.Metadata.builder() |
| .name("rule_with_computed_defaults") |
| .ancestors(BaseRuleClasses.NativeBuildRule.class) |
| .factoryClass(UnknownRuleConfiguredTarget.class) |
| .build(); |
| } |
| |
| @Override |
| public ConfiguredTarget create(RuleContext ruleContext) { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| |
| @Test |
| public void testComputedDefaultWithConfigurableDeps() throws Exception { |
| Rule rule = scratchRule("x", "bb", |
| "rule_with_computed_defaults(", |
| " name = 'bb',", |
| " configurable1 = select({':a': 'of', ':b': 'from'}),", |
| " configurable2 = select({':a': 'this', ':b': 'the'}),", |
| " nonconfigurable = 'bottom')"); |
| assertThat(AggregatingAttributeMapper.of(rule) |
| .visitAttribute("$computed_default_with_configurable_deps", STRING)) |
| .containsExactly("of this bottom", "from this bottom", "of the bottom", "from the bottom"); |
| } |
| |
| @Test |
| public void testComputedDefaultWithoutConfigurableDeps() throws Exception { |
| Rule rule = scratchRule("x", "bb", |
| "rule_with_computed_defaults(", |
| " name = 'bb',", |
| " nonconfigurable = 'swim up')"); |
| assertThat(AggregatingAttributeMapper.of(rule) |
| .visitAttribute("$computed_default_without_configurable_deps", STRING)) |
| .containsExactly("swim up"); |
| } |
| |
| private static Label label(String labelString) { |
| return Label.parseCanonicalUnchecked(labelString); |
| } |
| } |