| // 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.skyframe; |
| |
| import static com.google.common.base.Strings.nullToEmpty; |
| 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; |
| import static com.google.devtools.build.lib.syntax.Type.STRING; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.RuleDefinition; |
| import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment; |
| import com.google.devtools.build.lib.analysis.config.BuildConfiguration; |
| import com.google.devtools.build.lib.analysis.config.BuildOptions; |
| import com.google.devtools.build.lib.analysis.config.ConfigurationResolver; |
| import com.google.devtools.build.lib.analysis.config.PatchTransition; |
| import com.google.devtools.build.lib.analysis.config.TransitionResolver; |
| import com.google.devtools.build.lib.analysis.test.TestConfiguration; |
| import com.google.devtools.build.lib.analysis.util.MockRule; |
| import com.google.devtools.build.lib.analysis.util.TestAspects; |
| import com.google.devtools.build.lib.analysis.util.TestAspects.DummyRuleFactory; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.packages.Attribute; |
| import com.google.devtools.build.lib.packages.Attribute.SplitTransition; |
| import com.google.devtools.build.lib.packages.Attribute.Transition; |
| import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper; |
| import com.google.devtools.build.lib.packages.Rule; |
| import com.google.devtools.build.lib.packages.RuleClass; |
| import com.google.devtools.build.lib.packages.RuleTransitionFactory; |
| import com.google.devtools.build.lib.testutil.Suite; |
| import com.google.devtools.build.lib.testutil.TestSpec; |
| import com.google.devtools.build.lib.util.FileTypeSet; |
| import java.util.List; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** Runs an expanded set of ConfigurationsForTargetsTest with trimmed configurations. */ |
| @TestSpec(size = Suite.SMALL_TESTS) |
| @RunWith(JUnit4.class) |
| public class ConfigurationsForTargetsWithTrimmedConfigurationsTest |
| extends ConfigurationsForTargetsTest { |
| |
| private TransitionResolver transitionResolver; |
| |
| @Before |
| public void createTransitionResolver() { |
| transitionResolver = new TransitionResolver(ruleClassProvider.getDynamicTransitionMapper()); |
| } |
| |
| @Override |
| protected FlagBuilder defaultFlags() { |
| return super.defaultFlags().with(Flag.TRIMMED_CONFIGURATIONS); |
| } |
| |
| private static class EmptySplitTransition implements SplitTransition<BuildOptions> { |
| @Override |
| public List<BuildOptions> split(BuildOptions buildOptions) { |
| return ImmutableList.of(); |
| } |
| } |
| |
| private static class SetsHostCpuSplitTransition implements SplitTransition<BuildOptions> { |
| @Override |
| public List<BuildOptions> split(BuildOptions buildOptions) { |
| BuildOptions result = buildOptions.clone(); |
| result.get(BuildConfiguration.Options.class).hostCpu = "SET BY SPLIT"; |
| return ImmutableList.of(result); |
| } |
| } |
| |
| private static class SetsCpuSplitTransition implements SplitTransition<BuildOptions> { |
| |
| @Override |
| public List<BuildOptions> split(BuildOptions buildOptions) { |
| BuildOptions result = buildOptions.clone(); |
| result.get(BuildConfiguration.Options.class).cpu = "SET BY SPLIT"; |
| return ImmutableList.of(result); |
| } |
| } |
| |
| private static class SetsCpuPatchTransition implements PatchTransition { |
| |
| @Override |
| public BuildOptions apply(BuildOptions options) { |
| BuildOptions result = options.clone(); |
| result.get(BuildConfiguration.Options.class).cpu = "SET BY PATCH"; |
| return result; |
| } |
| } |
| |
| /** Base rule that depends on the test configuration fragment. */ |
| private static class TestBaseRule implements RuleDefinition { |
| @Override |
| public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) { |
| return builder.requiresConfigurationFragments(TestConfiguration.class).build(); |
| } |
| |
| @Override |
| public Metadata getMetadata() { |
| return RuleDefinition.Metadata.builder() |
| .name("test_base") |
| .factoryClass(DummyRuleFactory.class) |
| .ancestors(TestAspects.BaseRule.class) |
| .build(); |
| } |
| } |
| |
| /** A rule with an empty split transition on an attribute. */ |
| private static class EmptySplitRule implements RuleDefinition { |
| @Override |
| public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) { |
| return builder |
| .add( |
| attr("with_empty_transition", LABEL) |
| .allowedFileTypes(FileTypeSet.ANY_FILE) |
| .cfg(new EmptySplitTransition())) |
| .build(); |
| } |
| |
| @Override |
| public Metadata getMetadata() { |
| return RuleDefinition.Metadata.builder() |
| .name("empty_split") |
| .factoryClass(DummyRuleFactory.class) |
| .ancestors(TestBaseRule.class) |
| .build(); |
| } |
| } |
| |
| /** Rule with a split transition on an attribute. */ |
| private static class AttributeTransitionRule implements RuleDefinition { |
| |
| @Override |
| public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) { |
| return builder |
| .add(attr("without_transition", LABEL).allowedFileTypes(FileTypeSet.ANY_FILE)) |
| .add( |
| attr("with_cpu_transition", LABEL) |
| .allowedFileTypes(FileTypeSet.ANY_FILE) |
| .cfg(new SetsCpuSplitTransition())) |
| .add( |
| attr("with_host_cpu_transition", LABEL) |
| .allowedFileTypes(FileTypeSet.ANY_FILE) |
| .cfg(new SetsHostCpuSplitTransition())) |
| .build(); |
| } |
| |
| @Override |
| public Metadata getMetadata() { |
| return RuleDefinition.Metadata.builder() |
| .name("attribute_transition") |
| .factoryClass(DummyRuleFactory.class) |
| .ancestors(TestBaseRule.class) |
| .build(); |
| } |
| } |
| |
| /** Rule with rule class configuration transition. */ |
| private static class RuleClassTransitionRule implements RuleDefinition { |
| @Override |
| public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) { |
| return builder.cfg(new SetsCpuPatchTransition()).build(); |
| } |
| |
| @Override |
| public Metadata getMetadata() { |
| return RuleDefinition.Metadata.builder() |
| .name("rule_class_transition") |
| .factoryClass(DummyRuleFactory.class) |
| .ancestors(TestBaseRule.class) |
| .build(); |
| } |
| } |
| |
| private static class SetsTestFilterFromAttributePatchTransition implements PatchTransition { |
| private final String value; |
| |
| public SetsTestFilterFromAttributePatchTransition(String value) { |
| this.value = value; |
| } |
| |
| @Override |
| public BuildOptions apply(BuildOptions options) { |
| BuildOptions result = options.clone(); |
| result.get(TestConfiguration.TestOptions.class).testFilter = "SET BY PATCH FACTORY: " + value; |
| return result; |
| } |
| } |
| |
| private static class SetsTestFilterFromAttributeTransitionFactory |
| implements RuleTransitionFactory { |
| @Override |
| public Transition buildTransitionFor(Rule rule) { |
| NonconfigurableAttributeMapper attributes = NonconfigurableAttributeMapper.of(rule); |
| String value = attributes.get("sets_test_filter_to", STRING); |
| if (Strings.isNullOrEmpty(value)) { |
| return null; |
| } else { |
| return new SetsTestFilterFromAttributePatchTransition(value); |
| } |
| } |
| } |
| |
| /** |
| * Rule with a RuleTransitionFactory which sets the --test_filter flag according to its attribute. |
| */ |
| private static class UsesRuleTransitionFactoryRule implements RuleDefinition { |
| @Override |
| public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) { |
| return builder |
| .cfg(new SetsTestFilterFromAttributeTransitionFactory()) |
| .add( |
| attr("sets_test_filter_to", STRING) |
| .nonconfigurable("used in RuleTransitionFactory") |
| .value("")) |
| .build(); |
| } |
| |
| @Override |
| public Metadata getMetadata() { |
| return RuleDefinition.Metadata.builder() |
| .name("uses_rule_transition_factory") |
| .factoryClass(DummyRuleFactory.class) |
| .ancestors(TestBaseRule.class) |
| .build(); |
| } |
| } |
| |
| @Test |
| public void testRuleClassTransition() throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), |
| new TestBaseRule(), |
| new AttributeTransitionRule(), |
| new RuleClassTransitionRule()); |
| scratch.file("a/BUILD", |
| "attribute_transition(", |
| " name='attribute',", |
| " without_transition = ':rule_class',", |
| ")", |
| "rule_class_transition(name='rule_class')"); |
| List<ConfiguredTarget> deps = getConfiguredDeps("//a:attribute", "without_transition"); |
| BuildConfiguration ruleclass = Iterables.getOnlyElement(deps).getConfiguration(); |
| assertThat(ruleclass.getCpu()).isEqualTo("SET BY PATCH"); |
| } |
| |
| @Test |
| public void testNonConflictingAttributeAndRuleClassTransitions() throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), |
| new TestBaseRule(), |
| new AttributeTransitionRule(), |
| new RuleClassTransitionRule()); |
| scratch.file("a/BUILD", |
| "attribute_transition(", |
| " name='attribute',", |
| " with_host_cpu_transition = ':rule_class',", |
| ")", |
| "rule_class_transition(name='rule_class')"); |
| List<ConfiguredTarget> deps = getConfiguredDeps("//a:attribute", "with_host_cpu_transition"); |
| BuildConfiguration ruleclass = Iterables.getOnlyElement(deps).getConfiguration(); |
| assertThat(ruleclass.getCpu()).isEqualTo("SET BY PATCH"); |
| assertThat(ruleclass.getHostCpu()).isEqualTo("SET BY SPLIT"); |
| } |
| |
| @Test |
| public void testConflictingAttributeAndRuleClassTransitions() throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), |
| new TestBaseRule(), |
| new AttributeTransitionRule(), |
| new RuleClassTransitionRule()); |
| scratch.file("a/BUILD", |
| "attribute_transition(", |
| " name='attribute',", |
| " with_cpu_transition = ':rule_class',", |
| ")", |
| "rule_class_transition(name='rule_class')"); |
| List<ConfiguredTarget> deps = getConfiguredDeps("//a:attribute", "with_cpu_transition"); |
| BuildConfiguration ruleclass = Iterables.getOnlyElement(deps).getConfiguration(); |
| assertThat(ruleclass.getCpu()).isEqualTo("SET BY PATCH"); |
| } |
| |
| @Test |
| public void testEmptySplitDoesNotSuppressRuleClassTransition() throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), |
| new TestBaseRule(), |
| new EmptySplitRule(), |
| new RuleClassTransitionRule()); |
| scratch.file( |
| "a/BUILD", |
| "empty_split(", |
| " name = 'empty',", |
| " with_empty_transition = ':rule_class',", |
| ")", |
| "rule_class_transition(name='rule_class')"); |
| List<ConfiguredTarget> deps = getConfiguredDeps("//a:empty", "with_empty_transition"); |
| BuildConfiguration ruleclass = Iterables.getOnlyElement(deps).getConfiguration(); |
| assertThat(ruleclass.getCpu()).isEqualTo("SET BY PATCH"); |
| } |
| |
| @Test |
| public void testTopLevelRuleClassTransition() throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), new TestBaseRule(), new RuleClassTransitionRule()); |
| scratch.file( |
| "a/BUILD", |
| "rule_class_transition(", |
| " name = 'rule_class',", |
| ")"); |
| ConfiguredTarget target = |
| Iterables.getOnlyElement(update("//a:rule_class").getTargetsToBuild()); |
| assertThat(target.getConfiguration().getCpu()).isEqualTo("SET BY PATCH"); |
| } |
| |
| @Test |
| public void testTopLevelRuleClassTransitionAndNoTransition() throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), |
| new TestBaseRule(), |
| new RuleClassTransitionRule(), |
| new TestAspects.SimpleRule()); |
| scratch.file( |
| "a/BUILD", |
| "rule_class_transition(", |
| " name = 'rule_class',", |
| ")", |
| "simple(name='sim')"); |
| ConfiguredTarget target = |
| Iterables.getOnlyElement(update("//a:sim").getTargetsToBuild()); |
| assertThat(target.getConfiguration().getCpu()).isNotEqualTo("SET BY PATCH"); |
| } |
| |
| @Test |
| public void ruleTransitionFactoryUsesNonconfigurableAttributesToGenerateTransition() |
| throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), |
| new TestBaseRule(), |
| new AttributeTransitionRule(), |
| new UsesRuleTransitionFactoryRule()); |
| useConfiguration("--test_filter=SET ON COMMAND LINE: original and best"); |
| scratch.file( |
| "a/BUILD", |
| "attribute_transition(", |
| " name='top',", |
| " without_transition=':factory',", |
| ")", |
| "uses_rule_transition_factory(", |
| " name='factory',", |
| " sets_test_filter_to='funkiest',", |
| ")"); |
| List<ConfiguredTarget> deps = getConfiguredDeps("//a:top", "without_transition"); |
| BuildConfiguration config = Iterables.getOnlyElement(deps).getConfiguration(); |
| assertThat(config.getFragment(TestConfiguration.class).getTestFilter()) |
| .isEqualTo("SET BY PATCH FACTORY: funkiest"); |
| } |
| |
| @Test |
| public void ruleTransitionFactoryCanReturnNullToCauseNoTransition() throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), |
| new TestBaseRule(), |
| new AttributeTransitionRule(), |
| new UsesRuleTransitionFactoryRule()); |
| useConfiguration("--test_filter=SET ON COMMAND LINE: original and best"); |
| scratch.file( |
| "a/BUILD", |
| "attribute_transition(", |
| " name='top',", |
| " without_transition=':factory',", |
| ")", |
| "uses_rule_transition_factory(", |
| " name='factory',", |
| " sets_test_filter_to='',", |
| ")"); |
| List<ConfiguredTarget> deps = getConfiguredDeps("//a:top", "without_transition"); |
| BuildConfiguration config = Iterables.getOnlyElement(deps).getConfiguration(); |
| assertThat(config.getFragment(TestConfiguration.class).getTestFilter()) |
| .isEqualTo("SET ON COMMAND LINE: original and best"); |
| } |
| |
| @Test |
| public void topLevelRuleTransitionFactoryUsesNonconfigurableAttributes() throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), new TestBaseRule(), new UsesRuleTransitionFactoryRule()); |
| useConfiguration("--test_filter=SET ON COMMAND LINE: original and best"); |
| scratch.file( |
| "a/BUILD", |
| "uses_rule_transition_factory(", |
| " name='factory',", |
| " sets_test_filter_to='Maximum Dance',", |
| ")"); |
| ConfiguredTarget target = Iterables.getOnlyElement(update("//a:factory").getTargetsToBuild()); |
| assertThat(target.getConfiguration().getFragment(TestConfiguration.class).getTestFilter()) |
| .isEqualTo("SET BY PATCH FACTORY: Maximum Dance"); |
| } |
| |
| @Test |
| public void topLevelRuleTransitionFactoryCanReturnNullInTesting() throws Exception { |
| setRulesAvailableInTests( |
| new TestAspects.BaseRule(), new TestBaseRule(), new UsesRuleTransitionFactoryRule()); |
| useConfiguration("--test_filter=SET ON COMMAND LINE: original and best"); |
| scratch.file( |
| "a/BUILD", |
| "uses_rule_transition_factory(", |
| " name='factory',", |
| " sets_test_filter_to='',", |
| ")"); |
| update("@//a:factory"); |
| ConfiguredTarget target = getView().getConfiguredTargetForTesting( |
| reporter, |
| Label.parseAbsoluteUnchecked("@//a:factory"), |
| getTargetConfiguration()); |
| assertThat(target.getConfiguration().getFragment(TestConfiguration.class).getTestFilter()) |
| .isEqualTo("SET ON COMMAND LINE: original and best"); |
| } |
| |
| /** |
| * Returns a custom {@link PatchTransition} with the given value added to {@link |
| * TestConfiguration.TestOptions#testFilter}. |
| */ |
| private static PatchTransition newPatchTransition(final String value) { |
| return new PatchTransition() { |
| @Override |
| public BuildOptions apply(BuildOptions options) { |
| BuildOptions toOptions = options.clone(); |
| TestConfiguration.TestOptions baseOptions = |
| toOptions.get(TestConfiguration.TestOptions.class); |
| baseOptions.testFilter = (nullToEmpty(baseOptions.testFilter)) + value; |
| return toOptions; |
| } |
| }; |
| } |
| |
| /** |
| * Returns a custom {@link Attribute.SplitTransition} that splits {@link |
| * TestConfiguration.TestOptions#testFilter} down two paths: {@code += prefix + "1"} and {@code += |
| * prefix + "2"}. |
| */ |
| private static Attribute.SplitTransition<BuildOptions> newSplitTransition(final String prefix) { |
| return new Attribute.SplitTransition<BuildOptions>() { |
| @Override |
| public List<BuildOptions> split(BuildOptions buildOptions) { |
| ImmutableList.Builder<BuildOptions> result = ImmutableList.builder(); |
| for (int index = 1; index <= 2; index++) { |
| BuildOptions toOptions = buildOptions.clone(); |
| TestConfiguration.TestOptions baseOptions = |
| toOptions.get(TestConfiguration.TestOptions.class); |
| baseOptions.testFilter = |
| (baseOptions.testFilter == null ? "" : baseOptions.testFilter) + prefix + index; |
| result.add(toOptions); |
| } |
| return result.build(); |
| } |
| }; |
| } |
| |
| /** |
| * Returns the value of {@link TestConfiguration.TestOptions#testFilter} for a transition |
| * applied over the target configuration. |
| */ |
| private List<String> getTestFilterOptionValue(Transition transition) |
| throws Exception { |
| ImmutableList.Builder<String> outValues = ImmutableList.builder(); |
| for (BuildOptions toOptions : ConfigurationResolver.applyTransition( |
| getTargetConfiguration().getOptions(), transition, |
| ruleClassProvider.getAllFragments(), ruleClassProvider, false)) { |
| outValues.add(toOptions.get(TestConfiguration.TestOptions.class).testFilter); |
| } |
| return outValues.build(); |
| } |
| |
| @Test |
| public void composedStraightTransitions() throws Exception { |
| update(); // Creates the target configuration. |
| assertThat(getTestFilterOptionValue( |
| transitionResolver.composeTransitions( |
| newPatchTransition("foo"), |
| newPatchTransition("bar")))) |
| .containsExactly("foobar"); |
| } |
| |
| @Test |
| public void composedStraightTransitionThenSplitTransition() throws Exception { |
| update(); // Creates the target configuration. |
| assertThat(getTestFilterOptionValue( |
| transitionResolver.composeTransitions( |
| newPatchTransition("foo"), |
| newSplitTransition("split")))) |
| .containsExactly("foosplit1", "foosplit2"); |
| } |
| |
| @Test |
| public void composedSplitTransitionThenStraightTransition() throws Exception { |
| update(); // Creates the target configuration. |
| assertThat(getTestFilterOptionValue( |
| transitionResolver.composeTransitions( |
| newSplitTransition("split"), |
| newPatchTransition("foo")))) |
| .containsExactly("split1foo", "split2foo"); |
| } |
| |
| @Test |
| public void composedSplitTransitions() throws Exception { |
| update(); // Creates the target configuration. |
| assertThat(getTestFilterOptionValue( |
| transitionResolver.composeTransitions( |
| newSplitTransition("s"), |
| newSplitTransition("t")))) |
| .containsExactly("s1t1", "s1t2", "s2t1", "s2t2"); |
| } |
| |
| /** Sets {@link TestConfiguration.TestOptions#testFilter} to the rule class of the given rule. */ |
| private static final RuleTransitionFactory RULE_BASED_TEST_FILTER = |
| rule -> |
| (PatchTransition) |
| buildOptions -> { |
| BuildOptions toOptions = buildOptions.clone(); |
| toOptions.get(TestConfiguration.TestOptions.class).testFilter = rule.getRuleClass(); |
| return toOptions; |
| }; |
| |
| private static final RuleDefinition RULE_WITH_OUTGOING_TRANSITION = |
| (MockRule) |
| () -> |
| MockRule.define( |
| "change_deps", |
| (builder, env) -> |
| builder |
| .add(MockRule.DEPS_ATTRIBUTE) |
| .requiresConfigurationFragments(TestConfiguration.class) |
| .depsCfg(RULE_BASED_TEST_FILTER)); |
| |
| @Test |
| public void outgoingRuleTransition() throws Exception { |
| setRulesAvailableInTests( |
| RULE_WITH_OUTGOING_TRANSITION, |
| (MockRule) |
| () -> |
| MockRule.define( |
| "foo_rule", |
| (builder, env) -> |
| builder.requiresConfigurationFragments(TestConfiguration.class)), |
| (MockRule) |
| () -> |
| MockRule.define( |
| "bar_rule", |
| (builder, env) -> |
| builder.requiresConfigurationFragments(TestConfiguration.class))); |
| scratch.file("outgoing/BUILD", |
| "foo_rule(", |
| " name = 'foolib')", |
| "bar_rule(", |
| " name = 'barlib')", |
| "change_deps(", |
| " name = 'bin',", |
| " deps = [':foolib', ':barlib'])"); |
| |
| List<ConfiguredTarget> deps = getConfiguredDeps("//outgoing:bin", "deps"); |
| ImmutableMap<String, String> depLabelToTestFilterString = |
| ImmutableMap.of( |
| deps.get(0).getLabel().toString(), |
| deps.get(0).getConfiguration().getFragment(TestConfiguration.class).getTestFilter(), |
| deps.get(1).getLabel().toString(), |
| deps.get(1) |
| .getConfiguration() |
| .getFragment(TestConfiguration.class) |
| .getTestFilter()); |
| |
| assertThat(depLabelToTestFilterString).containsExactly( |
| "//outgoing:foolib", "foo_rule", |
| "//outgoing:barlib", "bar_rule"); |
| } |
| } |