/* 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)
{
}
}
}