WIP.
diff --git a/nlpcraft-tests/cli/pom.xml b/nlpcraft-tests/cli/pom.xml
new file mode 100644
index 0000000..7c2f588
--- /dev/null
+++ b/nlpcraft-tests/cli/pom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+-->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <name>NLPCraft CLI Tests</name>
+ <artifactId>nlpcraft-tests-cli</artifactId>
+
+ <parent>
+ <groupId>org.apache.nlpcraft</groupId>
+ <artifactId>nlpcraft-parent</artifactId>
+ <version>0.9.0</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>nlpcraft</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <!-- Test dependencies. -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/scala</directory>
+ </resource>
+ <resource>
+ <directory>src/main/resources</directory>
+ <includes>
+ <include>**/*.*</include>
+ </includes>
+ </resource>
+ </resources>
+ </build>
+</project>
diff --git a/nlpcraft-tests/cli/src/test/java/org/apache/nlpcraft/model/tools/cmdline/NCCliSpec.scala b/nlpcraft-tests/cli/src/test/java/org/apache/nlpcraft/model/tools/cmdline/NCCliSpec.scala
new file mode 100644
index 0000000..1486f4c
--- /dev/null
+++ b/nlpcraft-tests/cli/src/test/java/org/apache/nlpcraft/model/tools/cmdline/NCCliSpec.scala
@@ -0,0 +1,141 @@
+/*
+ * 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
+ *
+ * https://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.nlpcraft.model.tools.cmdline
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.nlpcraft.common.U
+import org.junit.jupiter.api.Test
+
+import java.io.{BufferedReader, File, FileFilter, InputStreamReader}
+import java.util.concurrent.{CountDownLatch, TimeUnit}
+import scala.collection.mutable
+import scala.util.Using
+
+/**
+ * This test designed only for maven tests (mvn clean verify)
+ * It cannot be started together with other server instances.
+ */
+class NCCliSpec {
+ private def check(dirs: File*): Unit = dirs.foreach(d => require(d.exists() && d.isDirectory, s"Invalid folder: $d"))
+
+ private def getAllDepsJar(dirTarget: File): File = {
+ val jars = dirTarget.listFiles(new FileFilter {
+ override def accept(f: File): Boolean = f.isFile && f.getName.toLowerCase().endsWith("all-deps.jar")
+ })
+
+ require(jars != null && jars.length == 1, s"Required jar file not found in ${dirTarget.getAbsolutePath}")
+
+ jars.head
+ }
+
+ private def makeProcess(
+ scriptArg: String,
+ script: String,
+ allDepsJar: File,
+ dirBin: File,
+ timeoutSecs: Int,
+ expectedLines: String*
+ ): Process = {
+ val args = if (SystemUtils.IS_OS_UNIX) Seq("bash", "-f", script, scriptArg) else Seq(script, scriptArg)
+
+ val builder = new ProcessBuilder(args: _*)
+
+ builder.environment().put("CP", allDepsJar.getAbsolutePath)
+ builder.directory(dirBin)
+ builder.redirectErrorStream(true)
+
+ val process = builder.start()
+
+ val cdl = new CountDownLatch(1)
+ var done = false
+
+ val thread = new Thread() {
+ override def run(): Unit = {
+ Using.resource { new BufferedReader(new InputStreamReader(process.getInputStream)) }(reader => {
+ var line: String = reader.readLine()
+
+ while (line != null && !done) {
+ if (expectedLines.exists(line.contains)) {
+ done = true
+
+ println(s"$scriptArg finished fine by expected line: '$line'")
+ println()
+
+ cdl.countDown()
+ }
+ else {
+ println(s"($scriptArg) $line")
+
+ line = reader.readLine()
+ }
+ }
+ })
+ }
+ }
+
+ thread.start()
+
+ cdl.await(timeoutSecs, TimeUnit.SECONDS)
+
+ U.stopThread(thread)
+
+ require(done, s"Command cannot be started: $scriptArg")
+
+ process
+ }
+
+ @Test
+ def test(): Unit = {
+ // All folders should be exists because tests started from maven phase, after build project.
+ val ext = if (SystemUtils.IS_OS_UNIX) "sh" else "cmd"
+ val dirUsr = new File(SystemUtils.USER_DIR).getParentFile.getParentFile
+ val dirBin = new File(dirUsr, "bin")
+ val dirTarget = new File(dirUsr, "nlpcraft/target")
+ val script = new File(dirBin, s"nlpcraft.$ext").getAbsolutePath
+
+ check(dirUsr, dirBin, dirTarget)
+
+ val allDepsJar = getAllDepsJar(dirTarget)
+
+ def make(scriptArg: String, timeoutSecs: Int, expectedLines: String*): Process =
+ makeProcess(scriptArg, script, allDepsJar, dirBin, timeoutSecs, expectedLines: _*)
+
+ val procs = mutable.Buffer.empty[Process]
+
+ def stopInstances(): Unit = {
+ // Both variant (stopped or already stopped) are fine.
+ def stop(cmd: String): Process = make(cmd, 10, "has been stopped", "not found")
+
+ procs += stop("stop-server")
+ procs += stop("stop-probe")
+ }
+
+ try {
+ stopInstances()
+
+ procs += make("start-server", 120, "Started on")
+ procs += make("start-probe", 30, "Started on")
+ procs += make("info", 10, "Local server")
+ }
+ finally
+ try
+ stopInstances()
+ finally
+ procs.foreach(_.destroy())
+ }
+}
diff --git a/pom.xml b/pom.xml
index 4a6649e..b27bfe7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -648,6 +648,13 @@
</profile>
<profile>
+ <id>tests</id>
+ <modules>
+ <module>nlpcraft-tests/cli</module>
+ </modules>
+ </profile>
+
+ <profile>
<id>release</id>
<activation>
<property>