blob: 33c21ebeb673bda3165d214843bedd2677b01b47 [file] [log] [blame]
/*
* 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.core.repl.executor;
import static java.util.stream.Collectors.flatMapping;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toUnmodifiableList;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.not;
import static org.mockito.Mockito.when;
import jakarta.inject.Inject;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.ignite.configuration.ConfigurationModule;
import org.apache.ignite.configuration.RootKey;
import org.apache.ignite.configuration.annotation.ConfigurationType;
import org.apache.ignite.internal.cli.commands.CliCommandTestInitializedIntegrationBase;
import org.apache.ignite.internal.cli.commands.TopLevelCliReplCommand;
import org.apache.ignite.internal.cli.core.repl.Session;
import org.apache.ignite.internal.cli.core.repl.SessionInfo;
import org.apache.ignite.internal.cli.core.repl.completer.DynamicCompleterActivationPoint;
import org.apache.ignite.internal.cli.core.repl.completer.DynamicCompleterRegistry;
import org.apache.ignite.internal.cli.core.repl.completer.filter.CompleterFilter;
import org.apache.ignite.internal.cli.core.repl.completer.filter.DynamicCompleterFilter;
import org.apache.ignite.internal.cli.core.repl.completer.filter.NonRepeatableOptionsFilter;
import org.apache.ignite.internal.cli.core.repl.completer.filter.ShortOptionsFilter;
import org.apache.ignite.internal.cli.event.EventPublisher;
import org.apache.ignite.internal.cli.event.Events;
import org.apache.ignite.internal.configuration.ServiceLoaderModulesProvider;
import org.assertj.core.util.Files;
import org.jline.reader.Candidate;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
import org.jline.reader.impl.DefaultParser;
import org.jline.reader.impl.completer.SystemCompleter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
/** Integration test for all completions in interactive mode. */
public class ItIgnitePicocliCommandsTest extends CliCommandTestInitializedIntegrationBase {
private static final String DEFAULT_REST_URL = "http://localhost:10300";
private static final List<String> DISTRIBUTED_CONFIGURATION_KEYS;
private static final List<String> LOCAL_CONFIGURATION_KEYS;
static {
Map<ConfigurationType, List<String>> configKeysByType = new ServiceLoaderModulesProvider()
.modules(ItIgnitePicocliCommandsTest.class.getClassLoader())
.stream()
.collect(groupingBy(
ConfigurationModule::type,
flatMapping(module -> module.rootKeys().stream().map(RootKey::key), toUnmodifiableList())
));
DISTRIBUTED_CONFIGURATION_KEYS = configKeysByType.get(ConfigurationType.DISTRIBUTED);
LOCAL_CONFIGURATION_KEYS = configKeysByType.get(ConfigurationType.LOCAL);
}
@Inject
DynamicCompleterRegistry dynamicCompleterRegistry;
@Inject
DynamicCompleterActivationPoint dynamicCompleterActivationPoint;
@Inject
DynamicCompleterFilter dynamicCompleterFilter;
@Inject
Session session;
@Inject
EventPublisher eventPublisher;
SystemCompleter completer;
LineReader lineReader;
@Override
protected Class<?> getCommandClass() {
return TopLevelCliReplCommand.class;
}
@Override
protected String nodeBootstrapConfigTemplate() {
return FAST_FAILURE_DETECTION_NODE_BOOTSTRAP_CFG_TEMPLATE;
}
@BeforeEach
void setupSystemCompleter() {
dynamicCompleterActivationPoint.activateDynamicCompleter(dynamicCompleterRegistry);
List<CompleterFilter> filters = List.of(
dynamicCompleterFilter,
new ShortOptionsFilter(),
new NonRepeatableOptionsFilter(commandLine().getCommandSpec())
);
IgnitePicocliCommands commandRegistry = new IgnitePicocliCommands(commandLine(), dynamicCompleterRegistry, filters);
// This completer is used by jline to suggest all completions
completer = commandRegistry.compileCompleters();
completer.compile();
lineReader = Mockito.mock(LineReader.class);
when(lineReader.getParser()).thenReturn(new DefaultParser());
}
private Stream<Arguments> helpAndVerboseAreNotCompletedSource() {
return Stream.of(
words(""), words("-"), words(" -"),
words("node"),
words("node", ""),
words("node", "config"),
words("node", "config", ""),
words("node", "config", "show"),
words("node", "config", "show", ""),
words("node", "config", "show", "--node-name", "name"),
words("node", "config", "show", "--node-name", "name", "")
).map(this::named).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("helpAndVerboseAreNotCompletedSource")
@DisplayName("--help and --verbose are not suggested")
void helpAndVerboseAreNotCompleted(ParsedLine givenParsedLine) {
// When
List<String> suggestions = complete(givenParsedLine);
// Then
assertThat(
"For given parsed words: " + givenParsedLine.words(),
suggestions,
not(hasItems("--help", "--verbose"))
);
}
private Stream<Arguments> helpAndVerboseCompletedSource() {
return Stream.of(
words("node", "config", "show", "-"),
words("node", "config", "show", "--"),
words("node", "status", "-"),
words("node", "status", "--"),
words("node", "config", "show", "--node-name", "name", "-")
).map(this::named).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("helpAndVerboseCompletedSource")
@DisplayName("--help and --verbose are suggested if '-' or '--' typed")
void helpAndVerboseAreCompleted(ParsedLine givenParsedLine) {
// When
List<String> suggestions = complete(givenParsedLine);
// Then
assertThat(
"For given parsed words: " + givenParsedLine.words(),
suggestions,
hasItems("--verbose", "--help")
);
}
private Stream<Arguments> helpCompletedSource() {
return Stream.of(
words("node", "-"),
words("node", "", "-"),
words("node", "config", "-"),
words("node", "config", "--"),
words("node", "config", " ", "-")
).map(this::named).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("helpCompletedSource")
@DisplayName("--help suggested if '-' or '--' typed for keywords that is not complete commands")
void helpSuggested(ParsedLine givenParsedLine) {
// When
List<String> suggestions = complete(givenParsedLine);
// Then
assertThat(
"For given parsed words: " + givenParsedLine.words(),
suggestions,
hasItem("--help")
);
}
private Stream<Arguments> nodeConfigShowSuggestedSource() {
return Stream.of(
words("node", "config", "show", ""),
words("node", "config", "show", " --node-name", "nodeName", ""),
words("node", "config", "show", " --verbose", ""),
words("node", "config", "show", " -v", "")
).map(this::named).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("nodeConfigShowSuggestedSource")
@DisplayName("node config show selector parameters suggested")
void nodeConfigShowSuggested(ParsedLine givenParsedLine) {
// Given
connected();
// wait for lazy init of node config completer
await("For given parsed words: " + givenParsedLine.words()).until(
() -> complete(givenParsedLine),
containsInAnyOrder(LOCAL_CONFIGURATION_KEYS.toArray(String[]::new))
);
}
private void connected() {
eventPublisher.publish(Events.connect(SessionInfo.builder().nodeUrl(DEFAULT_REST_URL).build()));
}
private Stream<Arguments> nodeConfigUpdateSuggestedSource() {
return Stream.of(
words("node", "config", "update", ""),
words("node", "config", "update", " --node-name", "nodeName", ""),
words("node", "config", "update", " --verbose", ""),
words("node", "config", "update", " -v", "")
).map(this::named).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("nodeConfigUpdateSuggestedSource")
@DisplayName("node config update selector parameters suggested")
void nodeConfigUpdateSuggested(ParsedLine givenParsedLine) {
// Given
connected();
// wait for lazy init of node config completer
await("For given parsed words: " + givenParsedLine.words()).until(
() -> complete(givenParsedLine),
containsInAnyOrder(
"rest",
"clientConnector",
"network",
"cluster",
"deployment",
"nodeAttributes",
"aimem",
"aipersist",
"rocksDb",
"storages"
)
);
}
private Stream<Arguments> clusterConfigShowSuggestedSource() {
return Stream.of(
words("cluster", "config", "show", ""),
words("cluster", "config", "show", " --node-name", "nodeName", ""),
words("cluster", "config", "show", " --verbose", ""),
words("cluster", "config", "show", " -v", "")
).map(this::named).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("clusterConfigShowSuggestedSource")
@DisplayName("cluster config selector parameters suggested")
void clusterConfigShowSuggested(ParsedLine givenParsedLine) {
// Given
connected();
// wait for lazy init of cluster config completer
await("For given parsed words: " + givenParsedLine.words()).until(
() -> complete(givenParsedLine),
containsInAnyOrder(DISTRIBUTED_CONFIGURATION_KEYS.toArray(String[]::new))
);
}
private Stream<Arguments> clusterConfigUpdateSuggestedSource() {
return Stream.of(
words("cluster", "config", "update", ""),
words("cluster", "config", "update", " --node-name", "nodeName", ""),
words("cluster", "config", "update", " --verbose", ""),
words("cluster", "config", "update", " -v", "")
).map(this::named).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("clusterConfigUpdateSuggestedSource")
@DisplayName("cluster config selector parameters suggested")
void clusterConfigUpdateSuggested(ParsedLine givenParsedLine) {
// Given
connected();
// wait for lazy init of cluster config completer
await("For given parsed words: " + givenParsedLine.words()).until(
() -> complete(givenParsedLine),
containsInAnyOrder(DISTRIBUTED_CONFIGURATION_KEYS.toArray(String[]::new))
);
}
private Stream<Arguments> nodeNamesSource() {
return Stream.of(
words("cluster", "config", "show", "--node-name", ""),
words("cluster", "config", "update", "--node-name", ""),
words("cluster", "status", "--node-name", ""),
words("cluster", "init", "--node-name", ""),
words("cluster", "init", "--cmg-node", ""),
words("cluster", "init", "--meta-storage-node", ""),
words("node", "config", "show", "--node-name", ""),
words("node", "config", "show", "--verbose", "--node-name", ""),
words("node", "config", "update", "--node-name", ""),
words("node", "status", "--node-name", ""),
words("node", "version", "--node-name", ""),
words("node", "metric", "list", "--node-name", "")
).map(this::named).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("nodeNamesSource")
@DisplayName("node names suggested after --node-name option")
void nodeNameSuggested(ParsedLine givenParsedLine) {
// Given
connected();
// And the first update is fetched
await().until(() -> nodeNameRegistry.names(), not(empty()));
// Then
assertThat(
"For given parsed words: " + givenParsedLine.words(),
complete(givenParsedLine),
containsInAnyOrder(allNodeNames().toArray())
);
}
@Test
@DisplayName("start/stop node affects --node-name suggestions")
void startStopNodeWhenCompleteNodeName() {
// Given
var igniteNodeName = allNodeNames().get(1);
// And
var givenParsedLine = words("node", "status", "--node-name", "");
// And
assertThat(nodeNameRegistry.names(), empty());
// Then
assertThat(complete(givenParsedLine), not(contains(igniteNodeName)));
// When
connected();
// And the first update is fetched
await().until(() -> nodeNameRegistry.names(), not(empty()));
// Then
assertThat(complete(givenParsedLine), containsInAnyOrder(allNodeNames().toArray()));
// When stop one node
stopNode(igniteNodeName);
var actualNodeNames = allNodeNames();
actualNodeNames.remove(igniteNodeName);
// Then node name suggestions does not contain the stopped node
await().until(() -> complete(givenParsedLine), containsInAnyOrder(actualNodeNames.toArray()));
// When start the node again
startNode(igniteNodeName);
// Then node name comes back to suggestions
await().until(() -> complete(givenParsedLine), containsInAnyOrder(allNodeNames().toArray()));
}
@Test
@DisplayName("jdbc url suggested after --jdbc-url option")
void suggestedJdbcUrl() {
// Given
connected();
// And the first update is fetched
await().until(() -> jdbcUrlRegistry.jdbcUrls(), not(empty()));
// Then
List<String> completions = complete(words("sql", "--jdbc-url", ""));
assertThat(completions, containsInAnyOrder(jdbcUrlRegistry.jdbcUrls().toArray()));
}
private Stream<Arguments> clusterUrlSource() {
return Stream.of(
words("cluster", "config", "show", "--cluster-endpoint-url", ""),
words("cluster", "config", "update", "--cluster-endpoint-url", ""),
words("cluster", "status", "--cluster-endpoint-url", ""),
words("cluster", "init", "--cluster-endpoint-url", "")
).map(this::named).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("clusterUrlSource")
@DisplayName("cluster url suggested after --cluster-endpoint-url option")
void suggestedClusterUrl(ParsedLine parsedLine) {
// Given
connected();
// And the first update is fetched
await().until(() -> nodeNameRegistry.urls(), not(empty()));
// Then
String[] expectedUrls = nodeNameRegistry.urls().toArray(String[]::new);
assertThat("For given parsed words: " + parsedLine.words(),
complete(parsedLine),
containsInAnyOrder(expectedUrls));
}
@Test
@DisplayName("files suggested after -script-file option")
void suggestedScriptFile() {
// Given files
// Create temp folders
File rootFolder = Files.newTemporaryFolder();
File emptyFolder = Files.newFolder(rootFolder.getPath() + File.separator + "emptyFolder");
emptyFolder.deleteOnExit();
File scriptsFolder = Files.newFolder(rootFolder.getPath() + File.separator + "scriptsFolder");
scriptsFolder.deleteOnExit();
// Create temp files
String script1 = scriptsFolder.getPath() + File.separator + "script1.sql";
Files.newFile(script1).deleteOnExit();
String script2 = scriptsFolder.getPath() + File.separator + "script2.sql";
Files.newFile(script2).deleteOnExit();
String someFile = scriptsFolder.getPath() + File.separator + "someFile.sql";
Files.newFile(someFile).deleteOnExit();
// When complete --script-file with folder typed
List<String> completions1 = complete(words("sql", "--script-file", rootFolder.getPath()));
// Then completions contain emptyFolder and scriptsFolder
assertThat(completions1, containsInAnyOrder(emptyFolder.getPath(), scriptsFolder.getPath()));
List<String> completions2 = complete(words("sql", "--script-file", scriptsFolder.getPath()));
// Then completions contain all given files
assertThat(completions2, containsInAnyOrder(script1, script2, someFile));
// When complete --script-file with partial path to script
List<String> completions3 = complete(words("sql", "--script-file", scriptsFolder.getPath() + File.separator + "script"));
// Then completions contain script1 and script2 files
assertThat(completions3, containsInAnyOrder(script1, script2));
}
List<String> complete(ParsedLine typedWords) {
List<Candidate> candidates = new ArrayList<>();
completer.complete(lineReader, typedWords, candidates);
return candidates.stream().map(Candidate::displ).collect(Collectors.toList());
}
Named<ParsedLine> named(ParsedLine parsedLine) {
return Named.of("cmd: " + String.join(" ", parsedLine.words()), parsedLine);
}
ParsedLine words(String... words) {
return new ParsedLine() {
@Override
public String word() {
return null;
}
@Override
public int wordCursor() {
return words.length - 1;
}
@Override
public int wordIndex() {
return words.length;
}
@Override
public List<String> words() {
return Arrays.stream(words).collect(Collectors.toList());
}
@Override
public String line() {
return null;
}
@Override
public int cursor() {
return 0;
}
};
}
}