aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar mstaib <mstaib@google.com>2017-06-28 23:45:56 +0200
committerGravatar Marcel Hlopko <hlopko@google.com>2017-06-29 09:33:43 +0200
commit199624bdc59a36cda9920f804e9933954de6ce95 (patch)
tree39ac46a0d2004afd98000699c51818e11c4cd207 /src
parent1b9933a96e6cc4217e8d8c3e6fe0646a345c680e (diff)
Add a FeaturePolicyConfiguration for supporting limited feature rollouts.
A common need is to limit access to a rule or feature - restricting a feature which is being deprecated or controlling the rollout of a new feature. This change adds the feature_control_policy flag, which allows developers to easily manage this process. RELNOTES: None. PiperOrigin-RevId: 160454166
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/google/devtools/build/lib/BUILD2
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/BUILD26
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyConfiguration.java124
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyLoader.java165
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyOptions.java48
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntry.java33
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntryConverter.java53
-rw-r--r--src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java7
-rw-r--r--src/test/java/com/google/devtools/build/lib/BUILD2
-rw-r--r--src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/BUILD32
-rw-r--r--src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyConfigurationTest.java121
-rw-r--r--src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyLoaderTest.java322
-rw-r--r--src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyOptionsTest.java57
-rw-r--r--src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntryConverterTest.java142
-rw-r--r--src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java4
15 files changed, 1138 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index fe6e13701f..9be7d831d6 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -37,6 +37,7 @@ filegroup(
"//src/main/java/com/google/devtools/build/lib/rules/genquery:srcs",
"//src/main/java/com/google/devtools/build/lib/rules/genrule:srcs",
"//src/main/java/com/google/devtools/build/lib/rules/objc:srcs",
+ "//src/main/java/com/google/devtools/build/lib/analysis/featurecontrol:srcs",
"//src/main/java/com/google/devtools/build/lib/analysis/platform:srcs",
"//src/main/java/com/google/devtools/build/lib/rules/platform:srcs",
"//src/main/java/com/google/devtools/build/lib/sandbox:srcs",
@@ -701,6 +702,7 @@ java_library(
":util",
":vfs",
"//src/main/java/com/google/devtools/build/lib/actions",
+ "//src/main/java/com/google/devtools/build/lib/analysis/featurecontrol",
"//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
"//src/main/java/com/google/devtools/build/lib/query2:query-output",
"//src/main/java/com/google/devtools/build/lib/rules/apple",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/BUILD b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/BUILD
new file mode 100644
index 0000000000..a20987fbe4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/BUILD
@@ -0,0 +1,26 @@
+# Description:
+# Configuration mechanisms for rolling out and deprecating pieces of Bazel functionality.
+
+package(
+ default_visibility = ["//src:__subpackages__"],
+)
+
+filegroup(
+ name = "srcs",
+ srcs = glob(["**"]),
+)
+
+java_library(
+ name = "featurecontrol",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib:build-base",
+ "//src/main/java/com/google/devtools/build/lib:packages-internal",
+ "//src/main/java/com/google/devtools/build/lib:util",
+ "//src/main/java/com/google/devtools/build/lib/cmdline",
+ "//src/main/java/com/google/devtools/common/options",
+ "//third_party:auto_value",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ ],
+)
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyConfiguration.java
new file mode 100644
index 0000000000..66e91ca127
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyConfiguration.java
@@ -0,0 +1,124 @@
+// 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.analysis.featurecontrol;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+import com.google.devtools.build.lib.util.Preconditions;
+
+/**
+ * A configuration fragment which controls access to features being rolled out or deprecated,
+ * allowing such features to be limited to particular packages to avoid excessive spread when
+ * rolling out or deprecating a feature. It's controlled by {@link FeaturePolicyOptions}.
+ *
+ * <p>A "feature", for this package's purposes, is any combination of related functionality which
+ * should be limited to specified packages. Because this is part of the configuration, it can only
+ * be accessed during the analysis phase; decisions which are made during the loading phase can't
+ * use this information. Some examples of use cases for this fragment:
+ * <ul>
+ * <li>A new rule class which could have a major impact on Blaze's memory usage is added. To
+ * limit this impact during the experimental phase, a feature policy is added which makes it
+ * an error for rules of that class to be created - or used by other rules - in any package
+ * other than those defined by the policy. The policy is populated with projects who are
+ * doing guided experimentation with the feature, and gradually expands as the feature rolls
+ * out. Then the feature's policy can be removed, making it generally available.
+ * <li>An attribute is being deprecated. To prevent rollback, a feature policy is added which
+ * makes it an error for rules in packages other than those defined by the policy to specify
+ * a value for that attribute. The policy is populated with existing users, and as those
+ * users are migrated, they are removed from the policy until it is completely empty. Then
+ * the attribute can be removed entirely.
+ * </ul>
+ *
+ * <p>To use this package:
+ * <ol>
+ * <li>Define a feature ID in the {@link FeaturePolicyLoader}'s constructor (in the
+ * RuleClassProvider). This is the string that will be used when checking for the feature in
+ * rule code, as well as the string used in the flag value for {@link FeaturePolicyOptions}.
+ * <li>In the RuleClass(es) which will change based on the feature's state, declare
+ * {@link FeaturePolicyConfiguration} as a required configuration fragment.
+ * <li>In the ConfiguredTargetFactory of those rules, get the FeaturePolicyConfiguration and
+ * check {@link #isFeatureEnabledForRule(String,Label)} with the feature ID created in
+ * step 1 and the label of the current rule. In the event that an error needs to be
+ * displayed, use {@link #getPolicyForFeature(String)} to show the user where the policy is.
+ * <li>Create a package_group containing the list of packages which should have access to this
+ * feature. It can be empty (no packages can access the feature) or contain //... (all
+ * packages can access the feature) to begin with.
+ * <li>After the a release containing the feature ID has been pushed, update the global RC file
+ * with a --feature_control_policy=(your_feature)=(your_package_group) flag. You can now
+ * alter access to your feature by changing the package_group.
+ * </ol>
+ *
+ * <p>To stop using this package:
+ * <ol>
+ * <li>Your policy should be at an end state - containing all packages (a rollout which has
+ * become generally available) or no packages (a deprecated feature which has been totally
+ * cleaned up).
+ * <li>Make the behavior the policy controlled permanent - remove a deprecated feature, or
+ * remove the check on a feature which is being rolled out.
+ * <li>After this new version is released, remove the flag from the global rc file, and remove
+ * the feature ID from the constructor for {@link FeaturePolicyLoader}.
+ * </ol>
+ *
+ * @see FeaturePolicyLoader
+ */
+public final class FeaturePolicyConfiguration extends BuildConfiguration.Fragment {
+
+ private final ImmutableSetMultimap<String, PackageSpecification> features;
+ private final ImmutableMap<String, String> policies;
+
+ /**
+ * Creates a new FeaturePolicyConfiguration.
+ *
+ * @param features Map mapping from a feature ID to the packages which are able to access it. If a
+ * feature ID is not present in this mapping, it is not accessible from any package.
+ * @param policies Map mapping from a feature ID to a string policy to be shown to the user. A
+ * string must be present as a key in this mapping to be considered a valid feature ID.
+ */
+ public FeaturePolicyConfiguration(
+ ImmutableSetMultimap<String, PackageSpecification> features,
+ ImmutableMap<String, String> policies) {
+ this.features = features;
+ this.policies = policies;
+ }
+
+ /**
+ * Returns whether, according to the current policy, the given feature is enabled for the given
+ * rule.
+ */
+ public boolean isFeatureEnabledForRule(String feature, Label rule) {
+ Preconditions.checkArgument(policies.containsKey(feature), "No such feature: %s", feature);
+ ImmutableSet<PackageSpecification> result = features.get(feature);
+ for (PackageSpecification spec : result) {
+ if (spec.containsPackage(rule.getPackageIdentifier())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a String suitable for presenting to the user which represents the policy used for the
+ * given feature.
+ */
+ public String getPolicyForFeature(String feature) {
+ String result = policies.get(feature);
+ Preconditions.checkArgument(result != null, "No such feature: %s", feature);
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyLoader.java b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyLoader.java
new file mode 100644
index 0000000000..9e2c856835
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyLoader.java
@@ -0,0 +1,165 @@
+// 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.analysis.featurecontrol;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.devtools.build.lib.analysis.RedirectChaser;
+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.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.util.Preconditions;
+import java.util.ArrayDeque;
+import java.util.LinkedHashSet;
+import java.util.Queue;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/** A loader for the FeaturePolicyConfiguration fragment. */
+public final class FeaturePolicyLoader implements ConfigurationFragmentFactory {
+
+ private final ImmutableSet<String> permittedFeatures;
+
+ public FeaturePolicyLoader(Iterable<String> permittedFeatures) {
+ this.permittedFeatures = ImmutableSet.copyOf(permittedFeatures);
+ for (String permittedFeature : this.permittedFeatures) {
+ Preconditions.checkArgument(
+ !permittedFeature.contains("="), "Feature names cannot contain =");
+ }
+ }
+
+ @Override
+ @Nullable
+ public BuildConfiguration.Fragment create(ConfigurationEnvironment env, BuildOptions buildOptions)
+ throws InvalidConfigurationException, InterruptedException {
+ ImmutableSetMultimap.Builder<String, PackageSpecification> features =
+ new ImmutableSetMultimap.Builder<>();
+ ImmutableMap.Builder<String, String> policies = new ImmutableMap.Builder<>();
+ LinkedHashSet<String> unseenFeatures = new LinkedHashSet<>(permittedFeatures);
+
+ for (PolicyEntry entry : buildOptions.get(FeaturePolicyOptions.class).policies) {
+ if (!permittedFeatures.contains(entry.getFeature())) {
+ // It would be so nice to be able to do this during parsing... but because options are
+ // parsed statically through reflection, we have no way of doing this until we get to the
+ // configuration loader.
+ throw new InvalidConfigurationException("No such feature: " + entry.getFeature());
+ }
+ if (!unseenFeatures.remove(entry.getFeature())) {
+ // TODO(mstaib): Perhaps we should allow for overriding or concatenation here?
+ throw new InvalidConfigurationException(
+ "Multiple definitions of the rollout policy for feature "
+ + entry.getFeature()
+ + ". To use multiple package_groups, use a package_group with the includes "
+ + "attribute instead.");
+ }
+
+ Iterable<PackageSpecification> packageSpecifications =
+ getAllPackageSpecificationsForPackageGroup(
+ env, entry.getPackageGroupLabel(), entry.getFeature());
+
+ if (packageSpecifications == null) {
+ return null;
+ }
+
+ features.putAll(entry.getFeature(), packageSpecifications);
+ policies.put(entry.getFeature(), entry.getPackageGroupLabel().toString());
+ }
+
+ // Default to universal access for all features not declared.
+ for (String unseenFeature : unseenFeatures) {
+ features.put(unseenFeature, PackageSpecification.everything());
+ policies.put(unseenFeature, "//...");
+ }
+
+ return new FeaturePolicyConfiguration(features.build(), policies.build());
+ }
+
+ /**
+ * Evaluates, recursively, the given package group. Returns {@code null} in the case of missing
+ * Skyframe dependencies.
+ */
+ @Nullable
+ private static Iterable<PackageSpecification> getAllPackageSpecificationsForPackageGroup(
+ ConfigurationEnvironment env, Label packageGroupLabel, String feature)
+ throws InvalidConfigurationException, InterruptedException {
+ String context = feature + " feature policy";
+ Label actualPackageGroupLabel = RedirectChaser.followRedirects(env, packageGroupLabel, context);
+ if (actualPackageGroupLabel == null) {
+ return null;
+ }
+
+ ImmutableSet.Builder<PackageSpecification> packages = new ImmutableSet.Builder<>();
+ Set<Label> alreadyVisitedPackageGroups = new LinkedHashSet<>();
+ Queue<Label> packageGroupsToVisit = new ArrayDeque<>();
+ packageGroupsToVisit.add(actualPackageGroupLabel);
+ alreadyVisitedPackageGroups.add(actualPackageGroupLabel);
+
+ while (!packageGroupsToVisit.isEmpty()) {
+ Target target;
+ try {
+ // This is guaranteed to succeed, because the RedirectChaser was used to get this label,
+ // and it will throw an InvalidConfigurationException if the target doesn't exist.
+ target = env.getTarget(packageGroupsToVisit.remove());
+ } catch (NoSuchPackageException | NoSuchTargetException impossible) {
+ throw new AssertionError(impossible);
+ }
+ if (target == null) {
+ return null;
+ }
+
+ if (!(target instanceof PackageGroup)) {
+ throw new InvalidConfigurationException(
+ target.getLabel() + " is not a package_group in " + context);
+ }
+
+ PackageGroup packageGroup = (PackageGroup) target;
+
+ packages.addAll(packageGroup.getPackageSpecifications());
+
+ for (Label include : packageGroup.getIncludes()) {
+ Label actualInclude = RedirectChaser.followRedirects(env, include, context);
+ if (actualInclude == null) {
+ return null;
+ }
+
+ if (alreadyVisitedPackageGroups.add(actualInclude)) {
+ packageGroupsToVisit.add(actualInclude);
+ }
+ }
+ }
+
+ return packages.build();
+ }
+
+ @Override
+ public Class<? extends BuildConfiguration.Fragment> creates() {
+ return FeaturePolicyConfiguration.class;
+ }
+
+ @Override
+ public ImmutableSet<Class<? extends FragmentOptions>> requiredOptions() {
+ return ImmutableSet.<Class<? extends FragmentOptions>>of(FeaturePolicyOptions.class);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyOptions.java
new file mode 100644
index 0000000000..2b43fc2e06
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyOptions.java
@@ -0,0 +1,48 @@
+// 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.analysis.featurecontrol;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsParser.OptionUsageRestrictions;
+import java.util.List;
+
+/** The options fragment which defines {@link FeaturePolicyConfiguration}. */
+public final class FeaturePolicyOptions extends FragmentOptions {
+ /** The mapping from features to their associated package groups. */
+ @Option(
+ name = "feature_control_policy",
+ help =
+ "Policy used to limit the rollout or deprecation of features within the Bazel binary to "
+ + "specific packages. Pass a mapping from a feature name to the package group used to "
+ + "control access to that feature, in the form feature=//label/of:package_group (note "
+ + "that unlike visibility, packages cannot be directly specified a la "
+ + "//package:__pkg__ or //visibility:public). Can be repeated to specify multiple "
+ + "features, but each feature must be specified only once.",
+ valueHelp = "a feature=label pair",
+ optionUsageRestrictions = OptionUsageRestrictions.UNDOCUMENTED,
+ converter = PolicyEntryConverter.class,
+ defaultValue = "n/a (default ignored for allowMultiple)",
+ allowMultiple = true
+ )
+ public List<PolicyEntry> policies = ImmutableList.<PolicyEntry>of();
+
+ @Override
+ public FeaturePolicyOptions getHost(boolean fallback) {
+ // host options are the same as target options
+ return (FeaturePolicyOptions) clone();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntry.java b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntry.java
new file mode 100644
index 0000000000..e0982a25df
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntry.java
@@ -0,0 +1,33 @@
+// 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.analysis.featurecontrol;
+
+import com.google.auto.value.AutoValue;
+import com.google.devtools.build.lib.cmdline.Label;
+
+/** Policy value object encoding the package group which can access a given feature. */
+@AutoValue
+public abstract class PolicyEntry {
+ /** Creates a new PolicyEntry for the given feature and package_group label. */
+ public static PolicyEntry create(String feature, Label packageGroupLabel) {
+ return new AutoValue_PolicyEntry(feature, packageGroupLabel);
+ }
+
+ /** Gets the feature identifier this policy is for. */
+ public abstract String getFeature();
+
+ /** Gets the label for the package group which controls access to this feature. */
+ public abstract Label getPackageGroupLabel();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntryConverter.java b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntryConverter.java
new file mode 100644
index 0000000000..a8f997b76b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntryConverter.java
@@ -0,0 +1,53 @@
+// 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.analysis.featurecontrol;
+
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.OptionsParsingException;
+
+/** A converter which creates a PolicyEntry from a flag value. */
+public final class PolicyEntryConverter implements Converter<PolicyEntry> {
+ @Override
+ public PolicyEntry convert(String input) throws OptionsParsingException {
+ int divider = input.indexOf('=');
+ if (divider == -1) {
+ throw new OptionsParsingException(
+ "value must be of the form feature=label; missing =");
+ }
+ String feature = input.substring(0, divider);
+ if (feature.isEmpty()) {
+ throw new OptionsParsingException(
+ "value must be of the form feature=label; feature cannot be empty");
+ }
+ String label = input.substring(divider + 1);
+ if (label.isEmpty()) {
+ throw new OptionsParsingException(
+ "value must be of the form feature=label; label cannot be empty");
+ }
+ try {
+ return PolicyEntry.create(feature, Label.parseAbsolute(label));
+ } catch (LabelSyntaxException ex) {
+ throw new OptionsParsingException(
+ "value must be of the form feature=label; " + ex.getMessage());
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a feature=label pair";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
index ff1542b565..354442c871 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
@@ -27,6 +27,8 @@ import com.google.devtools.build.lib.analysis.PlatformConfigurationLoader;
import com.google.devtools.build.lib.analysis.PlatformOptions;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.constraints.EnvironmentRule;
+import com.google.devtools.build.lib.analysis.featurecontrol.FeaturePolicyLoader;
+import com.google.devtools.build.lib.analysis.featurecontrol.FeaturePolicyOptions;
import com.google.devtools.build.lib.bazel.rules.BazelToolchainType.BazelToolchainTypeRule;
import com.google.devtools.build.lib.bazel.rules.android.AndroidNdkRepositoryRule;
import com.google.devtools.build.lib.bazel.rules.android.AndroidSdkRepositoryRule;
@@ -222,10 +224,15 @@ public class BazelRuleClassProvider {
}
};
+ public static final ImmutableSet<String> FEATURE_POLICY_FEATURES = ImmutableSet.<String>of();
+
public static final RuleSet CORE_RULES =
new RuleSet() {
@Override
public void init(Builder builder) {
+ builder.addConfigurationOptions(FeaturePolicyOptions.class);
+ builder.addConfigurationFragment(new FeaturePolicyLoader(FEATURE_POLICY_FEATURES));
+
builder.addRuleDefinition(new BaseRuleClasses.RootRule());
builder.addRuleDefinition(new BaseRuleClasses.BaseRule());
builder.addRuleDefinition(new BaseRuleClasses.RuleBase());
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index e90994734e..a33d48ffe1 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -38,6 +38,7 @@ filegroup(
"//src/test/java/com/google/devtools/build/lib/skyframe:srcs",
"//src/test/java/com/google/devtools/build/lib/rules/android:srcs",
"//src/test/java/com/google/devtools/build/lib/rules/config:srcs",
+ "//src/test/java/com/google/devtools/build/lib/analysis/featurecontrol:srcs",
"//src/test/java/com/google/devtools/build/lib/analysis/platform:srcs",
"//src/test/java/com/google/devtools/build/lib/rules/platform:srcs",
"//src/test/java/com/google/devtools/build/lib/rules/repository:srcs",
@@ -359,6 +360,7 @@ java_library(
"//src/main/java/com/google/devtools/build/lib:util",
"//src/main/java/com/google/devtools/build/lib:vfs",
"//src/main/java/com/google/devtools/build/lib/actions",
+ "//src/main/java/com/google/devtools/build/lib/analysis/featurecontrol",
"//src/main/java/com/google/devtools/build/lib/query2",
"//src/main/java/com/google/devtools/build/lib/query2:query-output",
"//src/main/java/com/google/devtools/build/lib/rules/apple",
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/BUILD b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/BUILD
new file mode 100644
index 0000000000..2f26a91fd6
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/BUILD
@@ -0,0 +1,32 @@
+# Description:
+# Tests for configuration mechanisms for rolling out and deprecating pieces of Bazel functionality.
+
+licenses(["notice"]) # Apache 2.0
+
+filegroup(
+ name = "srcs",
+ srcs = glob(
+ ["**"],
+ ),
+ visibility = ["//src/test/java/com/google/devtools/build/lib:__pkg__"],
+)
+
+java_test(
+ name = "FeatureRestrictionTests",
+ srcs = glob(["*.java"]),
+ test_class = "com.google.devtools.build.lib.AllTests",
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib:build-base",
+ "//src/main/java/com/google/devtools/build/lib:packages-internal",
+ "//src/main/java/com/google/devtools/build/lib/analysis/featurecontrol",
+ "//src/main/java/com/google/devtools/build/lib/cmdline",
+ "//src/main/java/com/google/devtools/common/options",
+ "//src/test/java/com/google/devtools/build/lib:analysis_testutil",
+ "//src/test/java/com/google/devtools/build/lib:packages_testutil",
+ "//src/test/java/com/google/devtools/build/lib:test_runner",
+ "//third_party:guava",
+ "//third_party:guava-testlib",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyConfigurationTest.java b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyConfigurationTest.java
new file mode 100644
index 0000000000..eaaf438215
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyConfigurationTest.java
@@ -0,0 +1,121 @@
+// 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.analysis.featurecontrol;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the FeaturePolicyConfiguration. */
+@RunWith(JUnit4.class)
+public final class FeaturePolicyConfigurationTest {
+
+ @Test
+ public void isFeatureEnabledForRule_FalseIfAbsentFromFeatureMap() throws Exception {
+ FeaturePolicyConfiguration config =
+ new FeaturePolicyConfiguration(
+ ImmutableSetMultimap.<String, PackageSpecification>of(),
+ ImmutableMap.of("newFeature", "//package_group:empty"));
+
+ assertThat(config.isFeatureEnabledForRule("newFeature", Label.parseAbsolute("//:rule")))
+ .isFalse();
+ }
+
+ @Test
+ public void isFeatureEnabledForRule_TrueIfMappedToEverything() throws Exception {
+ FeaturePolicyConfiguration config =
+ new FeaturePolicyConfiguration(
+ ImmutableSetMultimap.of("newFeature", PackageSpecification.everything()),
+ ImmutableMap.of("newFeature", "//..."));
+
+ assertThat(config.isFeatureEnabledForRule("newFeature", Label.parseAbsolute("//:rule")))
+ .isTrue();
+ }
+
+ @Test
+ public void isFeatureEnabledForRule_TrueIfInPackageSpecification() throws Exception {
+ FeaturePolicyConfiguration config =
+ new FeaturePolicyConfiguration(
+ ImmutableSetMultimap.of(
+ "newFeature",
+ PackageSpecification.fromString(RepositoryName.MAIN, "//allowed/...")),
+ ImmutableMap.of("newFeature", "//allowed/..."));
+
+ assertThat(config.isFeatureEnabledForRule("newFeature", Label.parseAbsolute("//allowed:rule")))
+ .isTrue();
+ }
+
+ @Test
+ public void isFeatureEnabledForRule_FalseIfNotInPackageSpecification() throws Exception {
+ FeaturePolicyConfiguration config =
+ new FeaturePolicyConfiguration(
+ ImmutableSetMultimap.of(
+ "newFeature",
+ PackageSpecification.fromString(RepositoryName.MAIN, "//allowed/...")),
+ ImmutableMap.of("newFeature", "//allowed/..."));
+
+ assertThat(
+ config.isFeatureEnabledForRule("newFeature", Label.parseAbsolute("//forbidden:rule")))
+ .isFalse();
+ }
+
+ @Test
+ public void isFeatureEnabledForRule_FailsIfNotPresentInPolicyList() throws Exception {
+ FeaturePolicyConfiguration config =
+ new FeaturePolicyConfiguration(
+ ImmutableSetMultimap.of("newFeature", PackageSpecification.everything()),
+ ImmutableMap.<String, String>of());
+
+ try {
+ config.isFeatureEnabledForRule("newFeature", Label.parseAbsolute("//:rule"));
+ fail("Expected an exception.");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected).hasMessageThat().contains("No such feature: newFeature");
+ }
+ }
+
+ @Test
+ public void getPolicyForFeature_ReturnsValueFromPolicy() throws Exception {
+ FeaturePolicyConfiguration config =
+ new FeaturePolicyConfiguration(
+ ImmutableSetMultimap.of("newFeature", PackageSpecification.everything()),
+ ImmutableMap.<String, String>of("newFeature", "my policy"));
+
+ assertThat(config.getPolicyForFeature("newFeature")).isEqualTo("my policy");
+ }
+
+ @Test
+ public void getPolicyForFeature_FailsIfNotPresentInPolicyList() throws Exception {
+ FeaturePolicyConfiguration config =
+ new FeaturePolicyConfiguration(
+ ImmutableSetMultimap.of("newFeature", PackageSpecification.everything()),
+ ImmutableMap.<String, String>of());
+
+ try {
+ config.getPolicyForFeature("newFeature");
+ fail("Expected an exception.");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected).hasMessageThat().contains("No such feature: newFeature");
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyLoaderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyLoaderTest.java
new file mode 100644
index 0000000000..a28752a0fd
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyLoaderTest.java
@@ -0,0 +1,322 @@
+// 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.analysis.featurecontrol;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the FeaturePolicyLoader. */
+@RunWith(JUnit4.class)
+public final class FeaturePolicyLoaderTest extends AnalysisTestCase {
+
+ private FeaturePolicyConfiguration getFragmentWithFeatures(
+ Iterable<String> allowedFeatures, Collection<String> args) throws Exception {
+ ConfigurationEnvironment env =
+ new ConfigurationEnvironment.TargetProviderEnvironment(
+ skyframeExecutor.getPackageManager(), reporter, directories);
+ BuildOptions options =
+ BuildOptions.of(
+ ImmutableList.<Class<? extends FragmentOptions>>of(FeaturePolicyOptions.class),
+ args.toArray(new String[0]));
+ return (FeaturePolicyConfiguration)
+ new FeaturePolicyLoader(allowedFeatures).create(env, options);
+ }
+
+ @Test
+ public void specifiedFeaturesGetListedAccessPolicy() throws Exception {
+ scratch.file(
+ "policy/BUILD",
+ "package_group(",
+ " name='featured',",
+ " packages=[",
+ " '//direct',",
+ " '//recursive/...',",
+ " ])");
+ FeaturePolicyConfiguration fragment =
+ getFragmentWithFeatures(
+ ImmutableSet.of("defaultFeature", "policiedFeature"),
+ ImmutableList.of("--feature_control_policy=policiedFeature=//policy:featured"));
+ assertThat(fragment.getPolicyForFeature("policiedFeature")).isEqualTo("//policy:featured");
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//:rule")))
+ .isFalse();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//arbitrary:rule")))
+ .isFalse();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//policy:featured")))
+ .isFalse();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//direct:allow")))
+ .isTrue();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//direct/child:allow")))
+ .isFalse();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//recursive:allow")))
+ .isTrue();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//recursive/child:allow")))
+ .isTrue();
+ }
+
+ @Test
+ public void resolvesIncludedPackageGroups() throws Exception {
+ scratch.file(
+ "policy/BUILD",
+ "package_group(",
+ " name='main',",
+ " packages=['//a'],",
+ " includes=[':include'])",
+ "package_group(",
+ " name='include',",
+ " packages=['//b'])");
+ FeaturePolicyConfiguration fragment =
+ getFragmentWithFeatures(
+ ImmutableSet.of("defaultFeature", "policiedFeature"),
+ ImmutableList.of("--feature_control_policy=policiedFeature=//policy:main"));
+ assertThat(fragment.getPolicyForFeature("policiedFeature")).isEqualTo("//policy:main");
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//arbitrary:rule")))
+ .isFalse();
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//a:a")))
+ .isTrue();
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//b:b")))
+ .isTrue();
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//c:c")))
+ .isFalse();
+ }
+
+ @Test
+ public void resolvesAliasToPolicy() throws Exception {
+ scratch.file(
+ "policy/BUILD",
+ "alias(",
+ " name='aliased',",
+ " actual=':featured')",
+ "package_group(",
+ " name='featured',",
+ " packages=[",
+ " '//direct',",
+ " '//recursive/...',",
+ " ])");
+ FeaturePolicyConfiguration fragment =
+ getFragmentWithFeatures(
+ ImmutableSet.of("defaultFeature", "policiedFeature"),
+ ImmutableList.of("--feature_control_policy=policiedFeature=//policy:aliased"));
+ assertThat(fragment.getPolicyForFeature("policiedFeature")).isEqualTo("//policy:aliased");
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//arbitrary:rule")))
+ .isFalse();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//policy:aliased")))
+ .isFalse();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//policy:featured")))
+ .isFalse();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//direct:allow")))
+ .isTrue();
+ }
+
+ @Test
+ public void resolvesAliasToIncludesInPackageGroups() throws Exception {
+ scratch.file(
+ "policy/BUILD",
+ "package_group(",
+ " name='main',",
+ " packages=['//a'],",
+ " includes=[':aliased'])",
+ "alias(",
+ " name='aliased',",
+ " actual=':include')",
+ "package_group(",
+ " name='include',",
+ " packages=['//b'])");
+ FeaturePolicyConfiguration fragment =
+ getFragmentWithFeatures(
+ ImmutableSet.of("defaultFeature", "policiedFeature"),
+ ImmutableList.of("--feature_control_policy=policiedFeature=//policy:main"));
+ assertThat(fragment.getPolicyForFeature("policiedFeature")).isEqualTo("//policy:main");
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//arbitrary:rule")))
+ .isFalse();
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//a:a")))
+ .isTrue();
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//b:b")))
+ .isTrue();
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//c:c")))
+ .isFalse();
+ }
+
+ @Test
+ public void allowsCyclesInPackageGroupsAndCoversAllMembersOfCycle() throws Exception {
+ scratch.file(
+ "policy/BUILD",
+ "package_group(",
+ " name='cycle',",
+ " packages=['//a'],",
+ " includes=[':elcyc'])",
+ "package_group(",
+ " name='elcyc',",
+ " packages=['//b'],",
+ " includes=[':cycle'])");
+ FeaturePolicyConfiguration fragment =
+ getFragmentWithFeatures(
+ ImmutableSet.of("defaultFeature", "policiedFeature"),
+ ImmutableList.of("--feature_control_policy=policiedFeature=//policy:cycle"));
+ assertThat(fragment.getPolicyForFeature("policiedFeature")).isEqualTo("//policy:cycle");
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "policiedFeature", Label.parseAbsolute("//arbitrary:rule")))
+ .isFalse();
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//a:a")))
+ .isTrue();
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//b:b")))
+ .isTrue();
+ assertThat(fragment.isFeatureEnabledForRule("policiedFeature", Label.parseAbsolute("//c:c")))
+ .isFalse();
+ }
+
+ @Test
+ public void unspecifiedFeaturesGetUniversalAccessPolicy() throws Exception {
+ scratch.file("null/BUILD", "package_group(name='null', packages=[])");
+ FeaturePolicyConfiguration fragment =
+ getFragmentWithFeatures(
+ ImmutableSet.of("defaultFeature", "policiedFeature"),
+ ImmutableList.of("--feature_control_policy=policiedFeature=//null:null"));
+ assertThat(fragment.getPolicyForFeature("defaultFeature")).isEqualTo("//...");
+ assertThat(fragment.isFeatureEnabledForRule("defaultFeature", Label.parseAbsolute("//:rule")))
+ .isTrue();
+ assertThat(
+ fragment.isFeatureEnabledForRule(
+ "defaultFeature", Label.parseAbsolute("//arbitrary:rule")))
+ .isTrue();
+ }
+
+ @Test
+ public void throwsForFeatureWithMultiplePolicyDefinitions() throws Exception {
+ scratch.file(
+ "null/BUILD",
+ "package_group(name='null', packages=[])",
+ "package_group(name='empty', packages=[])");
+ try {
+ getFragmentWithFeatures(
+ ImmutableSet.of("duplicateFeature"),
+ ImmutableList.of(
+ "--feature_control_policy=duplicateFeature=//null:null",
+ "--feature_control_policy=duplicateFeature=//null:empty"));
+ fail("Expected an exception");
+ } catch (InvalidConfigurationException expected) {
+ assertThat(expected).hasMessageThat().contains("Multiple definitions");
+ assertThat(expected).hasMessageThat().contains("duplicateFeature");
+ }
+ }
+
+ @Test
+ public void throwsForFeatureNotSpecifiedInLoader() throws Exception {
+ scratch.file("null/BUILD", "package_group(name='null', packages=[])");
+ try {
+ getFragmentWithFeatures(
+ ImmutableSet.of("otherFeature"),
+ ImmutableList.of("--feature_control_policy=missingFeature=//null:null"));
+ fail("Expected an exception");
+ } catch (InvalidConfigurationException expected) {
+ assertThat(expected).hasMessageThat().contains("No such feature");
+ assertThat(expected).hasMessageThat().contains("missingFeature");
+ }
+ }
+
+ @Test
+ public void throwsForFeatureWithNonexistentPolicy() throws Exception {
+ scratch.file("null/BUILD", "package_group(name='null', packages=[])");
+ try {
+ getFragmentWithFeatures(
+ ImmutableSet.of("brokenFeature"),
+ ImmutableList.of("--feature_control_policy=brokenFeature=//null:missing"));
+ fail("Expected an exception");
+ } catch (InvalidConfigurationException expected) {
+ assertThat(expected).hasMessageThat().contains("no such target '//null:missing'");
+ }
+ }
+
+ @Test
+ public void throwsForFeatureWithPolicyInNonexistentPackage() throws Exception {
+ try {
+ getFragmentWithFeatures(
+ ImmutableSet.of("brokenFeature"),
+ ImmutableList.of("--feature_control_policy=brokenFeature=//missing:missing"));
+ fail("Expected an exception");
+ } catch (InvalidConfigurationException expected) {
+ assertThat(expected).hasMessageThat().contains("no such package 'missing'");
+ }
+ }
+
+ @Test
+ public void throwsForFeatureWithNonPackageGroupPolicy() throws Exception {
+ scratch.file("policy/BUILD", "filegroup(name='non_package_group')");
+ try {
+ getFragmentWithFeatures(
+ ImmutableSet.of("brokenFeature"),
+ ImmutableList.of("--feature_control_policy=brokenFeature=//policy:non_package_group"));
+ fail("Expected an exception");
+ } catch (InvalidConfigurationException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .contains(
+ "//policy:non_package_group is not a package_group in brokenFeature feature policy");
+ }
+ }
+
+ @Test
+ public void throwsForFeatureWithNonRulePolicy() throws Exception {
+ scratch.file("policy/BUILD", "exports_files(['not_even_a_rule'])");
+ try {
+ getFragmentWithFeatures(
+ ImmutableSet.of("brokenFeature"),
+ ImmutableList.of("--feature_control_policy=brokenFeature=//policy:not_even_a_rule"));
+ fail("Expected an exception");
+ } catch (InvalidConfigurationException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .contains(
+ "//policy:not_even_a_rule is not a package_group in brokenFeature feature policy");
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyOptionsTest.java b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyOptionsTest.java
new file mode 100644
index 0000000000..72f17c66d2
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/FeaturePolicyOptionsTest.java
@@ -0,0 +1,57 @@
+// 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.analysis.featurecontrol;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.common.options.Options;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the FeaturePolicyOptions. */
+@RunWith(JUnit4.class)
+public final class FeaturePolicyOptionsTest {
+
+ @Test
+ public void testEquality() throws Exception {
+ new EqualsTester()
+ .addEqualityGroup(
+ Options.getDefaults(FeaturePolicyOptions.class),
+ Options.getDefaults(FeaturePolicyOptions.class))
+ .addEqualityGroup(
+ Options.parse(
+ FeaturePolicyOptions.class,
+ "--feature_control_policy=feature=//test:rest",
+ "--feature_control_policy=future=//nest:best")
+ .getOptions())
+ .testEquals();
+ }
+
+ @Test
+ public void testHostVersionCopiesPolicies() throws Exception {
+ FeaturePolicyOptions base =
+ Options.parse(
+ FeaturePolicyOptions.class,
+ "--feature_control_policy=feature=//test:rest",
+ "--feature_control_policy=future=//nest:best")
+ .getOptions();
+ FeaturePolicyOptions host = base.getHost(false);
+ FeaturePolicyOptions hostFallback = base.getHost(true);
+ assertThat(host.policies).containsExactlyElementsIn(base.policies);
+ assertThat(hostFallback.policies).containsExactlyElementsIn(base.policies);
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntryConverterTest.java b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntryConverterTest.java
new file mode 100644
index 0000000000..d673450efb
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/featurecontrol/PolicyEntryConverterTest.java
@@ -0,0 +1,142 @@
+// 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.analysis.featurecontrol;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.common.options.OptionsParsingException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the policy entry option converter. */
+@RunWith(JUnit4.class)
+public final class PolicyEntryConverterTest {
+
+ @Test
+ public void convert_successfullyConvertsValidPair() throws Exception {
+ assertThat(new PolicyEntryConverter().convert("jeepers=//creepers:peepers"))
+ .isEqualTo(PolicyEntry.create("jeepers", Label.parseAbsolute("//creepers:peepers")));
+ }
+
+ @Test
+ public void convert_successfullyConvertsValidLabelWithEqualsSign() throws Exception {
+ assertThat(new PolicyEntryConverter().convert("slamjam=//whoomp:it=there"))
+ .isEqualTo(PolicyEntry.create("slamjam", Label.parseAbsolute("//whoomp:it=there")));
+ }
+
+ @Test
+ public void convert_acceptsBarePackageNameAsDefaultTarget() throws Exception {
+ assertThat(new PolicyEntryConverter().convert("two=//go"))
+ .isEqualTo(PolicyEntry.create("two", Label.parseAbsolute("//go:go")));
+ }
+
+ @Test
+ public void convert_acceptsRepositoryPrefixedLabel() throws Exception {
+ assertThat(new PolicyEntryConverter().convert("here=@somewhere//else:where"))
+ .isEqualTo(PolicyEntry.create("here", Label.parseAbsolute("@somewhere//else:where")));
+ }
+
+ @Test
+ public void convert_acceptsBareRepository() throws Exception {
+ assertThat(new PolicyEntryConverter().convert("aliens=@space"))
+ .isEqualTo(PolicyEntry.create("aliens", Label.parseAbsolute("@space//:space")));
+ }
+
+ @Test
+ public void convert_failsToConvertWithoutDivider() throws Exception {
+ try {
+ new PolicyEntryConverter().convert("hack//sign:on");
+ fail("Expected an exception.");
+ } catch (OptionsParsingException expected) {
+ assertThat(expected).hasMessageThat().contains("missing =");
+ }
+ }
+
+ @Test
+ public void convert_failsToConvertLabelAlone() throws Exception {
+ try {
+ new PolicyEntryConverter().convert("//plain:label");
+ fail("Expected an exception.");
+ } catch (OptionsParsingException expected) {
+ assertThat(expected).hasMessageThat().contains("missing =");
+ }
+ }
+
+ @Test
+ public void convert_failsToConvertFeatureIdAlone() throws Exception {
+ try {
+ new PolicyEntryConverter().convert("plainFeature");
+ fail("Expected an exception.");
+ } catch (OptionsParsingException expected) {
+ assertThat(expected).hasMessageThat().contains("missing =");
+ }
+ }
+
+ @Test
+ public void convert_failsToConvertEmptyFeature() throws Exception {
+ try {
+ new PolicyEntryConverter().convert("=//R1:C1");
+ fail("Expected an exception.");
+ } catch (OptionsParsingException expected) {
+ assertThat(expected).hasMessageThat().contains("feature cannot be empty");
+ }
+ }
+
+ @Test
+ public void convert_failsToConvertEmptyLabel() throws Exception {
+ try {
+ new PolicyEntryConverter().convert("warp=");
+ fail("Expected an exception.");
+ } catch (OptionsParsingException expected) {
+ assertThat(expected).hasMessageThat().contains("label cannot be empty");
+ }
+ }
+
+ @Test
+ public void convert_failsToConvertInvalidLabel() throws Exception {
+ try {
+ new PolicyEntryConverter().convert("wrong=//wrong:wrong//wrong");
+ fail("Expected an exception.");
+ } catch (OptionsParsingException expected) {
+ assertThat(expected).hasMessageThat()
+ .contains("target names may not contain '//' path separators");
+ }
+ }
+
+ @Test
+ public void convert_failsToConvertRelativeLabel() throws Exception {
+ try {
+ new PolicyEntryConverter().convert("wrong=wrong:wrong");
+ fail("Expected an exception.");
+ } catch (OptionsParsingException expected) {
+ assertThat(expected).hasMessageThat()
+ .contains("invalid label: wrong:wrong");
+ }
+ }
+
+ @Test
+ public void convert_failsToConvertFilesystemPathLabel() throws Exception {
+ try {
+ new PolicyEntryConverter().convert("wrong=/usr/local/google/tmp/filename.ext");
+ fail("Expected an exception.");
+ } catch (OptionsParsingException expected) {
+ assertThat(expected).hasMessageThat()
+ .contains("invalid label: /usr/local/google/tmp/filename.ext");
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
index 1c8b7fcd45..34cfc71313 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
@@ -13,6 +13,8 @@
// limitations under the License.
package com.google.devtools.build.lib.analysis.mock;
+import static com.google.devtools.build.lib.bazel.rules.BazelRuleClassProvider.FEATURE_POLICY_FEATURES;
+
import com.google.common.base.Functions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -22,6 +24,7 @@ import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.PlatformConfigurationLoader;
import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.featurecontrol.FeaturePolicyLoader;
import com.google.devtools.build.lib.analysis.util.AnalysisMock;
import com.google.devtools.build.lib.bazel.rules.BazelConfiguration;
import com.google.devtools.build.lib.bazel.rules.BazelConfigurationCollection;
@@ -271,6 +274,7 @@ public final class BazelAnalysisMock extends AnalysisMock {
new J2ObjcConfiguration.Loader(),
new ProtoConfiguration.Loader(),
new ConfigFeatureFlagConfiguration.Loader(),
+ new FeaturePolicyLoader(FEATURE_POLICY_FEATURES),
new AndroidConfiguration.Loader(),
new PlatformConfigurationLoader());
}