// 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.rules.config; import static com.google.common.truth.Truth.assertThat; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.testing.EqualsTester; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import com.google.devtools.build.lib.analysis.ConfiguredTarget; import com.google.devtools.build.lib.analysis.skylark.SkylarkRuleContext; import com.google.devtools.build.lib.packages.ConfiguredAttributeMapper; import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData; import com.google.devtools.build.lib.skylark.util.SkylarkTestCase; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.testutil.TestRuleClassProvider; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for the config_feature_flag rule. */ @RunWith(JUnit4.class) public final class ConfigFeatureFlagTest extends SkylarkTestCase { @Before public void useTrimmedConfigurations() throws Exception { useConfiguration( "--experimental_dynamic_configs=on", "--enforce_transitive_configs_for_config_feature_flag"); } @Override protected ConfiguredRuleClassProvider getRuleClassProvider() { ConfiguredRuleClassProvider.Builder builder = new ConfiguredRuleClassProvider.Builder().addRuleDefinition(new FeatureFlagSetterRule()); TestRuleClassProvider.addStandardRules(builder); return builder.build(); } @Test public void configFeatureFlagProvider_fromTargetReturnsNullIfTargetDoesNotExportProvider() throws Exception { scratch.file( "test/BUILD", "feature_flag_setter(", " name = 'top',", " flag_values = {", " },", ")"); assertThat(ConfigFeatureFlagProvider.fromTarget(getConfiguredTarget("//test:top"))).isNull(); } @Test public void configFeatureFlagProvider_containsValueFromConfiguration() throws Exception { scratch.file( "test/BUILD", "feature_flag_setter(", " name = 'top',", " exports_flag = ':flag',", " flag_values = {", " ':flag': 'configured',", " },", " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", " allowed_values = ['default', 'configured', 'other'],", ")"); assertThat(ConfigFeatureFlagProvider.fromTarget(getConfiguredTarget("//test:top")).getFlagValue()) .isEqualTo("configured"); } @Test public void configFeatureFlagProvider_usesConfiguredValueOverDefault() throws Exception { scratch.file( "test/BUILD", "feature_flag_setter(", " name = 'top',", " exports_flag = ':flag',", " flag_values = {", " ':flag': 'configured',", " },", " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", " allowed_values = ['default', 'configured', 'other'],", " default_value = 'default',", ")"); assertThat(ConfigFeatureFlagProvider.fromTarget(getConfiguredTarget("//test:top")).getFlagValue()) .isEqualTo("configured"); } @Test public void configFeatureFlagProvider_skylarkConstructor() throws Exception { scratch.file( "test/wrapper.bzl", "def _flag_reading_wrapper_impl(ctx):", " pass", "flag_reading_wrapper = rule(", " implementation = _flag_reading_wrapper_impl,", " attrs = {'flag': attr.label()},", ")", "def _flag_propagating_wrapper_impl(ctx):", " return struct(providers = [config_common.FeatureFlagInfo(value='hello')])", "flag_propagating_wrapper = rule(", " implementation = _flag_propagating_wrapper_impl,", ")"); scratch.file( "test/BUILD", "load(':wrapper.bzl', 'flag_propagating_wrapper')", "flag_propagating_wrapper(", " name = 'propagator',", ")", "config_setting(name = 'hello_setting',", " flag_values = {':propagator': 'hello'})", "genrule(", " name = 'gen',", " srcs = [],", " outs = ['out'],", " cmd = select({", " ':hello_setting': 'hello',", " '//conditions:default': 'error'", " }))"); ConfiguredTargetAndData ctad = getConfiguredTargetAndData("//test:gen"); ConfiguredAttributeMapper attributeMapper = getMapperFromConfiguredTargetAndTarget(ctad); assertThat(attributeMapper.get("cmd", Type.STRING)).isEqualTo("hello"); } @Test public void configFeatureFlagProvider_valueIsAccessibleFromSkylark() throws Exception { scratch.file( "test/wrapper.bzl", "def _flag_reading_wrapper_impl(ctx):", " pass", "flag_reading_wrapper = rule(", " implementation = _flag_reading_wrapper_impl,", " attrs = {'flag': attr.label()},", ")"); scratch.file( "test/BUILD", "load(':wrapper.bzl', 'flag_reading_wrapper')", "feature_flag_setter(", " name = 'top',", " deps = [':wrapper'],", " flag_values = {", " ':flag': 'configured',", " },", " transitive_configs = [':flag'],", ")", "flag_reading_wrapper(", " name = 'wrapper',", " flag = ':flag',", " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", " allowed_values = ['default', 'configured', 'other'],", " default_value = 'default',", ")"); ConfiguredTarget top = getConfiguredTarget("//test:top"); ConfiguredTarget wrapper = (ConfiguredTarget) Iterables.getOnlyElement(getPrerequisites(top, "deps")); SkylarkRuleContext ctx = new SkylarkRuleContext(getRuleContextForSkylark(wrapper), null, getSkylarkSemantics()); update("ruleContext", ctx); update("config_common", new ConfigSkylarkCommon()); String value = (String) eval("ruleContext.attr.flag[config_common.FeatureFlagInfo].value"); assertThat(value).isEqualTo("configured"); } @Test public void configFeatureFlagProvider_validatesValuesUsingAllowedValuesAttribute() throws Exception { scratch.file( "test/BUILD", "config_feature_flag(", " name = 'flag',", " allowed_values = ['default', 'configured', 'other'],", " default_value = 'default',", ")"); ConfigFeatureFlagProvider provider = ConfigFeatureFlagProvider.fromTarget(getConfiguredTarget("//test:flag")); assertThat(provider.isValidValue("default")).isTrue(); assertThat(provider.isValidValue("configured")).isTrue(); assertThat(provider.isValidValue("other")).isTrue(); assertThat(provider.isValidValue("absent")).isFalse(); assertThat(provider.isValidValue("conFigured")).isFalse(); assertThat(provider.isValidValue(" other")).isFalse(); } @Test public void configFeatureFlagProvider_valueValidationIsPossibleFromSkylark() throws Exception { scratch.file( "test/wrapper.bzl", "def _flag_reading_wrapper_impl(ctx):", " pass", "flag_reading_wrapper = rule(", " implementation = _flag_reading_wrapper_impl,", " attrs = {'flag': attr.label()},", ")"); scratch.file( "test/BUILD", "load(':wrapper.bzl', 'flag_reading_wrapper')", "flag_reading_wrapper(", " name = 'wrapper',", " flag = ':flag',", " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", " allowed_values = ['default', 'configured', 'other'],", " default_value = 'default',", ")"); SkylarkRuleContext ctx = createRuleContext("//test:wrapper"); update("ruleContext", ctx); update("config_common", new ConfigSkylarkCommon()); String provider = "ruleContext.attr.flag[config_common.FeatureFlagInfo]"; Boolean isDefaultValid = (Boolean) eval(provider + ".is_valid_value('default')"); Boolean isConfiguredValid = (Boolean) eval(provider + ".is_valid_value('configured')"); Boolean isOtherValid = (Boolean) eval(provider + ".is_valid_value('other')"); Boolean isAbsentValid = (Boolean) eval(provider + ".is_valid_value('absent')"); Boolean isIncorrectCapitalizationValid = (Boolean) eval(provider + ".is_valid_value('conFigured')"); Boolean isIncorrectSpacingValid = (Boolean) eval(provider + ".is_valid_value(' other')"); assertThat(isDefaultValid).isTrue(); assertThat(isConfiguredValid).isTrue(); assertThat(isOtherValid).isTrue(); assertThat(isAbsentValid).isFalse(); assertThat(isIncorrectCapitalizationValid).isFalse(); assertThat(isIncorrectSpacingValid).isFalse(); } @Test public void configFeatureFlagProvider_usesDefaultValueIfConfigurationDoesntSetValue() throws Exception { scratch.file( "test/BUILD", "feature_flag_setter(", " name = 'top',", " exports_flag = ':flag',", " flag_values = {", " ':other': 'configured',", " },", " transitive_configs = [':flag', ':other'],", ")", "config_feature_flag(", " name = 'flag',", " allowed_values = ['other', 'default', 'configured'],", " default_value = 'default',", ")", "config_feature_flag(", " name = 'other',", " allowed_values = ['default', 'configured', 'other'],", " default_value = 'default',", ")"); assertThat(ConfigFeatureFlagProvider.fromTarget(getConfiguredTarget("//test:top")) .getFlagValue()) .isEqualTo("default"); } @Test public void configFeatureFlagProvider_throwsErrorIfNeitherDefaultNorConfiguredValueSet() throws Exception { reporter.removeHandler(failFastHandler); // expecting an error scratch.file( "test/BUILD", "feature_flag_setter(", " name = 'top',", " exports_flag = ':flag',", " flag_values = {", " ':other': 'configured',", " },", " transitive_configs = [':flag', ':other'],", ")", "config_feature_flag(", " name = 'flag',", " allowed_values = ['other', 'configured'],", ")", "config_feature_flag(", " name = 'other',", " allowed_values = ['default', 'configured', 'other'],", " default_value = 'default',", ")"); assertThat(getConfiguredTarget("//test:flag")).isNull(); assertContainsEvent( "in config_feature_flag rule //test:flag: " + "flag has no default and must be set, but was not set"); } @Test public void allowedValuesAttribute_cannotBeEmpty() throws Exception { reporter.removeHandler(failFastHandler); // expecting an error scratch.file( "test/BUILD", "config_feature_flag(", " name = 'flag',", " allowed_values = [],", " default_value = 'default',", ")"); assertThat(getConfiguredTarget("//test:flag")).isNull(); assertContainsEvent( "in allowed_values attribute of config_feature_flag rule //test:flag: " + "attribute must be non empty"); } @Test public void allowedValuesAttribute_cannotContainDuplicates() throws Exception { reporter.removeHandler(failFastHandler); // expecting an error scratch.file( "test/BUILD", "config_feature_flag(", " name = 'flag',", " allowed_values = ['double', 'double', 'toil', 'trouble'],", " default_value = 'trouble',", ")"); assertThat(getConfiguredTarget("//test:flag")).isNull(); assertContainsEvent( "in allowed_values attribute of config_feature_flag rule //test:flag: " + "cannot contain duplicates, but contained multiple of [\"double\"]"); } @Test public void defaultValueAttribute_mustBeMemberOfAllowedValuesIfPresent() throws Exception { reporter.removeHandler(failFastHandler); // expecting an error scratch.file( "test/BUILD", "feature_flag_setter(", " name = 'top',", " exports_flag = ':flag',", " flag_values = {", " ':flag': 'legal',", " },", " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", " allowed_values = ['legal', 'eagle'],", " default_value = 'beagle',", ")"); assertThat(getConfiguredTarget("//test:top")).isNull(); assertContainsEvent( "in default_value attribute of config_feature_flag rule //test:flag: " + "must be one of [\"eagle\", \"legal\"], but was \"beagle\""); } @Test public void configurationValue_mustBeMemberOfAllowedValuesIfPresent() throws Exception { reporter.removeHandler(failFastHandler); // expecting an error scratch.file( "test/BUILD", "feature_flag_setter(", " name = 'top',", " exports_flag = ':flag',", " flag_values = {", " ':flag': 'invalid',", " },", " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", " allowed_values = ['default', 'configured', 'other'],", " default_value = 'default',", ")"); assertThat(getConfiguredTarget("//test:top")).isNull(); // TODO(mstaib): when configurationError is implemented, switch to testing for that assertContainsEvent( "in config_feature_flag rule //test:flag: " + "value must be one of [\"configured\", \"default\", \"other\"], but was \"invalid\""); } @Test public void policy_mustContainRulesPackage() throws Exception { reporter.removeHandler(failFastHandler); // expecting an error scratch.overwriteFile( "tools/whitelists/config_feature_flag/BUILD", "package_group(name = 'config_feature_flag', packages = ['//some/other'])"); scratch.file( "test/BUILD", "config_feature_flag(", " name = 'flag',", " allowed_values = ['default', 'configured', 'other'],", " default_value = 'default',", ")"); assertThat(getConfiguredTarget("//test:flag")).isNull(); assertContainsEvent( "in config_feature_flag rule //test:flag: the config_feature_flag rule is not available in " + "package 'test'"); } @Test public void policy_doesNotBlockRuleIfInPackageGroup() throws Exception { scratch.overwriteFile( "tools/whitelists/config_feature_flag/BUILD", "package_group(name = 'config_feature_flag', packages = ['//test'])"); scratch.file( "test/BUILD", "config_feature_flag(", " name = 'flag',", " allowed_values = ['default', 'configured', 'other'],", " default_value = 'default',", ")"); assertThat(getConfiguredTarget("//test:flag")).isNotNull(); assertNoEvents(); } @Test public void equalsTester() { new EqualsTester() .addEqualityGroup( // Basic case. ConfigFeatureFlagProvider.create("flag1", Predicates.alwaysTrue())) .addEqualityGroup( // Will be distinct from the first group because CFFP instances are all distinct. ConfigFeatureFlagProvider.create("flag1", Predicates.alwaysTrue())) .addEqualityGroup( // Change the value, still distinct from the above. ConfigFeatureFlagProvider.create("flag2", Predicates.alwaysTrue())) .testEquals(); } }