/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * 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. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.cli; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionSet; import joptsimple.util.KeyValuePair; import org.junit.Before; import java.io.IOException; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; public class MultiCommandTests extends CommandTestCase { static class DummyMultiCommand extends MultiCommand { final AtomicBoolean closed = new AtomicBoolean(); DummyMultiCommand() { super("A dummy multi command", () -> {}); } @Override public void close() throws IOException { super.close(); if (this.closed.compareAndSet(false, true) == false) { throw new IllegalStateException("DummyMultiCommand already closed"); } } } static class DummySubCommand extends Command { final boolean throwsExceptionOnClose; final AtomicBoolean closeCalled = new AtomicBoolean(); DummySubCommand() { this(false); } DummySubCommand(final boolean throwsExceptionOnClose) { super("A dummy subcommand", () -> {}); this.throwsExceptionOnClose = throwsExceptionOnClose; } @Override protected void execute(Terminal terminal, OptionSet options) throws Exception { terminal.println("Arguments: " + options.nonOptionArguments().toString()); } @Override public void close() throws IOException { if (this.closeCalled.compareAndSet(false, true) == false) { throw new IllegalStateException("DummySubCommand already closed"); } if (throwsExceptionOnClose) { throw new IOException("Error occurred while closing DummySubCommand"); } } } static class DummySettingsSubCommand extends DummySubCommand { private final ArgumentAcceptingOptionSpec settingOption; DummySettingsSubCommand() { super(); this.settingOption = parser.accepts("E", "Configure a setting").withRequiredArg().ofType(KeyValuePair.class); } @Override protected void execute(Terminal terminal, OptionSet options) throws Exception { final List values = this.settingOption.values(options); terminal.println("Settings: " + values); super.execute(terminal, options); } } private DummyMultiCommand multiCommand; @Before public void setupCommand() { multiCommand = new DummyMultiCommand(); } @Override protected Command newCommand() { return multiCommand; } public void testNoCommandsConfigured() { IllegalStateException e = expectThrows(IllegalStateException.class, this::execute); assertEquals("No subcommands configured", e.getMessage()); } public void testUnknownCommand() { multiCommand.subcommands.put("something", new DummySubCommand()); UserException e = expectThrows(UserException.class, () -> execute("somethingelse")); assertEquals(ExitCodes.USAGE, e.exitCode); assertEquals("Unknown command [somethingelse]", e.getMessage()); } public void testMissingCommand() { multiCommand.subcommands.put("command1", new DummySubCommand()); UserException e = expectThrows(UserException.class, this::execute); assertEquals(ExitCodes.USAGE, e.exitCode); assertEquals("Missing command", e.getMessage()); } public void testHelp() throws Exception { multiCommand.subcommands.put("command1", new DummySubCommand()); multiCommand.subcommands.put("command2", new DummySubCommand()); execute("-h"); String output = terminal.getOutput(); assertTrue(output, output.contains("command1")); assertTrue(output, output.contains("command2")); } /** * Check that if -E arguments are passed to the main command, then they are accepted * and passed on to the subcommand. */ public void testSettingsOnMainCommand() throws Exception { multiCommand.subcommands.put("command1", new DummySettingsSubCommand()); execute("-Esetting1=value1", "-Esetting2=value2", "command1", "otherArg"); String output = terminal.getOutput(); assertThat(output, containsString("Settings: [setting1=value1, setting2=value2]")); assertThat(output, containsString("Arguments: [otherArg]")); } public void testSubcommandHelp() throws Exception { multiCommand.subcommands.put("command1", new DummySubCommand()); multiCommand.subcommands.put("command2", new DummySubCommand()); execute("command2", "-h"); String output = terminal.getOutput(); assertFalse(output, output.contains("command1")); assertTrue(output, output.contains("A dummy subcommand")); } public void testSubcommandArguments() throws Exception { multiCommand.subcommands.put("command1", new DummySubCommand()); execute("command1", "foo", "bar"); String output = terminal.getOutput(); assertFalse(output, output.contains("command1")); assertTrue(output, output.contains("Arguments: [foo, bar]")); } public void testClose() throws Exception { DummySubCommand subCommand1 = new DummySubCommand(); DummySubCommand subCommand2 = new DummySubCommand(); multiCommand.subcommands.put("command1", subCommand1); multiCommand.subcommands.put("command2", subCommand2); multiCommand.close(); assertTrue("MultiCommand was not closed when close method is invoked", multiCommand.closed.get()); assertTrue("SubCommand1 was not closed when close method is invoked", subCommand1.closeCalled.get()); assertTrue("SubCommand2 was not closed when close method is invoked", subCommand2.closeCalled.get()); } public void testCloseWhenSubCommandCloseThrowsException() throws Exception { final boolean command1Throws = randomBoolean(); final boolean command2Throws = randomBoolean(); final DummySubCommand subCommand1 = new DummySubCommand(command1Throws); final DummySubCommand subCommand2 = new DummySubCommand(command2Throws); multiCommand.subcommands.put("command1", subCommand1); multiCommand.subcommands.put("command2", subCommand2); if (command1Throws || command2Throws) { // verify exception is thrown, as well as other non failed sub-commands closed // properly. IOException ioe = expectThrows(IOException.class, multiCommand::close); assertEquals("Error occurred while closing DummySubCommand", ioe.getMessage()); if (command1Throws && command2Throws) { assertEquals(1, ioe.getSuppressed().length); assertTrue("Missing suppressed exceptions", ioe.getSuppressed()[0] instanceof IOException); assertEquals("Error occurred while closing DummySubCommand", ioe.getSuppressed()[0].getMessage()); } } else { multiCommand.close(); } assertTrue("SubCommand1 was not closed when close method is invoked", subCommand1.closeCalled.get()); assertTrue("SubCommand2 was not closed when close method is invoked", subCommand2.closeCalled.get()); } // Tests for multicommand error logging static class ErrorHandlingMultiCommand extends MultiCommand { ErrorHandlingMultiCommand() { super("error catching", () -> {}); } @Override protected boolean addShutdownHook() { return false; } } static class ErrorThrowingSubCommand extends Command { ErrorThrowingSubCommand() { super("error throwing", () -> {}); } @Override protected void execute(Terminal terminal, OptionSet options) throws Exception { throw new UserException(1, "Dummy error"); } @Override protected boolean addShutdownHook() { return false; } } public void testErrorDisplayedWithDefault() throws Exception { MockTerminal terminal = new MockTerminal(); MultiCommand mc = new ErrorHandlingMultiCommand(); mc.subcommands.put("throw", new ErrorThrowingSubCommand()); mc.main(new String[] { "throw", "--silent" }, terminal); assertThat(terminal.getOutput(), is(emptyString())); assertThat(terminal.getErrorOutput(), equalTo("ERROR: Dummy error\n")); } public void testNullErrorMessageSuppressesErrorOutput() throws Exception { MockTerminal terminal = new MockTerminal(); MultiCommand mc = new ErrorHandlingMultiCommand(); mc.subcommands.put("throw", new ErrorThrowingSubCommand() { @Override protected void execute(Terminal terminal, OptionSet options) throws Exception { throw new UserException(1, null); } }); mc.main(new String[] { "throw", "--silent" }, terminal); assertThat(terminal.getOutput(), is(emptyString())); assertThat(terminal.getErrorOutput(), is(emptyString())); } }