/* 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.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.ExitCode; import picocli.CommandLine.IExecutionExceptionHandler; import picocli.CommandLine.IExitCodeExceptionMapper; import picocli.CommandLine.IExitCodeGenerator; import picocli.CommandLine.IParameterExceptionHandler; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.UsageMessageSpec; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import static java.lang.String.format; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.*; import static picocli.CommandLine.Command; import static picocli.CommandLine.ExecutionException; import static picocli.CommandLine.Help; import static picocli.CommandLine.IExecutionStrategy; import static picocli.CommandLine.Model.UsageMessageSpec.keyValuesMap; import static picocli.CommandLine.Option; import static picocli.CommandLine.ParameterException; import static picocli.CommandLine.Parameters; import static picocli.CommandLine.ParseResult; import static picocli.CommandLine.RunAll; import static picocli.CommandLine.RunFirst; import static picocli.CommandLine.RunLast; import static picocli.CommandLine.Spec; public class ExecuteTest { @Rule public final ProvideSystemProperty ansiOFF = new ProvideSystemProperty("picocli.ansi", "false"); @Rule // allows tests to set any kind of properties they like, without having to individually roll them back public final TestRule restoreSystemProperties = new RestoreSystemProperties(); @Rule public final SystemErrRule systemErrRule = new SystemErrRule().enableLog().muteForSuccessfulTests(); @Rule public final SystemOutRule systemOutRule = new SystemOutRule().enableLog().muteForSuccessfulTests(); interface Factory { Object create(); } @Test public void testExecutionStrategyRunXxxFailsIfNotRunnableOrCallable() { @Command class App { @Parameters String[] params; } Factory factory = new Factory() { public Object create() {return new App();} }; String[] args = { "abc" }; verifyAllFail(factory, "Parsed command (picocli.ExecuteTest$", ") is not a Method, Runnable or Callable", args); } @Test public void testExecutionStrategyRunXxxWithSubcommandsFailsWithMissingSubcommandIfNotRunnableOrCallable() { @Command class App { @Parameters String[] params; @Command void sub() {} } int exitCode = new CommandLine(new App()).execute("abc"); assertEquals(2, exitCode); String expected = String.format("" + "Missing required subcommand%n" + "Usage:
[...] [COMMAND]%n" + " [...]%n" + "Commands:%n" + " sub%n"); assertEquals(expected, systemErrRule.getLog()); } @Test public void testExecutionStrategyRunXxxCatchesAndRethrowsExceptionFromRunnable() { @Command class App implements Runnable { @Parameters String[] params; public void run() { throw new IllegalStateException("TEST EXCEPTION"); } } Factory factory = new Factory() { public Object create() {return new App();} }; verifyAllFail(factory, "TEST EXCEPTION", "", new String[0]); } @Test public void testExecutionStrategyRunXxxCatchesAndRethrowsExceptionFromCallable() { @Command class App implements Callable { @Parameters String[] params; public Object call() { throw new IllegalStateException("TEST EXCEPTION2"); } } Factory factory = new Factory() { public Object create() {return new App();} }; verifyAllFail(factory, "TEST EXCEPTION2", "", new String[0]); } private void verifyAllFail(Factory factory, String prefix, String suffix, String[] args) { IExecutionStrategy[] strategies = new IExecutionStrategy[] { new RunFirst(), new RunLast(), new RunAll() }; for (IExecutionStrategy strategy : strategies) { String descr = strategy.getClass().getSimpleName(); int exitCode = new CommandLine(factory.create()) .setExecutionStrategy(strategy) .setExecutionExceptionHandler(createHandler(descr, prefix, suffix)) .execute(args); assertEquals(1, exitCode); } } private IExecutionExceptionHandler createHandler(final String descr, final String prefix, final String suffix) { return new IExecutionExceptionHandler() { public int handleExecutionException(Exception ex, CommandLine cmd, ParseResult parseResult) throws Exception { if (ex instanceof IllegalStateException || ex instanceof ExecutionException) { String actual = ex.getMessage(); assertTrue(descr + ": " + actual, actual.startsWith(prefix)); assertTrue(descr + ": " + actual, actual.endsWith(suffix)); } else { fail("Unexpected exception " + ex); } return 1; } }; } @Test public void testReturnDefaultExitCodeIfHelpRequested() { @Command(version = "abc 1.3.4") class App implements Callable { @Option(names = "-h", usageHelp = true) boolean requestHelp; @Option(names = "-V", versionHelp = true) boolean requestVersion; public Object call() { return "RETURN VALUE"; } } CommandLineFactory factory = new CommandLineFactory() { public CommandLine create() {return new CommandLine(new App());} }; verifyExitCodeForBuiltInHandlers(factory, ExitCode.OK, new String[] {"-h"}); verifyExitCodeForBuiltInHandlers(factory, ExitCode.OK, new String[] {"-V"}); } @Test public void testReturnExitCodeFromAnnotationIfHelpRequested_NonNumericCallable() { @Command(version = "abc 1.3.4", exitCodeOnUsageHelp = 234, exitCodeOnVersionHelp = 543) class App implements Callable { @Option(names = "-h", usageHelp = true) boolean requestHelp; @Option(names = "-V", versionHelp = true) boolean requestVersion; public Object call() { return "RETURN VALUE"; } } CommandLineFactory factory = new CommandLineFactory() { public CommandLine create() {return new CommandLine(new App());} }; verifyExitCodeForBuiltInHandlers(factory, 234, new String[] {"-h"}); verifyExitCodeForBuiltInHandlers(factory, 543, new String[] {"-V"}); } @Test public void testReturnExitCodeFromAnnotationIfHelpRequested_NumericCallable() { @Command(version = "abc 1.3.4", exitCodeOnUsageHelp = 234, exitCodeOnVersionHelp = 543) class App implements Callable { @Option(names = "-h", usageHelp = true) boolean requestHelp; @Option(names = "-V", versionHelp = true) boolean requestVersion; public Object call() { return 999; } // ignored (not executed) } CommandLineFactory factory = new CommandLineFactory() { public CommandLine create() {return new CommandLine(new App());} }; verifyExitCodeForBuiltInHandlers(factory, 234, new String[] {"-h"}); verifyExitCodeForBuiltInHandlers(factory, 543, new String[] {"-V"}); } @Test public void testReturnDefaultExitCodeOnSuccess() { @Command class App implements Callable { public Object call() { return "RETURN VALUE"; } } CommandLineFactory factory = new CommandLineFactory() { public CommandLine create() {return new CommandLine(new App());} }; verifyExitCodeForBuiltInHandlers(factory, ExitCode.OK, new String[0]); } @Test public void testReturnExitCodeFromAnnotationOnSuccess_NonNumericCallable() { @Command(exitCodeOnSuccess = 123) class App implements Callable { public Object call() { return "RETURN VALUE"; } } CommandLineFactory factory = new CommandLineFactory() { public CommandLine create() {return new CommandLine(new App());} }; verifyExitCodeForBuiltInHandlers(factory, 123, new String[0]); } @Test public void testReturnExitCodeFromAnnotationOnSuccess_NumericCallable() { @Command(exitCodeOnSuccess = 123) class App implements Callable { public Object call() { return 987; } } CommandLineFactory factory = new CommandLineFactory() { public CommandLine create() {return new CommandLine(new App());} }; verifyExitCodeForBuiltInHandlers(factory, 987, new String[0]); } interface CommandLineFactory { CommandLine create(); } private void verifyExitCodeForBuiltInHandlers(CommandLineFactory factory, int expected, String[] args) { IExecutionStrategy[] strategies = new IExecutionStrategy[] { new RunFirst(), new RunLast(), new RunAll() }; for (IExecutionStrategy strategy : strategies) { String descr = strategy.getClass().getSimpleName(); int actual = factory.create().setExecutionStrategy(strategy).execute(args); assertEquals(descr + ": return value", expected, actual); } } @Test public void testRunsRunnableIfParseSucceeds() throws Exception { final int[] runWasCalled = {0}; @Command class App implements Runnable { public void run() { runWasCalled[0]++; } } new CommandLine(new App()).execute(); assertEquals(1, runWasCalled[0]); } @Test public void testCallsCallableIfParseSucceeds() throws Exception { final int[] runWasCalled = {0}; @Command class App implements Callable { public Object call() { return runWasCalled[0]++; } } new CommandLine(new App()).execute(); assertEquals(1, runWasCalled[0]); } @Test public void testInvokesMethodIfParseSucceeds() throws Exception { final int[] runWasCalled = {0}; @Command class App { @Command public Object mySubcommand() { return runWasCalled[0]++; } } new CommandLine(new App()).execute("mySubcommand"); assertEquals(1, runWasCalled[0]); } @Test public void testPrintErrorOnInvalidInput() throws Exception { final int[] runWasCalled = {0}; class App implements Runnable { @Option(names = "-number") int number; public void run() { runWasCalled[0]++; } } { StringWriter sw = new StringWriter(); new CommandLine(new App()).setErr(new PrintWriter(sw)).execute("-number", "not a number"); assertEquals(0, runWasCalled[0]); assertEquals(String.format( "Invalid value for option '-number': 'not a number' is not an int%n" + "Usage:
[-number=]%n" + " -number=%n"), sw.toString()); } } @Test public void testReturnDefaultExitCodeOnInvalidInput() throws Exception { class App implements Callable { @Option(names = "-number") int number; public Boolean call() { return true; } } { int exitCode = new CommandLine(new App()).execute("-number", "not a number"); assertEquals(ExitCode.USAGE, exitCode); } } @Test public void testReturnExitCodeFromAnnotationOnInvalidInput_NumericCallable() throws Exception { @Command(exitCodeOnInvalidInput = 987) class App implements Callable { @Option(names = "-number") int number; public Boolean call() { return true; } } { int exitCode = new CommandLine(new App()).execute("-number", "not a number"); assertEquals(987, exitCode); } } @Test public void testExitCodeFromParameterExceptionHandlerHandler() { @Command class App implements Runnable { public void run() { throw new ParameterException(new CommandLine(this), "blah"); } } CustomParameterExceptionHandler handler = new CustomParameterExceptionHandler(); int exitCode = new CommandLine(new App()).setParameterExceptionHandler(handler).execute(); assertEquals(format("" + "Hi, this is my custom error message%n"), systemErrRule.getLog()); assertEquals(125, exitCode); } static class CustomParameterExceptionHandler implements IParameterExceptionHandler { public int handleParseException(ParameterException ex, String[] args) throws Exception { ex.getCommandLine().getErr().println("Hi, this is my custom error message"); return 125; } } @Command(name = "mycmd", mixinStandardHelpOptions = true, version = "MyCallable-1.0") static class MyCallable implements Callable { @Option(names = "-x", description = "this is an option") String option; public Object call() { throw new IllegalStateException("this is a test"); } } @Command(name = "mycmd", mixinStandardHelpOptions = true, version = "MyRunnable-1.0") static class MyRunnable implements Runnable { @Option(names = "-x", description = "this is an option") String option; public void run() { throw new IllegalStateException("this is a test"); } } private static final String MYCALLABLE_USAGE = format("" + "Usage: mycmd [-hV] [-x=