// 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.packages.BuildType.LABEL_LIST; 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.Iterables; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import com.google.devtools.build.lib.analysis.ConfiguredTarget; 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.TransitionResolver; import com.google.devtools.build.lib.analysis.config.transitions.ConfigurationTransition; import com.google.devtools.build.lib.analysis.config.transitions.NoTransition; import com.google.devtools.build.lib.analysis.config.transitions.PatchTransition; import com.google.devtools.build.lib.analysis.config.transitions.SplitTransition; 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.NonconfigurableAttributeMapper; import com.google.devtools.build.lib.packages.Rule; import com.google.devtools.build.lib.packages.RuleTransitionFactory; import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; import com.google.devtools.build.lib.testutil.Suite; import com.google.devtools.build.lib.testutil.TestRuleClassProvider; import com.google.devtools.build.lib.testutil.TestSpec; import com.google.devtools.build.lib.util.FileTypeSet; import java.util.List; 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 { @Override protected FlagBuilder defaultFlags() { return super.defaultFlags().with(Flag.TRIMMED_CONFIGURATIONS); } private static class EmptySplitTransition implements SplitTransition { @Override public List split(BuildOptions buildOptions) { return ImmutableList.of(); } } private static class SetsHostCpuSplitTransition implements SplitTransition { @Override public List 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 { @Override public List 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 final MockRule TEST_BASE_RULE = () -> MockRule.ancestor(TestAspects.BASE_RULE.getClass()).factory(DummyRuleFactory.class).define( "test_base", (builder, env) -> builder.requiresConfigurationFragments(TestConfiguration.class).build()); /** A rule with an empty split transition on an attribute. */ private static final MockRule EMPTY_SPLIT_RULE = () -> MockRule.ancestor(TEST_BASE_RULE.getClass()).factory(DummyRuleFactory.class).define( "empty_split", attr("with_empty_transition", LABEL) .allowedFileTypes(FileTypeSet.ANY_FILE) .cfg(new EmptySplitTransition())); /** Rule with a split transition on an attribute. */ private static final MockRule ATTRIBUTE_TRANSITION_RULE = () -> MockRule.ancestor(TEST_BASE_RULE.getClass()).factory(DummyRuleFactory.class).define( "attribute_transition", attr("without_transition", LABEL).allowedFileTypes(FileTypeSet.ANY_FILE), attr("with_cpu_transition", LABEL) .allowedFileTypes(FileTypeSet.ANY_FILE) .cfg(new SetsCpuSplitTransition()), attr("with_host_cpu_transition", LABEL) .allowedFileTypes(FileTypeSet.ANY_FILE) .cfg(new SetsHostCpuSplitTransition())); /** Rule with rule class configuration transition. */ private static final MockRule RULE_CLASS_TRANSITION_RULE = () -> MockRule.ancestor(TEST_BASE_RULE.getClass()).factory(DummyRuleFactory.class).define( "rule_class_transition", (builder, env) -> builder.cfg(new SetsCpuPatchTransition()).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; } } @AutoCodec.VisibleForSerialization @AutoCodec static class SetsTestFilterFromAttributeTransitionFactory implements RuleTransitionFactory { @Override public PatchTransition buildTransitionFor(Rule rule) { NonconfigurableAttributeMapper attributes = NonconfigurableAttributeMapper.of(rule); String value = attributes.get("sets_test_filter_to", STRING); if (Strings.isNullOrEmpty(value)) { return NoTransition.INSTANCE; } else { return new SetsTestFilterFromAttributePatchTransition(value); } } } /** * Rule with a RuleTransitionFactory which sets the --test_filter flag according to its attribute. */ private static final MockRule USES_RULE_TRANSITION_FACTORY_RULE = () -> MockRule.ancestor(TEST_BASE_RULE.getClass()).factory(DummyRuleFactory.class).define( "uses_rule_transition_factory", (builder, env) -> builder .cfg(new SetsTestFilterFromAttributeTransitionFactory()) .add(attr("sets_test_filter_to", STRING) .nonconfigurable("used in RuleTransitionFactory") .value(""))); private static final class AddArgumentToTestArgsTransition implements PatchTransition { private final String argument; public AddArgumentToTestArgsTransition(String argument) { this.argument = argument; } @Override public BuildOptions apply(BuildOptions options) { BuildOptions result = options.clone(); TestConfiguration.TestOptions testOpts = result.get(TestConfiguration.TestOptions.class); ImmutableList testArgs = new ImmutableList.Builder().addAll(testOpts.testArguments).add(argument).build(); testOpts.testArguments = testArgs; return result; } } /** Rule which adds an argument to the --test_args flag for its dependencies. */ private static final MockRule ADD_TEST_ARG_FOR_DEPS_RULE = () -> MockRule.ancestor(TEST_BASE_RULE.getClass()) .factory(DummyRuleFactory.class) .define( "add_test_arg_for_deps", attr("deps", LABEL_LIST) .allowedFileTypes(FileTypeSet.ANY_FILE) .cfg(new AddArgumentToTestArgsTransition("deps transition"))); /** Rule which adds an argument to the --test_args flag for itself. */ private static final MockRule ADD_TEST_ARG_FOR_SELF_RULE = () -> MockRule.ancestor(TEST_BASE_RULE.getClass()) .factory(DummyRuleFactory.class) .define( "add_test_arg_for_self", (builder, env) -> builder .cfg(new AddArgumentToTestArgsTransition("rule class transition")) .add(attr("deps", LABEL_LIST).allowedFileTypes(FileTypeSet.ANY_FILE))); @Test public void trimmingTransitionActivatesLastOnAllTargets() throws Exception { RuleTransitionFactory trimmingTransitionFactory = (rule) -> new AddArgumentToTestArgsTransition( "trimming transition for " + rule.getLabel().toString()); ConfiguredRuleClassProvider.Builder builder = new ConfiguredRuleClassProvider.Builder(); TestRuleClassProvider.addStandardRules(builder); builder.addRuleDefinition(TestAspects.BASE_RULE); builder.addRuleDefinition(TEST_BASE_RULE); builder.addRuleDefinition(ADD_TEST_ARG_FOR_DEPS_RULE); builder.addRuleDefinition(ADD_TEST_ARG_FOR_SELF_RULE); builder.overrideTrimmingTransitionFactoryForTesting(trimmingTransitionFactory); useRuleClassProvider(builder.build()); scratch.file( "a/skylark.bzl", "def _impl(ctx):", " return", "skylark_rule = rule(", " implementation = _impl,", " attrs = {", " 'deps': attr.label_list(),", " '_base': attr.label(default = '//a:base'),", " }", ")"); scratch.file( "a/BUILD", "load(':skylark.bzl', 'skylark_rule')", // ensure that all Skylark rules get the TestConfiguration fragment "test_base(name = 'base')", // skylark rules get trimmed "skylark_rule(name = 'skylark_solo', deps = [':base'])", // native rules get trimmed; top-level targets get trimmed after the rule-class transition "add_test_arg_for_self(name = 'test_arg_on_self')", // deps with dependency transitions get trimmed after the dependency transition "add_test_arg_for_deps(name = 'attribute_transition', deps = [':dep_after_transition'])", "skylark_rule(name = 'dep_after_transition')", // deps on rule-class transitions get trimmed after the rule-class transition "skylark_rule(name = 'dep_on_ruleclass', deps = [':ruleclass_transition'])", "add_test_arg_for_self(name = 'ruleclass_transition')", // when all three (rule-class, attribute, trimming transitions) collide it's okay "add_test_arg_for_deps(name = 'attribute_outer', deps = [':ruleclass_inner'])", "add_test_arg_for_self(name = 'ruleclass_inner')"); ConfiguredTarget configuredTarget; BuildConfiguration config; configuredTarget = Iterables.getOnlyElement(update("//a:skylark_solo").getTargetsToBuild()); config = getConfiguration(configuredTarget); assertThat(config.getFragment(TestConfiguration.class).getTestArguments()) .containsExactly("trimming transition for //a:skylark_solo"); configuredTarget = Iterables.getOnlyElement(update("//a:test_arg_on_self").getTargetsToBuild()); config = getConfiguration(configuredTarget); assertThat(config.getFragment(TestConfiguration.class).getTestArguments()) .containsExactly("rule class transition", "trimming transition for //a:test_arg_on_self") .inOrder(); configuredTarget = Iterables.getOnlyElement(update("//a:attribute_transition").getTargetsToBuild()); config = getConfiguration( Iterables.getOnlyElement(getConfiguredDeps(configuredTarget, "deps"))); assertThat(config.getFragment(TestConfiguration.class).getTestArguments()) .containsExactly( "trimming transition for //a:attribute_transition", "deps transition", "trimming transition for //a:dep_after_transition") .inOrder(); configuredTarget = Iterables.getOnlyElement(update("//a:dep_on_ruleclass").getTargetsToBuild()); config = getConfiguration( Iterables.getOnlyElement(getConfiguredDeps(configuredTarget, "deps"))); assertThat(config.getFragment(TestConfiguration.class).getTestArguments()) .containsExactly( "trimming transition for //a:dep_on_ruleclass", "rule class transition", "trimming transition for //a:ruleclass_transition") .inOrder(); configuredTarget = Iterables.getOnlyElement(update("//a:attribute_outer").getTargetsToBuild()); config = getConfiguration( Iterables.getOnlyElement(getConfiguredDeps(configuredTarget, "deps"))); assertThat(config.getFragment(TestConfiguration.class).getTestArguments()) .containsExactly( "trimming transition for //a:attribute_outer", "deps transition", "rule class transition", "trimming transition for //a:ruleclass_inner") .inOrder(); } @Test public void testRuleClassTransition() throws Exception { setRulesAvailableInTests(TestAspects.BASE_RULE, TEST_BASE_RULE, ATTRIBUTE_TRANSITION_RULE, RULE_CLASS_TRANSITION_RULE); scratch.file("a/BUILD", "attribute_transition(", " name='attribute',", " without_transition = ':rule_class',", ")", "rule_class_transition(name='rule_class')"); List deps = getConfiguredDeps("//a:attribute", "without_transition"); BuildConfiguration ruleclass = getConfiguration(Iterables.getOnlyElement(deps)); assertThat(ruleclass.getCpu()).isEqualTo("SET BY PATCH"); } @Test public void testNonConflictingAttributeAndRuleClassTransitions() throws Exception { setRulesAvailableInTests(TestAspects.BASE_RULE, TEST_BASE_RULE, ATTRIBUTE_TRANSITION_RULE, RULE_CLASS_TRANSITION_RULE); scratch.file("a/BUILD", "attribute_transition(", " name='attribute',", " with_host_cpu_transition = ':rule_class',", ")", "rule_class_transition(name='rule_class')"); List deps = getConfiguredDeps("//a:attribute", "with_host_cpu_transition"); BuildConfiguration ruleclass = getConfiguration(Iterables.getOnlyElement(deps)); assertThat(ruleclass.getCpu()).isEqualTo("SET BY PATCH"); assertThat(ruleclass.getHostCpu()).isEqualTo("SET BY SPLIT"); } @Test public void testConflictingAttributeAndRuleClassTransitions() throws Exception { setRulesAvailableInTests(TestAspects.BASE_RULE, TEST_BASE_RULE, ATTRIBUTE_TRANSITION_RULE, RULE_CLASS_TRANSITION_RULE); scratch.file("a/BUILD", "attribute_transition(", " name='attribute',", " with_cpu_transition = ':rule_class',", ")", "rule_class_transition(name='rule_class')"); List deps = getConfiguredDeps("//a:attribute", "with_cpu_transition"); BuildConfiguration ruleclass = getConfiguration(Iterables.getOnlyElement(deps)); assertThat(ruleclass.getCpu()).isEqualTo("SET BY PATCH"); } @Test public void testEmptySplitDoesNotSuppressRuleClassTransition() throws Exception { setRulesAvailableInTests(TestAspects.BASE_RULE, TEST_BASE_RULE, EMPTY_SPLIT_RULE, RULE_CLASS_TRANSITION_RULE); scratch.file( "a/BUILD", "empty_split(", " name = 'empty',", " with_empty_transition = ':rule_class',", ")", "rule_class_transition(name='rule_class')"); List deps = getConfiguredDeps("//a:empty", "with_empty_transition"); BuildConfiguration ruleclass = getConfiguration(Iterables.getOnlyElement(deps)); assertThat(ruleclass.getCpu()).isEqualTo("SET BY PATCH"); } @Test public void testTopLevelRuleClassTransition() throws Exception { setRulesAvailableInTests(TestAspects.BASE_RULE, TEST_BASE_RULE, RULE_CLASS_TRANSITION_RULE); scratch.file( "a/BUILD", "rule_class_transition(", " name = 'rule_class',", ")"); ConfiguredTarget target = Iterables.getOnlyElement(update("//a:rule_class").getTargetsToBuild()); assertThat(getConfiguration(target).getCpu()).isEqualTo("SET BY PATCH"); } @Test public void testTopLevelRuleClassTransitionAndNoTransition() throws Exception { setRulesAvailableInTests(TestAspects.BASE_RULE, TEST_BASE_RULE, RULE_CLASS_TRANSITION_RULE, TestAspects.SIMPLE_RULE); scratch.file( "a/BUILD", "rule_class_transition(", " name = 'rule_class',", ")", "simple(name='sim')"); ConfiguredTarget target = Iterables.getOnlyElement(update("//a:sim").getTargetsToBuild()); assertThat(getConfiguration(target).getCpu()).isNotEqualTo("SET BY PATCH"); } @Test public void ruleTransitionFactoryUsesNonconfigurableAttributesToGenerateTransition() throws Exception { setRulesAvailableInTests(TestAspects.BASE_RULE, TEST_BASE_RULE, ATTRIBUTE_TRANSITION_RULE, USES_RULE_TRANSITION_FACTORY_RULE); 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 deps = getConfiguredDeps("//a:top", "without_transition"); BuildConfiguration config = getConfiguration(Iterables.getOnlyElement(deps)); assertThat(config.getFragment(TestConfiguration.class).getTestFilter()) .isEqualTo("SET BY PATCH FACTORY: funkiest"); } @Test public void ruleTransitionFactoryCanReturnNullToCauseNoTransition() throws Exception { setRulesAvailableInTests(TestAspects.BASE_RULE, TEST_BASE_RULE, ATTRIBUTE_TRANSITION_RULE, USES_RULE_TRANSITION_FACTORY_RULE); 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 deps = getConfiguredDeps("//a:top", "without_transition"); BuildConfiguration config = getConfiguration(Iterables.getOnlyElement(deps)); assertThat(config.getFragment(TestConfiguration.class).getTestFilter()) .isEqualTo("SET ON COMMAND LINE: original and best"); } @Test public void topLevelRuleTransitionFactoryUsesNonconfigurableAttributes() throws Exception { setRulesAvailableInTests( TestAspects.BASE_RULE, TEST_BASE_RULE, USES_RULE_TRANSITION_FACTORY_RULE); 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(getConfiguration(target).getFragment(TestConfiguration.class).getTestFilter()) .isEqualTo("SET BY PATCH FACTORY: Maximum Dance"); } @Test public void topLevelRuleTransitionFactoryCanReturnNullInTesting() throws Exception { setRulesAvailableInTests( TestAspects.BASE_RULE, TEST_BASE_RULE, USES_RULE_TRANSITION_FACTORY_RULE); 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(getConfiguration(target).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 SplitTransition} that splits {@link * TestConfiguration.TestOptions#testFilter} down two paths: {@code += prefix + "1"} and {@code += * prefix + "2"}. */ private static SplitTransition newSplitTransition(final String prefix) { return buildOptions -> { ImmutableList.Builder 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 getTestFilterOptionValue(ConfigurationTransition transition) throws Exception { ImmutableList.Builder 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"); } }