// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args; import java.io.File; import java.io.PrintStream; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Ranges; import com.google.common.io.NullOutputStream; import org.junit.Before; import org.junit.Test; import com.twitter.common.args.ArgScannerTest.StandardArgs.Optimizations; import com.twitter.common.args.constraints.NotEmpty; import com.twitter.common.args.constraints.NotNegative; import com.twitter.common.args.constraints.NotNull; import com.twitter.common.args.constraints.Positive; import com.twitter.common.args.constraints.Range; import com.twitter.common.args.parsers.NonParameterizedTypeParser; import com.twitter.common.base.Command; import com.twitter.common.base.Function; import com.twitter.common.base.MorePreconditions; import com.twitter.common.collections.Pair; import com.twitter.common.quantity.Amount; import com.twitter.common.quantity.Data; import com.twitter.common.quantity.Time; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * @author William Farner */ public class ArgScannerTest { private static final Function<Class<?>, Predicate<Field>> TO_SCOPE_PREDICATE = new Function<Class<?>, Predicate<Field>>() { @Override public Predicate<Field> apply(final Class<?> cls) { return new Predicate<Field>() { @Override public boolean apply(Field field) { return field.getDeclaringClass() == cls; } }; } }; @Before public void setUp() { // Reset args in all classes before each test. for (Class<?> cls : this.getClass().getDeclaredClasses()) { resetArgs(cls); } } public static class StandardArgs { enum Optimizations { NONE, MINIMAL, ALL } @CmdLine(name = "enum", help = "help") static final Arg<Optimizations> ENUM_VAL = Arg.create(Optimizations.MINIMAL); @CmdLine(name = "string", help = "help") static final Arg<String> STRING_VAL = Arg.create("string"); @CmdLine(name = "char", help = "help") static final Arg<Character> CHAR_VAL = Arg.create('c'); @CmdLine(name = "byte", help = "help") static final Arg<Byte> BYTE_VAL = Arg.create((byte) 0); @CmdLine(name = "short", help = "help") static final Arg<Short> SHORT_VAL = Arg.create((short) 0); @CmdLine(name = "int", help = "help") static final Arg<Integer> INT_VAL = Arg.create(0); @CmdLine(name = "long", help = "help") static final Arg<Long> LONG_VAL = Arg.create(0L); @CmdLine(name = "float", help = "help") static final Arg<Float> FLOAT_VAL = Arg.create(0F); @CmdLine(name = "double", help = "help") static final Arg<Double> DOUBLE_VAL = Arg.create(0D); @CmdLine(name = "bool", help = "help") static final Arg<Boolean> BOOL = Arg.create(false); @CmdLine(name = "time_amount", help = "help") static final Arg<Amount<Long, Time>> TIME_AMOUNT = Arg.create(Amount.of(1L, Time.SECONDS)); @CmdLine(name = "data_amount", help = "help") static final Arg<Amount<Long, Data>> DATA_AMOUNT = Arg.create(Amount.of(1L, Data.MB)); @CmdLine(name = "range", help = "help") static final Arg<com.google.common.collect.Range<Integer>> RANGE = Arg.create(Ranges.closed(1, 5)); @Positional(help = "help") static final Arg<List<Amount<Long, Time>>> POSITIONAL = Arg.<List<Amount<Long, Time>>>create(ImmutableList.<Amount<Long, Time>>of()); } @Test public void testStandardArgs() { test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.ENUM_VAL.get(), is(Optimizations.ALL)); } }, "enum", "ALL"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.STRING_VAL.get(), is("newstring")); } }, "string", "newstring"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.CHAR_VAL.get(), is('x')); } }, "char", "x"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.BYTE_VAL.get(), is((byte) 10)); } }, "byte", "10"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.SHORT_VAL.get(), is((short) 10)); } }, "short", "10"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.INT_VAL.get(), is(10)); } }, "int", "10"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.LONG_VAL.get(), is(10L)); } }, "long", "10"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.FLOAT_VAL.get(), is(10f)); } }, "float", "10.0"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.DOUBLE_VAL.get(), is(10d)); } }, "double", "10.0"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.BOOL.get(), is(true)); } }, "bool", "true"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.BOOL.get(), is(true)); } }, "bool", ""); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.BOOL.get(), is(false)); } }, "no_bool", ""); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.BOOL.get(), is(true)); } }, "no_bool", "false"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.TIME_AMOUNT.get(), is(Amount.of(100L, Time.SECONDS))); } }, "time_amount", "100secs"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.DATA_AMOUNT.get(), is(Amount.of(1L, Data.Gb))); } }, "data_amount", "1Gb"); test(StandardArgs.class, new Command() { @Override public void execute() { assertThat(StandardArgs.RANGE.get(), is(Ranges.closed(1, 5))); } }, "range", "1-5"); resetArgs(StandardArgs.class); assertTrue(parse(StandardArgs.class, "1mins", "2secs")); assertEquals(ImmutableList.builder() .add(Amount.of(60L, Time.SECONDS)) .add(Amount.of(2L, Time.SECONDS)).build(), StandardArgs.POSITIONAL.get()); } public static class Name { private final String name; public Name(String name) { this.name = MorePreconditions.checkNotBlank(name); } public String getName() { return name; } @Override public int hashCode() { return this.name.hashCode(); } @Override public boolean equals(Object obj) { return (obj instanceof Name) && name.equals(((Name) obj).name); } } @ArgParser public static class NameParser extends NonParameterizedTypeParser<Name> { @Override public Name doParse(String raw) { return new Name(raw); } } public static class MeaningOfLife { private final Long answer; public MeaningOfLife(Long answer) { this.answer = Preconditions.checkNotNull(answer); } @Override public int hashCode() { return this.answer.hashCode(); } @Override public boolean equals(Object obj) { return (obj instanceof MeaningOfLife) && answer.equals(((MeaningOfLife) obj).answer); } } public static class Monty extends NonParameterizedTypeParser<MeaningOfLife> { @Override public MeaningOfLife doParse(String raw) { return new MeaningOfLife(42L); } } public static class CustomArgs { @CmdLine(name = "custom1", help = "help") static final Arg<Name> NAME_VAL = Arg.create(new Name("jim")); @CmdLine(name = "custom2", help = "help", parser = Monty.class) static final Arg<MeaningOfLife> MEANING_VAL = Arg.create(new MeaningOfLife(13L)); } @Test public void testCustomArgs() { test(CustomArgs.class, new Command() { @Override public void execute() { assertThat(CustomArgs.NAME_VAL.get(), is(new Name("jane"))); } }, "custom1", "jane"); test(CustomArgs.class, new Command() { @Override public void execute() { assertThat(CustomArgs.MEANING_VAL.get(), is(new MeaningOfLife(42L))); } }, "custom2", "jim"); } @Test public void testHelp() { assertFalse(parse(StandardArgs.class, "-h")); assertFalse(parse(StandardArgs.class, "-help")); } @Test public void testAllowsEmptyString() { parse(StandardArgs.class, "-string="); assertThat(StandardArgs.STRING_VAL.get(), is("")); resetArgs(StandardArgs.class); parse(StandardArgs.class, "-string=''"); assertThat(StandardArgs.STRING_VAL.get(), is("")); resetArgs(StandardArgs.class); parse(StandardArgs.class, "-string=\"\""); assertThat(StandardArgs.STRING_VAL.get(), is("")); } public static class CollectionArgs { @CmdLine(name = "stringList", help = "help") static final Arg<List<String>> STRING_LIST = Arg.create(null); @CmdLine(name = "intList", help = "help") static final Arg<List<Integer>> INT_LIST = Arg.create(null); @CmdLine(name = "stringSet", help = "help") static final Arg<Set<String>> STRING_SET = Arg.create(null); @CmdLine(name = "intSet", help = "help") static final Arg<Set<Integer>> INT_SET = Arg.create(null); @CmdLine(name = "stringStringMap", help = "help") static final Arg<Map<String, String>> STRING_STRING_MAP = Arg.create(null); @CmdLine(name = "intIntMap", help = "help") static final Arg<Map<Integer, Integer>> INT_INT_MAP = Arg.create(null); @CmdLine(name = "stringIntMap", help = "help") static final Arg<Map<String, Integer>> STRING_INT_MAP = Arg.create(null); @CmdLine(name = "intStringMap", help = "help") static final Arg<Map<Integer, String>> INT_STRING_MAP = Arg.create(null); @CmdLine(name = "stringStringPair", help = "help") static final Arg<Pair<String, String>> STRING_STRING_PAIR = Arg.create(null); @CmdLine(name = "intIntPair", help = "help") static final Arg<Pair<Integer, Integer>> INT_INT_PAIR = Arg.create(null); @CmdLine(name = "stringTimeAmountPair", help = "help") static final Arg<Pair<String, Amount<Long, Time>>> STRING_TIME_AMOUNT_PAIR = Arg.create(null); } @Test public void testCollectionArgs() { test(CollectionArgs.class, new Command() { @Override public void execute() { assertThat(CollectionArgs.STRING_LIST.get(), is(Arrays.asList("a", "b", "c", "d"))); } }, "stringList", "a,b,c,d"); test(CollectionArgs.class, new Command() { @Override public void execute() { assertThat(CollectionArgs.INT_LIST.get(), is(Arrays.asList(1, 2, 3, 4))); } }, "intList", "1, 2, 3, 4"); test(CollectionArgs.class, new Command() { @Override public void execute() { Set<String> expected = ImmutableSet.of("a", "b", "c", "d"); assertThat(CollectionArgs.STRING_SET.get(), is(expected)); } }, "stringSet", "a,b,c,d"); test(CollectionArgs.class, new Command() { @Override public void execute() { Set<Integer> expected = ImmutableSet.of(1, 2, 3, 4); assertThat(CollectionArgs.INT_SET.get(), is(expected)); } }, "intSet", "1, 2, 3, 4"); test(CollectionArgs.class, new Command() { @Override public void execute() { Map<String, String> expected = ImmutableMap.of("a", "b", "c", "d", "e", "f", "g", "h"); assertThat(CollectionArgs.STRING_STRING_MAP.get(), is(expected)); } }, "stringStringMap", "a=b, c=d, e=f, g=h"); test(CollectionArgs.class, new Command() { @Override public void execute() { Map<Integer, Integer> expected = ImmutableMap.of(1, 2, 3, 4, 5, 6, 7, 8); assertThat(CollectionArgs.INT_INT_MAP.get(), is(expected)); } }, "intIntMap", "1 = 2,3=4, 5=6 ,7=8"); test(CollectionArgs.class, new Command() { @Override public void execute() { Map<String, Integer> expected = ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4); assertThat(CollectionArgs.STRING_INT_MAP.get(), is(expected)); } }, "stringIntMap", "a=1 , b=2, c=3 ,d=4"); test(CollectionArgs.class, new Command() { @Override public void execute() { Map<Integer, String> expected = ImmutableMap.of(1, "1", 2, "2", 3, "3", 4, "4"); assertThat(CollectionArgs.INT_STRING_MAP.get(), is(expected)); } }, "intStringMap", " 1=1 , 2=2, 3=3,4=4"); test(CollectionArgs.class, new Command() { @Override public void execute() { assertThat(CollectionArgs.STRING_STRING_PAIR.get(), is(Pair.of("foo", "bar"))); } }, "stringStringPair", "foo , bar"); test(CollectionArgs.class, new Command() { @Override public void execute() { assertThat(CollectionArgs.INT_INT_PAIR.get(), is(Pair.of(10, 20))); } }, "intIntPair", "10 ,20"); test(CollectionArgs.class, new Command() { @Override public void execute() { assertThat(CollectionArgs.STRING_TIME_AMOUNT_PAIR.get(), is(Pair.of("fred", Amount.of(42L, Time.MINUTES)))); } }, "stringTimeAmountPair", "fred ,42mins"); test(CollectionArgs.class, new Command() { @Override public void execute() { CollectionArgs.STRING_TIME_AMOUNT_PAIR.get(); } }, true, "stringTimeAmountPair", "george,1MB"); } static class Serializable1 implements Serializable { } static class Serializable2 implements Serializable { } public static class WildcardArgs { @CmdLine(name = "class", help = "help") static final Arg<? extends Class<? extends Serializable>> CLAZZ = Arg.create(Serializable1.class); @CmdLine(name = "classList1", help = "help") static final Arg<List<Class<? extends Serializable>>> CLASS_LIST_1 = Arg.create(null); @CmdLine(name = "classList2", help = "help") static final Arg<List<? extends Class<? extends Serializable>>> CLASS_LIST_2 = Arg.create(null); } @Test public void testWildcardArgs() { test(WildcardArgs.class, new Command() { @Override public void execute() { assertSame(Serializable2.class, WildcardArgs.CLAZZ.get()); } }, "class", Serializable2.class.getName()); test(WildcardArgs.class, new Command() { @Override public void execute() { WildcardArgs.CLAZZ.get(); } }, true, "class", Runnable.class.getName()); test(WildcardArgs.class, new Command() { @Override public void execute() { assertEquals(ImmutableList.of(Serializable1.class, Serializable2.class), WildcardArgs.CLASS_LIST_1.get()); } }, "classList1", Serializable1.class.getName() + "," + Serializable2.class.getName()); test(WildcardArgs.class, new Command() { @Override public void execute() { assertEquals(ImmutableList.of(Serializable2.class), WildcardArgs.CLASS_LIST_2.get()); } }, "classList2", Serializable2.class.getName()); test(WildcardArgs.class, new Command() { @Override public void execute() { WildcardArgs.CLASS_LIST_2.get(); } }, true, "classList2", Serializable1.class.getName() + "," + Runnable.class.getName()); } @Target(FIELD) @Retention(RUNTIME) public static @interface Equals { String value(); } @VerifierFor(Equals.class) public static class SameName implements Verifier<Name> { @Override public void verify(Name value, Annotation annotation) { Preconditions.checkArgument(getValue(annotation).equals(value.getName())); } @Override public String toString(Class<? extends Name> argType, Annotation annotation) { return "name = " + getValue(annotation); } private String getValue(Annotation annotation) { return ((Equals) annotation).value(); } } public static class VerifyArgs { @Equals("jake") @CmdLine(name = "custom", help = "help") static final Arg<Name> CUSTOM_VAL = Arg.create(new Name("jake")); @NotEmpty @CmdLine(name = "string", help = "help") static final Arg<String> STRING_VAL = Arg.create("string"); @NotEmpty @CmdLine(name = "optional_string", help = "help") static final Arg<String> OPTIONAL_STRING_VAL = Arg.create(null); @Positive @CmdLine(name = "int", help = "help") static final Arg<Integer> INT_VAL = Arg.create(1); @NotNegative @CmdLine(name = "long", help = "help") static final Arg<Long> LONG_VAL = Arg.create(0L); @Range(lower = 10, upper = 20) @CmdLine(name = "float", help = "help") static final Arg<Float> FLOAT_VAL = Arg.create(10F); @CmdLine(name = "double", help = "help") static final Arg<Double> DOUBLE_VAL = Arg.create(0D); @CmdLine(name = "bool", help = "help") static final Arg<Boolean> BOOL = Arg.create(false); @CmdLine(name = "arg_without_default", help = "help") static final Arg<Boolean> ARG_WITHOUT_DEFAULT = Arg.create(); } @Test public void testEnforcesConstraints() { test(VerifyArgs.class, new Command() { @Override public void execute() { assertThat(VerifyArgs.STRING_VAL.get(), is("newstring")); assertThat(VerifyArgs.OPTIONAL_STRING_VAL.get(), nullValue(String.class)); } }, "string", "newstring"); testFails(VerifyArgs.class, "custom", "jane"); testFails(VerifyArgs.class, "string", ""); testFails(VerifyArgs.class, "optional_string", ""); testFails(VerifyArgs.class, "int", "0"); testFails(VerifyArgs.class, "long", "-1"); test(VerifyArgs.class, new Command() { @Override public void execute() { assertThat(VerifyArgs.FLOAT_VAL.get(), is(10.5f)); } }, "float", "10.5"); testFails(VerifyArgs.class, "float", "9"); } @Test public void testJoinKeysToValues() { assertThat(ArgScanner.joinKeysToValues(Arrays.asList("")), is(Arrays.asList(""))); assertThat(ArgScanner.joinKeysToValues(Arrays.asList("-a", "b", "-c", "-d")), is(Arrays.asList("-a=b", "-c", "-d"))); assertThat(ArgScanner.joinKeysToValues(Arrays.asList("-a='b'", "-c", "-d", "'e'")), is(Arrays.asList("-a='b'", "-c", "-d='e'"))); assertThat(ArgScanner.joinKeysToValues(Arrays.asList("-a=-b", "c", "-d", "\"e\"")), is(Arrays.asList("-a=-b", "c", "-d=\"e\""))); } public static class ShortHelpArg { @CmdLine(name = "h", help = "help") static final Arg<String> SHORT_HELP = Arg.create("string"); } @Test(expected = IllegalArgumentException.class) public void testShortHelpReserved() { parse(ShortHelpArg.class); } public static class LongHelpArg { @CmdLine(name = "help", help = "help") static final Arg<String> LONG_HELP = Arg.create("string"); } @Test(expected = IllegalArgumentException.class) public void testLongHelpReserved() { parse(LongHelpArg.class); } public static class DuplicateNames { @CmdLine(name = "string", help = "help") static final Arg<String> STRING_1 = Arg.create(); @CmdLine(name = "string", help = "help") static final Arg<String> STRING_2 = Arg.create(); } @Test(expected = IllegalArgumentException.class) public void testRejectsDuplicates() { parse(DuplicateNames.class, "-string-str"); } public static class OneRequired { @CmdLine(name = "string1", help = "help") static final Arg<String> STRING_1 = Arg.create(null); @NotNull @CmdLine(name = "string2", help = "help") static final Arg<String> STRING_2 = Arg.create(null); } @Test public void testRequiredProvided() { parse(OneRequired.class, "-string2=blah"); } @Test(expected = IllegalArgumentException.class) public void testMissingRequired() { parse(OneRequired.class, "-string1=blah"); } @Test(expected = IllegalArgumentException.class) public void testUnrecognizedArg() { parse(OneRequired.class, "-string2=blah", "-string3=blah"); } public static class NameClashA { @CmdLine(name = "string", help = "help") static final Arg<String> STRING = Arg.create(null); } public static class NameClashB { @CmdLine(name = "string", help = "help") static final Arg<String> STRING_1 = Arg.create(null); } @Test(expected = IllegalArgumentException.class) public void testDisallowsShortNameOnArgCollision() { parse(ImmutableList.of(NameClashA.class, NameClashB.class), "-string=blah"); } @Test public void testAllowsCanonicalNameOnArgCollision() { // TODO(William Farner): Fix. parse(ImmutableList.of(NameClashA.class, NameClashB.class), "-" + NameClashB.class.getCanonicalName() + ".string=blah"); } public static class AmountContainer { @CmdLine(name = "time_amount", help = "help") static final Arg<Amount<Integer, Time>> TIME_AMOUNT = Arg.create(null); } @Test(expected = IllegalArgumentException.class) public void testBadUnitType() { parse(ImmutableList.of(AmountContainer.class), "-time_amount=1Mb"); } @Test(expected = IllegalArgumentException.class) public void testUnrecognizedUnitType() { parse(ImmutableList.of(AmountContainer.class), "-time_amount=1abcd"); } static class Main1 { @Positional(help = "halp") static final Arg<List<String>> NAMES = Arg.create(null); } static class Main2 { @Positional(help = "halp") static final Arg<List<List<String>>> ROSTERS = Arg.create(null); } static class Main3 { @Positional(help = "halp") static final Arg<List<Double>> PERCENTILES = Arg.create(null); @Positional(help = "halp") static final Arg<List<File>> FILES = Arg.create(null); } private void resetMainArgs() { resetArgs(Main1.class); resetArgs(Main2.class); resetArgs(Main3.class); } @Test public void testMultiplePositionalsFails() { // Indivdually these should work. resetMainArgs(); assertTrue(parse(Main1.class, "jack,jill", "laurel,hardy")); assertEquals(ImmutableList.of("jack,jill", "laurel,hardy"), ImmutableList.copyOf(Main1.NAMES.get())); resetMainArgs(); assertTrue(parse(Main2.class, "jack,jill", "laurel,hardy")); assertEquals( ImmutableList.of( ImmutableList.of("jack", "jill"), ImmutableList.of("laurel", "hardy")), ImmutableList.copyOf(Main2.ROSTERS.get())); // But if combined in the same class or across classes the @Positional is ambiguous and we // should fail fast. resetMainArgs(); try { parse(ImmutableList.of(Main1.class, Main2.class), "jack,jill", "laurel,hardy"); fail("Expected more than 1 in-scope @Positional Arg List to trigger a failure."); } catch (IllegalArgumentException e) { // expected } resetMainArgs(); try { parse(Main3.class, "50", "90", "99", "99.9"); fail("Expected more than 1 in-scope @Positional Arg List to trigger a failure."); } catch (IllegalArgumentException e) { // expected } } // TODO(William Farner): Do we want to support nested parameterized args? If so, need to define a // syntax for that and build it in. // e.g. List<List<Integer>>, List<Pair<String, String>> private static void testFails(Class<?> scope, String arg, String value) { test(scope, null, true, arg, value); } private static void test(Class<?> scope, Command validate, String arg, String value) { test(scope, validate, false, arg, value); } private static void test(Class<?> scope, Command validate, boolean expectFails, String arg, String value) { String canonicalName = scope.getCanonicalName() + "." + arg; if (value.isEmpty()) { testValidate(scope, validate, expectFails, String.format("-%s", arg)); testValidate(scope, validate, expectFails, String.format("-%s", canonicalName)); } else { testValidate(scope, validate, expectFails, String.format("-%s=%s", arg, value)); testValidate(scope, validate, expectFails, String.format("-%s=%s", canonicalName, value)); testValidate(scope, validate, expectFails, String.format("-%s='%s'", arg, value)); testValidate(scope, validate, expectFails, String.format("-%s='%s'", canonicalName, value)); testValidate(scope, validate, expectFails, String.format("-%s=\"%s\"", arg, value)); testValidate(scope, validate, expectFails, String.format("-%s=\"%s\"", canonicalName, value)); testValidate(scope, validate, expectFails, String.format("-%s", arg), value); testValidate(scope, validate, expectFails, String.format("-%s", canonicalName), value); testValidate(scope, validate, expectFails, String.format("-%s", arg), String.format("'%s'", value)); testValidate(scope, validate, expectFails, String.format("-%s", canonicalName), String.format("'%s'", value)); testValidate(scope, validate, expectFails, String.format("-%s \"%s\"", arg, value)); testValidate(scope, validate, expectFails, String.format("-%s \"%s\"", canonicalName, value)); testValidate(scope, validate, expectFails, String.format("-%s", arg), String.format("%s", value)); testValidate(scope, validate, expectFails, String.format("-%s", canonicalName), String.format("%s", value)); } } private static void testValidate(Class<?> scope, Command validate, boolean expectFails, String... args) { resetArgs(scope); IllegalArgumentException exception = null; try { assertTrue(parse(scope, args)); } catch (IllegalArgumentException e) { exception = e; } if (!expectFails && exception != null) { throw exception; } if (expectFails && exception == null) { fail("Expected exception."); } if (validate != null) { validate.execute(); } resetArgs(scope); } private static void resetArgs(Class<?> scope) { for (Field field : scope.getDeclaredFields()) { if (Arg.class.isAssignableFrom(field.getType()) && Modifier.isStatic(field.getModifiers())) { try { ((Arg) field.get(null)).reset(); } catch (IllegalAccessException e) { fail(e.getMessage()); } } } } private static boolean parse(final Class<?> scope, String... args) { return parse(ImmutableList.of(scope), args); } private static boolean parse(Iterable<? extends Class<?>> scopes, String... args) { Predicate<Field> filter = Predicates.or(Iterables.transform(scopes, TO_SCOPE_PREDICATE)); PrintStream devNull = new PrintStream(new NullOutputStream()); return new ArgScanner(devNull).parse(filter, Arrays.asList(args)); } }