blob: 053f2e89f189f5460acb3d08eb17141636208233 [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.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();
}
}