| // 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.runtime; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; |
| import static org.junit.Assert.fail; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy; |
| import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.UseDefault; |
| import com.google.devtools.common.options.Converters; |
| import com.google.devtools.common.options.ExpansionFunction; |
| import com.google.devtools.common.options.InvocationPolicyEnforcer; |
| import com.google.devtools.common.options.IsolatedOptionsData; |
| import com.google.devtools.common.options.Option; |
| import com.google.devtools.common.options.OptionDocumentationCategory; |
| import com.google.devtools.common.options.OptionEffectTag; |
| import com.google.devtools.common.options.OptionMetadataTag; |
| import com.google.devtools.common.options.OptionsBase; |
| import com.google.devtools.common.options.OptionsParser; |
| import com.google.devtools.common.options.OptionsParsingException; |
| import java.util.List; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * Tests for the Incompatible Changes system (--incompatible_* flags). These go in their own suite |
| * because the options parser doesn't know the business logic for incompatible changes. |
| */ |
| @RunWith(JUnit4.class) |
| public class AllIncompatibleChangesExpansionTest { |
| |
| /** Dummy comment (linter suppression) */ |
| public static class ExampleOptions extends OptionsBase { |
| @Option( |
| name = "all", |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "null", |
| expansionFunction = AllIncompatibleChangesExpansion.class |
| ) |
| public Void all; |
| |
| @Option( |
| name = "X", |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false" |
| ) |
| public boolean x; |
| |
| @Option( |
| name = "Y", |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "true" |
| ) |
| public boolean y; |
| |
| @Option( |
| name = "incompatible_A", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "Migrate to A" |
| ) |
| public boolean incompatibleA; |
| |
| @Option( |
| name = "incompatible_B", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "Migrate to B" |
| ) |
| public boolean incompatibleB; |
| |
| @Option( |
| name = "incompatible_C", |
| oldName = "experimental_C", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "true", |
| help = "Migrate to C" |
| ) |
| public boolean incompatibleC; |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class ExampleExpansionOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_expX", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "null", |
| expansion = {"--X"}, |
| help = "Start using X" |
| ) |
| public Void incompatibleExpX; |
| |
| /** Dummy comment (linter suppression) */ |
| public static class NoYExpansion implements ExpansionFunction { |
| @Override |
| public ImmutableList<String> getExpansion(IsolatedOptionsData optionsData) { |
| return ImmutableList.of("--noY"); |
| } |
| } |
| |
| @Option( |
| name = "incompatible_expY", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "null", |
| expansionFunction = NoYExpansion.class, |
| help = "Stop using Y" |
| ) |
| public Void incompatibleExpY; |
| } |
| |
| @Test |
| public void noChangesSelected() throws OptionsParsingException { |
| OptionsParser parser = OptionsParser.builder().optionsClasses(ExampleOptions.class).build(); |
| parser.parse(""); |
| ExampleOptions opts = parser.getOptions(ExampleOptions.class); |
| assertThat(opts.x).isFalse(); |
| assertThat(opts.y).isTrue(); |
| assertThat(opts.incompatibleA).isFalse(); |
| assertThat(opts.incompatibleB).isFalse(); |
| assertThat(opts.incompatibleC).isTrue(); |
| } |
| |
| @Test |
| public void allChangesSelected() throws OptionsParsingException { |
| OptionsParser parser = OptionsParser.builder().optionsClasses(ExampleOptions.class).build(); |
| parser.parse("--all"); |
| ExampleOptions opts = parser.getOptions(ExampleOptions.class); |
| assertThat(opts.x).isFalse(); |
| assertThat(opts.y).isTrue(); |
| assertThat(opts.incompatibleA).isTrue(); |
| assertThat(opts.incompatibleB).isTrue(); |
| assertThat(opts.incompatibleC).isTrue(); |
| } |
| |
| @Test |
| public void rightmostOverrides() throws OptionsParsingException { |
| // Check that all-expansion behaves just like any other expansion flag: |
| // the rightmost setting of any individual option wins. |
| OptionsParser parser = OptionsParser.builder().optionsClasses(ExampleOptions.class).build(); |
| parser.parse("--noincompatible_A", "--all", "--noincompatible_B"); |
| ExampleOptions opts = parser.getOptions(ExampleOptions.class); |
| assertThat(opts.incompatibleA).isTrue(); |
| assertThat(opts.incompatibleB).isFalse(); |
| } |
| |
| @Test |
| public void expansionOptions() throws OptionsParsingException { |
| // Check that all-expansion behaves just like any other expansion flag: |
| // the rightmost setting of any individual option wins. |
| OptionsParser parser = |
| OptionsParser.builder() |
| .optionsClasses(ExampleOptions.class, ExampleExpansionOptions.class) |
| .build(); |
| parser.parse("--all"); |
| ExampleOptions opts = parser.getOptions(ExampleOptions.class); |
| assertThat(opts.x).isTrue(); |
| assertThat(opts.y).isFalse(); |
| assertThat(opts.incompatibleA).isTrue(); |
| assertThat(opts.incompatibleB).isTrue(); |
| } |
| |
| @Test |
| public void invocationPolicy() throws OptionsParsingException { |
| // Check that all-expansion behaves just like any other expansion flag and can be filtered |
| // by invocation policy. |
| InvocationPolicy.Builder invocationPolicyBuilder = InvocationPolicy.newBuilder(); |
| invocationPolicyBuilder.addFlagPoliciesBuilder() |
| .setFlagName("incompatible_A") |
| .setUseDefault(UseDefault.getDefaultInstance()) |
| .build(); |
| InvocationPolicy policy = invocationPolicyBuilder.build(); |
| InvocationPolicyEnforcer enforcer = new InvocationPolicyEnforcer(policy); |
| |
| OptionsParser parser = OptionsParser.builder().optionsClasses(ExampleOptions.class).build(); |
| parser.parse("--all"); |
| enforcer.enforce(parser); |
| |
| ExampleOptions opts = parser.getOptions(ExampleOptions.class); |
| assertThat(opts.x).isFalse(); |
| assertThat(opts.y).isTrue(); |
| assertThat(opts.incompatibleA).isFalse(); // A should have been removed from the expansion. |
| assertThat(opts.incompatibleB).isTrue(); // B, without a policy, should have been left alone. |
| } |
| |
| /** Option with the right prefix, but the wrong metadata tag. */ |
| public static class IncompatibleChangeTagOption extends OptionsBase { |
| @Option( |
| name = "some_option_with_a_tag", |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| metadataTags = {OptionMetadataTag.INCOMPATIBLE_CHANGE}, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "nohelp" |
| ) |
| public boolean opt; |
| } |
| |
| @Test |
| public void incompatibleChangeTagDoesNotTriggerAllIncompatibleChangesCheck() { |
| try { |
| OptionsParser.builder() |
| .optionsClasses(ExampleOptions.class, IncompatibleChangeTagOption.class) |
| .build(); |
| } catch (OptionsParser.ConstructionException e) { |
| fail( |
| "some_option_with_a_tag should not trigger the expansion, so there should be no checks " |
| + "on it having the right prefix and metadata tags. Instead, the following exception " |
| + "was thrown: " |
| + e.getMessage()); |
| } |
| } |
| |
| // There's no unit test to check that the expansion of --all is sorted. IsolatedOptionsData is not |
| // exposed from OptionsParser, making it difficult to check, and it's not clear that exposing it |
| // would be worth it. |
| |
| /** |
| * Ensure that we get an {@link OptionsParser.ConstructionException} containing {@code message} |
| * when the incompatible changes in the given {@link OptionsBase} subclass are validated. |
| */ |
| // Because javadoc can't resolve inner classes. |
| @SuppressWarnings("javadoc") |
| private static void assertBadness(Class<? extends OptionsBase> optionsBaseClass, String message) { |
| OptionsParser.ConstructionException e = |
| assertThrows( |
| "Should have failed with message \"" + message + "\"", |
| OptionsParser.ConstructionException.class, |
| () -> |
| OptionsParser.builder() |
| .optionsClasses(ExampleOptions.class, optionsBaseClass) |
| .build()); |
| assertThat(e).hasMessageThat().contains(message); |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class BadNameOptions extends OptionsBase { |
| @Option( |
| name = "badname", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "nohelp" |
| ) |
| public boolean bad; |
| } |
| |
| @Test |
| public void badName() { |
| assertBadness( |
| BadNameOptions.class, |
| "Incompatible change option '--badname' must have name " |
| + "starting with \"incompatible_\""); |
| } |
| |
| /** Option with the right prefix, but the wrong metadata tag. */ |
| public static class MissingTriggeredByTagOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| metadataTags = {OptionMetadataTag.INCOMPATIBLE_CHANGE}, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "nohelp" |
| ) |
| public boolean bad; |
| } |
| |
| @Test |
| public void badTag() { |
| assertBadness( |
| MissingTriggeredByTagOptions.class, |
| "must have metadata tag OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES"); |
| } |
| |
| /** Option with the right prefix, but the wrong metadata tag. */ |
| public static class MissingIncompatibleTagOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| metadataTags = {OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES}, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "nohelp" |
| ) |
| public boolean bad; |
| } |
| |
| @Test |
| public void otherBadTag() { |
| assertBadness( |
| MissingIncompatibleTagOptions.class, |
| "must have metadata tag OptionMetadataTag.INCOMPATIBLE_CHANGE"); |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class BadTypeOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "0", |
| help = "nohelp" |
| ) |
| public int bad; |
| } |
| |
| @Test |
| public void badType() { |
| assertBadness(BadTypeOptions.class, "must have boolean type"); |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class BadHelpOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false" |
| ) |
| public boolean bad; |
| } |
| |
| @Test |
| public void badHelp() { |
| assertBadness(BadHelpOptions.class, "must have a \"help\" string"); |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class BadAbbrevOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "nohelp", |
| abbrev = 'x' |
| ) |
| public boolean bad; |
| } |
| |
| @Test |
| public void badAbbrev() { |
| assertBadness(BadAbbrevOptions.class, "must not use the abbrev field"); |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class BadValueHelpOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "nohelp", |
| valueHelp = "x" |
| ) |
| public boolean bad; |
| } |
| |
| @Test |
| public void badValueHelp() { |
| assertBadness(BadValueHelpOptions.class, "must not use the valueHelp field"); |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class BadConverterOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "nohelp", |
| converter = Converters.BooleanConverter.class |
| ) |
| public boolean bad; |
| } |
| |
| @Test |
| public void badConverter() { |
| assertBadness(BadConverterOptions.class, "must not use the converter field"); |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class BadAllowMultipleOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "null", |
| help = "nohelp", |
| allowMultiple = true |
| ) |
| public List<String> bad; |
| } |
| |
| @Test |
| public void badAllowMutliple() { |
| assertBadness(BadAllowMultipleOptions.class, "must not use the allowMultiple field"); |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class BadImplicitRequirementsOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "nohelp", |
| implicitRequirements = "--x" |
| ) |
| public boolean bad; |
| } |
| |
| @Test |
| public void badImplicitRequirements() { |
| assertBadness( |
| BadImplicitRequirementsOptions.class, "must not use the implicitRequirements field"); |
| } |
| |
| /** Dummy comment (linter suppression) */ |
| public static class BadOldNameOptions extends OptionsBase { |
| @Option( |
| name = "incompatible_bad", |
| metadataTags = { |
| OptionMetadataTag.INCOMPATIBLE_CHANGE, |
| OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES |
| }, |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, |
| effectTags = {OptionEffectTag.NO_OP}, |
| defaultValue = "false", |
| help = "nohelp", |
| oldName = "x" |
| ) |
| public boolean bad; |
| } |
| |
| @Test |
| public void badOldName() { |
| assertBadness(BadOldNameOptions.class, "must not use the oldName field"); |
| } |
| } |