blob: e70dab3a8434a8e62a9416740426ef5c5314dfb3 [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.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");
}
}