| // Copyright 2014 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.common.options; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.devtools.common.options.OptionsParser.newOptionsParser; | |
| import static java.util.Arrays.asList; | |
| import static org.junit.Assert.fail; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter; | |
| import com.google.devtools.common.options.OptionPriority.PriorityCategory; | |
| import java.io.ByteArrayInputStream; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.ObjectInputStream; | |
| import java.io.ObjectOutputStream; | |
| import java.io.Serializable; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.function.Consumer; | |
| import java.util.function.Predicate; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests {@link OptionsParser}. */ | |
| @RunWith(JUnit4.class) | |
| public class OptionsParserTest { | |
| /** Dummy comment (linter suppression) */ | |
| public static class BadOptions extends OptionsBase { | |
| @Option( | |
| name = "foo", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean foo1; | |
| @Option( | |
| name = "foo", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean foo2; | |
| } | |
| @Test | |
| public void errorsDuringConstructionAreWrapped() { | |
| try { | |
| newOptionsParser(BadOptions.class); | |
| fail(); | |
| } catch (OptionsParser.ConstructionException e) { | |
| assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class); | |
| } | |
| } | |
| public static class ExampleFoo extends OptionsBase { | |
| @Option( | |
| name = "foo", | |
| category = "one", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "defaultFoo" | |
| ) | |
| public String foo; | |
| @Option( | |
| name = "bar", | |
| category = "two", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "42" | |
| ) | |
| public int bar; | |
| @Option( | |
| name = "bing", | |
| category = "one", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "", | |
| allowMultiple = true | |
| ) | |
| public List<String> bing; | |
| @Option( | |
| name = "bang", | |
| category = "one", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "", | |
| converter = StringConverter.class, | |
| allowMultiple = true | |
| ) | |
| public List<String> bang; | |
| @Option( | |
| name = "nodoc", | |
| documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "", | |
| allowMultiple = false | |
| ) | |
| public String nodoc; | |
| } | |
| public static class ExampleBaz extends OptionsBase { | |
| @Option( | |
| name = "baz", | |
| category = "one", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "defaultBaz" | |
| ) | |
| public String baz; | |
| } | |
| /** Subclass of an options class. */ | |
| public static class ExampleBazSubclass extends ExampleBaz { | |
| @Option( | |
| name = "baz_subclass", | |
| category = "one", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "defaultBazSubclass" | |
| ) | |
| public String bazSubclass; | |
| } | |
| /** | |
| * Example with empty to null string converter | |
| */ | |
| public static class ExampleBoom extends OptionsBase { | |
| @Option( | |
| name = "boom", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "defaultBoom", | |
| converter = EmptyToNullStringConverter.class | |
| ) | |
| public String boom; | |
| } | |
| /** | |
| * Example with internal options | |
| */ | |
| public static class ExampleInternalOptions extends OptionsBase { | |
| @Option( | |
| name = "internal_boolean", | |
| metadataTags = {OptionMetadataTag.INTERNAL}, | |
| documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "true" | |
| ) | |
| public boolean privateBoolean; | |
| @Option( | |
| name = "internal_string", | |
| metadataTags = {OptionMetadataTag.INTERNAL}, | |
| documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "super secret" | |
| ) | |
| public String privateString; | |
| } | |
| public static class StringConverter implements Converter<String> { | |
| @Override | |
| public String convert(String input) { | |
| return input; | |
| } | |
| @Override | |
| public String getTypeDescription() { | |
| return "a string"; | |
| } | |
| } | |
| /** | |
| * A converter that defaults to null if the input is the empty string | |
| */ | |
| public static class EmptyToNullStringConverter extends StringConverter { | |
| @Override | |
| public String convert(String input) { | |
| return input.isEmpty() ? null : input; | |
| } | |
| } | |
| @Test | |
| public void parseWithMultipleOptionsInterfaces() | |
| throws OptionsParsingException { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class); | |
| parser.parse("--baz=oops", "--bar", "17"); | |
| ExampleFoo foo = parser.getOptions(ExampleFoo.class); | |
| assertThat(foo.foo).isEqualTo("defaultFoo"); | |
| assertThat(foo.bar).isEqualTo(17); | |
| ExampleBaz baz = parser.getOptions(ExampleBaz.class); | |
| assertThat(baz.baz).isEqualTo("oops"); | |
| } | |
| @Test | |
| public void parseWithOptionsInheritance() throws OptionsParsingException { | |
| OptionsParser parser = newOptionsParser(ExampleBazSubclass.class); | |
| parser.parse("--baz_subclass=cat", "--baz=dog"); | |
| ExampleBazSubclass subclassOptions = parser.getOptions(ExampleBazSubclass.class); | |
| assertThat(subclassOptions.bazSubclass).isEqualTo("cat"); | |
| assertThat(subclassOptions.baz).isEqualTo("dog"); | |
| ExampleBaz options = parser.getOptions(ExampleBaz.class); | |
| // This is a test showcasing the lack of functionality for retrieving parsed options at a | |
| // superclass type class type. If there's a need for this functionality, we can add it later. | |
| assertThat(options).isNull(); | |
| } | |
| @Test | |
| public void parserWithUnknownOption() { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class); | |
| try { | |
| parser.parse("--unknown", "option"); | |
| fail(); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e.getInvalidArgument()).isEqualTo("--unknown"); | |
| assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: --unknown"); | |
| } | |
| assertThat(parser.getResidue()).isEmpty(); | |
| } | |
| @Test | |
| public void parserWithSingleDashOption() throws OptionsParsingException { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class); | |
| try { | |
| parser.parse("-baz=oops", "-bar", "17"); | |
| fail(); | |
| } catch (OptionsParsingException expected) {} | |
| parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class); | |
| parser.setAllowSingleDashLongOptions(true); | |
| parser.parse("-baz=oops", "-bar", "17"); | |
| ExampleFoo foo = parser.getOptions(ExampleFoo.class); | |
| assertThat(foo.foo).isEqualTo("defaultFoo"); | |
| assertThat(foo.bar).isEqualTo(17); | |
| ExampleBaz baz = parser.getOptions(ExampleBaz.class); | |
| assertThat(baz.baz).isEqualTo("oops"); | |
| } | |
| @Test | |
| public void parsingFailsWithUnknownOptions() { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class); | |
| List<String> unknownOpts = asList("--unknown", "option", "--more_unknowns"); | |
| try { | |
| parser.parse(unknownOpts); | |
| fail(); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e.getInvalidArgument()).isEqualTo("--unknown"); | |
| assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: --unknown"); | |
| assertThat(parser.getOptions(ExampleFoo.class)).isNotNull(); | |
| assertThat(parser.getOptions(ExampleBaz.class)).isNotNull(); | |
| } | |
| } | |
| @Test | |
| public void parsingFailsWithInternalBooleanOptionAsIfUnknown() { | |
| OptionsParser parser = newOptionsParser(ExampleInternalOptions.class); | |
| List<String> internalOpts = asList("--internal_boolean"); | |
| try { | |
| parser.parse(internalOpts); | |
| fail(); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e.getInvalidArgument()).isEqualTo("--internal_boolean"); | |
| assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: --internal_boolean"); | |
| assertThat(parser.getOptions(ExampleInternalOptions.class)).isNotNull(); | |
| } | |
| } | |
| @Test | |
| public void parsingFailsWithNegatedInternalBooleanOptionAsIfUnknown() { | |
| OptionsParser parser = newOptionsParser(ExampleInternalOptions.class); | |
| List<String> internalOpts = asList("--nointernal_boolean"); | |
| try { | |
| parser.parse(internalOpts); | |
| fail(); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e.getInvalidArgument()).isEqualTo("--nointernal_boolean"); | |
| assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: --nointernal_boolean"); | |
| assertThat(parser.getOptions(ExampleInternalOptions.class)).isNotNull(); | |
| } | |
| } | |
| @Test | |
| public void parsingFailsForInternalOptionWithValueInSameArgAsIfUnknown() { | |
| OptionsParser parser = newOptionsParser(ExampleInternalOptions.class); | |
| List<String> internalOpts = asList("--internal_string=any_value"); | |
| try { | |
| parser.parse(internalOpts); | |
| fail("parsing should have failed for including a private option"); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e.getInvalidArgument()).isEqualTo("--internal_string=any_value"); | |
| assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: --internal_string=any_value"); | |
| assertThat(parser.getOptions(ExampleInternalOptions.class)).isNotNull(); | |
| } | |
| } | |
| @Test | |
| public void parsingFailsForInternalOptionWithValueInSeparateArgAsIfUnknown() { | |
| OptionsParser parser = newOptionsParser(ExampleInternalOptions.class); | |
| List<String> internalOpts = asList("--internal_string", "any_value"); | |
| try { | |
| parser.parse(internalOpts); | |
| fail("parsing should have failed for including a private option"); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e.getInvalidArgument()).isEqualTo("--internal_string"); | |
| assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: --internal_string"); | |
| assertThat(parser.getOptions(ExampleInternalOptions.class)).isNotNull(); | |
| } | |
| } | |
| @Test | |
| public void parseKnownAndUnknownOptions() { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class); | |
| List<String> opts = asList("--bar", "17", "--unknown", "option"); | |
| try { | |
| parser.parse(opts); | |
| fail(); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e.getInvalidArgument()).isEqualTo("--unknown"); | |
| assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: --unknown"); | |
| assertThat(parser.getOptions(ExampleFoo.class)).isNotNull(); | |
| assertThat(parser.getOptions(ExampleBaz.class)).isNotNull(); | |
| } | |
| } | |
| @Test | |
| public void parseAndOverrideWithEmptyStringToObtainNullValueInOption() | |
| throws OptionsParsingException { | |
| OptionsParser parser = newOptionsParser(ExampleBoom.class); | |
| // Override --boom value to the empty string | |
| parser.parse("--boom="); | |
| ExampleBoom boom = parser.getOptions(ExampleBoom.class); | |
| // The converted value is intentionally null since boom uses the EmptyToNullStringConverter | |
| assertThat(boom.boom).isNull(); | |
| } | |
| public static class CategoryTest extends OptionsBase { | |
| @Option( | |
| name = "swiss_bank_account_number", | |
| documentationCategory = | |
| OptionDocumentationCategory.UNDOCUMENTED, // Not printed in usage messages! | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "123456789" | |
| ) | |
| public int swissBankAccountNumber; | |
| @Option( | |
| name = "student_bank_account_number", | |
| category = "one", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "987654321" | |
| ) | |
| public int studentBankAccountNumber; | |
| } | |
| @Test | |
| public void getOptionsAndGetResidueWithNoCallToParse() { | |
| // With no call to parse(), all options are at default values, and there's | |
| // no reside. | |
| assertThat(newOptionsParser(ExampleFoo.class).getOptions(ExampleFoo.class).foo) | |
| .isEqualTo("defaultFoo"); | |
| assertThat(newOptionsParser(ExampleFoo.class).getResidue()).isEmpty(); | |
| } | |
| @Test | |
| public void parserCanBeCalledRepeatedly() throws OptionsParsingException { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class); | |
| parser.parse("--foo", "foo1"); | |
| assertThat(parser.getOptions(ExampleFoo.class).foo).isEqualTo("foo1"); | |
| parser.parse(); | |
| assertThat(parser.getOptions(ExampleFoo.class).foo).isEqualTo("foo1"); // no change | |
| parser.parse("--foo", "foo2"); | |
| assertThat(parser.getOptions(ExampleFoo.class).foo).isEqualTo("foo2"); // updated | |
| } | |
| @Test | |
| public void multipleOccuringOption() throws OptionsParsingException { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class); | |
| parser.parse("--bing", "abcdef", "--foo", "foo1", "--bing", "123456" ); | |
| assertThat(parser.getOptions(ExampleFoo.class).bing).containsExactly("abcdef", "123456"); | |
| } | |
| @Test | |
| public void multipleOccurringOptionWithConverter() throws OptionsParsingException { | |
| // --bang is the same as --bing except that it has a "converter" specified. | |
| // This test also tests option values with embedded commas and spaces. | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class); | |
| parser.parse("--bang", "abc,def ghi", "--foo", "foo1", "--bang", "123456" ); | |
| assertThat(parser.getOptions(ExampleFoo.class).bang).containsExactly("abc,def ghi", "123456"); | |
| } | |
| @Test | |
| public void parserIgnoresOptionsAfterMinusMinus() | |
| throws OptionsParsingException { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class); | |
| parser.parse("--foo", "well", "--baz", "here", "--", "--bar", "ignore"); | |
| ExampleFoo foo = parser.getOptions(ExampleFoo.class); | |
| ExampleBaz baz = parser.getOptions(ExampleBaz.class); | |
| assertThat(foo.foo).isEqualTo("well"); | |
| assertThat(baz.baz).isEqualTo("here"); | |
| assertThat(foo.bar).isEqualTo(42); // the default! | |
| assertThat(parser.getResidue()).containsExactly("--bar", "ignore").inOrder(); | |
| } | |
| @Test | |
| public void parserThrowsExceptionIfResidueIsNotAllowed() { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class); | |
| parser.setAllowResidue(false); | |
| try { | |
| parser.parse("residue", "is", "not", "OK"); | |
| fail(); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e).hasMessageThat().isEqualTo("Unrecognized arguments: residue is not OK"); | |
| } | |
| } | |
| @Test | |
| public void multipleCallsToParse() throws Exception { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class); | |
| parser.setAllowResidue(true); | |
| parser.parse("--foo", "one", "--bar", "43", "unknown1"); | |
| parser.parse("--foo", "two", "unknown2"); | |
| ExampleFoo foo = parser.getOptions(ExampleFoo.class); | |
| assertThat(foo.foo).isEqualTo("two"); // second call takes precedence | |
| assertThat(foo.bar).isEqualTo(43); | |
| assertThat(parser.getResidue()).containsExactly("unknown1", "unknown2").inOrder(); | |
| } | |
| // Regression test for a subtle bug! The toString of each options interface | |
| // instance was printing out key=value pairs for all flags in the | |
| // OptionsParser, not just those belonging to the specific interface type. | |
| @Test | |
| public void toStringDoesntIncludeFlagsForOtherOptionsInParserInstance() | |
| throws Exception { | |
| OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class); | |
| parser.parse("--foo", "foo", "--bar", "43", "--baz", "baz"); | |
| String fooString = parser.getOptions(ExampleFoo.class).toString(); | |
| if (!fooString.contains("foo=foo") || | |
| !fooString.contains("bar=43") || | |
| !fooString.contains("ExampleFoo") || | |
| fooString.contains("baz=baz")) { | |
| fail("ExampleFoo.toString() is incorrect: " + fooString); | |
| } | |
| String bazString = parser.getOptions(ExampleBaz.class).toString(); | |
| if (!bazString.contains("baz=baz") || | |
| !bazString.contains("ExampleBaz") || | |
| bazString.contains("foo=foo") || | |
| bazString.contains("bar=43")) { | |
| fail("ExampleBaz.toString() is incorrect: " + bazString); | |
| } | |
| } | |
| // Regression test for another subtle bug! The toString was printing all the | |
| // explicitly-specified options, even if they were at their default values, | |
| // causing toString equivalence to diverge from equals(). | |
| @Test | |
| public void toStringIsIndependentOfExplicitCommandLineOptions() throws Exception { | |
| ExampleFoo foo1 = Options.parse(ExampleFoo.class).getOptions(); | |
| ExampleFoo foo2 = Options.parse(ExampleFoo.class, "--bar", "42").getOptions(); | |
| assertThat(foo2).isEqualTo(foo1); | |
| assertThat(foo2.toString()).isEqualTo(foo1.toString()); | |
| Map<String, Object> expectedMap = new ImmutableMap.Builder<String, Object>(). | |
| put("bing", Collections.emptyList()). | |
| put("bar", 42). | |
| put("nodoc", ""). | |
| put("bang", Collections.emptyList()). | |
| put("foo", "defaultFoo").build(); | |
| assertThat(foo1.asMap()).isEqualTo(expectedMap); | |
| assertThat(foo2.asMap()).isEqualTo(expectedMap); | |
| } | |
| // Regression test for yet another subtle bug! The inherited options weren't | |
| // being printed by toString. One day, a real rain will come and wash all | |
| // this scummy code off the streets. | |
| public static class DerivedBaz extends ExampleBaz { | |
| @Option( | |
| name = "derived", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "defaultDerived" | |
| ) | |
| public String derived; | |
| } | |
| @Test | |
| public void toStringPrintsInheritedOptionsToo_Duh() throws Exception { | |
| DerivedBaz derivedBaz = Options.parse(DerivedBaz.class).getOptions(); | |
| String derivedBazString = derivedBaz.toString(); | |
| if (!derivedBazString.contains("derived=defaultDerived") || | |
| !derivedBazString.contains("baz=defaultBaz")) { | |
| fail("DerivedBaz.toString() is incorrect: " + derivedBazString); | |
| } | |
| } | |
| // Tests for new default value override mechanism | |
| public static class CustomOptions extends OptionsBase { | |
| @Option( | |
| name = "simple", | |
| category = "custom", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "simple default" | |
| ) | |
| public String simple; | |
| @Option( | |
| name = "multipart_name", | |
| category = "custom", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "multipart default" | |
| ) | |
| public String multipartName; | |
| } | |
| @Test | |
| public void assertDefaultStringsForCustomOptions() throws OptionsParsingException { | |
| CustomOptions options = Options.parse(CustomOptions.class).getOptions(); | |
| assertThat(options.simple).isEqualTo("simple default"); | |
| assertThat(options.multipartName).isEqualTo("multipart default"); | |
| } | |
| public static class NullTestOptions extends OptionsBase { | |
| @Option( | |
| name = "simple", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String simple; | |
| } | |
| @Test | |
| public void defaultNullStringGivesNull() throws Exception { | |
| NullTestOptions options = Options.parse(NullTestOptions.class).getOptions(); | |
| assertThat(options.simple).isNull(); | |
| } | |
| public static class ImplicitDependencyOptions extends OptionsBase { | |
| @Option( | |
| name = "first", | |
| implicitRequirements = "--second=second", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String first; | |
| @Option( | |
| name = "second", | |
| implicitRequirements = "--third=third", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String second; | |
| @Option( | |
| name = "third", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String third; | |
| } | |
| @Test | |
| public void implicitDependencyHasImplicitDependency() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyOptions.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--first=first")); | |
| assertThat(parser.getOptions(ImplicitDependencyOptions.class).first).isEqualTo("first"); | |
| assertThat(parser.getOptions(ImplicitDependencyOptions.class).second).isEqualTo("second"); | |
| assertThat(parser.getOptions(ImplicitDependencyOptions.class).third).isEqualTo("third"); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| public static class BadImplicitDependencyOptions extends OptionsBase { | |
| @Option( | |
| name = "first", | |
| implicitRequirements = "xxx", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String first; | |
| } | |
| @Test | |
| public void badImplicitDependency() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(BadImplicitDependencyOptions.class); | |
| try { | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--first=first")); | |
| } catch (AssertionError e) { | |
| /* Expected error. */ | |
| return; | |
| } | |
| fail(); | |
| } | |
| public static class BadExpansionOptions extends OptionsBase { | |
| @Option( | |
| name = "first", | |
| expansion = {"xxx"}, | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public Void first; | |
| } | |
| @Test | |
| public void badExpansionOptions() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(BadExpansionOptions.class); | |
| try { | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--first")); | |
| } catch (AssertionError e) { | |
| /* Expected error. */ | |
| return; | |
| } | |
| fail(); | |
| } | |
| /** NullExpansionOptions */ | |
| public static class NullExpansionsOptions extends OptionsBase { | |
| /** ExpFunc */ | |
| public static class ExpFunc implements ExpansionFunction { | |
| @Override | |
| public ImmutableList<String> getExpansion(IsolatedOptionsData optionsData) { | |
| return null; | |
| } | |
| } | |
| @Option( | |
| name = "badness", | |
| expansionFunction = ExpFunc.class, | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public Void badness; | |
| } | |
| @Test | |
| public void nullExpansions() throws Exception { | |
| // Ensure that we get the NPE at the time of parser construction, not later when actually | |
| // parsing. | |
| try { | |
| newOptionsParser(NullExpansionsOptions.class); | |
| fail("Should have failed due to null expansion function result"); | |
| } catch (OptionsParser.ConstructionException e) { | |
| assertThat(e).hasCauseThat().isInstanceOf(NullPointerException.class); | |
| assertThat(e).hasCauseThat().hasMessageThat().contains("null value in entry"); | |
| } | |
| } | |
| /** ExpansionOptions */ | |
| public static class ExpansionOptions extends OptionsBase { | |
| @Option( | |
| name = "underlying", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String underlying; | |
| @Option( | |
| name = "expands", | |
| expansion = {"--underlying=from_expansion"}, | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public Void expands; | |
| /** ExpFunc */ | |
| public static class ExpFunc implements ExpansionFunction { | |
| @Override | |
| public ImmutableList<String> getExpansion(IsolatedOptionsData optionsData) { | |
| return ImmutableList.of("--expands"); | |
| } | |
| } | |
| @Option( | |
| name = "expands_by_function", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| expansionFunction = ExpFunc.class | |
| ) | |
| public Void expandsByFunction; | |
| } | |
| /** ExpansionMultipleOptions */ | |
| public static class ExpansionMultipleOptions extends OptionsBase { | |
| @Option( | |
| name = "underlying", | |
| defaultValue = "null", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| allowMultiple = true | |
| ) | |
| public List<String> underlying; | |
| /** ExpFunc */ | |
| public static class ExpFunc implements ExpansionFunction { | |
| @Override | |
| public ImmutableList<String> getExpansion(IsolatedOptionsData optionsData) { | |
| return ImmutableList.of("--underlying=pre_value", "--underlying=post_value"); | |
| } | |
| } | |
| @Option( | |
| name = "expands_by_function", | |
| defaultValue = "null", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| expansionFunction = ExpFunc.class | |
| ) | |
| public Void expandsByFunction; | |
| } | |
| @Test | |
| public void describeOptionsWithExpansion() throws Exception { | |
| // We have to test this here rather than in OptionsTest because expansion functions require | |
| // that an options parser be constructed. | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionOptions.class); | |
| String usage = | |
| parser.describeOptionsWithDeprecatedCategories( | |
| ImmutableMap.<String, String>of(), OptionsParser.HelpVerbosity.LONG); | |
| assertThat(usage).contains(" --expands\n Expands to: --underlying=from_expansion"); | |
| assertThat(usage).contains(" --expands_by_function\n Expands to: --expands"); | |
| } | |
| @Test | |
| public void overrideExpansionWithExplicit() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionOptions.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| null, | |
| Arrays.asList("--expands", "--underlying=direct_value")); | |
| ExpansionOptions options = parser.getOptions(ExpansionOptions.class); | |
| assertThat(options.underlying).isEqualTo("direct_value"); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| @Test | |
| public void testExpansionOriginIsPropagatedToOption() throws OptionsParsingException { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--expands")); | |
| OptionValueDescription expansionDescription = parser.getOptionValueDescription("expands"); | |
| assertThat(expansionDescription).isNotNull(); | |
| // In order to have access to the ParsedOptionDescription tracked by the value of 'underlying' | |
| // we have to know that this option is a "single valued" option. | |
| OptionValueDescription optionValue = parser.getOptionValueDescription("underlying"); | |
| assertThat(optionValue).isNotNull(); | |
| assertThat(optionValue.getSourceString()).matches("expanded from option '--expands'"); | |
| assertThat(optionValue.getCanonicalInstances()).isNotNull(); | |
| assertThat(optionValue.getCanonicalInstances()).hasSize(1); | |
| ParsedOptionDescription effectiveInstance = optionValue.getCanonicalInstances().get(0); | |
| assertThat(effectiveInstance.getExpandedFrom().getOptionDefinition()) | |
| .isSameAs(expansionDescription.getOptionDefinition()); | |
| assertThat(effectiveInstance.getImplicitDependent()).isNull(); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| @Test | |
| public void overrideExplicitWithExpansion() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionOptions.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| null, | |
| Arrays.asList("--underlying=direct_value", "--expands")); | |
| ExpansionOptions options = parser.getOptions(ExpansionOptions.class); | |
| assertThat(options.underlying).isEqualTo("from_expansion"); | |
| assertThat(parser.getWarnings()) | |
| .containsExactly( | |
| "option '--expands' was expanded and now overrides the explicit option " | |
| + "--underlying=direct_value with --underlying=from_expansion"); | |
| } | |
| @Test | |
| public void noWarningsWhenOverrideExplicitWithExpansion() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionOptions.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.RC_FILE, | |
| null, | |
| Arrays.asList("--underlying=direct_value", "--expands")); | |
| ExpansionOptions options = parser.getOptions(ExpansionOptions.class); | |
| assertThat(options.underlying).isEqualTo("from_expansion"); | |
| assertThat(parser.getWarnings()) | |
| .doesNotContain( | |
| "option '--expands' was expanded and now overrides the explicit option " | |
| + "--underlying=direct_value with --underlying=from_expansion"); | |
| } | |
| @Test | |
| public void noWarningsWhenValueNotChanged() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionOptions.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| null, | |
| Arrays.asList("--underlying=from_expansion", "--expands")); | |
| ExpansionOptions options = parser.getOptions(ExpansionOptions.class); | |
| assertThat(options.underlying).isEqualTo("from_expansion"); | |
| // The expansion option overrides the explicit option, but it is the same value, so expect | |
| // no warning. | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| // Makes sure the expansion options are expanded in the right order if they affect flags that | |
| // allow multiples. | |
| @Test | |
| public void multipleExpansionOptionsWithValue() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionMultipleOptions.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| null, | |
| Arrays.asList( | |
| "--expands_by_function", "--underlying=direct_value", "--expands_by_function")); | |
| ExpansionMultipleOptions options = parser.getOptions(ExpansionMultipleOptions.class); | |
| assertThat(options.underlying) | |
| .containsExactly("pre_value", "post_value", "direct_value", "pre_value", "post_value") | |
| .inOrder(); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| @Test | |
| public void checkExpansionValueWarning() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionMultipleOptions.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| null, | |
| Arrays.asList("--expands_by_function=no")); | |
| ExpansionMultipleOptions options = parser.getOptions(ExpansionMultipleOptions.class); | |
| assertThat(options.underlying).containsExactly("pre_value", "post_value").inOrder(); | |
| assertThat(parser.getWarnings()) | |
| .containsExactly( | |
| "option '--expands_by_function' is an expansion option. It does not accept values, " | |
| + "and does not change its expansion based on the value provided. " | |
| + "Value 'no' will be ignored."); | |
| } | |
| @Test | |
| public void overrideWithHigherPriority() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.RC_FILE, null, Arrays.asList("--simple=a")); | |
| assertThat(parser.getOptions(NullTestOptions.class).simple).isEqualTo("a"); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--simple=b")); | |
| assertThat(parser.getOptions(NullTestOptions.class).simple).isEqualTo("b"); | |
| } | |
| @Test | |
| public void overrideWithLowerPriority() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--simple=a")); | |
| assertThat(parser.getOptions(NullTestOptions.class).simple).isEqualTo("a"); | |
| parser.parse(OptionPriority.PriorityCategory.RC_FILE, null, Arrays.asList("--simple=b")); | |
| assertThat(parser.getOptions(NullTestOptions.class).simple).isEqualTo("a"); | |
| } | |
| @Test | |
| public void getOptionValueDescriptionWithNonExistingOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class); | |
| try { | |
| parser.getOptionValueDescription("notexisting"); | |
| fail(); | |
| } catch (IllegalArgumentException e) { | |
| /* Expected exception. */ | |
| } | |
| } | |
| @Test | |
| public void getOptionValueDescriptionWithoutValue() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class); | |
| assertThat(parser.getOptionValueDescription("simple")).isNull(); | |
| } | |
| @Test | |
| public void getOptionValueDescriptionWithValue() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| "my description", | |
| Arrays.asList("--simple=abc")); | |
| OptionValueDescription result = parser.getOptionValueDescription("simple"); | |
| assertThat(result).isNotNull(); | |
| assertThat(result.getOptionDefinition().getOptionName()).isEqualTo("simple"); | |
| assertThat(result.getValue()).isEqualTo("abc"); | |
| assertThat(result.getSourceString()).isEqualTo("my description"); | |
| assertThat(result.getCanonicalInstances()).isNotNull(); | |
| assertThat(result.getCanonicalInstances()).hasSize(1); | |
| ParsedOptionDescription singleOptionInstance = result.getCanonicalInstances().get(0); | |
| assertThat(singleOptionInstance.getPriority().getPriorityCategory()) | |
| .isEqualTo(OptionPriority.PriorityCategory.COMMAND_LINE); | |
| assertThat(singleOptionInstance.getOptionDefinition().isExpansionOption()).isFalse(); | |
| assertThat(singleOptionInstance.getImplicitDependent()).isNull(); | |
| assertThat(singleOptionInstance.getExpandedFrom()).isNull(); | |
| } | |
| public static class ImplicitDependencyWarningOptions extends OptionsBase { | |
| @Option( | |
| name = "first", | |
| implicitRequirements = "--second=requiredByFirst", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean first; | |
| @Option( | |
| name = "second", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String second; | |
| @Option( | |
| name = "third", | |
| implicitRequirements = "--second=requiredByThird", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String third; | |
| } | |
| @Test | |
| public void warningForImplicitOverridingExplicitOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class); | |
| parser.parse("--second=second", "--first"); | |
| assertThat(parser.getWarnings()) | |
| .containsExactly( | |
| "option '--second' is implicitly defined by option '--first'; the implicitly set value " | |
| + "overrides the previous one"); | |
| } | |
| @Test | |
| public void warningForExplicitOverridingImplicitOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class); | |
| parser.parse("--first"); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| parser.parse("--second=second"); | |
| assertThat(parser.getWarnings()) | |
| .containsExactly( | |
| "A new value for option '--second' overrides a previous implicit setting of that " | |
| + "option by option '--first'"); | |
| } | |
| @Test | |
| public void warningForExplicitOverridingImplicitOptionInSameCall() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class); | |
| parser.parse("--first", "--second=second"); | |
| assertThat(parser.getWarnings()) | |
| .containsExactly( | |
| "A new value for option '--second' overrides a previous implicit setting of that " | |
| + "option by option '--first'"); | |
| } | |
| @Test | |
| public void warningForImplicitOverridingImplicitOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class); | |
| parser.parse("--first"); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| parser.parse("--third=third"); | |
| assertThat(parser.getWarnings()) | |
| .containsExactly( | |
| "option '--second' is implicitly defined by both option '--first' and " | |
| + "option '--third=third'"); | |
| } | |
| @Test | |
| public void noWarningsForNonConflictingOverrides() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class); | |
| parser.parse("--first", "--second=requiredByFirst"); | |
| ImplicitDependencyWarningOptions options = | |
| parser.getOptions(ImplicitDependencyWarningOptions.class); | |
| assertThat(options.first).isTrue(); | |
| assertThat(options.second).isEqualTo("requiredByFirst"); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| @Test | |
| public void warningForImplicitRequirementsExpandedForDefaultValue() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class); | |
| parser.parse("--nofirst"); | |
| ImplicitDependencyWarningOptions options = | |
| parser.getOptions(ImplicitDependencyWarningOptions.class); | |
| assertThat(options.first).isFalse(); | |
| assertThat(options.second).isEqualTo("requiredByFirst"); | |
| assertThat(parser.getWarnings()) | |
| .containsExactly( | |
| "--nofirst sets option '--first' to its default value. Since this option has implicit " | |
| + "requirements that are set whenever the option is explicitly provided, " | |
| + "regardless of the value, this will behave differently than letting a default " | |
| + "be a default. Specifically, this options expands to " | |
| + "{--second=requiredByFirst}."); | |
| } | |
| @Test | |
| public void testDependentOriginIsPropagatedToOption() throws OptionsParsingException { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--first")); | |
| OptionValueDescription first = parser.getOptionValueDescription("first"); | |
| assertThat(first).isNotNull(); | |
| assertThat(first.getCanonicalInstances()).hasSize(1); | |
| OptionValueDescription second = parser.getOptionValueDescription("second"); | |
| assertThat(second).isNotNull(); | |
| assertThat(second.getSourceString()).matches("implicit requirement of option '--first'"); | |
| // Implicit requirements don't get listed as canonical. Check that this claims to be empty, | |
| // which tells us that the option instance is correctly tracking that is originated as an | |
| // implicit requirement. | |
| assertThat(second.getCanonicalInstances()).isNotNull(); | |
| assertThat(second.getCanonicalInstances()).hasSize(0); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| /** | |
| * Options for testing the behavior of canonicalization when an option implicitly requires a | |
| * repeatable option. | |
| */ | |
| public static class ImplicitDependencyOnAllowMultipleOptions extends OptionsBase { | |
| @Option( | |
| name = "first", | |
| implicitRequirements = "--second=requiredByFirst", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean first; | |
| @Option( | |
| name = "second", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| allowMultiple = true | |
| ) | |
| public List<String> second; | |
| @Option( | |
| name = "third", | |
| implicitRequirements = "--second=requiredByThird", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String third; | |
| } | |
| @Test | |
| public void testCanonicalizeExcludesImplicitDependencyOnRepeatableOption() | |
| throws OptionsParsingException { | |
| OptionsParser parser = | |
| OptionsParser.newOptionsParser(ImplicitDependencyOnAllowMultipleOptions.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| null, | |
| Arrays.asList("--first", "--second=explicitValue")); | |
| OptionValueDescription first = parser.getOptionValueDescription("first"); | |
| assertThat(first).isNotNull(); | |
| assertThat(first.getCanonicalInstances()).hasSize(1); | |
| OptionValueDescription second = parser.getOptionValueDescription("second"); | |
| assertThat(second).isNotNull(); | |
| assertThat(second.getSourceString()).matches("implicit requirement of option '--first', null"); | |
| // Implicit requirements don't get listed as canonical. Check that this excludes the implicit | |
| // value, but still tracks the explicit one. | |
| assertThat(second.getCanonicalInstances()).isNotNull(); | |
| assertThat(second.getCanonicalInstances()).hasSize(1); | |
| assertThat(parser.canonicalize()).containsExactly("--first=1", "--second=explicitValue"); | |
| ImplicitDependencyOnAllowMultipleOptions options = | |
| parser.getOptions(ImplicitDependencyOnAllowMultipleOptions.class); | |
| assertThat(options.first).isTrue(); | |
| assertThat(options.second).containsExactly("explicitValue", "requiredByFirst"); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| @Test | |
| public void testCanonicalizeExcludesImplicitDependencyForOtherwiseUnmentionedRepeatableOption() | |
| throws OptionsParsingException { | |
| OptionsParser parser = | |
| OptionsParser.newOptionsParser(ImplicitDependencyOnAllowMultipleOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--first")); | |
| OptionValueDescription first = parser.getOptionValueDescription("first"); | |
| assertThat(first).isNotNull(); | |
| assertThat(first.getCanonicalInstances()).hasSize(1); | |
| OptionValueDescription second = parser.getOptionValueDescription("second"); | |
| assertThat(second).isNotNull(); | |
| assertThat(second.getSourceString()).matches("implicit requirement of option '--first'"); | |
| // Implicit requirements don't get listed as canonical. Check that this excludes the implicit | |
| // value, leaving behind no mention of second. | |
| assertThat(second.getCanonicalInstances()).isNotNull(); | |
| assertThat(second.getCanonicalInstances()).isEmpty(); | |
| assertThat(parser.canonicalize()).containsExactly("--first=1"); | |
| ImplicitDependencyOnAllowMultipleOptions options = | |
| parser.getOptions(ImplicitDependencyOnAllowMultipleOptions.class); | |
| assertThat(options.first).isTrue(); | |
| assertThat(options.second).containsExactly("requiredByFirst"); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| public static class WarningOptions extends OptionsBase { | |
| @Deprecated | |
| @Option( | |
| name = "first", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public Void first; | |
| @Deprecated | |
| @Option( | |
| name = "second", | |
| allowMultiple = true, | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public List<String> second; | |
| @Deprecated | |
| @Option( | |
| name = "third", | |
| expansion = "--fourth=true", | |
| abbrev = 't', | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public Void third; | |
| @Option( | |
| name = "fourth", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean fourth; | |
| } | |
| @Test | |
| public void deprecationWarning() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(WarningOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--first")); | |
| assertThat(parser.getWarnings()).isEqualTo(Arrays.asList("Option 'first' is deprecated")); | |
| } | |
| @Test | |
| public void deprecationWarningForListOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(WarningOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--second=a")); | |
| assertThat(parser.getWarnings()).isEqualTo(Arrays.asList("Option 'second' is deprecated")); | |
| } | |
| @Test | |
| public void deprecationWarningForExpansionOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(WarningOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--third")); | |
| assertThat(parser.getWarnings()).isEqualTo(Arrays.asList("Option 'third' is deprecated")); | |
| assertThat(parser.getOptions(WarningOptions.class).fourth).isTrue(); | |
| } | |
| @Test | |
| public void deprecationWarningForAbbreviatedExpansionOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(WarningOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("-t")); | |
| assertThat(parser.getWarnings()).isEqualTo(Arrays.asList("Option 'third' is deprecated")); | |
| assertThat(parser.getOptions(WarningOptions.class).fourth).isTrue(); | |
| } | |
| public static class NewWarningOptions extends OptionsBase { | |
| @Option( | |
| name = "first", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| deprecationWarning = "it's gone" | |
| ) | |
| public Void first; | |
| @Option( | |
| name = "second", | |
| allowMultiple = true, | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| deprecationWarning = "sorry, no replacement" | |
| ) | |
| public List<String> second; | |
| @Option( | |
| name = "third", | |
| expansion = "--fourth=true", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| deprecationWarning = "use --forth instead" | |
| ) | |
| public Void third; | |
| @Option( | |
| name = "fourth", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean fourth; | |
| } | |
| @Test | |
| public void newDeprecationWarning() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(NewWarningOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--first")); | |
| assertThat(parser.getWarnings()) | |
| .isEqualTo(Arrays.asList("Option 'first' is deprecated: it's gone")); | |
| } | |
| @Test | |
| public void newDeprecationWarningForListOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(NewWarningOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--second=a")); | |
| assertThat(parser.getWarnings()) | |
| .isEqualTo(Arrays.asList("Option 'second' is deprecated: sorry, no replacement")); | |
| } | |
| @Test | |
| public void newDeprecationWarningForExpansionOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(NewWarningOptions.class); | |
| parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList("--third")); | |
| assertThat(parser.getWarnings()) | |
| .isEqualTo(Arrays.asList("Option 'third' is deprecated: use --forth instead")); | |
| assertThat(parser.getOptions(NewWarningOptions.class).fourth).isTrue(); | |
| } | |
| public static class ExpansionWarningOptions extends OptionsBase { | |
| @Option( | |
| name = "first", | |
| expansion = "--underlying=expandedFromFirst", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public Void first; | |
| @Option( | |
| name = "second", | |
| expansion = "--underlying=expandedFromSecond", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public Void second; | |
| @Option( | |
| name = "underlying", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public String underlying; | |
| } | |
| @Test | |
| public void warningForExpansionOverridingExplicitOption() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionWarningOptions.class); | |
| parser.parse("--underlying=underlying", "--first"); | |
| assertThat(parser.getWarnings()) | |
| .containsExactly( | |
| "option '--first' was expanded and now overrides the explicit option " | |
| + "--underlying=underlying with --underlying=expandedFromFirst"); | |
| } | |
| @Test | |
| public void warningForTwoConflictingExpansionOptions() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExpansionWarningOptions.class); | |
| parser.parse("--first", "--second"); | |
| assertThat(parser.getWarnings()) | |
| .containsExactly( | |
| "option '--underlying' was expanded to from both option '--first' and option " | |
| + "'--second'"); | |
| } | |
| // This test is here to make sure that nobody accidentally changes the | |
| // order of the enum values and breaks the implicit assumptions elsewhere | |
| // in the code. | |
| @Test | |
| public void optionPrioritiesAreCorrectlyOrdered() throws Exception { | |
| assertThat(OptionPriority.PriorityCategory.values()).hasLength(6); | |
| assertThat(OptionPriority.PriorityCategory.DEFAULT) | |
| .isLessThan(OptionPriority.PriorityCategory.COMPUTED_DEFAULT); | |
| assertThat(OptionPriority.PriorityCategory.COMPUTED_DEFAULT) | |
| .isLessThan(OptionPriority.PriorityCategory.RC_FILE); | |
| assertThat(OptionPriority.PriorityCategory.RC_FILE) | |
| .isLessThan(OptionPriority.PriorityCategory.COMMAND_LINE); | |
| assertThat(OptionPriority.PriorityCategory.COMMAND_LINE) | |
| .isLessThan(OptionPriority.PriorityCategory.INVOCATION_POLICY); | |
| assertThat(OptionPriority.PriorityCategory.INVOCATION_POLICY) | |
| .isLessThan(OptionPriority.PriorityCategory.SOFTWARE_REQUIREMENT); | |
| } | |
| public static class IntrospectionExample extends OptionsBase { | |
| @Option( | |
| name = "alpha", | |
| category = "one", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "alphaDefaultValue" | |
| ) | |
| public String alpha; | |
| @Option( | |
| name = "beta", | |
| category = "one", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "betaDefaultValue" | |
| ) | |
| public String beta; | |
| @Option( | |
| name = "gamma", | |
| documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "gammaDefaultValue" | |
| ) | |
| public String gamma; | |
| @Option( | |
| name = "delta", | |
| documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "deltaDefaultValue" | |
| ) | |
| public String delta; | |
| @Option( | |
| name = "echo", | |
| metadataTags = {OptionMetadataTag.HIDDEN}, | |
| documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "echoDefaultValue" | |
| ) | |
| public String echo; | |
| } | |
| @Test | |
| public void asListOfUnparsedOptions() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(IntrospectionExample.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| "source", | |
| Arrays.asList("--alpha=one", "--gamma=two", "--echo=three")); | |
| List<ParsedOptionDescription> result = parser.asCompleteListOfParsedOptions(); | |
| assertThat(result).isNotNull(); | |
| assertThat(result).hasSize(3); | |
| assertThat(result.get(0).getOptionDefinition().getOptionName()).isEqualTo("alpha"); | |
| assertThat(result.get(0).isDocumented()).isTrue(); | |
| assertThat(result.get(0).isHidden()).isFalse(); | |
| assertThat(result.get(0).getUnconvertedValue()).isEqualTo("one"); | |
| assertThat(result.get(0).getSource()).isEqualTo("source"); | |
| assertThat(result.get(0).getPriority().getPriorityCategory()) | |
| .isEqualTo(OptionPriority.PriorityCategory.COMMAND_LINE); | |
| assertThat(result.get(1).getOptionDefinition().getOptionName()).isEqualTo("gamma"); | |
| assertThat(result.get(1).isDocumented()).isFalse(); | |
| assertThat(result.get(1).isHidden()).isFalse(); | |
| assertThat(result.get(1).getUnconvertedValue()).isEqualTo("two"); | |
| assertThat(result.get(1).getSource()).isEqualTo("source"); | |
| assertThat(result.get(1).getPriority().getPriorityCategory()) | |
| .isEqualTo(OptionPriority.PriorityCategory.COMMAND_LINE); | |
| assertThat(result.get(2).getOptionDefinition().getOptionName()).isEqualTo("echo"); | |
| assertThat(result.get(2).isDocumented()).isFalse(); | |
| assertThat(result.get(2).isHidden()).isTrue(); | |
| assertThat(result.get(2).getUnconvertedValue()).isEqualTo("three"); | |
| assertThat(result.get(2).getSource()).isEqualTo("source"); | |
| assertThat(result.get(2).getPriority().getPriorityCategory()) | |
| .isEqualTo(OptionPriority.PriorityCategory.COMMAND_LINE); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| @Test | |
| public void asListOfExplicitOptions() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(IntrospectionExample.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| "source", | |
| Arrays.asList("--alpha=one", "--gamma=two")); | |
| List<ParsedOptionDescription> result = parser.asListOfExplicitOptions(); | |
| assertThat(result).isNotNull(); | |
| assertThat(result).hasSize(2); | |
| assertThat(result.get(0).getOptionDefinition().getOptionName()).isEqualTo("alpha"); | |
| assertThat(result.get(0).isDocumented()).isTrue(); | |
| assertThat(result.get(0).getUnconvertedValue()).isEqualTo("one"); | |
| assertThat(result.get(0).getSource()).isEqualTo("source"); | |
| assertThat(result.get(0).getPriority().getPriorityCategory()) | |
| .isEqualTo(OptionPriority.PriorityCategory.COMMAND_LINE); | |
| assertThat(result.get(1).getOptionDefinition().getOptionName()).isEqualTo("gamma"); | |
| assertThat(result.get(1).isDocumented()).isFalse(); | |
| assertThat(result.get(1).getUnconvertedValue()).isEqualTo("two"); | |
| assertThat(result.get(1).getSource()).isEqualTo("source"); | |
| assertThat(result.get(1).getPriority().getPriorityCategory()) | |
| .isEqualTo(OptionPriority.PriorityCategory.COMMAND_LINE); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| private void assertOptionValue( | |
| String expectedName, Object expectedValue, OptionValueDescription actual) { | |
| assertThat(actual).isNotNull(); | |
| assertThat(actual.getOptionDefinition().getOptionName()).isEqualTo(expectedName); | |
| assertThat(actual.getValue()).isEqualTo(expectedValue); | |
| } | |
| private void assertOptionValue( | |
| String expectedName, | |
| Object expectedValue, | |
| OptionPriority.PriorityCategory expectedPriority, | |
| String expectedSource, | |
| OptionValueDescription actual) { | |
| assertOptionValue(expectedName, expectedValue, actual); | |
| assertThat(actual.getSourceString()).isEqualTo(expectedSource); | |
| assertThat(actual.getCanonicalInstances()).isNotEmpty(); | |
| assertThat(actual.getCanonicalInstances().get(0).getPriority().getPriorityCategory()) | |
| .isEqualTo(expectedPriority); | |
| } | |
| @Test | |
| public void asListOfEffectiveOptions() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(IntrospectionExample.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| "command line source", | |
| Arrays.asList("--alpha=alphaValueSetOnCommandLine", "--gamma=gammaValueSetOnCommandLine")); | |
| List<OptionValueDescription> result = parser.asListOfOptionValues(); | |
| assertThat(result).isNotNull(); | |
| assertThat(result).hasSize(5); | |
| HashMap<String,OptionValueDescription> map = new HashMap<String,OptionValueDescription>(); | |
| for (OptionValueDescription description : result) { | |
| map.put(description.getOptionDefinition().getOptionName(), description); | |
| } | |
| // All options in IntrospectionExample are single-valued options, and so have a 1:1 relationship | |
| // with the --flag=value option instance they came from (if any). | |
| assertOptionValue( | |
| "alpha", | |
| "alphaValueSetOnCommandLine", | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| "command line source", | |
| map.get("alpha")); | |
| assertOptionValue( | |
| "gamma", | |
| "gammaValueSetOnCommandLine", | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| "command line source", | |
| map.get("gamma")); | |
| assertOptionValue("beta", "betaDefaultValue", map.get("beta")); | |
| assertOptionValue("delta", "deltaDefaultValue", map.get("delta")); | |
| assertOptionValue("echo", "echoDefaultValue", map.get("echo")); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| public static class ListExample extends OptionsBase { | |
| @Option( | |
| name = "alpha", | |
| converter = StringConverter.class, | |
| allowMultiple = true, | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public List<String> alpha; | |
| } | |
| // Regression tests for bug: | |
| // "--option from blazerc unexpectedly overrides --option from command line" | |
| @Test | |
| public void overrideListOptions() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ListExample.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| "command line source", | |
| Arrays.asList("--alpha=cli")); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.RC_FILE, | |
| "rc file origin", | |
| Arrays.asList("--alpha=rc1", "--alpha=rc2")); | |
| assertThat(parser.getOptions(ListExample.class).alpha) | |
| .isEqualTo(Arrays.asList("rc1", "rc2", "cli")); | |
| } | |
| @Test | |
| public void testDashDash() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ExampleFoo.class); | |
| parser.parse( | |
| PriorityCategory.COMMAND_LINE, | |
| "command line source", | |
| Arrays.asList("--foo=woohoo", "residue", "--", "--bar=42")); | |
| assertThat(parser.getResidue()).hasSize(2); | |
| assertThat(parser.getResidue()).containsExactly("residue", "--bar=42"); | |
| assertThat(parser.getPreDoubleDashResidue()).hasSize(1); | |
| assertThat(parser.getPreDoubleDashResidue()).containsExactly("residue"); | |
| } | |
| @Test | |
| public void listOptionsHaveCorrectPriorities() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(ListExample.class); | |
| parser.parse( | |
| PriorityCategory.COMMAND_LINE, | |
| "command line source, part 1", | |
| Arrays.asList("--alpha=cli1", "--alpha=cli2")); | |
| parser.parse( | |
| PriorityCategory.COMMAND_LINE, | |
| "command line source, part 2", | |
| Arrays.asList("--alpha=cli3", "--alpha=cli4")); | |
| parser.parse( | |
| PriorityCategory.RC_FILE, "rc file origin", Arrays.asList("--alpha=rc1", "--alpha=rc2")); | |
| OptionValueDescription alphaValue = parser.getOptionValueDescription("alpha"); | |
| List<ParsedOptionDescription> parsedOptions = alphaValue.getCanonicalInstances(); | |
| System.out.println("parsedOptions:\n" + parsedOptions); | |
| assertThat(parsedOptions).hasSize(6); | |
| assertThat(parsedOptions.get(0).getSource()).matches("rc file origin"); | |
| assertThat(parsedOptions.get(0).getUnconvertedValue()).matches("rc1"); | |
| assertThat(parsedOptions.get(1).getSource()).matches("rc file origin"); | |
| assertThat(parsedOptions.get(1).getUnconvertedValue()).matches("rc2"); | |
| assertThat(parsedOptions.get(2).getSource()).matches("command line source, part 1"); | |
| assertThat(parsedOptions.get(2).getUnconvertedValue()).matches("cli1"); | |
| assertThat(parsedOptions.get(3).getSource()).matches("command line source, part 1"); | |
| assertThat(parsedOptions.get(3).getUnconvertedValue()).matches("cli2"); | |
| assertThat(parsedOptions.get(4).getSource()).matches("command line source, part 2"); | |
| assertThat(parsedOptions.get(4).getUnconvertedValue()).matches("cli3"); | |
| assertThat(parsedOptions.get(5).getSource()).matches("command line source, part 2"); | |
| assertThat(parsedOptions.get(5).getUnconvertedValue()).matches("cli4"); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| public static class CommaSeparatedOptionsExample extends OptionsBase { | |
| @Option( | |
| name = "alpha", | |
| converter = CommaSeparatedOptionListConverter.class, | |
| allowMultiple = true, | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null" | |
| ) | |
| public List<String> alpha; | |
| } | |
| @Test | |
| public void commaSeparatedOptionsWithAllowMultiple() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(CommaSeparatedOptionsExample.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| "command line source", | |
| Arrays.asList("--alpha=one", "--alpha=two,three")); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.RC_FILE, | |
| "rc file origin", | |
| Arrays.asList("--alpha=rc1,rc2")); | |
| assertThat(parser.getOptions(CommaSeparatedOptionsExample.class).alpha) | |
| .isEqualTo(Arrays.asList("rc1", "rc2", "one", "two", "three")); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| @Test | |
| public void commaSeparatedListOptionsHaveCorrectPriorities() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(CommaSeparatedOptionsExample.class); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.COMMAND_LINE, | |
| "command line source", | |
| Arrays.asList("--alpha=one", "--alpha=two,three")); | |
| parser.parse( | |
| OptionPriority.PriorityCategory.RC_FILE, | |
| "rc file origin", | |
| Arrays.asList("--alpha=rc1,rc2,rc3")); | |
| OptionValueDescription alphaValue = parser.getOptionValueDescription("alpha"); | |
| List<ParsedOptionDescription> parsedOptions = alphaValue.getCanonicalInstances(); | |
| assertThat(parsedOptions).hasSize(3); | |
| assertThat(parsedOptions.get(0).getSource()).matches("rc file origin"); | |
| assertThat(parsedOptions.get(0).getUnconvertedValue()).matches("rc1,rc2,rc3"); | |
| assertThat(parsedOptions.get(1).getSource()).matches("command line source"); | |
| assertThat(parsedOptions.get(1).getUnconvertedValue()).matches("one"); | |
| assertThat(parsedOptions.get(2).getSource()).matches("command line source"); | |
| assertThat(parsedOptions.get(2).getUnconvertedValue()).matches("two,three"); | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| public static class Yesterday extends OptionsBase { | |
| @Option( | |
| name = "a", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "a" | |
| ) | |
| public String a; | |
| @Option( | |
| name = "b", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "b" | |
| ) | |
| public String b; | |
| @Option( | |
| name = "c", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| expansion = {"--a=cExpansion"} | |
| ) | |
| public Void c; | |
| @Option( | |
| name = "d", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| allowMultiple = true | |
| ) | |
| public List<String> d; | |
| @Option( | |
| name = "e", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| implicitRequirements = {"--a=eRequirement"} | |
| ) | |
| public String e; | |
| @Option( | |
| name = "f", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| implicitRequirements = {"--b=fRequirement"} | |
| ) | |
| public String f; | |
| @Option( | |
| name = "g", | |
| abbrev = 'h', | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean g; | |
| } | |
| public static List<String> canonicalize(Class<? extends OptionsBase> optionsClass, String... args) | |
| throws OptionsParsingException { | |
| OptionsParser parser = OptionsParser.newOptionsParser( | |
| ImmutableList.<Class<? extends OptionsBase>>of(optionsClass)); | |
| parser.setAllowResidue(false); | |
| parser.parse(Arrays.asList(args)); | |
| return parser.canonicalize(); | |
| } | |
| @Test | |
| public void canonicalizeEasy() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--a=x")).containsExactly("--a=x"); | |
| } | |
| @Test | |
| public void canonicalizeSkipDuplicate() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--a=y", "--a=x")).containsExactly("--a=x"); | |
| } | |
| @Test | |
| public void canonicalizeExpands() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--c")).containsExactly("--a=cExpansion"); | |
| } | |
| @Test | |
| public void canonicalizeExpansionOverridesExplicit() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--a=x", "--c")).containsExactly("--a=cExpansion"); | |
| } | |
| @Test | |
| public void canonicalizeExplicitOverridesExpansion() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--c", "--a=x")).containsExactly("--a=x"); | |
| } | |
| @Test | |
| public void canonicalizeDoesNotReorder() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--b=y", "--d=x", "--a=z")) | |
| .containsExactly("--b=y", "--d=x", "--a=z") | |
| .inOrder(); | |
| } | |
| @Test | |
| public void canonicalizeImplicitDepsNotListed() throws Exception { | |
| // e's requirement overrides the explicit "a" here, so the "a" value is not in the canonical | |
| // form - the effective value is implied and the overridden value is lost. | |
| assertThat(canonicalize(Yesterday.class, "--a=x", "--e=y")).containsExactly("--e=y"); | |
| } | |
| @Test | |
| public void canonicalizeSkipsDuplicateAndStillOmmitsImplicitDeps() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--e=x", "--e=y")).containsExactly("--e=y"); | |
| } | |
| @Test | |
| public void implicitDepsAreNotInTheCanonicalOrderWhenTheyAreOverridden() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--e=y", "--a=x")) | |
| .containsExactly("--e=y", "--a=x") | |
| .inOrder(); | |
| } | |
| @Test | |
| public void implicitDepsAreNotInTheCanonicalOrder() throws Exception { | |
| // f requires a value of b, that is absent because it is implied. | |
| assertThat(canonicalize(Yesterday.class, "--f=z", "--a=x")) | |
| .containsExactly("--f=z", "--a=x") | |
| .inOrder(); | |
| } | |
| @Test | |
| public void canonicalizeDoesNotSkipAllowMultiple() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--d=a", "--d=b")) | |
| .containsExactly("--d=a", "--d=b").inOrder(); | |
| } | |
| @Test | |
| public void canonicalizeReplacesAbbrevWithName() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "-h")).containsExactly("--g=1"); | |
| } | |
| /** | |
| * Check that all forms of boolean flags are canonicalizes to the same form. | |
| * | |
| * The list of accepted values is from | |
| * {@link com.google.devtools.common.options.Converters.BooleanConverter}, and the value-less | |
| * --[no] form is controlled by {@link OptionsParserImpl#identifyOptionAndPossibleArgument}. | |
| */ | |
| @Test | |
| public void canonicalizeNormalizesBooleanFlags() throws Exception { | |
| assertThat(canonicalize(Yesterday.class, "--g")).containsExactly("--g=1"); | |
| assertThat(canonicalize(Yesterday.class, "--g=1")).containsExactly("--g=1"); | |
| assertThat(canonicalize(Yesterday.class, "--g=true")).containsExactly("--g=1"); | |
| assertThat(canonicalize(Yesterday.class, "--g=t")).containsExactly("--g=1"); | |
| assertThat(canonicalize(Yesterday.class, "--g=yes")).containsExactly("--g=1"); | |
| assertThat(canonicalize(Yesterday.class, "--g=y")).containsExactly("--g=1"); | |
| assertThat(canonicalize(Yesterday.class, "--nog")).containsExactly("--g=0"); | |
| assertThat(canonicalize(Yesterday.class, "--g=0")).containsExactly("--g=0"); | |
| assertThat(canonicalize(Yesterday.class, "--g=false")).containsExactly("--g=0"); | |
| assertThat(canonicalize(Yesterday.class, "--g=f")).containsExactly("--g=0"); | |
| assertThat(canonicalize(Yesterday.class, "--g=no")).containsExactly("--g=0"); | |
| assertThat(canonicalize(Yesterday.class, "--g=n")).containsExactly("--g=0"); | |
| } | |
| public static class LongValueExample extends OptionsBase { | |
| @Option( | |
| name = "longval", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "2147483648" | |
| ) | |
| public long longval; | |
| @Option( | |
| name = "intval", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "2147483647" | |
| ) | |
| public int intval; | |
| } | |
| @Test | |
| public void parseLong() throws OptionsParsingException { | |
| OptionsParser parser = newOptionsParser(LongValueExample.class); | |
| parser.parse(""); | |
| LongValueExample result = parser.getOptions(LongValueExample.class); | |
| assertThat(result.longval).isEqualTo(2147483648L); | |
| assertThat(result.intval).isEqualTo(2147483647); | |
| parser.parse("--longval", Long.toString(Long.MIN_VALUE)); | |
| result = parser.getOptions(LongValueExample.class); | |
| assertThat(result.longval).isEqualTo(Long.MIN_VALUE); | |
| parser.parse("--longval", "100"); | |
| result = parser.getOptions(LongValueExample.class); | |
| assertThat(result.longval).isEqualTo(100); | |
| } | |
| @Test | |
| public void intOutOfBounds() { | |
| OptionsParser parser = newOptionsParser(LongValueExample.class); | |
| try { | |
| parser.parse("--intval=2147483648"); | |
| fail(); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e).hasMessageThat().contains("'2147483648' is not an int"); | |
| } | |
| } | |
| public static class OldNameExample extends OptionsBase { | |
| @Option( | |
| name = "new_name", | |
| oldName = "old_name", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "defaultValue" | |
| ) | |
| public String flag; | |
| } | |
| @Test | |
| public void testOldName() throws OptionsParsingException { | |
| OptionsParser parser = newOptionsParser(OldNameExample.class); | |
| parser.parse("--old_name=foo"); | |
| OldNameExample result = parser.getOptions(OldNameExample.class); | |
| assertThat(result.flag).isEqualTo("foo"); | |
| // Should also work by its new name. | |
| parser = newOptionsParser(OldNameExample.class); | |
| parser.parse("--new_name=foo"); | |
| result = parser.getOptions(OldNameExample.class); | |
| assertThat(result.flag).isEqualTo("foo"); | |
| // Should be no warnings if the new name is used. | |
| assertThat(parser.getWarnings()).isEmpty(); | |
| } | |
| @Test | |
| public void testOldNameCanonicalization() throws Exception { | |
| assertThat(canonicalize(OldNameExample.class, "--old_name=foo")) | |
| .containsExactly("--new_name=foo"); | |
| } | |
| public static class ExampleBooleanFooOptions extends OptionsBase { | |
| @Option( | |
| name = "foo", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean foo; | |
| } | |
| @Test | |
| public void testBooleanUnderscorePrefixError() { | |
| try { | |
| OptionsParser parser = newOptionsParser(ExampleBooleanFooOptions.class); | |
| parser.parse("--no_foo"); | |
| fail("--no_foo should fail to parse."); | |
| } catch (OptionsParsingException e) { | |
| assertThat(e).hasMessageThat().contains("Unrecognized option: --no_foo"); | |
| } | |
| } | |
| /** Dummy options that declares it uses only core types. */ | |
| @UsesOnlyCoreTypes | |
| public static class CoreTypesOptions extends OptionsBase implements Serializable { | |
| @Option( | |
| name = "foo", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean foo; | |
| @Option( | |
| name = "bar", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "abc" | |
| ) | |
| public String bar; | |
| } | |
| /** Dummy options that does not declare using only core types. */ | |
| public static class NonCoreTypesOptions extends OptionsBase { | |
| @Option( | |
| name = "foo", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean foo; | |
| } | |
| /** Dummy options that incorrectly claims to use only core types. */ | |
| @UsesOnlyCoreTypes | |
| public static class BadCoreTypesOptions extends OptionsBase { | |
| /** Dummy unsafe type. */ | |
| public static class Foo { | |
| public int i = 0; | |
| } | |
| /** Converter for Foo. */ | |
| public static class FooConverter implements Converter<Foo> { | |
| @Override | |
| public Foo convert(String input) throws OptionsParsingException { | |
| Foo foo = new Foo(); | |
| foo.i = Integer.parseInt(input); | |
| return foo; | |
| } | |
| @Override | |
| public String getTypeDescription() { | |
| return "a foo"; | |
| } | |
| } | |
| @Option( | |
| name = "foo", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| converter = FooConverter.class | |
| ) | |
| public Foo foo; | |
| } | |
| /** Dummy options that is unsafe for @UsesOnlyCoreTypes but doesn't use the annotation. */ | |
| public static class SuperBadCoreTypesOptions extends OptionsBase { | |
| @Option( | |
| name = "foo", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "null", | |
| converter = BadCoreTypesOptions.FooConverter.class | |
| ) | |
| public BadCoreTypesOptions.Foo foo; | |
| } | |
| /** | |
| * Dummy options that illegally advertises @UsesOnlyCoreTypes, when its direct fields are fine but | |
| * its inherited fields are not. | |
| */ | |
| @UsesOnlyCoreTypes | |
| public static class InheritedBadCoreTypesOptions extends SuperBadCoreTypesOptions { | |
| @Option( | |
| name = "bar", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean bar; | |
| } | |
| @Test | |
| public void testUsesOnlyCoreTypes() { | |
| assertThat(OptionsParser.getUsesOnlyCoreTypes(CoreTypesOptions.class)).isTrue(); | |
| assertThat(OptionsParser.getUsesOnlyCoreTypes(NonCoreTypesOptions.class)).isFalse(); | |
| } | |
| @Test | |
| public void testValidationOfUsesOnlyCoreTypes() { | |
| try { | |
| OptionsParser.getUsesOnlyCoreTypes(BadCoreTypesOptions.class); | |
| fail("Should have detected illegal use of @UsesOnlyCoreTypes"); | |
| } catch (OptionsParser.ConstructionException expected) { | |
| assertThat(expected) | |
| .hasMessageThat() | |
| .matches( | |
| "Options class '.*BadCoreTypesOptions' is marked as @UsesOnlyCoreTypes, but field " | |
| + "'foo' has type '.*Foo'"); | |
| } | |
| } | |
| @Test | |
| public void testValidationOfUsesOnlyCoreTypes_Inherited() { | |
| try { | |
| OptionsParser.getUsesOnlyCoreTypes(InheritedBadCoreTypesOptions.class); | |
| fail("Should have detected illegal use of @UsesOnlyCoreTypes " | |
| + "(due to inheritance from bad superclass)"); | |
| } catch (OptionsParser.ConstructionException expected) { | |
| assertThat(expected) | |
| .hasMessageThat() | |
| .matches( | |
| "Options class '.*InheritedBadCoreTypesOptions' is marked as @UsesOnlyCoreTypes, but " | |
| + "field 'foo' has type '.*Foo'"); | |
| } | |
| } | |
| @Test | |
| public void serializable() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(CoreTypesOptions.class); | |
| parser.parse("--foo=true", "--bar=xyz"); | |
| CoreTypesOptions options = parser.getOptions(CoreTypesOptions.class); | |
| ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
| ObjectOutputStream objOut = new ObjectOutputStream(bos); | |
| objOut.writeObject(options); | |
| objOut.flush(); | |
| ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); | |
| ObjectInputStream objIn = new ObjectInputStream(bis); | |
| Object obj = objIn.readObject(); | |
| assertThat(obj).isEqualTo(options); | |
| } | |
| @Test | |
| public void stableSerialization() throws Exception { | |
| // Construct options two different ways to get the same result, and confirm that the serialized | |
| // representation is identical. | |
| OptionsParser parser1 = OptionsParser.newOptionsParser(CoreTypesOptions.class); | |
| parser1.parse("--foo=true", "--bar=xyz"); | |
| CoreTypesOptions options1 = parser1.getOptions(CoreTypesOptions.class); | |
| OptionsParser parser2 = OptionsParser.newOptionsParser(CoreTypesOptions.class); | |
| parser2.parse("--bar=abc", "--foo=1"); | |
| CoreTypesOptions options2 = parser2.getOptions(CoreTypesOptions.class); | |
| options2.bar = "xyz"; | |
| // We use two different pairs of streams because ObjectOutputStream#reset does not actually | |
| // wipe all the internal state. (The first time it's used, there's an additional header that | |
| // does not reappear afterwards.) | |
| ByteArrayOutputStream bos1 = new ByteArrayOutputStream(); | |
| ObjectOutputStream objOut1 = new ObjectOutputStream(bos1); | |
| objOut1.writeObject(options1); | |
| objOut1.flush(); | |
| byte[] data1 = bos1.toByteArray(); | |
| ByteArrayOutputStream bos2 = new ByteArrayOutputStream(); | |
| ObjectOutputStream objOut2 = new ObjectOutputStream(bos2); | |
| objOut2.writeObject(options2); | |
| objOut2.flush(); | |
| byte[] data2 = bos2.toByteArray(); | |
| assertThat(data1).isEqualTo(data2); | |
| } | |
| /** Dummy options for testing getHelpCompletion() and visitOptions(). */ | |
| public static class CompletionOptions extends OptionsBase implements Serializable { | |
| @Option( | |
| name = "secret", | |
| documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean secret; | |
| @Option( | |
| name = "b", | |
| documentationCategory = OptionDocumentationCategory.LOGGING, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean b; | |
| @Option( | |
| name = "a", | |
| documentationCategory = OptionDocumentationCategory.QUERY, | |
| effectTags = {OptionEffectTag.NO_OP}, | |
| defaultValue = "false" | |
| ) | |
| public boolean a; | |
| } | |
| @Test | |
| public void getOptionsCompletionShouldFilterUndocumentedOptions() throws Exception { | |
| OptionsParser parser = OptionsParser.newOptionsParser(CompletionOptions.class); | |
| assertThat(parser.getOptionsCompletion().split("\n")) | |
| .isEqualTo(new String[] {"--a", "--noa", "--b", "--nob"}); | |
| } | |
| @Test | |
| public void visitOptionsShouldFailWithoutPredicate() throws Exception { | |
| checkThatVisitOptionsThrowsNullPointerException(null, option -> {}, "Missing predicate."); | |
| } | |
| @Test | |
| public void visitOptionsShouldFailWithoutVisitor() throws Exception { | |
| checkThatVisitOptionsThrowsNullPointerException(option -> true, null, "Missing visitor."); | |
| } | |
| private void checkThatVisitOptionsThrowsNullPointerException( | |
| Predicate<OptionDefinition> predicate, | |
| Consumer<OptionDefinition> visitor, | |
| String expectedMessage) | |
| throws Exception { | |
| try { | |
| OptionsParser.newOptionsParser(CompletionOptions.class).visitOptions(predicate, visitor); | |
| fail("Expected a NullPointerException."); | |
| } catch (NullPointerException ex) { | |
| assertThat(ex).hasMessageThat().isEqualTo(expectedMessage); | |
| } | |
| } | |
| @Test | |
| public void visitOptionsShouldReturnAllOptionsInOrder() throws Exception { | |
| assertThat(visitOptionsToCollectTheirNames(option -> true)).containsExactly("a", "b", "secret"); | |
| } | |
| @Test | |
| public void visitOptionsShouldObeyPredicate() throws Exception { | |
| assertThat(visitOptionsToCollectTheirNames(option -> false)).isEmpty(); | |
| assertThat(visitOptionsToCollectTheirNames(option -> option.getOptionName().length() > 1)) | |
| .containsExactly("secret"); | |
| } | |
| private List<String> visitOptionsToCollectTheirNames(Predicate<OptionDefinition> predicate) { | |
| List<String> names = new ArrayList<>(); | |
| Consumer<OptionDefinition> visitor = option -> names.add(option.getOptionName()); | |
| OptionsParser parser = OptionsParser.newOptionsParser(CompletionOptions.class); | |
| parser.visitOptions(predicate, visitor); | |
| return names; | |
| } | |
| } |