| // 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.starlark.StarlarkRuleContext; |
| import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; |
| import com.google.devtools.build.lib.packages.ConfiguredAttributeMapper; |
| import com.google.devtools.build.lib.packages.Type; |
| import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData; |
| import com.google.devtools.build.lib.starlark.util.BazelEvaluationTestCase; |
| 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 BuildViewTestCase { |
| |
| private final BazelEvaluationTestCase ev = new BazelEvaluationTestCase(); |
| |
| private StarlarkRuleContext createRuleContext(String label) throws Exception { |
| return new StarlarkRuleContext(getRuleContextForStarlark(getConfiguredTarget(label)), null); |
| } |
| |
| @Before |
| public void enforceTransitiveConfigs() throws Exception { |
| useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); |
| } |
| |
| @Override |
| protected ConfiguredRuleClassProvider createRuleClassProvider() { |
| 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_starlarkConstructor() 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 [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_valueIsAccessibleFromStarlark() 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", |
| flag_values = { |
| ":flag": "configured", |
| }, |
| transitive_configs = [":flag"], |
| deps = [":wrapper"], |
| ) |
| |
| 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")); |
| StarlarkRuleContext ctx = new StarlarkRuleContext(getRuleContextForStarlark(wrapper), null); |
| ev.update("ruleContext", ctx); |
| ev.update("config_common", new ConfigStarlarkCommon()); |
| String value = (String) ev.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_valueValidationIsPossibleFromStarlark() 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", |
| ) |
| """); |
| StarlarkRuleContext ctx = createRuleContext("//test:wrapper"); |
| ev.update("ruleContext", ctx); |
| ev.update("config_common", new ConfigStarlarkCommon()); |
| String provider = "ruleContext.attr.flag[config_common.FeatureFlagInfo]"; |
| Boolean isDefaultValid = (Boolean) ev.eval(provider + ".is_valid_value('default')"); |
| Boolean isConfiguredValid = (Boolean) ev.eval(provider + ".is_valid_value('configured')"); |
| Boolean isOtherValid = (Boolean) ev.eval(provider + ".is_valid_value('other')"); |
| Boolean isAbsentValid = (Boolean) ev.eval(provider + ".is_valid_value('absent')"); |
| Boolean isIncorrectCapitalizationValid = |
| (Boolean) ev.eval(provider + ".is_valid_value('conFigured')"); |
| Boolean isIncorrectSpacingValid = (Boolean) ev.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_ignoresUnusedFlagWithNeitherDefaultNorConfiguredValueSet() |
| 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", |
| "configured", |
| ], |
| ) |
| |
| config_feature_flag( |
| name = "other", |
| allowed_values = [ |
| "default", |
| "configured", |
| "other", |
| ], |
| default_value = "default", |
| ) |
| """); |
| assertThat(getConfiguredTarget("//test:top")).isNotNull(); |
| assertNoEvents(); |
| } |
| |
| @Test |
| public void |
| configFeatureFlagProvider_throwsErrorIfReadFlagWithNeitherDefaultNorConfiguredValueSet() |
| 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", |
| ], |
| deps = [":reader"], |
| ) |
| |
| filegroup( |
| name = "reader", |
| srcs = select({ |
| ":flag@configured": ["a.txt"], |
| "//conditions:default": ["b.txt"], |
| }), |
| transitive_configs = [ |
| ":flag", |
| ":other", |
| ], |
| ) |
| |
| config_setting( |
| name = "flag@configured", |
| flag_values = {":flag": "configured"}, |
| transitive_configs = [":flag"], |
| ) |
| |
| 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:top")).isNull(); |
| assertContainsEvent( |
| "config_setting //test:flag@configured is unresolvable because: Feature flag //test:flag" |
| + " has no default but no value was explicitly specified."); |
| } |
| |
| @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(b/140635901): 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/allowlists/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 " |
| + "this package"); |
| } |
| |
| @Test |
| public void policy_doesNotBlockRuleIfInPackageGroup() throws Exception { |
| scratch.overwriteFile( |
| "tools/allowlists/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", null, Predicates.<String>alwaysTrue())) |
| .addEqualityGroup( |
| // Will be distinct from the first group because CFFP instances are all distinct. |
| ConfigFeatureFlagProvider.create("flag1", null, Predicates.<String>alwaysTrue())) |
| .addEqualityGroup( |
| // Set the error, still distinct from the above. |
| ConfigFeatureFlagProvider.create(null, "error", Predicates.<String>alwaysTrue())) |
| .addEqualityGroup( |
| // Change the value, still distinct from the above. |
| ConfigFeatureFlagProvider.create("flag2", null, Predicates.<String>alwaysTrue())) |
| .testEquals(); |
| } |
| } |