blob: 3f0c8a373de03cc0f1c2e37875b30d8850c30c25 [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.cli;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;
import org.apache.ignite.cli.builtins.init.InitIgniteCommand;
import org.apache.ignite.cli.builtins.module.ModuleManager;
import org.apache.ignite.cli.builtins.module.ModuleStorage;
import org.apache.ignite.cli.builtins.module.StandardModuleDefinition;
import org.apache.ignite.cli.builtins.node.NodeManager;
import org.apache.ignite.cli.spec.IgniteCliSpec;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import picocli.CommandLine;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("ignite")
@ExtendWith(MockitoExtension.class)
public class IgniteCliInterfaceTest {
ApplicationContext applicationContext;
ByteArrayOutputStream err;
ByteArrayOutputStream out;
@Mock CliPathsConfigLoader cliPathsConfigLoader;
@BeforeEach
void setup() {
applicationContext = ApplicationContext.run(Environment.TEST);
applicationContext.registerSingleton(cliPathsConfigLoader);
err = new ByteArrayOutputStream();
out = new ByteArrayOutputStream();
}
CommandLine commandLine(ApplicationContext applicationContext) {
CommandLine.IFactory factory = new CommandFactory(applicationContext);
return new CommandLine(IgniteCliSpec.class, factory)
.setErr(new PrintWriter(err, true))
.setOut(new PrintWriter(out, true));
}
@DisplayName("init")
@Nested
class Init {
@Test
@DisplayName("init")
void init() {
var initIgniteCommand = mock(InitIgniteCommand.class);
applicationContext.registerSingleton(InitIgniteCommand.class, initIgniteCommand);
CommandLine cli = commandLine(applicationContext);
Assertions.assertEquals(0, cli.execute("init"));
verify(initIgniteCommand).init(any(), any(), any());
}
}
@DisplayName("module")
@Nested
class Module {
@Mock ModuleManager moduleManager;
@Mock ModuleStorage moduleStorage;
@BeforeEach
void setUp() {
applicationContext.registerSingleton(moduleManager);
applicationContext.registerSingleton(moduleStorage);
}
@Test
@DisplayName("add mvn:groupId:artifact:version")
void add() {
IgnitePaths paths = new IgnitePaths(Path.of("binDir"),
Path.of("worksDir"), "version");
when(cliPathsConfigLoader.loadIgnitePathsOrThrowError()).thenReturn(paths);
var exitCode =
commandLine(applicationContext).execute("module add mvn:groupId:artifactId:version".split(" "));
verify(moduleManager).addModule("mvn:groupId:artifactId:version", paths, Arrays.asList());
Assertions.assertEquals(0, exitCode);
}
@Test
@DisplayName("add mvn:groupId:artifact:version --repo http://mvnrepo.com/repostiory")
void addWithCustomRepo() throws MalformedURLException {
doNothing().when(moduleManager).addModule(any(), any(), any());
IgnitePaths paths = new IgnitePaths(Path.of("binDir"),
Path.of("worksDir"), "version");
when(cliPathsConfigLoader.loadIgnitePathsOrThrowError()).thenReturn(paths);
var exitCode =
commandLine(applicationContext)
.execute("module add mvn:groupId:artifactId:version --repo http://mvnrepo.com/repostiory".split(" "));
verify(moduleManager).addModule("mvn:groupId:artifactId:version", paths,
Arrays.asList(new URL("http://mvnrepo.com/repostiory")));
Assertions.assertEquals(0, exitCode);
}
@Test
@DisplayName("add test-module")
void addBuiltinModule() {
doNothing().when(moduleManager).addModule(any(), any(), any());
IgnitePaths paths = new IgnitePaths(Path.of("binDir"),
Path.of("worksDir"), "version");
when(cliPathsConfigLoader.loadIgnitePathsOrThrowError()).thenReturn(paths);
var exitCode =
commandLine(applicationContext).execute("module add test-module".split(" "));
verify(moduleManager).addModule("test-module", paths, Collections.emptyList());
Assertions.assertEquals(0, exitCode);
}
@Test
@DisplayName("remove builtin-module")
void remove() {
var moduleName = "builtin-module";
when(moduleManager.removeModule(moduleName)).thenReturn(true);
var exitCode =
commandLine(applicationContext).execute("module remove builtin-module".split(" "));
verify(moduleManager).removeModule(moduleName);
Assertions.assertEquals(0, exitCode);
assertEquals("Module " + moduleName + " was removed successfully.\n", out.toString());
}
@Test
@DisplayName("remove unknown-module")
void removeUnknownModule() {
var moduleName = "unknown-module";
when(moduleManager.removeModule(moduleName)).thenReturn(false);
var exitCode =
commandLine(applicationContext).execute("module remove unknown-module".split(" "));
verify(moduleManager).removeModule(moduleName);
Assertions.assertEquals(0, exitCode);
assertEquals("Nothing to do: module " + moduleName + " is not yet added.\n", out.toString());
}
@Test
@DisplayName("list")
void list() {
var module1 = new StandardModuleDefinition("module1", "description1", Collections.singletonList("artifact1"), Collections.singletonList("cli-artifact1"));
var module2 = new StandardModuleDefinition("module2", "description2", Collections.singletonList("artifact2"), Collections.singletonList("cli-artifact2"));
when(moduleManager.builtinModules()).thenReturn(Arrays.asList(module1, module2));
var externalModule = new ModuleStorage.ModuleDefinition(
"org.apache.ignite:snapshot:2.9.0",
Collections.emptyList(),
Collections.emptyList(),
ModuleStorage.SourceType.Maven, "mvn:org.apache.ignite:snapshot:2.9.0");
when(moduleStorage.listInstalled()).thenReturn(
new ModuleStorage.ModuleDefinitionsRegistry(
Arrays.asList(
new ModuleStorage.ModuleDefinition(
module1.name,
Collections.emptyList(),
Collections.emptyList(),
ModuleStorage.SourceType.Standard, ""), externalModule)));
var exitCode =
commandLine(applicationContext).execute("module list".split(" "));
verify(moduleManager).builtinModules();
Assertions.assertEquals(0, exitCode);
var expectedOutput = "Optional Ignite Modules\n" +
"+---------+--------------+------------+\n" +
"| Name | Description | Installed? |\n" +
"+---------+--------------+------------+\n" +
"| module1 | description1 | Yes |\n" +
"+---------+--------------+------------+\n" +
"| module2 | description2 | No |\n" +
"+---------+--------------+------------+\n" +
"\n" +
"Additional Maven Dependencies\n" +
"+-------------------+-------------+---------+\n" +
"| Group ID | Artifact ID | Version |\n" +
"+-------------------+-------------+---------+\n" +
"| org.apache.ignite | snapshot | 2.9.0 |\n" +
"+-------------------+-------------+---------+\n" +
"Type ignite module remove <groupId>:<artifactId>:<version> to remove a dependency.\n";
assertEquals(expectedOutput, out.toString());
}
}
@Nested
@DisplayName("node")
class Node {
@Mock NodeManager nodeManager;
@BeforeEach
void setUp() {
applicationContext.registerSingleton(nodeManager);
}
@Test
@DisplayName("start node1 --config conf.json")
void start() {
var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
var nodeName = "node1";
var node =
new NodeManager.RunningNode(1, nodeName, Path.of("logfile"));
when(nodeManager.start(any(), any(), any(), any(), any()))
.thenReturn(node);
when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
.thenReturn(ignitePaths);
CommandLine cli = commandLine(applicationContext);
var exitCode = cli.execute(("node start " + nodeName + " --config conf.json").split(" "));
Assertions.assertEquals(0, exitCode);
verify(nodeManager).start(nodeName, ignitePaths.workDir, ignitePaths.cliPidsDir(), Path.of("conf.json"), cli.getOut());
assertEquals("Starting a new Ignite node...\n\nNode is successfully started. To stop, type ignite node stop " + nodeName + "\n\n" +
"+---------------+---------+\n" +
"| Consistent ID | node1 |\n" +
"+---------------+---------+\n" +
"| PID | 1 |\n" +
"+---------------+---------+\n" +
"| Log File | logfile |\n" +
"+---------------+---------+\n",
out.toString());
}
@Test
@DisplayName("stop node1")
void stopRunning() {
var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
var nodeName = "node1";
when(nodeManager.stopWait(any(), any()))
.thenReturn(true);
when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
.thenReturn(ignitePaths);
var exitCode =
commandLine(applicationContext).execute(("node stop " + nodeName).split(" "));
Assertions.assertEquals(0, exitCode);
verify(nodeManager).stopWait(nodeName, ignitePaths.cliPidsDir());
assertEquals("Stopping locally running node with consistent ID " + nodeName + "... Done!\n",
out.toString());
}
@Test
@DisplayName("stop unknown-node")
void stopUnknown() {
var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
var nodeName = "unknown-node";
when(nodeManager.stopWait(any(), any()))
.thenReturn(false);
when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
.thenReturn(ignitePaths);
var exitCode =
commandLine(applicationContext).execute(("node stop " + nodeName).split(" "));
Assertions.assertEquals(0, exitCode);
verify(nodeManager).stopWait(nodeName, ignitePaths.cliPidsDir());
assertEquals("Stopping locally running node with consistent ID " + nodeName + "... Failed\n",
out.toString());
}
@Test
@DisplayName("list")
void list() {
var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
when(nodeManager.getRunningNodes(any(), any()))
.thenReturn(Arrays.asList(
new NodeManager.RunningNode(1, "new1", Path.of("logFile1")),
new NodeManager.RunningNode(2, "new2", Path.of("logFile2"))
));
when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
.thenReturn(ignitePaths);
var exitCode =
commandLine(applicationContext).execute("node list".split(" "));
Assertions.assertEquals(0, exitCode);
verify(nodeManager).getRunningNodes(ignitePaths.workDir, ignitePaths.cliPidsDir());
assertEquals("Currently, there are 2 locally running nodes.\n\n" +
"+---------------+-----+----------+\n" +
"| Consistent ID | PID | Log File |\n" +
"+---------------+-----+----------+\n" +
"| new1 | 1 | logFile1 |\n" +
"+---------------+-----+----------+\n" +
"| new2 | 2 | logFile2 |\n" +
"+---------------+-----+----------+\n",
out.toString());
}
@Test
@DisplayName("list")
void listEmpty() {
var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
when(nodeManager.getRunningNodes(any(), any()))
.thenReturn(Arrays.asList());
when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
.thenReturn(ignitePaths);
var exitCode =
commandLine(applicationContext).execute("node list".split(" "));
Assertions.assertEquals(0, exitCode);
verify(nodeManager).getRunningNodes(ignitePaths.workDir, ignitePaths.cliPidsDir());
assertEquals("Currently, there are no locally running nodes.\n\n" +
"Use the ignite node start command to start a new node.\n", out.toString());
}
@Test
@DisplayName("classpath")
void classpath() throws IOException {
when(nodeManager.classpathItems()).thenReturn(Arrays.asList("item1", "item2"));
var exitCode = commandLine(applicationContext).execute("node classpath".split(" "));
Assertions.assertEquals(0, exitCode);
verify(nodeManager).classpathItems();
assertEquals("Current Ignite node classpath:\n item1\n item2\n", out.toString());
}
}
@Nested
@DisplayName("config")
class Config {
@Mock private HttpClient httpClient;
@Mock private HttpResponse<String> response;
@BeforeEach
void setUp() {
applicationContext.registerSingleton(httpClient);
}
@Test
@DisplayName("get --node-endpoint localhost:8081")
void get() throws IOException, InterruptedException {
when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
when(response.body()).thenReturn("{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}");
when(httpClient.<String>send(any(), any())).thenReturn(response);
var exitCode =
commandLine(applicationContext).execute("config get --node-endpoint localhost:8081".split(" "));
Assertions.assertEquals(0, exitCode);
verify(httpClient).send(
argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/") &&
r.headers().firstValue("Content-Type").get().equals("application/json")),
any());
assertEquals("{\n" +
" \"baseline\" : {\n" +
" \"autoAdjust\" : {\n" +
" \"enabled\" : true\n" +
" }\n" +
" }\n" +
"}\n", out.toString());
}
@Test
@DisplayName("get --node-endpoint localhost:8081 --selector local.baseline")
void getSubtree() throws IOException, InterruptedException {
when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
when(response.body()).thenReturn("{\"autoAdjust\":{\"enabled\":true}}");
when(httpClient.<String>send(any(), any())).thenReturn(response);
var exitCode =
commandLine(applicationContext).execute(("config get --node-endpoint localhost:8081 " +
"--selector local.baseline").split(" "));
Assertions.assertEquals(0, exitCode);
verify(httpClient).send(
argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/local.baseline") &&
r.headers().firstValue("Content-Type").get().equals("application/json")),
any());
assertEquals("{\n" +
" \"autoAdjust\" : {\n" +
" \"enabled\" : true\n" +
" }\n" +
"}\n", out.toString());
}
@Test
@DisplayName("set --node-endpoint localhost:8081 local.baseline.autoAdjust.enabled=true")
void setHocon() throws IOException, InterruptedException {
when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
when(httpClient.<String>send(any(), any())).thenReturn(response);
var expectedSentContent = "{\"local\":{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}}";
var exitCode =
commandLine(applicationContext).execute(("config set --node-endpoint localhost:8081 " +
"local.baseline.autoAdjust.enabled=true"
).split(" "));
Assertions.assertEquals(0, exitCode);
verify(httpClient).send(
argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/") &&
r.method().equals("POST") &&
// TODO: body matcher should be fixed to more appropriate
r.bodyPublisher().get().contentLength() == expectedSentContent.getBytes().length &&
r.headers().firstValue("Content-Type").get().equals("application/json")),
any());
assertEquals("Configuration was updated successfully.\n\n" +
"Use the ignite config get command to view the updated configuration.\n", out.toString());
}
@Test
@DisplayName("set --node-endpoint localhost:8081 {\"local\":{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}}")
void setJson() throws IOException, InterruptedException {
when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
when(httpClient.<String>send(any(), any())).thenReturn(response);
var expectedSentContent = "{\"local\":{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}}";
var exitCode =
commandLine(applicationContext).execute(("config set --node-endpoint localhost:8081 " +
"local.baseline.autoAdjust.enabled=true"
).split(" "));
Assertions.assertEquals(0, exitCode);
verify(httpClient).send(
argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/") &&
r.method().equals("POST") &&
// TODO: body matcher should be fixed to more appropriate
r.bodyPublisher().get().contentLength() == expectedSentContent.getBytes().length &&
r.headers().firstValue("Content-Type").get().equals("application/json")),
any());
assertEquals("Configuration was updated successfully.\n\n" +
"Use the ignite config get command to view the updated configuration.\n", out.toString());
}
}
private static void assertEquals(String expected, String actual) {
Assertions.assertEquals(
expected.lines().collect(Collectors.toList()),
actual.lines().collect(Collectors.toList())
);
}
}