/* 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.Ignore; import org.junit.Rule; import org.junit.Test; 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 java.text.ParseException; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import static org.junit.Assert.*; /** * Tests collecting errors instead of throwing them. */ public class LenientParsingTest { // 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(); @Test public void testMultiValueOptionArityAloneIsInsufficient() throws Exception { CommandLine.Model.CommandSpec spec = CommandLine.Model.CommandSpec.create(); CommandLine.Model.OptionSpec option = CommandLine.Model.OptionSpec.builder("-c", "--count").arity("3").type(int.class).build(); assertFalse(option.isMultiValue()); spec.addOption(option); spec.parser().collectErrors(true); CommandLine commandLine = new CommandLine(spec); commandLine.parseArgs("-c", "1", "2", "3"); assertEquals(1, commandLine.getParseResult().errors().size()); assertEquals("Unmatched arguments from index 2: '2', '3'", commandLine.getParseResult().errors().get(0).getMessage()); } @Test public void testMultiValuePositionalParamArityAloneIsInsufficient() throws Exception { CommandLine.Model.CommandSpec spec = CommandLine.Model.CommandSpec.create(); CommandLine.Model.PositionalParamSpec positional = CommandLine.Model.PositionalParamSpec.builder().index("0").arity("3").type(int.class).build(); assertFalse(positional.isMultiValue()); spec.addPositional(positional); spec.parser().collectErrors(true); CommandLine commandLine = new CommandLine(spec); commandLine.parseArgs("1", "2", "3"); assertEquals(1, commandLine.getParseResult().errors().size()); assertEquals("Unmatched arguments from index 1: '2', '3'", commandLine.getParseResult().errors().get(0).getMessage()); } @Test public void testMissingRequiredParams() { class Example { @Parameters(index = "1", arity = "0..1") String optional; @Parameters(index = "0") String mandatory; } CommandLine cmd = new CommandLine(new Example()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs(new String[0]); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Missing required parameter: '<mandatory>'", cmd.getParseResult().errors().get(0).getMessage()); } @Test public void testMissingRequiredParams1() { class Tricky1 { @Parameters(index = "2") String anotherMandatory; @Parameters(index = "1", arity = "0..1") String optional; @Parameters(index = "0") String mandatory; } CommandLine cmd = new CommandLine(new Tricky1()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs(new String[0]); assertEquals(2, cmd.getParseResult().errors().size()); assertEquals("Missing required parameters: '<mandatory>', '<anotherMandatory>'", cmd.getParseResult().errors().get(0).getMessage()); // assertEquals("Missing required parameters: '<mandatory>', '<anotherMandatory>'", cmd.getParseResult().errors().get(1).getMessage()); assertEquals("Missing required parameter: '<anotherMandatory>'", cmd.getParseResult().errors().get(1).getMessage()); cmd.parseArgs(new String[] {"firstonly"}); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Missing required parameter: '<anotherMandatory>'", cmd.getParseResult().errors().get(0).getMessage()); } @Test public void testMissingRequiredParams2() { class Tricky2 { @Parameters(index = "2", arity = "0..1") String anotherOptional; @Parameters(index = "1", arity = "0..1") String optional; @Parameters(index = "0") String mandatory; } CommandLine cmd = new CommandLine(new Tricky2()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs(new String[0]); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Missing required parameter: '<mandatory>'", cmd.getParseResult().errors().get(0).getMessage()); } @Test public void testMissingRequiredParamsWithOptions() { class Tricky3 { @Option(names="-v", required = true) boolean more; @Option(names="-t", required = true) boolean any; @Parameters(index = "1") String alsoMandatory; @Parameters(index = "0") String mandatory; } CommandLine cmd = new CommandLine(new Tricky3()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs(new String[] {"-t", "-v", "mandatory"}); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Missing required parameter: '<alsoMandatory>'", cmd.getParseResult().errors().get(0).getMessage()); cmd.parseArgs(new String[] {"-t"}); assertEquals(3, cmd.getParseResult().errors().size()); assertEquals("Missing required options and parameters: '-v', '<mandatory>', '<alsoMandatory>'", cmd.getParseResult().errors().get(0).getMessage()); // assertEquals("Missing required options and parameters: '-v', '<mandatory>', '<alsoMandatory>'", cmd.getParseResult().errors().get(1).getMessage()); // assertEquals("Missing required options and parameters: '-v', '<mandatory>', '<alsoMandatory>'", cmd.getParseResult().errors().get(2).getMessage()); assertEquals("Missing required parameters: '<mandatory>', '<alsoMandatory>'", cmd.getParseResult().errors().get(1).getMessage()); assertEquals("Missing required parameter: '<alsoMandatory>'", cmd.getParseResult().errors().get(2).getMessage()); } @Test public void testMissingRequiredParamWithOption() { class Tricky3 { @Option(names="-t") boolean any; @Parameters(index = "0") String mandatory; } CommandLine cmd = new CommandLine(new Tricky3()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs(new String[] {"-t"}); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Missing required parameter: '<mandatory>'", cmd.getParseResult().errors().get(0).getMessage()); } @Test public void testNonVarargArrayParametersWithArity0() { class NonVarArgArrayParamsZeroArity { @Parameters(arity = "0") List<String> params; } CommandLine cmd = new CommandLine(new NonVarArgArrayParamsZeroArity()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs("a", "b", "c"); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Unmatched arguments from index 0: 'a', 'b', 'c'", cmd.getParseResult().errors().get(0).getMessage()); cmd.parseArgs("a"); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Unmatched argument at index 0: 'a'", cmd.getParseResult().errors().get(0).getMessage()); } @Test public void testSingleValueFieldDefaultMinArityIsOne() { class App { @Option(names = "-boolean") boolean booleanField; @Option(names = "-Long") Long aLongField; } CommandLine cmd = new CommandLine(new App()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs("-Long", "-boolean"); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Expected parameter for option '-Long' but found '-boolean'", cmd.getParseResult().errors().get(0).getMessage()); } @Test public void testBooleanOptionsArity0_nFailsIfAttachedParamNotABoolean() { // ignores varargs CommandLine cmd = new CommandLine(new ArityTest.BooleanOptionsArity0_nAndParameters()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs("-bool=123 -other".split(" ")); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Invalid value for option '-bool': '123' is not a boolean", cmd.getParseResult().errors().get(0).getMessage()); } @Test public void testBooleanOptionsArity0_nShortFormFailsIfAttachedParamNotABoolean() { // ignores varargs CommandLine cmd = new CommandLine(new ArityTest.BooleanOptionsArity0_nAndParameters()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs("-rv234 -bool".split(" ")); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Unknown option: '-234' (while processing option: '-rv234')", cmd.getParseResult().errors().get(0).getMessage()); } @Test public void test365_StricterArityValidation() { class Cmd { @Option(names = "-a", arity = "2") String[] a; @Option(names = "-b", arity = "1..2") String[] b; @Option(names = "-c", arity = "2..3") String[] c; @Option(names = "-v") boolean verbose; } assertMissing(Arrays.asList("Expected parameter 2 (of 2 mandatory parameters) for option '-a' but found '-a'", "option '-a' at index 0 (<a>) requires at least 2 values, but only 1 were specified: [2]", "Unmatched argument at index 3: '2'"), new Cmd(), "-a", "1", "-a", "2"); assertMissing(Arrays.asList("Expected parameter 2 (of 2 mandatory parameters) for option '-a' but found '-v'"), new Cmd(), "-a", "1", "-v"); assertMissing(Arrays.asList("Expected parameter for option '-b' but found '-v'"), new Cmd(), "-b", "-v"); assertMissing(Arrays.asList("option '-c' at index 0 (<c>) requires at least 2 values, but only 1 were specified: [-a]", "option '-a' at index 0 (<a>) requires at least 2 values, but none were specified."), new Cmd(), "-c", "-a"); assertMissing(Arrays.asList("Expected parameter 1 (of 2 mandatory parameters) for option '-c' but found '-a'"), new Cmd(), "-c", "-a", "1", "2"); assertMissing(Arrays.asList("Expected parameter 2 (of 2 mandatory parameters) for option '-c' but found '-a'", "option '-a' at index 0 (<a>) requires at least 2 values, but none were specified."), new Cmd(), "-c", "1", "-a"); } @Test public void test365_StricterArityValidationWithMaps() { class Cmd { @Option(names = "-a", arity = "2") Map<String,String> a; @Option(names = "-b", arity = "1..2") Map<String,String> b; @Option(names = "-c", arity = "2..3") Map<String,String> c; @Option(names = "-v") boolean verbose; } assertMissing(Arrays.asList("Expected parameter 2 (of 2 mandatory parameters) for option '-a' but found '-a'", "option '-a' at index 0 (<String=String>) requires at least 2 values, but only 1 were specified: [C=D]", "Unmatched argument at index 3: 'C=D'"), new Cmd(), "-a", "A=B", "-a", "C=D"); assertMissing(Arrays.asList("Expected parameter 2 (of 2 mandatory parameters) for option '-a' but found '-v'"), new Cmd(), "-a", "A=B", "-v"); assertMissing(Arrays.asList("Expected parameter for option '-b' but found '-v'"), new Cmd(), "-b", "-v"); assertMissing(Arrays.asList("option '-c' at index 0 (<String=String>) requires at least 2 values, but only 1 were specified: [-a]", "option '-a' at index 0 (<String=String>) requires at least 2 values, but none were specified."), new Cmd(), "-c", "-a"); assertMissing(Arrays.asList("Expected parameter 1 (of 2 mandatory parameters) for option '-c' but found '-a'"), new Cmd(), "-c", "-a", "A=B", "C=D"); assertMissing(Arrays.asList("Expected parameter 2 (of 2 mandatory parameters) for option '-c' but found '-a'", "option '-a' at index 0 (<String=String>) requires at least 2 values, but none were specified."), new Cmd(), "-c", "A=B", "-a"); } private void assertMissing(List<String> expected, Object command, String... args) { CommandLine cmd = new CommandLine(command); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs(args); List<Exception> errors = cmd.getParseResult().errors(); assertEquals(errors.toString(), expected.size(), errors.size()); int i = 0; for (String msg : expected) { assertEquals(msg, errors.get(i++).getMessage()); } } @Test public void testEnumListTypeConversionFailsForInvalidInput() { CommandLine cmd = new CommandLine(new TypeConversionTest.EnumParams()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs("-timeUnitList", "SECONDS", "b", "c"); assertEquals(2, cmd.getParseResult().errors().size()); Exception ex = cmd.getParseResult().errors().get(0); String prefix = "Invalid value for option '-timeUnitList' at index 1 (<timeUnitList>): expected one of "; String suffix = " but was 'b'"; assertEquals(prefix, ex.getMessage().substring(0, prefix.length())); assertEquals(suffix, ex.getMessage().substring(ex.getMessage().length() - suffix.length(), ex.getMessage().length())); assertEquals("Unmatched argument at index 3: 'c'", cmd.getParseResult().errors().get(1).getMessage()); } @Test public void testTimeFormatHHmmssSSSInvalidError() throws ParseException { CommandLine cmd = new CommandLine(new TypeConversionTest.SupportedTypes()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs("-Time", "23:59:58;123"); assertEquals(1, cmd.getParseResult().errors().size()); assertEquals("Invalid value for option '-Time': '23:59:58;123' is not a HH:mm[:ss[.SSS]] time", cmd.getParseResult().errors().get(0).getMessage()); } @Test public void testByteFieldsAreDecimal() { CommandLine cmd = new CommandLine(new TypeConversionTest.SupportedTypes()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs("-byte", "0x1F", "-Byte", "0x0F"); assertEquals(2, cmd.getParseResult().errors().size()); assertEquals("Invalid value for option '-byte': '0x1F' is not a byte", cmd.getParseResult().errors().get(0).getMessage()); assertEquals("Invalid value for option '-Byte': '0x0F' is not a byte", cmd.getParseResult().errors().get(1).getMessage()); } @Test public void testUnknownOption() { class App { @Option(names = "-x") int x; @Parameters(index = "0") int first; @Parameters(index = "*") String all; } CommandLine cmd = new CommandLine(new App()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs("NOT_AN_INT", "-x=b", "-unknown", "1", "2", "3"); ParseResult parseResult = cmd.getParseResult(); assertEquals(Arrays.asList("NOT_AN_INT", "-unknown", "2", "3"), parseResult.unmatched()); assertEquals(6, parseResult.errors().size()); assertEquals("Invalid value for positional parameter at index 0 (<first>): 'NOT_AN_INT' is not an int", parseResult.errors().get(0).getMessage()); assertEquals("Invalid value for option '-x': 'b' is not an int", parseResult.errors().get(1).getMessage()); assertEquals("positional parameter at index 0..* (<all>) should be specified only once", parseResult.errors().get(2).getMessage()); assertEquals("positional parameter at index 0..* (<all>) should be specified only once", parseResult.errors().get(3).getMessage()); assertEquals("positional parameter at index 0..* (<all>) should be specified only once", parseResult.errors().get(4).getMessage()); assertEquals("Unmatched arguments from index 0: 'NOT_AN_INT', '-unknown', '2', '3'", parseResult.errors().get(5).getMessage()); } // @Ignore("Requires https://github.com/remkop/picocli/issues/995") @Test public void testAnyExceptionWrappedInParameterException() { class App { @Option(names = "-queue", type = String.class, split = ",") ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(2); } CommandLine cmd = new CommandLine(new App()); cmd.getCommandSpec().parser().collectErrors(true); cmd.parseArgs("-queue", "a,b,c"); ParseResult parseResult = cmd.getParseResult(); assertTrue(parseResult.unmatched().isEmpty()); assertEquals(1, parseResult.errors().size()); assertTrue(parseResult.errors().get(0) instanceof ParameterException); assertTrue(parseResult.errors().get(0).getCause() instanceof NoSuchMethodException); assertEquals("NoSuchMethodException: java.util.concurrent.ArrayBlockingQueue.<init>() while processing argument at or before arg[1] 'a,b,c' in [-queue, a,b,c]: java.lang.NoSuchMethodException: java.util.concurrent.ArrayBlockingQueue.<init>()", parseResult.errors().get(0).getMessage()); assertEquals("java.util.concurrent.ArrayBlockingQueue.<init>()", parseResult.errors().get(0).getCause().getMessage()); } }