This is an automated email from the ASF dual-hosted git repository. apkhmv pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push: new 0fdb5e4f90 IGNITE-21828 Fix config update commands without parameters (#3550) 0fdb5e4f90 is described below commit 0fdb5e4f9089d4c26298cc37a82d598292964fa4 Author: Vadim Pakhnushev <8614891+valep...@users.noreply.github.com> AuthorDate: Thu Apr 4 11:10:40 2024 +0300 IGNITE-21828 Fix config update commands without parameters (#3550) --- .../ignite/internal/cli/CliIntegrationTest.java | 6 + .../internal/cli/commands/ItConfigCommandTest.java | 191 --------------------- .../configuration/ItConfigCommandTest.java | 37 +++- .../cli/commands/SpacedParameterMixin.java | 2 +- .../internal/cli/commands/CliCommandTestBase.java | 48 ++++-- .../internal/cli/commands/ProfileMixinTest.java | 45 +---- .../cluster/config/ConfigUpdateCommandTest.java | 131 ++++++++++++++ 7 files changed, 213 insertions(+), 247 deletions(-) diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/CliIntegrationTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/CliIntegrationTest.java index 0740d85c51..eb13a09cb0 100644 --- a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/CliIntegrationTest.java +++ b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/CliIntegrationTest.java @@ -151,6 +151,12 @@ public abstract class CliIntegrationTest extends ClusterPerClassIntegrationTest .contains(expectedOutput); } + protected void assertOutputDoesNotContain(String expectedOutput) { + assertThat(sout.toString()) + .as("Expected command output to not contain: " + expectedOutput + " but was " + sout.toString()) + .doesNotContain(expectedOutput); + } + protected void assertOutputMatches(String regex) { assertThat(sout.toString()) .as("Expected command output to match regex: " + regex + " but it is not: " + sout.toString()) diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java deleted file mode 100644 index a8046dca15..0000000000 --- a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF 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.apache.ignite.internal.cli.commands; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.ignite.internal.cli.core.style.AnsiStringSupport.fg; -import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName; -import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - -import java.nio.file.Path; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import org.apache.ignite.Ignite; -import org.apache.ignite.IgnitionManager; -import org.apache.ignite.InitParameters; -import org.apache.ignite.internal.app.IgniteImpl; -import org.apache.ignite.internal.cli.AbstractCliTest; -import org.apache.ignite.internal.cli.core.style.AnsiStringSupport.Color; -import org.apache.ignite.internal.testframework.TestIgnitionManager; -import org.apache.ignite.internal.testframework.WorkDirectory; -import org.apache.ignite.internal.testframework.WorkDirectoryExtension; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; -import org.junit.jupiter.api.extension.ExtendWith; - -/** - * Integration test for {@code ignite node/cluster config} commands. - */ -@ExtendWith(WorkDirectoryExtension.class) -public class ItConfigCommandTest extends AbstractCliTest { - /** Node. */ - private IgniteImpl node; - - @BeforeEach - void setup(@WorkDirectory Path workDir, TestInfo testInfo) { - String nodeName = testNodeName(testInfo, 0); - - CompletableFuture<Ignite> future = TestIgnitionManager.start(nodeName, null, workDir); - - InitParameters initParameters = InitParameters.builder() - .destinationNodeName(nodeName) - .metaStorageNodeNames(List.of(nodeName)) - .clusterName("cluster") - .build(); - - TestIgnitionManager.init(initParameters); - - assertThat(future, willCompleteSuccessfully()); - - node = (IgniteImpl) future.join(); - } - - @AfterEach - void tearDown(TestInfo testInfo) { - IgnitionManager.stop(testNodeName(testInfo, 0)); - } - - @Test - public void setAndGetWithManualHost() { - int exitCode = execute( - "node", - "config", - "update", - "--node-url", - "http://localhost:" + node.restHttpAddress().port(), - "network.shutdownQuietPeriod=1" - ); - - assertEquals(0, exitCode); - assertThat( - out.toString(UTF_8), - containsString("Node configuration updated. " - + fg(Color.YELLOW).mark("Restart the node to apply changes.")) - - ); - - resetStreams(); - - exitCode = execute( - "node", - "config", - "show", - "--node-url", - "http://localhost:" + node.restHttpAddress().port() - ); - - assertEquals(0, exitCode); - - assertThat( - out.toString(UTF_8), - containsString("\"shutdownQuietPeriod\" : 1") - ); - } - - @Test - public void setWithWrongData() { - int exitCode = execute( - "node", - "config", - "update", - "--node-url", - "http://localhost:" + node.restHttpAddress().port(), - "network.foo=\"bar\"" - ); - - assertEquals(1, exitCode); - assertThat( - err.toString(UTF_8), - containsString("'network' configuration doesn't have the 'foo' sub-configuration") - ); - - resetStreams(); - - exitCode = execute( - "node", - "config", - "update", - "--node-url", - "http://localhost:" + node.restHttpAddress().port(), - "network.shutdownQuietPeriod=asd" - ); - - assertEquals(1, exitCode); - assertThat( - err.toString(UTF_8), - containsString("'long' is expected as a type for the 'network.shutdownQuietPeriod' configuration value") - ); - } - - @Test - public void partialGet() { - int exitCode = execute( - "node", - "config", - "show", - "--node-url", - "http://localhost:" + node.restHttpAddress().port(), - "network" - ); - - assertEquals(0, exitCode); - - assertThat( - out.toString(UTF_8), - containsString("\"inbound\"") - ); - - assertFalse(out.toString(UTF_8).contains("\"node\"")); - } - - /** - * Unescapes quotes in the input string. - * - * @param input String. - * @return String with unescaped quotes. - */ - private static String unescapeQuotes(String input) { - return input.replace("\\\"", "\""); - } - - /** - * Removes trailing quotes from the input string. - * - * @param input String. - * @return String without trailing quotes. - */ - private static String removeTrailingQuotes(String input) { - return input.substring(1, input.length() - 1); - } -} diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/configuration/ItConfigCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/configuration/ItConfigCommandTest.java index 7eced9cf6e..24a33ac214 100644 --- a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/configuration/ItConfigCommandTest.java +++ b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/configuration/ItConfigCommandTest.java @@ -18,9 +18,11 @@ package org.apache.ignite.internal.cli.commands.configuration; +import static org.apache.ignite.internal.cli.core.style.AnsiStringSupport.fg; import static org.junit.jupiter.api.Assertions.assertAll; import org.apache.ignite.internal.cli.CliIntegrationTest; +import org.apache.ignite.internal.cli.core.style.AnsiStringSupport.Color; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -77,7 +79,8 @@ class ItConfigCommandTest extends CliIntegrationTest { assertAll( this::assertExitCodeIsZero, this::assertErrOutputIsEmpty, - this::assertOutputIsNotEmpty + () -> assertOutputContains("Node configuration updated. " + + fg(Color.YELLOW).mark("Restart the node to apply changes.")) ); // When read the updated cluster configuration @@ -196,4 +199,36 @@ class ItConfigCommandTest extends CliIntegrationTest { this::assertOutputIsNotEmpty ); } + + @Test + void updateWithWrongData() { + execute("node", "config", "update", "--node-url", NODE_URL, "network.foo=\"bar\""); + + assertAll( + () -> assertExitCodeIs(1), + () -> assertErrOutputContains("'network' configuration doesn't have the 'foo' sub-configuration"), + this::assertOutputIsEmpty + ); + + resetOutput(); + + execute("node", "config", "update", "--node-url", NODE_URL, "network.shutdownQuietPeriod=asd"); + + assertAll( + () -> assertExitCodeIs(1), + () -> assertErrOutputContains("'long' is expected as a type for the 'network.shutdownQuietPeriod' configuration value"), + this::assertOutputIsEmpty + ); + } + + @Test + public void partialGet() { + execute("node", "config", "show", "--node-url", NODE_URL, "network"); + assertAll( + this::assertExitCodeIsZero, + this::assertErrOutputIsEmpty, + () -> assertOutputContains("\"inbound\""), + () -> assertOutputDoesNotContain("\"node\"") + ); + } } diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/SpacedParameterMixin.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/SpacedParameterMixin.java index 3ab058095e..c9c6b714e7 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/SpacedParameterMixin.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/SpacedParameterMixin.java @@ -26,7 +26,7 @@ import picocli.CommandLine.Parameters; * With this mixin, we allow the user to specify one parameter with spaces without quotation. */ public class SpacedParameterMixin { - @Parameters(index = "0..*") + @Parameters(arity = "1") private String[] args; @Override diff --git a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java index 3e5ee65bf4..19994ab7a6 100644 --- a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java +++ b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java @@ -18,6 +18,10 @@ package org.apache.ignite.internal.cli.commands; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import io.micronaut.configuration.picocli.MicronautFactory; import io.micronaut.context.ApplicationContext; @@ -26,38 +30,27 @@ import jakarta.inject.Inject; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Arrays; -import org.apache.ignite.internal.cli.core.repl.registry.NodeNameRegistry; -import org.junit.jupiter.api.BeforeEach; +import org.apache.ignite.internal.cli.core.call.Call; +import org.apache.ignite.internal.cli.core.call.CallInput; +import org.apache.ignite.internal.cli.core.call.DefaultCallOutput; +import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; +import org.mockito.ArgumentCaptor; import picocli.CommandLine; /** * Base class for testing CLI commands. */ @MicronautTest -public abstract class CliCommandTestBase { +public abstract class CliCommandTestBase extends BaseIgniteAbstractTest { @Inject private ApplicationContext context; - @Inject - private NodeNameRegistry nodeNameRegistry; - - private CommandLine cmd; - private StringWriter sout; private StringWriter serr; private int exitCode = Integer.MIN_VALUE; - @BeforeEach - public void setUp() { - cmd = new CommandLine(getCommandClass(), new MicronautFactory(context)); - sout = new StringWriter(); - serr = new StringWriter(); - cmd.setOut(new PrintWriter(sout)); - cmd.setErr(new PrintWriter(serr)); - } - protected abstract Class<?> getCommandClass(); protected void execute(String argsLine) { @@ -65,6 +58,14 @@ public abstract class CliCommandTestBase { } protected void execute(String... args) { + // Create command just before execution as some tests could register singletons which should be used by the command + CommandLine cmd = new CommandLine(getCommandClass(), new MicronautFactory(context)); + + sout = new StringWriter(); + serr = new StringWriter(); + cmd.setOut(new PrintWriter(sout)); + cmd.setErr(new PrintWriter(serr)); + exitCode = cmd.execute(args); } @@ -125,4 +126,17 @@ public abstract class CliCommandTestBase { .as("Expected command error output to contain: " + expectedErrOutput + " but was " + serr.toString()) .contains(expectedErrOutput); } + + protected <IT extends CallInput, OT, T extends Call<IT, OT>> T registerMockCall(Class<T> callClass) { + T mock = mock(callClass); + context.registerSingleton(mock); + when(mock.execute(any())).thenReturn(DefaultCallOutput.empty()); + return mock; + } + + protected static <IT extends CallInput, OT, T extends Call<IT, OT>> IT verifyCallInput(T call, Class<IT> inputClass) { + ArgumentCaptor<IT> captor = ArgumentCaptor.forClass(inputClass); + verify(call).execute(captor.capture()); + return captor.getValue(); + } } diff --git a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java index 039e053c87..0e50d92139 100644 --- a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java +++ b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java @@ -19,15 +19,7 @@ package org.apache.ignite.internal.cli.commands; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import io.micronaut.configuration.picocli.MicronautFactory; -import io.micronaut.context.ApplicationContext; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import jakarta.inject.Inject; + import java.util.function.Function; import java.util.stream.Stream; import org.apache.ignite.internal.cli.call.cluster.ClusterInitCall; @@ -45,21 +37,16 @@ import org.apache.ignite.internal.cli.call.configuration.NodeConfigUpdateCallInp import org.apache.ignite.internal.cli.call.node.status.NodeStatusCall; import org.apache.ignite.internal.cli.core.call.Call; import org.apache.ignite.internal.cli.core.call.CallInput; -import org.apache.ignite.internal.cli.core.call.DefaultCallOutput; import org.apache.ignite.internal.cli.core.call.UrlCallInput; -import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.ArgumentCaptor; -import picocli.CommandLine; /** * Test for --profile override for --node-url and --cluster-endpoint-url options. */ -@MicronautTest -public class ProfileMixinTest extends BaseIgniteAbstractTest { +public class ProfileMixinTest extends CliCommandTestBase { /** * Cluster URL from default profile in integration_tests.ini. */ @@ -75,9 +62,6 @@ public class ProfileMixinTest extends BaseIgniteAbstractTest { */ private static final String URL_FROM_CMD = "http://localhost:10302"; - @Inject - private ApplicationContext ctx; - @ParameterizedTest @DisplayName("Should take URL from default profile") @MethodSource("allCallsProvider") @@ -162,7 +146,7 @@ public class ProfileMixinTest extends BaseIgniteAbstractTest { assertEquals(URL_FROM_CMD, urlSupplier.apply(callInput)); } - static Stream<Arguments> nodeCallsProvider() { + private static Stream<Arguments> nodeCallsProvider() { return Stream.of( arguments( "node config show", @@ -185,7 +169,7 @@ public class ProfileMixinTest extends BaseIgniteAbstractTest { ); } - static Stream<Arguments> clusterCallsProvider() { + private static Stream<Arguments> clusterCallsProvider() { return Stream.of( arguments( "cluster config show", @@ -220,25 +204,12 @@ public class ProfileMixinTest extends BaseIgniteAbstractTest { ); } - static Stream<Arguments> allCallsProvider() { + private static Stream<Arguments> allCallsProvider() { return Stream.concat(nodeCallsProvider(), clusterCallsProvider()); } - private void execute(String cmdLine) { - CommandLine cmd = new CommandLine(TopLevelCliCommand.class, new MicronautFactory(ctx)); - cmd.execute(cmdLine.split(" ")); - } - - private <IT extends CallInput, OT, T extends Call<IT, OT>> T registerMockCall(Class<T> callClass) { - T mock = mock(callClass); - ctx.registerSingleton(mock); - when(mock.execute(any())).thenReturn(DefaultCallOutput.empty()); - return mock; - } - - private <IT extends CallInput, OT, T extends Call<IT, OT>> IT verifyCallInput(T call, Class<IT> inputClass) { - ArgumentCaptor<IT> captor = ArgumentCaptor.forClass(inputClass); - verify(call).execute(captor.capture()); - return captor.getValue(); + @Override + protected Class<?> getCommandClass() { + return TopLevelCliCommand.class; } } diff --git a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/config/ConfigUpdateCommandTest.java b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/config/ConfigUpdateCommandTest.java new file mode 100644 index 0000000000..7cd3d67ab3 --- /dev/null +++ b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/config/ConfigUpdateCommandTest.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.ignite.internal.cli.commands.cluster.config; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.function.Function; +import java.util.stream.Stream; +import org.apache.ignite.internal.cli.call.configuration.ClusterConfigUpdateCall; +import org.apache.ignite.internal.cli.call.configuration.ClusterConfigUpdateCallInput; +import org.apache.ignite.internal.cli.call.configuration.NodeConfigUpdateCall; +import org.apache.ignite.internal.cli.call.configuration.NodeConfigUpdateCallInput; +import org.apache.ignite.internal.cli.commands.CliCommandTestBase; +import org.apache.ignite.internal.cli.commands.TopLevelCliCommand; +import org.apache.ignite.internal.cli.core.call.Call; +import org.apache.ignite.internal.cli.core.call.CallInput; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ConfigUpdateCommandTest extends CliCommandTestBase { + @Override + protected Class<?> getCommandClass() { + return TopLevelCliCommand.class; + } + + private static Stream<Arguments> calls() { + return Stream.of( + arguments( + "cluster config update", + ClusterConfigUpdateCall.class, + ClusterConfigUpdateCallInput.class, + (Function<ClusterConfigUpdateCallInput, String>) ClusterConfigUpdateCallInput::getConfig + ), + arguments( + "node config update", + NodeConfigUpdateCall.class, + NodeConfigUpdateCallInput.class, + (Function<NodeConfigUpdateCallInput, String>) NodeConfigUpdateCallInput::getConfig + ) + ); + } + + @ParameterizedTest + @MethodSource("calls") + void noParameter(String command) { + // When executed without arguments + execute(command); + + assertAll( + () -> assertExitCodeIs(2), + this::assertOutputIsEmpty, + () -> assertErrOutputContains("Missing required parameter: '<args>'") + ); + } + + @ParameterizedTest + @MethodSource("calls") + <IT extends CallInput, OT, T extends Call<IT, OT>> void unquotedParameter( + String command, + Class<T> callClass, + Class<IT> callInputClass, + Function<IT, String> configFunction + ) { + checkParameters(command, callClass, callInputClass, configFunction, "test", "test"); + } + + @ParameterizedTest + @MethodSource("calls") + <IT extends CallInput, OT, T extends Call<IT, OT>> void quotedParameter( + String command, + Class<T> callClass, + Class<IT> callInputClass, + Function<IT, String> configFunction + ) { + checkParameters(command, callClass, callInputClass, configFunction, "\"test\"", "test"); + } + + @ParameterizedTest + @MethodSource("calls") + <IT extends CallInput, OT, T extends Call<IT, OT>> void unquotedParameters( + String command, + Class<T> callClass, + Class<IT> callInputClass, + Function<IT, String> configFunction + ) { + checkParameters(command, callClass, callInputClass, configFunction, "test1 test2", "test1 test2"); + } + + @ParameterizedTest + @MethodSource("calls") + <IT extends CallInput, OT, T extends Call<IT, OT>> void quotedParameters( + String command, + Class<T> callClass, + Class<IT> callInputClass, + Function<IT, String> configFunction + ) { + checkParameters(command, callClass, callInputClass, configFunction, "\"test1\" \"test2\"", "test1 test2"); + } + + private <IT extends CallInput, OT, T extends Call<IT, OT>> void checkParameters( + String command, + Class<T> callClass, + Class<IT> callInputClass, + Function<IT, String> configFunction, + String parameters, + String expected + ) { + T call = registerMockCall(callClass); + execute(command + " " + parameters); + IT callInput = verifyCallInput(call, callInputClass); + assertEquals(expected, configFunction.apply(callInput)); + } +}