blob: 64206bc43f8cd19244cff73455f3b22dc7c69e72 [file] [log] [blame]
// 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 [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.<String>alwaysTrue()))
.addEqualityGroup(
// Will be distinct from the first group because CFFP instances are all distinct.
ConfigFeatureFlagProvider.create("flag1", Predicates.<String>alwaysTrue()))
.addEqualityGroup(
// Change the value, still distinct from the above.
ConfigFeatureFlagProvider.create("flag2", Predicates.<String>alwaysTrue()))
.testEquals();
}
}