/* Copyright 2017 Remko Popma 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 picocli; import org.junit.*; import org.junit.contrib.java.lang.system.ProvideSystemProperty; import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.contrib.java.lang.system.SystemErrRule; import org.junit.contrib.java.lang.system.SystemOutRule; import org.junit.rules.TestRule; import picocli.CommandLine.*; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.IScope; import picocli.CommandLine.Model.MethodParam; import picocli.CommandLine.Model.ObjectScope; import picocli.CommandLine.Model.TypedMember; import picocli.CommandLineTest.CompactFields; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.PrintStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import static org.junit.Assert.*; import static picocli.CommandLine.Command; import static picocli.CommandLine.IParseResultHandler; import static picocli.CommandLine.MissingParameterException; import static picocli.CommandLine.Model; import static picocli.CommandLine.Option; import static picocli.CommandLine.Parameters; import static picocli.CommandLine.UnmatchedArgumentException; import static picocli.CommandLineTest.verifyCompact; import static picocli.TestUtil.setTraceLevel; /** * Tests for {@code @Command} methods. */ @SuppressWarnings("deprecation") public class CommandMethodTest { // allows tests to set any kind of properties they like, without having to individually roll them back @Rule public final TestRule restoreSystemProperties = new RestoreSystemProperties(); @Rule public final ProvideSystemProperty ansiOFF = new ProvideSystemProperty("picocli.ansi", "false"); @Rule public final SystemErrRule systemErrRule = new SystemErrRule().enableLog().muteForSuccessfulTests(); @Rule public final SystemOutRule systemOutRule = new SystemOutRule().enableLog().muteForSuccessfulTests(); @Before public void setUp() { System.clearProperty("picocli.trace"); } @After public void tearDown() { System.clearProperty("picocli.trace"); } static class MethodAppBase { @Command(name="run-0") public void run0() {} } @Command(name="method") static class MethodApp extends MethodAppBase { @Command(name="run-1") int run1(int a) { return a; } @Command(name="run-2") int run2(int a, @Option(names="-b", required=true) int b) { return a*b; } } @SuppressWarnings("deprecation") @Test public void testAnnotateMethod_noArg() throws Exception { setTraceLevel("OFF"); Method m = CommandLine.getCommandMethods(MethodApp.class, "run0").get(0); CommandLine cmd1 = new CommandLine(m); assertEquals("run-0", cmd1.getCommandName()); assertEquals(Arrays.asList(), cmd1.getCommandSpec().args()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); cmd1.parseWithHandler(((IParseResultHandler) null), new PrintStream(baos), new String[]{"--y"}); assertEquals(Arrays.asList("--y"), cmd1.getUnmatchedArguments()); // test execute Object ret = CommandLine.invoke(m.getName(), MethodApp.class, new PrintStream(new ByteArrayOutputStream())); assertNull("return value", ret); setTraceLevel("WARN"); } @Test public void testAnnotateMethod_unannotatedPositional() throws Exception { Method m = CommandLine.getCommandMethods(MethodApp.class, "run1").get(0); // test required try { CommandLine.populateCommand(m); fail("Missing required field should have thrown exception"); } catch (MissingParameterException ex) { assertEquals("Missing required parameter: ''", ex.getMessage()); } // test execute Object ret = CommandLine.invoke(m.getName(), MethodApp.class, new PrintStream(new ByteArrayOutputStream()), "42"); assertEquals("return value", 42, ((Number)ret).intValue()); } @Command static class UnannotatedPositional { @Command public void x(int a, int b, int c, int[] x, String[] y) {} } @Test public void testAnnotateMethod_unannotatedPositional_indexByParameterOrder() throws Exception { Method m = CommandLine.getCommandMethods(UnannotatedPositional.class, "x").get(0); CommandLine cmd = new CommandLine(m); CommandSpec spec = cmd.getCommandSpec(); List positionals = spec.positionalParameters(); String[] labels = { "", "", "", "", ""}; assertEquals(positionals.size(), labels.length); String[] ranges = { "0", "1", "2", "3..*", "4..*" }; for (int i = 0; i < positionals.size(); i++) { Model.PositionalParamSpec positional = positionals.get(i); assertEquals(positional.paramLabel() + " at index " + i, CommandLine.Range.valueOf(ranges[i]), positional.index()); assertEquals(labels[i], positional.paramLabel()); } } @Command static class PositionalsMixedWithOptions { @Command public void mixed(int a, @Option(names = "-b") int b, @Option(names = "-c") String c, int[] x, String[] y) {} } @Test public void testAnnotateMethod_unannotatedPositionalMixedWithOptions_indexByParameterOrder() throws Exception { Method m = CommandLine.getCommandMethods(PositionalsMixedWithOptions.class, "mixed").get(0); CommandLine cmd = new CommandLine(m); CommandSpec spec = cmd.getCommandSpec(); List positionals = spec.positionalParameters(); String[] labels = { "", "", ""}; assertEquals(positionals.size(), labels.length); String[] ranges = { "0", "1..*", "2..*" }; for (int i = 0; i < positionals.size(); i++) { Model.PositionalParamSpec positional = positionals.get(i); assertEquals(positional.paramLabel() + " at index " + i, CommandLine.Range.valueOf(ranges[i]), positional.index()); assertEquals(labels[i], positional.paramLabel()); } assertEquals(2, spec.options().size()); assertEquals(int.class, spec.findOption("-b").type()); assertEquals(String.class, spec.findOption("-c").type()); } @Command static class SomeMixin { @Option(names = "-a") int a; @Option(names = "-b") long b; } static class UnannotatedClassWithMixinParameters { @Command void withMixin(@Mixin SomeMixin mixin) { } @Command void posAndMixin(int[] x, @Mixin SomeMixin mixin) { } @Command void posAndOptAndMixin(int[] x, @Option(names = "-y") String[] y, @Mixin SomeMixin mixin) { } @Command void mixinFirst(@Mixin SomeMixin mixin, int[] x, @Option(names = "-y") String[] y) { } } @Test public void testAnnotateMethod_mixinParameter() { Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinParameters.class, "withMixin").get(0); CommandLine cmd = new CommandLine(m); CommandSpec spec = cmd.getCommandSpec(); assertEquals(1, spec.mixins().size()); spec = spec.mixins().get("arg0"); assertEquals(SomeMixin.class, spec.userObject().getClass()); } @Test public void testAnnotateMethod_positionalAndMixinParameter() { Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinParameters.class, "posAndMixin").get(0); CommandLine cmd = new CommandLine(m); CommandSpec spec = cmd.getCommandSpec(); assertEquals(1, spec.mixins().size()); assertEquals(1, spec.positionalParameters().size()); spec = spec.mixins().get("arg1"); assertEquals(SomeMixin.class, spec.userObject().getClass()); } @Test public void testAnnotateMethod_positionalAndOptionsAndMixinParameter() { Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinParameters.class, "posAndOptAndMixin").get(0); CommandLine cmd = new CommandLine(m); CommandSpec spec = cmd.getCommandSpec(); assertEquals(1, spec.mixins().size()); assertEquals(1, spec.positionalParameters().size()); assertEquals(3, spec.options().size()); spec = spec.mixins().get("arg2"); assertEquals(SomeMixin.class, spec.userObject().getClass()); } @Test public void testAnnotateMethod_mixinParameterFirst() { Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinParameters.class, "mixinFirst").get(0); CommandLine cmd = new CommandLine(m); CommandSpec spec = cmd.getCommandSpec(); assertEquals(1, spec.mixins().size()); assertEquals(1, spec.positionalParameters().size()); assertEquals(3, spec.options().size()); spec = spec.mixins().get("arg0"); assertEquals(SomeMixin.class, spec.userObject().getClass()); } static class UnannotatedClassWithMixinAndOptionsAndPositionals { @Command(name="sum") long sum(@Option(names = "-y") String[] y, @Mixin SomeMixin subMixin, int[] x) { return y.length + subMixin.a + subMixin.b + x.length; } } @Test public void testUnannotatedCommandWithMixin() throws Exception { Method m = CommandLine.getCommandMethods(UnannotatedClassWithMixinAndOptionsAndPositionals.class, "sum").get(0); CommandLine commandLine = new CommandLine(m); List parsed = commandLine.parse("-y foo -y bar -a 7 -b 11 13 42".split(" ")); assertEquals(1, parsed.size()); // get method args Object[] methodArgValues = parsed.get(0).getCommandSpec().commandMethodParamValues(); assertNotNull(methodArgValues); // verify args String[] arg0 = (String[]) methodArgValues[0]; assertArrayEquals(new String[] {"foo", "bar"}, arg0); SomeMixin arg1 = (SomeMixin) methodArgValues[1]; assertEquals(7, arg1.a); assertEquals(11, arg1.b); int[] arg2 = (int[]) methodArgValues[2]; assertArrayEquals(new int[] {13, 42}, arg2); // verify method is callable with args long result = (Long) m.invoke(new UnannotatedClassWithMixinAndOptionsAndPositionals(), methodArgValues); assertEquals(22, result); // verify same result with result handler List results = new RunLast().handleParseResult(parsed, System.out, CommandLine.Help.Ansi.OFF); assertEquals(1, results.size()); assertEquals(22L, results.get(0)); } @Command static class AnnotatedClassWithMixinParameters { @Mixin SomeMixin mixin; @Command(name="sum") long sum(@Option(names = "-y") String[] y, @Mixin SomeMixin subMixin, int[] x) { return mixin.a + mixin.b + y.length + subMixin.a + subMixin.b + x.length; } } @Test public void testAnnotatedSubcommandWithDoubleMixin() throws Exception { AnnotatedClassWithMixinParameters command = new AnnotatedClassWithMixinParameters(); CommandLine commandLine = new CommandLine(command); List parsed = commandLine.parse("-a 3 -b 5 sum -y foo -y bar -a 7 -b 11 13 42".split(" ")); assertEquals(2, parsed.size()); // get method args Object[] methodArgValues = parsed.get(1).getCommandSpec().commandMethodParamValues(); assertNotNull(methodArgValues); // verify args String[] arg0 = (String[]) methodArgValues[0]; assertArrayEquals(new String[] {"foo", "bar"}, arg0); SomeMixin arg1 = (SomeMixin) methodArgValues[1]; assertEquals(7, arg1.a); assertEquals(11, arg1.b); int[] arg2 = (int[]) methodArgValues[2]; assertArrayEquals(new int[] {13, 42}, arg2); // verify method is callable with args Method m = AnnotatedClassWithMixinParameters.class.getDeclaredMethod("sum", String[].class, SomeMixin.class, int[].class); long result = (Long) m.invoke(command, methodArgValues); assertEquals(30, result); // verify same result with result handler List results = new RunLast().handleParseResult(parsed, System.out, CommandLine.Help.Ansi.OFF); assertEquals(1, results.size()); assertEquals(30L, results.get(0)); } @Command static class OtherMixin { @Option(names = "-c") int c; } static class AnnotatedClassWithMultipleMixinParameters { @Command(name="sum") long sum(@Mixin SomeMixin mixin1, @Mixin OtherMixin mixin2) { return mixin1.a + mixin1.b + mixin2.c; } } @Test public void testAnnotatedMethodMultipleMixinsSubcommandWithDoubleMixin() throws Exception { Method m = CommandLine.getCommandMethods(AnnotatedClassWithMultipleMixinParameters.class, "sum").get(0); CommandLine commandLine = new CommandLine(m); List parsed = commandLine.parse("-a 3 -b 5 -c 7".split(" ")); assertEquals(1, parsed.size()); // get method args Object[] methodArgValues = parsed.get(0).getCommandSpec().commandMethodParamValues(); assertNotNull(methodArgValues); // verify args SomeMixin arg0 = (SomeMixin) methodArgValues[0]; assertEquals(3, arg0.a); assertEquals(5, arg0.b); OtherMixin arg1 = (OtherMixin) methodArgValues[1]; assertEquals(7, arg1.c); // verify method is callable with args long result = (Long) m.invoke(new AnnotatedClassWithMultipleMixinParameters(), methodArgValues); assertEquals(15, result); // verify same result with result handler List results = new RunLast().handleParseResult(parsed, System.out, CommandLine.Help.Ansi.OFF); assertEquals(1, results.size()); assertEquals(15L, results.get(0)); } @Command static class EmptyMixin {} static class AnnotatedClassWithMultipleEmptyParameters { @Command(name="sum") long sum(@Option(names = "-a") int a, @Mixin EmptyMixin mixin) { return a; } } @Test public void testAnnotatedMethodMultipleMixinsSubcommandWithEmptyMixin() throws Exception { Method m = CommandLine.getCommandMethods(AnnotatedClassWithMultipleEmptyParameters.class, "sum").get(0); CommandLine commandLine = new CommandLine(m); List parsed = commandLine.parse("-a 3".split(" ")); assertEquals(1, parsed.size()); // get method args Object[] methodArgValues = parsed.get(0).getCommandSpec().commandMethodParamValues(); assertNotNull(methodArgValues); // verify args int arg0 = (Integer) methodArgValues[0]; assertEquals(3, arg0); EmptyMixin arg1 = (EmptyMixin) methodArgValues[1]; // verify method is callable with args long result = (Long) m.invoke(new AnnotatedClassWithMultipleEmptyParameters(), methodArgValues); assertEquals(3, result); // verify same result with result handler List results = new RunLast().handleParseResult(parsed, System.out, CommandLine.Help.Ansi.OFF); assertEquals(1, results.size()); assertEquals(3L, results.get(0)); } @Test public void testAnnotateMethod_annotated() throws Exception { Method m = CommandLine.getCommandMethods(MethodApp.class, "run2").get(0); // test required try { CommandLine.populateCommand(m, "0"); fail("Missing required option should have thrown exception"); } catch (MissingParameterException ex) { assertEquals("Missing required option: '-b='", ex.getMessage()); } // test execute Object ret = CommandLine.invoke(m.getName(), MethodApp.class, new PrintStream(new ByteArrayOutputStream()), "13", "-b", "-1"); assertEquals("return value", -13, ((Number)ret).intValue()); } @Test public void testCommandMethodsFromSuperclassAddedToSubcommands() throws Exception { CommandLine cmd = new CommandLine(MethodApp.class); assertEquals("method", cmd.getCommandName()); assertEquals(3, cmd.getSubcommands().size()); assertEquals(0, cmd.getSubcommands().get("run-0").getCommandSpec().args().size()); assertEquals(1, cmd.getSubcommands().get("run-1").getCommandSpec().args().size()); assertEquals(2, cmd.getSubcommands().get("run-2").getCommandSpec().args().size()); //CommandLine.usage(cmd.getSubcommands().get("run-2"), System.out); } /** @see CompactFields */ private static class CompactFieldsMethod { @Command public CompactFields run( @Option(names = "-v", paramLabel="" /* useless, but required for Assert.equals() */) boolean verbose, @Option(names = "-r", paramLabel="" /* useless, but required for Assert.equals() */) boolean recursive, @Option(names = "-o", paramLabel="" /* required only for Assert.equals() */) File outputFile, @Parameters(paramLabel="" /* required only for Assert.equals() */) File[] inputFiles) { CompactFields ret = new CommandLineTest.CompactFields(); ret.verbose = verbose; ret.recursive = recursive; ret.outputFile = outputFile; ret.inputFiles = inputFiles; return ret; } } @Test public void testAnnotateMethod_matchesAnnotatedClass() throws Exception { setTraceLevel("OFF"); CommandLine classCmd = new CommandLine(new CompactFields()); Method m = CompactFieldsMethod.class.getDeclaredMethod("run", new Class[] {boolean.class, boolean.class, File.class, File[].class}); CommandLine methodCmd = new CommandLine(m); assertEquals("run", methodCmd.getCommandName()); assertEquals("argument count", classCmd.getCommandSpec().args().size(), methodCmd.getCommandSpec().args().size()); for (int i = 0; i < classCmd.getCommandSpec().args().size(); i++) { Model.ArgSpec classArg = classCmd.getCommandSpec().args().get(i); Model.ArgSpec methodArg = methodCmd.getCommandSpec().args().get(i); assertEquals("arg #" + i, classArg, methodArg); } setTraceLevel("WARN"); } /** replicate {@link CommandLineTest#testCompactFieldsAnyOrder()} but using * {@link CompactFieldsMethod#run(boolean, boolean, File, File[])} * as source of the {@link Command} annotation. */ @Test public void testCompactFieldsAnyOrder_method() throws Exception { final Method m = CompactFieldsMethod.class.getDeclaredMethod("run", new Class[] {boolean.class, boolean.class, File.class, File[].class}); String[] tests = { "-rvoout", "-vroout", "-vro=out", "-rv p1 p2", "p1 p2", "-voout p1 p2", "-voout -r p1 p2", "-r -v -oout p1 p2", "-rv -o out p1 p2", "-oout -r -v p1 p2", "-rvo out p1 p2", }; for (String test : tests) { // parse CompactFields compact = CommandLine.populateCommand(new CompactFields(), test.split(" ")); List result = new CommandLine(m).parse(test.split(" ")); // extract arg values assertEquals(1, result.size()); Object[] methodArgValues = result.get(0).getCommandSpec().commandMethodParamValues(); assertNotNull(methodArgValues); // verify parsing had the same result verifyCompact(compact, (Boolean)methodArgValues[0], (Boolean)methodArgValues[1], methodArgValues[2] == null ? null : String.valueOf(methodArgValues[2]), (File[])methodArgValues[3]); // verify method is callable (args have the correct/assignable type) CompactFields methodCompact = (CompactFields) m.invoke(new CompactFieldsMethod(), methodArgValues); // should not throw // verify passed args are the same assertNotNull(methodCompact); assertEquals(compact.verbose, methodCompact.verbose); assertEquals(compact.recursive, methodCompact.recursive); assertEquals(compact.outputFile, methodCompact.outputFile); assertArrayEquals(compact.inputFiles, methodCompact.inputFiles); } try { CommandLine.populateCommand(m, "-oout -r -vp1 p2".split(" ")); fail("should fail: -v does not take an argument"); } catch (UnmatchedArgumentException ex) { assertEquals("Unknown option: '-p1' (while processing option: '-vp1')", ex.getMessage()); } } static class CommandMethod1 { @Command(mixinStandardHelpOptions = true, version = "1.2.3") public int times(@Option(names = "-l", defaultValue = "2") int left, @Option(names = "-r", defaultValue = "3") int right) { return left * right; } } @Test public void testCommandMethodDefaults() { Object timesResultBothDefault = CommandLine.invoke("times", CommandMethod1.class); assertEquals("both default", 6, ((Integer) timesResultBothDefault).intValue()); Object timesResultLeftDefault = CommandLine.invoke("times", CommandMethod1.class, "-r", "8"); assertEquals("right default", 16, ((Integer) timesResultLeftDefault).intValue()); Object timesResultRightDefault = CommandLine.invoke("times", CommandMethod1.class, "-l", "8"); assertEquals("left default", 24, ((Integer) timesResultRightDefault).intValue()); Object timesResultNoDefault = CommandLine.invoke("times", CommandMethod1.class, "-r", "4", "-l", "5"); assertEquals("no default", 20, ((Integer) timesResultNoDefault).intValue()); } @Test public void testCommandMethodMixinHelp() { CommandLine.invoke("times", CommandMethod1.class, "-h"); String expected = String.format("" + "Usage: times [-hV] [-l=] [-r=]%n" + " -h, --help Show this help message and exit.%n" + " -l=%n" + " -r=%n" + " -V, --version Print version information and exit.%n" + ""); assertEquals(expected, systemOutRule.getLog()); } @Test public void testCommandMethodMixinVersion() { CommandLine.invoke("times", CommandMethod1.class, "--version"); String expected = String.format("1.2.3%n"); assertEquals(expected, systemOutRule.getLog()); } static class UnAnnotatedClassWithoutAnnotatedFields { @Command public void cmd1(@Option(names = "-x") int x, File f) { } @Command public void cmd2(@Option(names = "-x") int x, File f) { } } @Test public void testMethodCommandsAreNotSubcommandsOfNonAnnotatedClass() { Object userObject = new UnAnnotatedClassWithoutAnnotatedFields(); try { new CommandLine(userObject); fail("expected exception"); } catch (CommandLine.InitializationException ex) { assertEquals(userObject + " " + "is not a command: it has no @Command, @Option, " + "@Parameters or @Unmatched annotations", ex.getMessage()); } } static class UnAnnotatedClassWithAnnotatedField { @Option(names = "-y") int y; @Command public void cmd1(@Option(names = "-x") int x, File f) { } @Command public void cmd2(@Option(names = "-x") int x, File f) { } } @Test public void testMethodCommandsAreSubcommandsOfNonAnnotatedClassWithAnnotatedFields() { CommandLine cmd = new CommandLine(new UnAnnotatedClassWithAnnotatedField()); assertNotNull(cmd.getCommandSpec().findOption('y')); assertFalse(cmd.getSubcommands().isEmpty()); assertNotNull(cmd.getSubcommands().get("cmd1").getCommandSpec().findOption('x')); } @Command static class AnnotatedClassWithoutAnnotatedFields { @Command public void cmd1(@Option(names = "-x") int x, File f) { } @Command public void cmd2(@Option(names = "-x") int x, File f) { } } @Test public void testMethodCommandsAreSubcommandsOfAnnotatedClass() { CommandLine cmd = new CommandLine(new AnnotatedClassWithoutAnnotatedFields()); assertNull(cmd.getCommandSpec().findOption('x')); assertEquals(2, cmd.getSubcommands().size()); assertEquals(set("cmd1", "cmd2"), cmd.getSubcommands().keySet()); String expected = String.format("" + "Usage:
[COMMAND]%n" + "Commands:%n" + " cmd1%n" + " cmd2%n"); assertEquals(expected, cmd.getUsageMessage()); } @Command(addMethodSubcommands = false) static class SwitchedOff { @Command public void cmd1(@Option(names = "-x") int x, File f) { } @Command public void cmd2(@Option(names = "-x") int x, File f) { } } @Test public void testMethodCommandsAreNotAddedAsSubcommandsIfAnnotationSaysSo() { CommandLine cmd = new CommandLine(new SwitchedOff()); assertEquals(0, cmd.getSubcommands().size()); String expected = String.format("" + "Usage:
%n"); assertEquals(expected, cmd.getUsageMessage()); } /** Exemple from the documentation. */ static class Cat { public static void main(String[] args) { CommandLine.invoke("cat", Cat.class, args); } @Command(description = "Concatenate FILE(s) to standard output.", mixinStandardHelpOptions = true, version = "3.6.0") void cat(@Option(names = {"-E", "--show-ends"}) boolean showEnds, @Option(names = {"-n", "--number"}) boolean number, @Option(names = {"-T", "--show-tabs"}) boolean showTabs, @Option(names = {"-v", "--show-nonprinting"}) boolean showNonPrinting, @Parameters(paramLabel = "FILE") File[] files) { // process files } } @Test public void testCatUsageHelpMessage() { CommandLine cmd = new CommandLine(CommandLine.getCommandMethods(Cat.class, "cat").get(0)); String expected = String.format("" + "Usage: cat [-EhnTvV] [FILE...]%n" + "Concatenate FILE(s) to standard output.%n" + " [FILE...]%n" + " -E, --show-ends%n" + " -h, --help Show this help message and exit.%n" + " -n, --number%n" + " -T, --show-tabs%n" + " -v, --show-nonprinting%n" + " -V, --version Print version information and exit.%n"); assertEquals(expected, cmd.getUsageMessage()); } @Command(name = "git", mixinStandardHelpOptions = true, version = "picocli-3.6.0", description = "Version control system.") static class Git { @Option(names = "--git-dir", description = "Set the path to the repository") File path; @Command(description = "Clone a repository into a new directory") void clone(@Option(names = {"-l", "--local"}) boolean local, @Option(names = "-q", description = "Operate quietly.") boolean quiet, @Option(names = "-v", description = "Run verbosely.") boolean verbose, @Option(names = {"-b", "--branch"}) String branch, @Parameters(paramLabel = "") String repo) { // ... implement business logic } @Command(description = "Record changes to the repository") void commit(@Option(names = {"-m", "--message"}) String commitMessage, @Option(names = "--squash", paramLabel = "") String squash, @Parameters(paramLabel = "") File[] files) { // ... implement business logic } @Command(description = "Update remote refs along with associated objects") void push(@Option(names = {"-f", "--force"}) boolean force, @Option(names = "--tags") boolean tags, @Parameters(paramLabel = "") String repo) { // ... implement business logic } } @Test public void testGitUsageHelpMessage() { CommandLine cmd = new CommandLine(new Git()); String expected = String.format("" + "Usage: git [-hV] [--git-dir=] [COMMAND]%n" + "Version control system.%n" + " --git-dir= Set the path to the repository%n" + " -h, --help Show this help message and exit.%n" + " -V, --version Print version information and exit.%n" + "Commands:%n" + " clone Clone a repository into a new directory%n" + " commit Record changes to the repository%n" + " push Update remote refs along with associated objects%n"); assertEquals(expected, cmd.getUsageMessage()); } @Test public void testParamIndex() { CommandLine git = new CommandLine(new Git()); CommandLine clone = git.getSubcommands().get("clone"); Model.PositionalParamSpec repo = clone.getCommandSpec().positionalParameters().get(0); assertEquals(CommandLine.Range.valueOf("0"), repo.index()); } @Command static class AnnotatedParams { @Command public void method(@Parameters int a, @Parameters int b, @Parameters int c, int x, int y, int z) {} } @Test public void testParamIndexAnnotatedAndUnAnnotated() { CommandLine git = new CommandLine(new AnnotatedParams()); CommandLine method = git.getSubcommands().get("method"); List positionals = method.getCommandSpec().positionalParameters(); for (int i = 0; i < positionals.size(); i++) { assertEquals(CommandLine.Range.valueOf("" + i), positionals.get(i).index()); } } /** https://github.com/remkop/picocli/issues/538 */ static class CommandMethodWithDefaults { @Command public String cmd(@Option(names = "-a", defaultValue = "2") Integer a, @Option(names = "-b" ) Integer b, @Option(names = "-c", defaultValue = "abc") String c, @Option(names = "-d" ) String d, @Option(names = "-e", defaultValue = "a=b") Map e, @Option(names = "-f" ) Map f) { return String.format("a=%s, b=%s, c=%s, d=%s, e=%s, f=%s", a, b, c, d, e, f); } } @Test // for #538 public void testCommandMethodObjectDefaults() { Object s1 = CommandLine.invoke("cmd", CommandMethodWithDefaults.class); assertEquals("nothing matched", "a=2, b=null, c=abc, d=null, e={a=b}, f=null", s1); // fails Object s2 = CommandLine.invoke("cmd", CommandMethodWithDefaults.class, "-a1", "-b2", "-cX", "-dY", "-eX=Y", "-fA=B"); assertEquals("all matched", "a=1, b=2, c=X, d=Y, e={X=Y}, f={A=B}", s2); } private static class PrimitiveWrapper { @Option(names = "-0") private boolean aBool; @Option(names = "-1") private Boolean boolWrapper; @Option(names = "-b") private byte aByte; @Option(names = "-B") private Byte byteWrapper; @Option(names = "-c") private char aChar; @Option(names = "-C") private Character aCharacter; @Option(names = "-s") private short aShort; @Option(names = "-S") private Short shortWrapper; @Option(names = "-i") private int anInt; @Option(names = "-I") private Integer intWrapper; @Option(names = "-l") private long aLong; @Option(names = "-L") private Long longWrapper; @Option(names = "-d") private double aDouble; @Option(names = "-D") private Double doubleWrapper; @Option(names = "-f") private float aFloat; @Option(names = "-F") private Float floatWrapper; } @Test // for #538: check no regression public void testPrimitiveWrappersNotInitializedIfNotMatched() { PrimitiveWrapper s1 = CommandLine.populateCommand(new PrimitiveWrapper()); assertEquals(false, s1.aBool); assertNull(s1.boolWrapper); assertEquals(0, s1.aByte); assertNull(s1.byteWrapper); assertEquals(0, s1.aChar); assertNull(s1.aCharacter); assertEquals(0, s1.aShort); assertNull(s1.shortWrapper); assertEquals(0, s1.anInt); assertNull(s1.intWrapper); assertEquals(0, s1.aLong); assertNull(s1.longWrapper); assertEquals(0d, s1.aDouble, 0.00001D); assertNull(s1.doubleWrapper); assertEquals(0f, s1.aFloat, 0.00001F); assertNull(s1.floatWrapper); } private static Set set(String... elements) { return new HashSet(Arrays.asList(elements)); } /** Test for https://github.com/remkop/picocli/issues/554 */ @Command(name = "maincommand") class MainCommand implements Runnable { @Spec CommandSpec spec; public void run() { throw new UnsupportedOperationException("must specify a subcommand"); } @Command public void subcommand(@Option(names = "-x") String x) { System.out.println("x=" + x); } @Command public void explicit(@Option(names = "-v") boolean v) { CommandLine commandLine = spec.subcommands().get("explicit"); throw new CommandLine.ParameterException(commandLine, "Validation failed"); } } @Test public void testSubcommandMethodInvalidInputHandling() { String expected = String.format("" + "Unknown option: '-y'%n" + "Usage: maincommand subcommand [-x=]%n" + " -x=%n"); CommandLine.run(new MainCommand(), "subcommand", "-y"); assertEquals(expected, this.systemErrRule.getLog()); assertEquals("", this.systemOutRule.getLog()); } @Test public void testSubcommandMethodThrowingParameterException() { String expected = String.format("" + "Validation failed%n" + "Usage: maincommand explicit [-v]%n" + " -v%n"); CommandLine.run(new MainCommand(), "explicit", "-v"); assertEquals(expected, this.systemErrRule.getLog()); assertEquals("", this.systemOutRule.getLog()); } // test (1/2) for https://github.com/remkop/picocli/issues/570 @Test public void testOptionalListParameterInCommandClass() { @Command() class TestCommand implements Callable { @Parameters(arity="0..*") private List values; public String call() throws Exception { return values == null ? "null" : values.toString(); } } // seems to be working for @Command-class @Parameters CommandLine commandLine = new CommandLine(new TestCommand()); List firstExecutionResultWithParametersGiven = commandLine.parseWithHandlers( new RunLast(), new DefaultExceptionHandler>(), new String[] {"arg0", "arg1"}); List secondExecutionResultWithoutParameters = commandLine.parseWithHandlers( new RunLast(), new DefaultExceptionHandler>(), new String[] {}); assertEquals("[arg0, arg1]", firstExecutionResultWithParametersGiven.get(0)); assertEquals("null", secondExecutionResultWithoutParameters.get(0)); } // test (2/2) for https://github.com/remkop/picocli/issues/570 @Test public void testOptionalListParameterShouldNotRememberValuesInCommandMethods() { @Command() class TestCommand { @Command(name="method") public String methodCommand(@Parameters(arity="0..*") List methodValues) { return methodValues == null ? "null" : methodValues.toString(); } } CommandLine commandLine = new CommandLine(new TestCommand()); // problematic for @Command-method @Parameters List methodFirstExecutionResultWithParametersGiven = commandLine.parseWithHandlers( new RunLast(), new DefaultExceptionHandler>(), new String[] {"method","arg0", "arg1"}); List methodSecondExecutionResultWithoutParameters = commandLine.parseWithHandlers( new RunLast(), new DefaultExceptionHandler>(), new String[] {"method"}); assertEquals("[arg0, arg1]", methodFirstExecutionResultWithParametersGiven.get(0)); // fails, still "[arg0, arg1]" assertEquals("null", methodSecondExecutionResultWithoutParameters.get(0)); } @Command(addMethodSubcommands = false) static class StaticMethodCommand { @Spec static CommandSpec spec; public StaticMethodCommand(int constructorParam) {} @Command public static int staticCommand(@Option(names = "-x") int x) { return x * 3; } @Command public void cannotBeCalled(@Option(names = "-v") boolean v) { } @Command public static void throwsExecutionException() { throw new ExecutionException(new CommandLine(new StaticMethodCommand(8)), "abc"); } @Command public static void throwsOtherException() { throw new IndexOutOfBoundsException(); } } @Test public void testStaticCommandMethod() { assertEquals(9, CommandLine.invoke("staticCommand", StaticMethodCommand.class, "-x", "3")); } @Test public void testInvokeMethodClassPrintStreamAnsi() { assertEquals(9, CommandLine.invoke("staticCommand", StaticMethodCommand.class, System.out, Help.Ansi.OFF, "-x", "3")); } @Test public void testCommandMethodsRequireNonArgConstructor() { try { CommandLine.invoke("cannotBeCalled", StaticMethodCommand.class); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof NoSuchMethodException); } } @Test public void testCommandMethodsThatThrowsExecutionException() { try { CommandLine.invoke("throwsExecutionException", StaticMethodCommand.class); } catch (ExecutionException ex) { assertEquals("abc", ex.getMessage()); } } @Test public void testCommandMethodsThatThrowsException() { try { CommandLine.invoke("throwsOtherException", StaticMethodCommand.class); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof IndexOutOfBoundsException); } } @Command(addMethodSubcommands = false) static class ErroringCommand { public ErroringCommand() { // InvocationTargetException when invoking constructor throw new IllegalStateException("boom"); } @Command public void cannotBeCalled() { } } @SuppressWarnings("deprecation") @Test public void testCommandMethodsWhereConstructorThrowsException() { try { CommandLine.invoke("cannotBeCalled", ErroringCommand.class); } catch (ExecutionException ex) { // InvocationTargetException when invoking constructor assertTrue(ex.getCause() instanceof IllegalStateException); assertTrue(ex.getMessage(), ex.getMessage().startsWith("Error while calling command (")); } } @Test public void testCommandMethodsUnexpectedError() throws Exception { Method method = CommandMethod1.class.getDeclaredMethod("times", int.class, int.class); CommandLine cmd = new CommandLine(method); Method execute = CommandLine.class.getDeclaredMethod("executeUserObject", CommandLine.class, List.class); execute.setAccessible(true); try { execute.invoke(null, cmd, null); } catch (InvocationTargetException ex) { ExecutionException actual = (ExecutionException) ex.getCause(); assertTrue(actual.getMessage(), actual.getMessage().startsWith("Unhandled error while calling command (")); } } static class Duplicate { @Command int mycommand() { return 1; } @Command int mycommand(String[] args) { return 2;} } @SuppressWarnings("deprecation") @Test public void testDuplicateCommandMethodNames() { try { CommandLine.invoke("mycommand", Duplicate.class, System.out, System.out, Help.Ansi.OFF, "abd"); } catch (InitializationException ex) { assertTrue(ex.getMessage().startsWith("Expected exactly one @Command-annotated method for ")); } } @Test public void testAddMethodSubcommands() { CommandSpec spec = CommandSpec.wrapWithoutInspection(new StaticMethodCommand(1)); assertEquals(0, spec.subcommands().size()); spec.addMethodSubcommands(); assertEquals(4, spec.subcommands().size()); } @Test public void testAddMethodSubcommands_DisallowedIfUserObjectIsMethod() throws Exception { Method m = MethodApp.class.getDeclaredMethod("run1", int.class); CommandSpec spec = CommandSpec.wrapWithoutInspection(m); try { spec.addMethodSubcommands(); } catch (InitializationException ex) { assertEquals("Cannot discover subcommand methods of this Command Method: int picocli.CommandMethodTest$MethodApp.run1(int)", ex.getMessage()); } } @Test public void testMethodParam_getDeclaringExecutable() throws Exception { Method m = MethodApp.class.getDeclaredMethod("run1", int.class); MethodParam param = new MethodParam(m, 0); assertSame(m, param.getDeclaringExecutable()); } @Test public void testMethodParam_isAccessible() throws Exception { Method m = MethodApp.class.getDeclaredMethod("run1", int.class); MethodParam param = new MethodParam(m, 0); assertFalse(param.isAccessible()); m.setAccessible(true); assertTrue(param.isAccessible()); } static class TypedMemberObj { void getterNorSetter1() {} Void getterNorSetter2() {return null;} int getter() { return 0; } void setter(String str) { throw new IllegalStateException(); } } @Test public void testTypedMemberConstructorRejectsGetterNorSetter() throws Exception { Constructor constructor = TypedMember.class.getDeclaredConstructor(Method.class, IScope.class, CommandSpec.class); constructor.setAccessible(true); Method getterNorSetter1 = TypedMemberObj.class.getDeclaredMethod("getterNorSetter1"); Method getterNorSetter2 = TypedMemberObj.class.getDeclaredMethod("getterNorSetter2"); try { constructor.newInstance(getterNorSetter1, new ObjectScope(new TypedMemberObj()), CommandSpec.create()); fail("expect exception"); } catch (InvocationTargetException ex) { InitializationException ex2 = (InitializationException) ex.getCause(); assertEquals("Invalid method, must be either getter or setter: void picocli.CommandMethodTest$TypedMemberObj.getterNorSetter1()", ex2.getMessage()); } try { constructor.newInstance(getterNorSetter2, new ObjectScope(new TypedMemberObj()), CommandSpec.create()); fail("expect exception"); } catch (InvocationTargetException ex) { InitializationException ex2 = (InitializationException) ex.getCause(); assertEquals("Invalid method, must be either getter or setter: java.lang.Void picocli.CommandMethodTest$TypedMemberObj.getterNorSetter2()", ex2.getMessage()); } } @Test public void testTypedMemberConstructorNonProxyObject() throws Exception { Constructor constructor = TypedMember.class.getDeclaredConstructor(Method.class, IScope.class, CommandSpec.class); constructor.setAccessible(true); Method getter = TypedMemberObj.class.getDeclaredMethod("getter"); TypedMember typedMember = constructor.newInstance(getter, new ObjectScope(new TypedMemberObj()), CommandSpec.create()); assertSame(typedMember.getter(), typedMember.setter()); assertTrue(typedMember.getter() instanceof Model.MethodBinding); } @Test public void testTypedMemberInitializeInitialValue() throws Exception { Constructor constructor = TypedMember.class.getDeclaredConstructor(Method.class, IScope.class, CommandSpec.class); constructor.setAccessible(true); Method setter = TypedMemberObj.class.getDeclaredMethod("setter", String.class); TypedMember typedMember = constructor.newInstance(setter, new ObjectScope(new TypedMemberObj()), CommandSpec.create()); Method initializeInitialValue = TypedMember.class.getDeclaredMethod("initializeInitialValue", Object.class); initializeInitialValue.setAccessible(true); try { initializeInitialValue.invoke(typedMember, "boom"); } catch (InvocationTargetException ite) { InitializationException ex = (InitializationException) ite.getCause(); assertTrue(ex.getMessage().startsWith("Could not set initial value for boom")); } } @Test public void testTypedMemberPropertyName() { assertEquals("aBC", TypedMember.propertyName("ABC")); assertEquals("blah", TypedMember.propertyName("setBlah")); assertEquals("blah", TypedMember.propertyName("getBlah")); assertEquals("isBlah", TypedMember.propertyName("isBlah")); assertEquals("isBlah", TypedMember.propertyName("IsBlah")); assertEquals("", TypedMember.propertyName("")); } @Test public void testTypedMemberDecapitalize() throws Exception { Method decapitalize = TypedMember.class.getDeclaredMethod("decapitalize", String.class); decapitalize.setAccessible(true); assertNull(decapitalize.invoke(null, (String) null)); } @Command static class Issue905ParentCommand implements Runnable { public void run() {} @Command private int parameterless() { return 23; } } @Test public void testIssue905ParameterlessCommandMethodsException() { int actual = new CommandLine(new Issue905ParentCommand()).execute("parameterless"); assertEquals(23, actual); } }