/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.cli; import com.google.common.collect.ImmutableMap; import org.apache.commons.cli.CommandLine; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.node.internal.InternalSettingsPreparer; import org.junit.Test; import java.io.IOException; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.elasticsearch.common.cli.CliTool.ExitStatus.OK; import static org.elasticsearch.common.cli.CliTool.ExitStatus.USAGE; import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd; import static org.hamcrest.Matchers.*; /** * */ public class CliToolTests extends CliToolTestCase { @Test public void testOK() throws Exception { Terminal terminal = new MockTerminal(); final AtomicReference<Boolean> executed = new AtomicReference<>(false); final NamedCommand cmd = new NamedCommand("cmd", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) { executed.set(true); return OK; } }; SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); CliTool.ExitStatus status = tool.execute(); assertStatus(status, OK); assertCommandHasBeenExecuted(executed); } @Test public void testUsageError() throws Exception { Terminal terminal = new MockTerminal(); final AtomicReference<Boolean> executed = new AtomicReference<>(false); final NamedCommand cmd = new NamedCommand("cmd", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) { executed.set(true); return CliTool.ExitStatus.USAGE; } }; SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); CliTool.ExitStatus status = tool.execute(); assertStatus(status, CliTool.ExitStatus.USAGE); assertCommandHasBeenExecuted(executed); } @Test public void testIOError() throws Exception { Terminal terminal = new MockTerminal(); final AtomicReference<Boolean> executed = new AtomicReference<>(false); final NamedCommand cmd = new NamedCommand("cmd", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { executed.set(true); throw new IOException("io error"); } }; SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); CliTool.ExitStatus status = tool.execute(); assertStatus(status, CliTool.ExitStatus.IO_ERROR); assertCommandHasBeenExecuted(executed); } @Test public void testCodeError() throws Exception { Terminal terminal = new MockTerminal(); final AtomicReference<Boolean> executed = new AtomicReference<>(false); final NamedCommand cmd = new NamedCommand("cmd", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { executed.set(true); throw new Exception("random error"); } }; SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); CliTool.ExitStatus status = tool.execute(); assertStatus(status, CliTool.ExitStatus.CODE_ERROR); assertCommandHasBeenExecuted(executed); } @Test public void testMultiCommand() { Terminal terminal = new MockTerminal(); int count = randomIntBetween(2, 7); final AtomicReference<Boolean>[] executed = new AtomicReference[count]; for (int i = 0; i < executed.length; i++) { executed[i] = new AtomicReference<>(false); } NamedCommand[] cmds = new NamedCommand[count]; for (int i = 0; i < count; i++) { final int index = i; cmds[i] = new NamedCommand("cmd" + index, terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { executed[index].set(true); return OK; } }; } MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); int cmdIndex = randomIntBetween(0, count-1); CliTool.ExitStatus status = tool.execute("cmd" + cmdIndex); assertThat(status, is(OK)); for (int i = 0; i < executed.length; i++) { assertThat(executed[i].get(), is(i == cmdIndex)); } } @Test public void testMultiCommand_UnknownCommand() { Terminal terminal = new MockTerminal(); int count = randomIntBetween(2, 7); final AtomicReference<Boolean>[] executed = new AtomicReference[count]; for (int i = 0; i < executed.length; i++) { executed[i] = new AtomicReference<>(false); } NamedCommand[] cmds = new NamedCommand[count]; for (int i = 0; i < count; i++) { final int index = i; cmds[i] = new NamedCommand("cmd" + index, terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { executed[index].set(true); return OK; } }; } MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); CliTool.ExitStatus status = tool.execute("cmd" + count); // "cmd" + count doesn't exist assertThat(status, is(CliTool.ExitStatus.USAGE)); for (int i = 0; i < executed.length; i++) { assertThat(executed[i].get(), is(false)); } } @Test public void testSingleCommand_ToolHelp() throws Exception { CaptureOutputTerminal terminal = new CaptureOutputTerminal(); final AtomicReference<Boolean> executed = new AtomicReference<>(false); final NamedCommand cmd = new NamedCommand("cmd1", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { executed.set(true); throw new IOException("io error"); } }; SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); CliTool.ExitStatus status = tool.execute(args("-h")); assertStatus(status, CliTool.ExitStatus.OK_AND_EXIT); assertThat(terminal.getTerminalOutput(), hasSize(3)); assertThat(terminal.getTerminalOutput(), hasItem(containsString("cmd1 help"))); } @Test public void testMultiCommand_ToolHelp() { CaptureOutputTerminal terminal = new CaptureOutputTerminal(); NamedCommand[] cmds = new NamedCommand[2]; cmds[0] = new NamedCommand("cmd0", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { return OK; } }; cmds[1] = new NamedCommand("cmd1", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { return OK; } }; MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); CliTool.ExitStatus status = tool.execute(args("-h")); assertStatus(status, CliTool.ExitStatus.OK_AND_EXIT); assertThat(terminal.getTerminalOutput(), hasSize(3)); assertThat(terminal.getTerminalOutput(), hasItem(containsString("tool help"))); } @Test public void testMultiCommand_CmdHelp() { CaptureOutputTerminal terminal = new CaptureOutputTerminal(); NamedCommand[] cmds = new NamedCommand[2]; cmds[0] = new NamedCommand("cmd0", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { return OK; } }; cmds[1] = new NamedCommand("cmd1", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { return OK; } }; MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); CliTool.ExitStatus status = tool.execute(args("cmd1 -h")); assertStatus(status, CliTool.ExitStatus.OK_AND_EXIT); assertThat(terminal.getTerminalOutput(), hasSize(3)); assertThat(terminal.getTerminalOutput(), hasItem(containsString("cmd1 help"))); } @Test public void testThatThrowExceptionCanBeLogged() throws Exception { CaptureOutputTerminal terminal = new CaptureOutputTerminal(); NamedCommand cmd = new NamedCommand("cmd", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { throw new ElasticsearchException("error message"); } }; SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); assertStatus(tool.execute(), CliTool.ExitStatus.CODE_ERROR); assertThat(terminal.getTerminalOutput(), hasSize(1)); assertThat(terminal.getTerminalOutput(), hasItem(containsString("error message"))); // set env... and log stack trace try { System.setProperty(Terminal.DEBUG_SYSTEM_PROPERTY, "true"); terminal = new CaptureOutputTerminal(); assertStatus(new SingleCmdTool("tool", terminal, cmd).execute(), CliTool.ExitStatus.CODE_ERROR); assertThat(terminal.getTerminalOutput(), hasSize(2)); assertThat(terminal.getTerminalOutput(), hasItem(containsString("error message"))); // This class must be part of the stack strace assertThat(terminal.getTerminalOutput(), hasItem(containsString(getClass().getName()))); } finally { System.clearProperty(Terminal.DEBUG_SYSTEM_PROPERTY); } } @Test public void testMultipleLaunch() throws Exception { Terminal terminal = new MockTerminal(); final AtomicReference<Boolean> executed = new AtomicReference<>(false); final NamedCommand cmd = new NamedCommand("cmd", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) { executed.set(true); return OK; } }; SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); tool.parse("cmd", Strings.splitStringByCommaToArray("--verbose")); tool.parse("cmd", Strings.splitStringByCommaToArray("--silent")); tool.parse("cmd", Strings.splitStringByCommaToArray("--help")); } @Test public void testPromptForSetting() throws Exception { final AtomicInteger counter = new AtomicInteger(); final AtomicReference<String> promptedSecretValue = new AtomicReference<>(null); final AtomicReference<String> promptedTextValue = new AtomicReference<>(null); final Terminal terminal = new MockTerminal() { @Override public char[] readSecret(String text, Object... args) { counter.incrementAndGet(); assertThat(args, arrayContaining((Object) "foo.password")); return "changeit".toCharArray(); } @Override public String readText(String text, Object... args) { counter.incrementAndGet(); assertThat(args, arrayContaining((Object) "replace")); return "replaced"; } }; final NamedCommand cmd = new NamedCommand("noop", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) { promptedSecretValue.set(settings.get("foo.password")); promptedTextValue.set(settings.get("replace")); return OK; } }; System.setProperty("es.foo.password", InternalSettingsPreparer.SECRET_PROMPT_VALUE); System.setProperty("es.replace", InternalSettingsPreparer.TEXT_PROMPT_VALUE); try { new SingleCmdTool("tool", terminal, cmd).execute(); } finally { System.clearProperty("es.foo.password"); System.clearProperty("es.replace"); } assertThat(counter.intValue(), is(2)); assertThat(promptedSecretValue.get(), is("changeit")); assertThat(promptedTextValue.get(), is("replaced")); } @Test public void testStopAtNonOptionParsing() throws Exception { final CliToolConfig.Cmd lenientCommand = cmd("lenient", CliTool.Command.Exit.class).stopAtNonOption(true).build(); final CliToolConfig.Cmd strictCommand = cmd("strict", CliTool.Command.Exit.class).stopAtNonOption(false).build(); final CliToolConfig config = CliToolConfig.config("elasticsearch", CliTool.class).cmds(lenientCommand, strictCommand).build(); final CaptureOutputTerminal terminal = new CaptureOutputTerminal(); final CliTool cliTool = new CliTool(config, terminal) { @Override protected Command parse(String cmdName, CommandLine cli) throws Exception { return new NamedCommand(cmdName, terminal) { @Override public ExitStatus execute(Settings settings, Environment env) throws Exception { return OK; } }; } }; // known parameters, no error assertStatus(cliTool.execute(args("lenient --verbose")), OK); assertStatus(cliTool.execute(args("lenient -v")), OK); // unknown parameters, no error assertStatus(cliTool.execute(args("lenient --unknown")), OK); assertStatus(cliTool.execute(args("lenient -u")), OK); // unknown parameters, error assertStatus(cliTool.execute(args("strict --unknown")), USAGE); assertThat(terminal.getTerminalOutput(), hasItem(containsString("Unrecognized option: --unknown"))); terminal.getTerminalOutput().clear(); assertStatus(cliTool.execute(args("strict -u")), USAGE); assertThat(terminal.getTerminalOutput(), hasItem(containsString("Unrecognized option: -u"))); } private void assertStatus(CliTool.ExitStatus status, CliTool.ExitStatus expectedStatus) { assertThat(status, is(expectedStatus)); } private void assertCommandHasBeenExecuted(AtomicReference<Boolean> executed) { assertThat("Expected command atomic reference counter to be set to true", executed.get(), is(Boolean.TRUE)); } private static class SingleCmdTool extends CliTool { private final Command command; private SingleCmdTool(String name, Terminal terminal, NamedCommand command) { super(CliToolConfig.config(name, SingleCmdTool.class) .cmds(cmd(command.name, command.getClass())) .build(), terminal); this.command = command; } @Override protected Command parse(String cmdName, CommandLine cli) throws Exception { return command; } } private static class MultiCmdTool extends CliTool { private final Map<String, Command> commands; private MultiCmdTool(String name, Terminal terminal, NamedCommand... commands) { super(CliToolConfig.config(name, MultiCmdTool.class) .cmds(cmds(commands)) .build(), terminal); ImmutableMap.Builder<String, Command> commandByName = ImmutableMap.builder(); for (int i = 0; i < commands.length; i++) { commandByName.put(commands[i].name, commands[i]); } this.commands = commandByName.build(); } @Override protected Command parse(String cmdName, CommandLine cli) throws Exception { return commands.get(cmdName); } private static CliToolConfig.Cmd[] cmds(NamedCommand... commands) { CliToolConfig.Cmd[] cmds = new CliToolConfig.Cmd[commands.length]; for (int i = 0; i < commands.length; i++) { cmds[i] = cmd(commands[i].name, commands[i].getClass()).build(); } return cmds; } } private static abstract class NamedCommand extends CliTool.Command { private final String name; private NamedCommand(String name, Terminal terminal) { super(terminal); this.name = name; } } }