/* Copyright 2013 Jonatan Jönsson * * 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 se.softhouse.jargo.commands; import static java.util.Collections.emptyList; import static org.fest.assertions.Assertions.assertThat; import static org.junit.Assert.fail; import static se.softhouse.common.strings.StringsUtil.NEWLINE; import static se.softhouse.common.strings.StringsUtil.TAB; import static se.softhouse.jargo.Arguments.command; import static se.softhouse.jargo.Arguments.stringArgument; import static se.softhouse.jargo.utils.Assertions2.assertThat; import static se.softhouse.jargo.utils.ExpectedTexts.expected; import java.io.File; import java.util.Arrays; import java.util.List; import org.junit.Test; import se.softhouse.common.strings.Describable; import se.softhouse.common.strings.Describers; import se.softhouse.jargo.Argument; import se.softhouse.jargo.ArgumentBuilder.CommandBuilder; import se.softhouse.jargo.ArgumentException; import se.softhouse.jargo.Command; import se.softhouse.jargo.CommandLineParser; import se.softhouse.jargo.ParsedArguments; import se.softhouse.jargo.Usage; import se.softhouse.jargo.commands.Build.BuildTarget; import se.softhouse.jargo.commands.Commit.Repository; import se.softhouse.jargo.commands.Commit.Revision; import se.softhouse.jargo.internal.Texts.UserErrors; import com.google.common.base.Predicates; import com.google.common.base.Suppliers; import com.google.common.collect.Lists; /** * Tests for subclassing {@link Command} */ public class CommandTest { /** * Simulate the behavior of <a href="http://maven.apache.org/">maven</a> * goals. Simple commands without any parameters. */ @Test public void testExecutingSeveralCommandsFromOneInvocation() throws ArgumentException { String[] arguments = {"clean", "build"}; BuildTarget target = new BuildTarget(); CommandLineParser.withCommands(new Build(target), new Clean(target)).parse(arguments); assertThat(target.isBuilt()).isTrue(); assertThat(target.isClean()).isTrue(); } @Test public void testMultipleCommandsEachWithSpecificArguments() throws ArgumentException { String[] logArgs = {"log", "--limit", "20"}; String[] commitArgs = {"commit", "--amend", "--author=jjonsson", "A.java", "B.java"}; Repository repo = new Repository(); CommandLineParser parser = CommandLineParser.withCommands(new Commit(repo), new Log(repo)); // Log parser.parse(logArgs); assertThat(repo.logLimit).isEqualTo(20); assertThat(repo.commits).isEmpty(); repo.logLimit = 10; // Commit parser.parse(commitArgs); Revision commit = repo.commits.get(0); assertThat(commit.amend).isTrue(); assertThat(commit.author).isEqualTo("jjonsson"); assertThat(commit.files).isEqualTo(Arrays.asList(new File("A.java"), new File("B.java"))); // Make sure that the parsing of the commit command didn't change the logLimit assertThat(repo.logLimit).isEqualTo(10); } /** * Tests that the concept Maven has with multiple goals with the command concept from Git works * in concert. Or simply put that multiple commands with command specific parameters can be * given at the same time. * As commit ends with a variable arity parameter (files to commit) that command has to be last. */ @Test public void testThatCombinedCommandsFromTheSameCommandLineBothAreExecuted() throws ArgumentException { String[] combinedInvocation = {"log", "--limit", "30", "commit", "--author=jjonsson"}; Repository repo = new Repository(); CommandLineParser parser = CommandLineParser.withCommands(new Commit(repo), new Log(repo)); parser.parse(combinedInvocation); assertThat(repo.logLimit).isEqualTo(30); Revision commit = repo.commits.get(0); assertThat(commit.amend).isFalse(); assertThat(commit.author).isEqualTo("jjonsson"); assertThat(commit.files).isEqualTo(emptyList()); } private static boolean didStart = false; /** * An alternative to {@link Command} that is based on interfaces instead */ public enum Service implements Runnable, Describable { START { @Override public void run() { didStart = true; } @Override public String description() { return "Starts the service"; } }; } @Test public void testMapOfCommands() throws Exception { CommandLineParser parser = CommandLineParser.withCommands(Service.class); parser.parse("start"); assertThat(didStart).isTrue(); Usage usage = parser.usage(); assertThat(usage).contains("start").contains("Starts the service"); } static final Argument<ParsedArguments> COMMIT = command(new Commit(new Repository())).build(); static final Argument<?> LOG = command(new Log(new Repository())).build(); @Test public void testCommandWithMissingRequiredArgument() { try { COMMIT.parse("commit");// No author fail("--author=??? wasn't given in the input and it should have been required"); } catch(ArgumentException expected) { assertThat(expected).hasMessage(String.format(UserErrors.MISSING_COMMAND_ARGUMENTS, "commit", "[--author]")); } } @Test public void testThatUnhandledArgumentIsCaught() { String[] args = {"log", "-verbose", "commit"}; try { CommandLineParser.withArguments(COMMIT, LOG).parse(args); fail("-verbose should have been reported as an unhandled argument"); } catch(ArgumentException expected) { assertThat(expected).hasMessage("Unexpected argument: -verbose, previous argument: log"); } } @Test public void testThatRequiredArgumentsAreResetBetweenParsings() throws ArgumentException { String[] invalidArgs = {"commit"}; String[] validArgs = {"commit", "--author=jjonsson"}; // First make a successful parsing COMMIT.parse(validArgs); for(int i = 0; i < 2; i++) { try { // Then make sure that the previous --author didn't "stick" COMMIT.parse(invalidArgs); fail("--author=??? wasn't given in the input and it should have been required"); } catch(ArgumentException expected) { assertThat(expected).hasMessage(String.format(UserErrors.MISSING_COMMAND_ARGUMENTS, "commit", "[--author]")); } } } @Test public void testThatRepeatedParsingsWithACommandParserDoesNotAffectEachOther() throws ArgumentException { String[] firstArgs = {"commit", "--author=jjonsson"}; String[] secondArgs = {"commit", "--author=nobody"}; Repository repo = new Repository(); CommandLineParser parser = CommandLineParser.withCommands(new Commit(repo)); parser.parse(firstArgs); parser.parse(secondArgs); assertThat(repo.commits.get(0).author).isEqualTo("jjonsson"); assertThat(repo.commits.get(1).author).isEqualTo("nobody"); } @Test public void testUsageForCommands() { BuildTarget target = new BuildTarget(); CommandLineParser parser = CommandLineParser.withCommands(new Build(target), new Clean(target), new Commit(new Repository())); Usage usage = parser.usage(); assertThat(usage).isEqualTo(expected("commandsWithArguments")); } @Test public void testCommandWithMissingIndexedArgument() { Argument<?> command = command(new CommandWithTwoIndexedArguments()).build(); try { command.parse("two_args", "1"); fail("two_args argument should require two parameters"); } catch(ArgumentException missingSecondParameterForIndexedArgument) { assertThat(missingSecondParameterForIndexedArgument).hasMessage(String.format( UserErrors.MISSING_NTH_PARAMETER, "second", "<integer>", "two_args")); } } @Test public void testThatTheInnerMostCommandIsPrintedInErrorMessage() { Argument<?> superCommand = command(new Command(command(new CommandWithTwoIndexedArguments()).build()){ @Override protected String commandName() { return "superCommand"; } @Override protected void execute(ParsedArguments parsedArguments) { } }).build(); try { superCommand.parse("superCommand", "two_args", "1"); fail("two_args argument should require two parameters"); } catch(ArgumentException missingSecondParameterForIndexedArgument) { assertThat(missingSecondParameterForIndexedArgument).hasMessage(String.format( UserErrors.MISSING_NTH_PARAMETER, "second", "<integer>", "two_args")); } } /** * Tests several commands that each have their own indexed arguments */ @Test public void testMultipleCommandEachWithIndexedArguments() throws ArgumentException { List<Command> executedCommands = Lists.newLinkedList(); CommandWithOneIndexedArgument first = new CommandWithOneIndexedArgument(executedCommands); CommandWithTwoIndexedArguments second = new CommandWithTwoIndexedArguments(executedCommands); CommandWithThreeIndexedArguments third = new CommandWithThreeIndexedArguments(executedCommands); CommandLineParser parser = CommandLineParser.withCommands(first, second, third); parser.parse("one_arg", "1", "two_args", "1", "2", "three_args", "1", "2", "3"); assertThat(executedCommands).containsExactly(first, second, third); } @Test public void testThatCorrectCommandIsMentionedInErrorMessage() { List<Command> executedCommands = Lists.newLinkedList(); CommandWithOneIndexedArgument first = new CommandWithOneIndexedArgument(executedCommands); CommandWithTwoIndexedArguments second = new CommandWithTwoIndexedArguments(executedCommands); CommandWithThreeIndexedArguments third = new CommandWithThreeIndexedArguments(executedCommands); CommandLineParser parser = CommandLineParser.withCommands(first, second, third); try { // Switched order of two_args and three_args for extra test harness parser.parse("one_arg", "1", "three_args", "1", "2", "3", "two_args", "1"); fail("two_args should require two args"); } catch(ArgumentException expected) { assertThat(expected).hasMessage("Missing second <integer> parameter for two_args"); assertThat(executedCommands).containsExactly(first, third); } } @Test public void testThatParserForCommandArgumentsIsOnlyCreatedWhenCommandIsExecuted() throws ArgumentException { InvalidCommand profiler = new InvalidCommand(); Argument<?> command = command(profiler).build(); try { command.parse("profile"); fail("Invalid arguments passed to super constructor in InvalidCommand should cause a lazy initialization error"); } catch(IllegalArgumentException expected) { assertThat(expected).hasMessage("-n is handled by several arguments"); } } @Test public void testThatCommandIsExecutedOnlyOnce() throws ArgumentException { ProfilingExecuteCommand profiler = new ProfilingExecuteCommand(); Argument<?> command = command(profiler).build(); assertThat(profiler.numberOfCallsToExecute).isZero(); command.parse("profile"); assertThat(profiler.numberOfCallsToExecute).isEqualTo(1); } @Test public void testRepeatedCommands() throws ArgumentException { assertThat(command(new Clean()).repeated().parse("clean", "clean", "clean")).hasSize(3); } @Test public void testAddingCommandsInChainedFashion() throws ArgumentException { BuildTarget target = new BuildTarget(); CommandLineParser.withArguments().andCommands(new Clean(target)).andCommands(new Build(target)).parse("clean", "build"); assertThat(target.isClean()).isTrue(); assertThat(target.isBuilt()).isTrue(); } @Test public void testThatSuitableCommandArgumentAreSuggestedForMissspelling() throws Exception { try { CommandLineParser.withArguments(LOG).parse("log", "-limit"); fail("-limit should be detected as being missspelled"); } catch(ArgumentException expected) { assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "-limit", "--limit" + NEWLINE + TAB + "-l")); } } @Test public void testThatArgumentsToSubcommandsAreSuggested() throws Exception { try { CommandLineParser.withCommands(new CommandWithOneIndexedArgument()).parse("one_arg", "1", "cm"); fail("cmd should be suggested for cm"); } catch(ArgumentException expected) { assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "cm", "cmd")); } } @Test public void testThatMissspelledArgumentIsNotSuggestedForAlreadyExecutedCommand() throws Exception { CommandLineParser parser = CommandLineParser.withCommands(new CommandWithOneIndexedArgument(), new CommandWithTwoIndexedArguments()); try { // As one_arg already has been executed, by the time two_args is seen, // suggesting --bool (optional argument to one_arg) would be an error parser.parse("one_arg", "1", "two_args", "1", "2", "-boo"); fail("-boo not detected as unhandled argument"); } catch(ArgumentException expected) { assertThat(expected).hasMessage("Unexpected argument: -boo, previous argument: 2"); } } @Test public void testThatInvalidParameterStopsExecuteFromBeingCalled() throws Exception { ProfilingExecuteCommand profilingCommand = new ProfilingExecuteCommand(); try { CommandLineParser.withCommands(profilingCommand).parse("profile", "-limit"); fail("-limit should not be handled by profile command"); } catch(ArgumentException expected) { assertThat(expected.getMessage()).startsWith("Unexpected argument: -limit"); assertThat(profilingCommand.numberOfCallsToExecute).as("profile should not have been called as -limit should be an invalid argument: ") .isZero(); } } @Test public void testThatInvalidParameterDoesNotStopEarlierCommandsFromBeingExecuted() throws Exception { ProfilingExecuteCommand profilingCommand = new ProfilingExecuteCommand(); try { CommandLineParser.withArguments(command(profilingCommand).repeated().build()).parse("profile", "profile", "-limit"); fail("-limit should not be handled by profile command"); } catch(ArgumentException expected) { assertThat(expected.getMessage()).startsWith("Unexpected argument: -limit"); assertThat(profilingCommand.numberOfCallsToExecute) .as("profile should have been called once since previous commands should be executed once a new command is given").isEqualTo(1); } } @Test public void testThatSubcommandsAreExecutedBeforeMainCommands() throws Exception { List<Command> executedCommands = Lists.newLinkedList(); ProfilingSubcommand profilingSubcommand = new ProfilingSubcommand(executedCommands); CommandLineParser parser = CommandLineParser.withCommands(profilingSubcommand); parser.parse("main", "c", "one_arg", "1"); assertThat(executedCommands).containsExactly(ProfilingSubcommand.subCommand, profilingSubcommand); assertThat(parser.usage()).isEqualTo(expected("commandWithSubCommand")); } @Test public void testThatCommandArgumentsCanBeFetchedAfterExecution() throws Exception { ParsedArguments commitArguments = COMMIT.parse("commit", "--author=joj"); assertThat(commitArguments.get(Commit.AUTHOR)).isEqualTo("joj"); } @Test public void testThatDescriptionForCommandIsLazilyCreated() throws ArgumentException { FailInDescription failDescription = new FailInDescription(); command(failDescription).parse("fail_description"); } private static final class FailInDescription extends Command { @Override public String commandName() { return "fail_description"; } @Override protected void execute(ParsedArguments parsedArguments) { } @Override public String description() { fail("Describable should only be called if usage is printed"); return "Unreachable description"; } } @Test public void testThatEndOfOptionsStopCommandsFromParsingArgumentNames() throws Exception { final Argument<String> option = stringArgument("--option").defaultValue("one").build(); Command command = new Command(option){ @Override protected String commandName() { return "command"; } @Override protected void execute(ParsedArguments parsedArguments) { assertThat(parsedArguments.get(option)).isEqualTo("one"); } }; Argument<?> indexed = stringArgument().build(); ParsedArguments result = CommandLineParser.withCommands(command).andArguments(indexed).parse("command", "--", "--option"); assertThat(result.get(indexed)).isEqualTo("--option"); } // This is what's tested @SuppressWarnings("deprecation") @Test public void testThatInvalidArgumentPropertiesOnCommandIsDeprecated() { CommandBuilder builder = command(new Build()); try { builder.arity(2); fail("method should throw as it's deprecated"); } catch(IllegalStateException expected) { } try { builder.defaultValue(null); fail("method should throw as it's deprecated"); } catch(IllegalStateException expected) { } try { builder.defaultValueDescriber(Describers.toStringDescriber()); fail("method should throw as it's deprecated"); } catch(IllegalStateException expected) { } try { builder.defaultValueSupplier(Suppliers.<ParsedArguments>ofInstance(null)); fail("method should throw as it's deprecated"); } catch(IllegalStateException expected) { } try { builder.defaultValueDescription(""); fail("method should throw as it's deprecated"); } catch(IllegalStateException expected) { } try { builder.limitTo(Predicates.alwaysFalse()); fail("method should throw as it's deprecated"); } catch(IllegalStateException expected) { } try { builder.required(); fail("method should throw as it's deprecated"); } catch(IllegalStateException expected) { } try { builder.splitWith("-"); fail("method should throw as it's deprecated"); } catch(IllegalStateException expected) { } try { builder.variableArity(); fail("method should throw as it's deprecated"); } catch(IllegalStateException expected) { } } }