diff --git a/.gitignore b/.gitignore
index ff1ababa..2e7b975c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
out
target
build
+testroot
diff --git a/.travis.yml b/.travis.yml
index e834c3e9..eefedf82 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,5 +12,5 @@ before_script:
- src/test/travis.pre.sh
script:
- - mvn test
- - sudo cat /var/log/tarantool/jdk-testing.log
+ - mvn verify
+ - cat testroot/jdk-testing.log
diff --git a/README.md b/README.md
index c41fc8db..f1ea60f3 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,37 @@ Remember that `TarantoolClient` adopts a
[fail-fast](https://en.wikipedia.org/wiki/Fail-fast) policy
when a client is not connected.
+The `TarantoolClient` will stop functioning if your implementation of a socket
+channel provider raises an exception or returns a null. You will need a new
+instance of client to recover. Hence, you should only throw in case you have
+met unrecoverable error.
+
+Below is an example of `SocketChannelProvider` implementation that handles short
+tarantool restarts.
+
+```java
+SocketChannelProvider socketChannelProvider = new SocketChannelProvider() {
+ @Override
+ public SocketChannel get(int retryNumber, Throwable lastError) {
+ long deadline = System.currentTimeMillis() + RESTART_TIMEOUT;
+ while (!Thread.currentThread().isInterrupted()) {
+ try {
+ return SocketChannel.open(new InetSocketAddress("localhost", 3301));
+ } catch (IOException e) {
+ if (deadline < System.currentTimeMillis())
+ throw new RuntimeException(e);
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ throw new RuntimeException(new TimeoutException("Connect timed out."));
+ }
+};
+```
+
4. Create a client.
```java
diff --git a/pom.xml b/pom.xml
index da426ce6..e66df25c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,6 +50,19 @@
maven-surefire-plugin
2.22.0
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 2.22.0
+
+
+
+ integration-test
+ verify
+
+
+
+
@@ -66,6 +79,12 @@
1.9.5
test
+
+ org.yaml
+ snakeyaml
+ 1.23
+ test
+
diff --git a/src/main/java/org/tarantool/TarantoolClientImpl.java b/src/main/java/org/tarantool/TarantoolClientImpl.java
index e2567ae5..67c176c7 100644
--- a/src/main/java/org/tarantool/TarantoolClientImpl.java
+++ b/src/main/java/org/tarantool/TarantoolClientImpl.java
@@ -503,7 +503,7 @@ public List exec(Code code, Object... args) {
@Override
public void close() {
- throw new IllegalStateException("You should close TarantoolClient to make this");
+ throw new IllegalStateException("You should close TarantoolClient instead.");
}
}
@@ -525,7 +525,7 @@ public Long exec(Code code, Object... args) {
@Override
public void close() {
- throw new IllegalStateException("You should close TarantoolClient to make this");
+ throw new IllegalStateException("You should close TarantoolClient instead.");
}
}
diff --git a/src/test/.tarantoolctl b/src/test/.tarantoolctl
new file mode 100644
index 00000000..3dea4514
--- /dev/null
+++ b/src/test/.tarantoolctl
@@ -0,0 +1,15 @@
+-- Options for tarantoolctl.
+
+local workdir = os.getenv('TEST_WORKDIR')
+default_cfg = {
+ pid_file = workdir,
+ wal_dir = workdir,
+ memtx_dir = workdir,
+ vinyl_dir = workdir,
+ log = workdir,
+ background = true,
+}
+
+instance_dir = workdir
+
+-- vim: set ft=lua :
diff --git a/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java b/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java
new file mode 100644
index 00000000..38772915
--- /dev/null
+++ b/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java
@@ -0,0 +1,187 @@
+package org.tarantool;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Abstract test. Provides environment control and frequently used functions.
+ */
+public abstract class AbstractTarantoolConnectorIT {
+ protected static final String host = System.getProperty("tntHost", "localhost");
+ protected static final int port = Integer.parseInt(System.getProperty("tntPort", "3301"));
+ protected static final int consolePort = Integer.parseInt(System.getProperty("tntConsolePort", "3313"));
+ protected static final String username = System.getProperty("tntUser", "test_admin");
+ protected static final String password = System.getProperty("tntPass", "4pWBZmLEgkmKK5WP");
+
+ protected static final int TIMEOUT = 500;
+ protected static final int RESTART_TIMEOUT = 2000;
+
+ protected static final SocketChannelProvider socketChannelProvider = new TestSocketChannelProvider(host, port,
+ RESTART_TIMEOUT);
+
+ protected static TarantoolControl control;
+ protected static TarantoolConsole console;
+
+ protected static final String SPACE_NAME = "basic_test";
+ protected static final String MULTIPART_SPACE_NAME = "multipart_test";
+
+ protected static int SPACE_ID;
+ protected static int MULTI_PART_SPACE_ID;
+
+ protected static int PK_INDEX_ID;
+ protected static int MPK_INDEX_ID;
+ protected static int VIDX_INDEX_ID;
+
+ private static final String[] setupScript = new String[] {
+ "box.schema.space.create('basic_test', { format = " +
+ "{{name = 'id', type = 'integer'}," +
+ " {name = 'val', type = 'string'} } })",
+
+ "box.space.basic_test:create_index('pk', { type = 'TREE', parts = {'id'} } )",
+ "box.space.basic_test:create_index('vidx', { type = 'TREE', unique = false, parts = {'val'} } )",
+
+ "box.space.basic_test:replace{1, 'one'}",
+ "box.space.basic_test:replace{2, 'two'}",
+ "box.space.basic_test:replace{3, 'three'}",
+
+ "box.schema.space.create('multipart_test', { format = " +
+ "{{name = 'id1', type = 'integer'}," +
+ " {name = 'id2', type = 'string'}," +
+ " {name = 'val1', type = 'string'} } })",
+
+ "box.space.multipart_test:create_index('pk', { type = 'TREE', parts = {'id1', 'id2'} })",
+ "box.space.multipart_test:create_index('vidx', { type = 'TREE', unique = false, parts = {'val1'} })",
+
+ "box.space.multipart_test:replace{1, 'one', 'o n e'}",
+ "box.space.multipart_test:replace{2, 'two', 't w o'}",
+ "box.space.multipart_test:replace{3, 'three', 't h r e e'}",
+
+ "function echo(...) return ... end"
+ };
+
+ private static final String[] cleanScript = new String[] {
+ "box.space.basic_test and box.space.basic_test:drop()",
+ "box.space.multipart_test and box.space.multipart_test:drop()"
+ };
+
+ @BeforeAll
+ public static void setupEnv() {
+ control = new TarantoolControl();
+ control.start("jdk-testing");
+
+ console = openConsole();
+
+ executeLua(cleanScript);
+ executeLua(setupScript);
+
+ SPACE_ID = console.eval("box.space.basic_test.id");
+ PK_INDEX_ID = console.eval("box.space.basic_test.index.pk.id");
+ VIDX_INDEX_ID = console.eval("box.space.basic_test.index.vidx.id");
+
+ MULTI_PART_SPACE_ID = console.eval("box.space.multipart_test.id");
+ MPK_INDEX_ID = console.eval("box.space.multipart_test.index.pk.id");
+ }
+
+ @AfterAll
+ public static void cleanupEnv() {
+ executeLua(cleanScript);
+
+ console.close();
+ control.stop("jdk-testing");
+ }
+
+ private static void executeLua(String[] exprs) {
+ for (String expr : exprs) {
+ console.exec(expr);
+ }
+ }
+
+ protected void checkTupleResult(Object res, List tuple) {
+ assertNotNull(res);
+ assertTrue(List.class.isAssignableFrom(res.getClass()));
+ List list = (List)res;
+ assertEquals(1, list.size());
+ assertNotNull(list.get(0));
+ assertTrue(List.class.isAssignableFrom(list.get(0).getClass()));
+ assertEquals(tuple, list.get(0));
+ }
+
+ protected TarantoolClient makeClient() {
+ TarantoolClientConfig config = new TarantoolClientConfig();
+ config.username = username;
+ config.password = password;
+ config.initTimeoutMillis = 1000;
+ config.sharedBufferSize = 128;
+
+ return new TarantoolClientImpl(socketChannelProvider, config);
+ }
+
+ protected static TarantoolConsole openConsole() {
+ return TarantoolConsole.open(host, consolePort);
+ }
+
+ protected static TarantoolConsole openConsole(String instance) {
+ return TarantoolConsole.open(control.tntCtlWorkDir, instance);
+ }
+
+ protected TarantoolConnection openConnection() {
+ Socket socket = new Socket();
+ try {
+ socket.connect(new InetSocketAddress(host, port));
+ } catch (IOException e) {
+ throw new RuntimeException("Test failed due to invalid environment.", e);
+ }
+ try {
+ return new TarantoolConnection(username, password, socket);
+ } catch (Exception e) {
+ try {
+ socket.close();
+ } catch (IOException ignored) {
+ // No-op.
+ }
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected List> consoleSelect(String spaceName, Object key) {
+ StringBuilder sb = new StringBuilder("box.space.");
+ sb.append(spaceName);
+ sb.append(":select{");
+ if (List.class.isAssignableFrom(key.getClass())) {
+ List parts = (List)key;
+ for (int i = 0; i < parts.size(); i++) {
+ if (i != 0)
+ sb.append(", ");
+ Object k = parts.get(i);
+ if (k.getClass().isAssignableFrom(String.class)) {
+ sb.append('\'');
+ sb.append(k);
+ sb.append('\'');
+ } else {
+ sb.append(k);
+ }
+ }
+ } else {
+ sb.append(key);
+ }
+ sb.append("}");
+ return console.eval(sb.toString());
+ }
+
+ protected void stopTarantool(String instance) {
+ control.stop(instance);
+ }
+
+ protected void startTarantool(String instance) {
+ control.start(instance);
+ }
+}
diff --git a/src/test/java/org/tarantool/AbstractTarantoolOpsIT.java b/src/test/java/org/tarantool/AbstractTarantoolOpsIT.java
new file mode 100644
index 00000000..6df4a465
--- /dev/null
+++ b/src/test/java/org/tarantool/AbstractTarantoolOpsIT.java
@@ -0,0 +1,371 @@
+package org.tarantool;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests operations available in {@link TarantoolClientOps} interface.
+ */
+public abstract class AbstractTarantoolOpsIT extends AbstractTarantoolConnectorIT {
+ protected abstract TarantoolClientOps, Object, List>> getOps();
+
+ @Test
+ public void testSelectOne() {
+ List> res = getOps().select(SPACE_ID, PK_INDEX_ID, Collections.singletonList(1), 0, 1, Iterator.EQ);
+ checkTupleResult(res, Arrays.asList(1, "one"));
+ }
+
+ @Test
+ public void testSelectMany() {
+ List> res = getOps().select(SPACE_ID, PK_INDEX_ID, Collections.singletonList(10), 0, 10,
+ Iterator.LT);
+
+ assertNotNull(res);
+ assertEquals(3, res.size());
+
+ // Descending order.
+ assertEquals(Arrays.asList(3, "three"), res.get(0));
+ assertEquals(Arrays.asList(2, "two"), res.get(1));
+ assertEquals(Arrays.asList(1, "one"), res.get(2));
+ }
+
+ @Test
+ public void testSelectOffsetLimit() {
+ List> res = getOps().select(SPACE_ID, PK_INDEX_ID, Collections.singletonList(10), 1, 1, Iterator.LT);
+ assertNotNull(res);
+ assertEquals(1, res.size());
+
+ assertEquals(Arrays.asList(2, "two"), res.get(0));
+ }
+
+ @Test
+ public void testSelectUsingSecondaryIndex() {
+ List> res = getOps().select(SPACE_ID, VIDX_INDEX_ID, Collections.singletonList("one"), 0, 1, Iterator.EQ);
+ checkTupleResult(res, Arrays.asList(1, "one"));
+ }
+
+ @Test
+ public void testInsertSimple() {
+ List tup = Arrays.asList(100, "hundred");
+ List> res = getOps().insert(SPACE_ID, tup);
+
+ checkTupleResult(res, tup);
+
+ // Check it actually was inserted.
+ checkTupleResult(consoleSelect(SPACE_NAME, 100), tup);
+ }
+
+ @Test
+ public void testInsertMultiPart() {
+ List tup = Arrays.asList(100, "hundred", "h u n d r e d");
+ List> res = getOps().insert(MULTI_PART_SPACE_ID, tup);
+
+ checkTupleResult(res, tup);
+
+ // Check it actually was inserted.
+ checkTupleResult(consoleSelect(MULTIPART_SPACE_NAME, Arrays.asList(100, "hundred")), tup);
+ }
+
+ @Test
+ public void testReplaceSimple() {
+ checkReplace(SPACE_NAME,
+ SPACE_ID,
+ Collections.singletonList(10),
+ Arrays.asList(10, "10"),
+ Arrays.asList(10, "ten"));
+ }
+
+ @Test
+ public void testReplaceMultiPartKey() {
+ checkReplace(MULTIPART_SPACE_NAME,
+ MULTI_PART_SPACE_ID,
+ Arrays.asList(10, "10"),
+ Arrays.asList(10, "10", "10"),
+ Arrays.asList(10, "10", "ten"));
+ }
+
+ private void checkReplace(String space, int spaceId, List key, List createTuple, List updateTuple) {
+ List> res = getOps().replace(spaceId, createTuple);
+ checkTupleResult(res, createTuple);
+
+ // Check it actually was created.
+ checkTupleResult(consoleSelect(space, key), createTuple);
+
+ // Update
+ res = getOps().replace(spaceId, updateTuple);
+ checkTupleResult(res, updateTuple);
+
+ // Check it actually was updated.
+ checkTupleResult(consoleSelect(space, key), updateTuple);
+ }
+
+ @Test
+ public void testUpdateNonExistingHasNoEffect() {
+ List op0 = Arrays.asList("=", 3, "trez");
+
+ List res = getOps().update(SPACE_ID, Collections.singletonList(30), op0);
+
+ assertNotNull(res);
+ assertEquals(0, res.size());
+
+ // Check it doesn't exist.
+ res = getOps().select(SPACE_ID, PK_INDEX_ID, Collections.singletonList(30), 0, 1, Iterator.EQ);
+ assertNotNull(res);
+ assertEquals(0, res.size());
+ }
+
+ @Test
+ public void testUpdate() {
+ checkUpdate(SPACE_NAME,
+ SPACE_ID,
+ // key
+ Collections.singletonList(30),
+ // init tuple
+ Arrays.asList(30, "30"),
+ // expected tuple
+ Arrays.asList(30, "thirty"),
+ // operations
+ Arrays.asList("!", 1, "thirty"),
+ Arrays.asList("#", 2, 1));
+ }
+
+ @Test
+ public void testUpdateMultiPart() {
+ checkUpdate(MULTIPART_SPACE_NAME,
+ MULTI_PART_SPACE_ID,
+ Arrays.asList(30, "30"),
+ Arrays.asList(30, "30", "30"),
+ Arrays.asList(30, "30", "thirty"),
+ Arrays.asList("=", 2, "thirty"));
+ }
+
+ private void checkUpdate(String space, int spaceId, List key, List initTuple, List expectedTuple,
+ Object ... ops) {
+ // Try update non-existing key.
+ List> res = getOps().update(spaceId, key, ops);
+ assertNotNull(res);
+ assertEquals(0, res.size());
+
+ // Check it still doesn't exists.
+ assertEquals(Collections.emptyList(), consoleSelect(space, key));
+
+ // Create the tuple.
+ res = getOps().insert(spaceId, initTuple);
+ checkTupleResult(res, initTuple);
+
+ // Apply the update operations.
+ res = getOps().update(spaceId, key, ops);
+ checkTupleResult(res, expectedTuple);
+
+ // Check that update was actually performed.
+ checkTupleResult(consoleSelect(space, key), expectedTuple);
+ }
+
+ @Test
+ public void testUpsertSimple() {
+ checkUpsert(SPACE_NAME,
+ SPACE_ID,
+ Collections.singletonList(40),
+ Arrays.asList(40, "40"),
+ Arrays.asList(40, "fourty"),
+ Arrays.asList("=", 1, "fourty"));
+ }
+
+ @Test
+ public void testUpsertMultiPart() {
+ checkUpsert(MULTIPART_SPACE_NAME,
+ MULTI_PART_SPACE_ID,
+ Arrays.asList(40, "40"),
+ Arrays.asList(40, "40", "40"),
+ Arrays.asList(40, "40", "fourty"),
+ Arrays.asList("=", 2, "fourty"));
+ }
+
+ private void checkUpsert(String space, int spaceId, List key, List defTuple, List expectedTuple,
+ Object ... ops) {
+ // Check that key doesn't exist.
+ assertEquals(Collections.emptyList(), consoleSelect(space, key));
+
+ // Try upsert non-existing key.
+ List> res = getOps().upsert(spaceId, key, defTuple, ops);
+ assertNotNull(res);
+ assertEquals(0, res.size());
+
+ // Check that default tuple was inserted.
+ checkTupleResult(consoleSelect(space, key), defTuple);
+
+ // Apply the operations.
+ res = getOps().upsert(spaceId, key, defTuple, ops);
+ assertNotNull(res);
+ assertEquals(0, res.size());
+
+ // Check that update was actually performed.
+ checkTupleResult(consoleSelect(space, key), expectedTuple);
+ }
+
+ @Test
+ public void testDeleteSimple() {
+ checkDelete(SPACE_NAME,
+ SPACE_ID,
+ Collections.singletonList(50),
+ Arrays.asList(50, "fifty"));
+ }
+
+ @Test
+ public void testDeleteMultiPartKey() {
+ checkDelete(MULTIPART_SPACE_NAME,
+ MULTI_PART_SPACE_ID,
+ Arrays.asList(50, "50"),
+ Arrays.asList(50, "50", "fifty"));
+ }
+
+ private void checkDelete(String space, int spaceId, List key, List tuple) {
+ // Check the key doesn't exists.
+ assertEquals(Collections.emptyList(), consoleSelect(space, key));
+
+ // Try to delete non-existing key.
+ List> res = getOps().delete(spaceId, key);
+ assertNotNull(res);
+ assertEquals(0, res.size());
+
+ // Create tuple.
+ res = getOps().insert(spaceId, tuple);
+ checkTupleResult(res, tuple);
+
+ // Check the tuple was created.
+ checkTupleResult(consoleSelect(space, key), tuple);
+
+ // Delete it.
+ res = getOps().delete(spaceId, key);
+ checkTupleResult(res, tuple);
+
+ // Check it actually was deleted.
+ assertEquals(Collections.emptyList(), consoleSelect(space, key));
+ }
+
+ @Test
+ public void testEval() {
+ assertEquals(Collections.singletonList("true"), getOps().eval("return echo(...)", "true"));
+ }
+
+ @Test
+ public void testCall() {
+ assertEquals(Collections.singletonList(Collections.singletonList("true")), getOps().call("echo", "true"));
+ }
+
+ @Test
+ public void testPing() {
+ getOps().ping();
+ }
+
+ @Test
+ public void testDeleteFromNonExistingSpace() {
+ TarantoolException ex = assertThrows(TarantoolException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ getOps().delete(5555, Collections.singletonList(2));
+ }
+ });
+ assertEquals("Space '5555' does not exist", ex.getMessage());
+ }
+
+ @Test
+ public void testSelectUnsupportedIterator() {
+ TarantoolException ex = assertThrows(TarantoolException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ getOps().select(SPACE_ID, PK_INDEX_ID, Collections.singletonList(1), 0, 1, Iterator.OVERLAPS);
+ }
+ });
+ assertEquals(
+ "Index 'pk' (TREE) of space 'basic_test' (memtx) does not support requested iterator type",
+ ex.getMessage());
+ }
+
+ @Test
+ public void testSelectNonExistingKey() {
+ List> res = getOps().select(SPACE_ID, PK_INDEX_ID, Collections.singletonList(5555), 0, 1,
+ Iterator.EQ);
+
+ assertNotNull(res);
+ assertEquals(0, res.size());
+ }
+
+ @Test
+ public void testSelectFromNonExistingIndex() {
+ TarantoolException ex = assertThrows(TarantoolException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ getOps().select(SPACE_ID, 5555, Collections.singletonList(2), 0, 1, Iterator.EQ);
+ }
+ });
+ assertEquals("No index #5555 is defined in space 'basic_test'", ex.getMessage());
+ }
+
+ @Test
+ public void testSelectFromNonExistingSpace() {
+ TarantoolException ex = assertThrows(TarantoolException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ getOps().select(5555, 0, Collections.singletonList(5555), 0, 1, Iterator.EQ);
+ }
+ });
+ assertEquals("Space '5555' does not exist", ex.getMessage());
+ }
+
+ @Test
+ public void testInsertDuplicateKey() {
+ final List tup = Arrays.asList(1, "uno");
+ TarantoolException ex = assertThrows(TarantoolException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ getOps().insert(SPACE_ID, tup);
+ }
+ });
+ assertEquals("Duplicate key exists in unique index 'pk' in space 'basic_test'", ex.getMessage());
+
+ // Check the tuple stayed intact.
+ checkTupleResult(consoleSelect(SPACE_NAME, 1), Arrays.asList(1, "one"));
+ }
+
+ @Test
+ public void testInsertToNonExistingSpace() {
+ TarantoolException ex = assertThrows(TarantoolException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ getOps().insert(5555, Arrays.asList(1, "one"));
+ }
+ });
+ assertEquals("Space '5555' does not exist", ex.getMessage());
+ }
+
+ @Test
+ public void testInsertInvalidData() {
+ // Invalid types.
+ TarantoolException ex = assertThrows(TarantoolException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ getOps().insert(SPACE_ID, Arrays.asList("one", 1));
+ }
+ });
+ assertEquals("Tuple field 1 type does not match one required by operation: expected integer", ex.getMessage());
+
+ // Invalid tuple size.
+ ex = assertThrows(TarantoolException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ getOps().insert(SPACE_ID, Collections.singletonList("one"));
+ }
+ });
+ assertEquals("Tuple field count 1 is less than required by space format or defined indexes " +
+ "(expected at least 2)", ex.getMessage());
+ }
+}
diff --git a/src/test/java/org/tarantool/AsyncClientOperationsIT.java b/src/test/java/org/tarantool/AsyncClientOperationsIT.java
new file mode 100644
index 00000000..5d7852a2
--- /dev/null
+++ b/src/test/java/org/tarantool/AsyncClientOperationsIT.java
@@ -0,0 +1,119 @@
+package org.tarantool;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for asynchronous operations provided by {@link TarantoolClientImpl} class.
+ */
+public class AsyncClientOperationsIT extends AbstractTarantoolConnectorIT {
+ private TarantoolClient client;
+
+ @BeforeEach
+ public void setup() {
+ client = makeClient();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ client.close();
+ }
+
+ @Test
+ public void testPing() {
+ // This ping is still synchronous due to API declaration returning void.
+ client.asyncOps().ping();
+ }
+
+ @Test
+ public void testClose() {
+ assertTrue(client.isAlive());
+ client.asyncOps().close();
+ assertFalse(client.isAlive());
+ }
+
+ @Test
+ public void testAsyncError() {
+ // Attempt to insert duplicate key.
+ final Future> res = client.asyncOps().insert(SPACE_ID, Arrays.asList(1, "one"));
+
+ // Check that error is delivered when trying to obtain future result.
+ ExecutionException e = assertThrows(ExecutionException.class, new Executable() {
+ @Override
+ public void execute() throws ExecutionException, InterruptedException, TimeoutException {
+ res.get(TIMEOUT, TimeUnit.MILLISECONDS);
+ }
+ });
+ assertNotNull(e.getCause());
+ assertTrue(TarantoolException.class.isAssignableFrom(e.getCause().getClass()));
+ }
+
+ @Test
+ public void testOperations() throws ExecutionException, InterruptedException, TimeoutException {
+ TarantoolClientOps, Object, Future>> ops = client.asyncOps();
+
+ List>> futs = new ArrayList>>();
+
+ futs.add(ops.insert(SPACE_ID, Arrays.asList(10, "10")));
+ futs.add(ops.delete(SPACE_ID, Collections.singletonList(10)));
+
+ futs.add(ops.insert(SPACE_ID, Arrays.asList(10, "10")));
+ futs.add(ops.update(SPACE_ID, Collections.singletonList(10), Arrays.asList("=", 1, "ten")));
+
+ futs.add(ops.replace(SPACE_ID, Arrays.asList(20, "20")));
+ futs.add(ops.upsert(SPACE_ID, Collections.singletonList(20), Arrays.asList(20, "twenty"),
+ Arrays.asList("=", 1, "twenty")));
+
+ futs.add(ops.insert(SPACE_ID, Arrays.asList(30, "30")));
+ futs.add(ops.call("box.space.basic_test:delete", Collections.singletonList(30)));
+
+ // Wait completion of all operations.
+ for (Future> f : futs)
+ f.get(TIMEOUT, TimeUnit.MILLISECONDS);
+
+ // Check the effects.
+ checkTupleResult(consoleSelect(SPACE_NAME, 10), Arrays.asList(10, "ten"));
+ checkTupleResult(consoleSelect(SPACE_NAME, 20), Arrays.asList(20, "twenty"));
+ assertEquals(consoleSelect(SPACE_NAME, 30), Collections.emptyList());
+ }
+
+ @Test
+ public void testSelect() throws ExecutionException, InterruptedException, TimeoutException {
+ Future> fut = client.asyncOps().select(SPACE_ID, PK_INDEX_ID, Collections.singletonList(1), 0, 1,
+ Iterator.EQ);
+
+ List> res = fut.get(TIMEOUT, TimeUnit.MILLISECONDS);
+
+ checkTupleResult(res, Arrays.asList(1, "one"));
+ }
+
+ @Test
+ public void testEval() throws ExecutionException, InterruptedException, TimeoutException {
+ Future> fut = client.asyncOps().eval("return true");
+ assertEquals(Collections.singletonList(true), fut.get(TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testCall() throws ExecutionException, InterruptedException, TimeoutException {
+ Future> fut = client.asyncOps().call("echo", "hello");
+ assertEquals(Collections.singletonList(Collections.singletonList("hello")),
+ fut.get(TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+}
diff --git a/src/test/java/org/tarantool/ClientOperationsIT.java b/src/test/java/org/tarantool/ClientOperationsIT.java
new file mode 100644
index 00000000..22d1965a
--- /dev/null
+++ b/src/test/java/org/tarantool/ClientOperationsIT.java
@@ -0,0 +1,46 @@
+package org.tarantool;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests for synchronous operations of {@link TarantoolClientImpl} class.
+ *
+ * Actual tests reside in base class.
+ */
+public class ClientOperationsIT extends AbstractTarantoolOpsIT {
+ private TarantoolClient client;
+
+ @BeforeEach
+ public void setup() {
+ client = makeClient();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ client.close();
+ }
+
+ @Override
+ protected TarantoolClientOps, Object, List>> getOps() {
+ return client.syncOps();
+ }
+
+ @Test
+ public void testClose() {
+ IllegalStateException e = assertThrows(IllegalStateException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ getOps().close();
+ }
+ });
+ assertEquals(e.getMessage(), "You should close TarantoolClient instead.");
+ }
+}
diff --git a/src/test/java/org/tarantool/ClientReconnectIT.java b/src/test/java/org/tarantool/ClientReconnectIT.java
new file mode 100644
index 00000000..46b02b01
--- /dev/null
+++ b/src/test/java/org/tarantool/ClientReconnectIT.java
@@ -0,0 +1,59 @@
+package org.tarantool;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ClientReconnectIT extends AbstractTarantoolConnectorIT {
+ private static final String INSTANCE_NAME = "jdk-testing";
+ private TarantoolClient client;
+
+ @BeforeEach
+ public void setup() {
+ client = makeClient();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ client.close();
+
+ // Re-open console for cleanupEnv() to work.
+ console.close();
+ console = openConsole();
+ }
+
+ @Test
+ public void testReconnect() throws Exception {
+ client.syncOps().ping();
+
+ stopTarantool(INSTANCE_NAME);
+
+ Exception e = assertThrows(Exception.class, new Executable() {
+ @Override
+ public void execute() {
+ client.syncOps().ping();
+ }
+ });
+
+ assertTrue(CommunicationException.class.isAssignableFrom(e.getClass()) ||
+ IllegalStateException.class.isAssignableFrom(e.getClass()));
+
+ assertNotNull(((TarantoolClientImpl) client).getThumbstone());
+
+ assertFalse(client.isAlive());
+
+ startTarantool(INSTANCE_NAME);
+
+ assertTrue(client.waitAlive(TIMEOUT, TimeUnit.MILLISECONDS));
+
+ client.syncOps().ping();
+ }
+}
diff --git a/src/test/java/org/tarantool/ConnectionIT.java b/src/test/java/org/tarantool/ConnectionIT.java
new file mode 100644
index 00000000..0d70342c
--- /dev/null
+++ b/src/test/java/org/tarantool/ConnectionIT.java
@@ -0,0 +1,36 @@
+package org.tarantool;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+/**
+ * Test operations of {@link TarantoolConnection} class.
+ *
+ * Actual tests reside in base class.
+ */
+public class ConnectionIT extends AbstractTarantoolOpsIT {
+ private TarantoolConnection conn;
+
+ @BeforeEach
+ public void setup() {
+ conn = openConnection();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ conn.close();
+ }
+
+ @Override
+ protected TarantoolClientOps, Object, List>> getOps() {
+ return conn;
+ }
+
+ @Test
+ public void testClose() {
+ conn.close();
+ }
+}
diff --git a/src/test/java/org/tarantool/FireAndForgetClientOperationsIT.java b/src/test/java/org/tarantool/FireAndForgetClientOperationsIT.java
new file mode 100644
index 00000000..3d81fbe2
--- /dev/null
+++ b/src/test/java/org/tarantool/FireAndForgetClientOperationsIT.java
@@ -0,0 +1,83 @@
+package org.tarantool;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test "fire & forget" operations available in {@link TarantoolClientImpl} class.
+ */
+public class FireAndForgetClientOperationsIT extends AbstractTarantoolConnectorIT {
+ private TarantoolClient client;
+
+ @BeforeEach
+ public void setup() {
+ client = makeClient();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ client.close();
+ }
+
+ @Test
+ public void testPing() {
+ // Half-ping actually.
+ client.fireAndForgetOps().ping();
+ }
+
+ @Test
+ public void testClose() {
+ IllegalStateException e = assertThrows(IllegalStateException.class, new Executable() {
+ @Override
+ public void execute() throws Throwable {
+ client.fireAndForgetOps().close();
+ }
+ });
+ assertEquals(e.getMessage(), "You should close TarantoolClient instead.");
+ }
+
+ @Test
+ public void testFireAndForgetOperations() {
+ TarantoolClientOps, Object, Long> ffOps = client.fireAndForgetOps();
+
+ Set syncIds = new HashSet();
+
+ syncIds.add(ffOps.insert(SPACE_ID, Arrays.asList(10, "10")));
+ syncIds.add(ffOps.delete(SPACE_ID, Collections.singletonList(10)));
+
+ syncIds.add(ffOps.insert(SPACE_ID, Arrays.asList(10, "10")));
+ syncIds.add(ffOps.update(SPACE_ID, Collections.singletonList(10), Arrays.asList("=", 1, "ten")));
+
+ syncIds.add(ffOps.replace(SPACE_ID, Arrays.asList(20, "20")));
+ syncIds.add(ffOps.upsert(SPACE_ID, Collections.singletonList(20), Arrays.asList(20, "twenty"),
+ Arrays.asList("=", 1, "twenty")));
+
+ syncIds.add(ffOps.insert(SPACE_ID, Arrays.asList(30, "30")));
+ syncIds.add(ffOps.call("box.space.basic_test:delete", Collections.singletonList(30)));
+
+ // Check the syncs.
+ assertFalse(syncIds.contains(0L));
+ assertEquals(8, syncIds.size());
+
+ // The reply for synchronous ping will
+ // indicate to us that previous fire & forget operations are completed.
+ client.syncOps().ping();
+
+ // Check the effects
+ checkTupleResult(consoleSelect(SPACE_NAME, 10), Arrays.asList(10, "ten"));
+ checkTupleResult(consoleSelect(SPACE_NAME, 20), Arrays.asList(20, "twenty"));
+ assertEquals(consoleSelect(SPACE_NAME, 30), Collections.emptyList());
+ }
+}
diff --git a/src/test/java/org/tarantool/TarantoolConsole.java b/src/test/java/org/tarantool/TarantoolConsole.java
new file mode 100644
index 00000000..9ae1106b
--- /dev/null
+++ b/src/test/java/org/tarantool/TarantoolConsole.java
@@ -0,0 +1,241 @@
+package org.tarantool;
+
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Blocking console connection for test control purposes.
+ *
+ * Provides the means of lua commands evaluation given
+ * the host and port or the instance name of tarantool.
+ */
+public abstract class TarantoolConsole implements Closeable {
+ private final static Pattern GREETING_PATTERN = Pattern.compile("^Tarantool.+\n.+\n");
+ private final static Pattern CONNECTED_PATTERN = Pattern.compile("^connected to (.*)\n");
+ private final static Pattern REPLY_PATTERN = Pattern.compile("^.*\\n\\.{3}\\n",
+ Pattern.UNIX_LINES | Pattern.DOTALL);
+
+ private final static int TIMEOUT = 2000;
+ private final StringBuilder unmatched = new StringBuilder();
+
+ protected BufferedReader reader;
+ protected OutputStreamWriter writer;
+
+ private Matcher checkMatch(Pattern p) {
+ if (unmatched.length() == 0)
+ return null;
+
+ Matcher m = p.matcher(unmatched.toString());
+
+ if (m.find() && !m.requireEnd()) {
+ unmatched.delete(0, m.end());
+ return m;
+ }
+
+ return null;
+ }
+
+ protected Matcher expect(Pattern p) {
+ Matcher result = checkMatch(p);
+ if (result != null)
+ return result;
+
+ char[] buf = new char[4096];
+ int rc;
+ try {
+ while ((rc = reader.read(buf, 0, buf.length)) > 0) {
+ appendApplyBackspaces(unmatched, buf, rc);
+
+ result = checkMatch(p);
+ if (result != null)
+ return result;
+ }
+ } catch (SocketTimeoutException e) {
+ throw new RuntimeException("Timeout occurred. Unmatched: " + unmatched.toString() + ", pattern:" + p, e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ throw new RuntimeException("Unexpected end of input.");
+ }
+
+ private static void appendApplyBackspaces(StringBuilder sb, char[] buf, int len) {
+ for (int i = 0; i < len ; i++) {
+ char c = buf[i];
+ if (c == '\b') {
+ if (sb.length() > 0) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+
+ protected void write(String expr) {
+ try {
+ writer.write(expr + '\n');
+ writer.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ // No-op.
+ }
+ try {
+ writer.close();
+ } catch (IOException e) {
+ // No-op.
+ }
+ }
+
+ protected void suppressPrompt() {
+ // No-op, override.
+ }
+
+ protected void suppressEcho(String expr) {
+ // No-op, override.
+ }
+
+ public void exec(String expr) {
+ suppressPrompt();
+ write(expr);
+ suppressEcho(expr);
+ expect(REPLY_PATTERN);
+ }
+
+ public T eval(String expr) {
+ suppressPrompt();
+ write(expr);
+ suppressEcho(expr);
+ Matcher m = expect(REPLY_PATTERN);
+ Yaml yaml = new Yaml();
+ List result = yaml.load(m.group(0));
+ return result.get(0);
+ }
+
+ /**
+ * A direct tarantool console connection.
+ *
+ * @param host Tarantool host name.
+ * @param port Console port of tarantool instance.
+ * @return Console connection object.
+ */
+ public static TarantoolConsole open(String host, int port) {
+ return new TarantoolTcpConsole(host, port);
+ }
+
+ /**
+ * An indirect tarantool console connection via tarantoolctl utility.
+ *
+ * > tarantoolctl enter <instance>
+ *
+ * This facility is aimed at support of multi-instance tests in future.
+ *
+ * @param workDir Directory where .tarantoolctl file is located.
+ * @param instance Tarantool instance name as per command.
+ * @return Console connection object.
+ */
+ public static TarantoolConsole open(String workDir, String instance) {
+ return new TarantoolLocalConsole(workDir, instance);
+ }
+
+ /**
+ * A direct tarantool console connection (via TCP connection).
+ */
+ private static class TarantoolTcpConsole extends TarantoolConsole {
+ final Socket socket;
+
+ TarantoolTcpConsole(String host, int port) {
+ socket = new TestSocketChannelProvider(host, port, TIMEOUT).get(1, null).socket();
+ try {
+ reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ writer = new OutputStreamWriter(socket.getOutputStream());
+ } catch (IOException e) {
+ try {
+ socket.close();
+ } catch (IOException ignored) {
+ // No-op.
+ }
+ throw new RuntimeException("Couldn't connect to console at " + host + ":" + port, e);
+ }
+ expect(GREETING_PATTERN);
+ }
+
+ @Override
+ public void close() {
+ super.close();
+
+ try {
+ socket.close();
+ } catch (IOException e) {
+ // No-op.
+ }
+ }
+ }
+
+ /**
+ * An indirect tarantool console connection via tarantoolctl utility.
+ */
+ private static class TarantoolLocalConsole extends TarantoolConsole {
+ final Process process;
+ final String name;
+ final Pattern prompt;
+
+ TarantoolLocalConsole(String workDir, String instance) {
+ ProcessBuilder builder = new ProcessBuilder("env", "tarantoolctl", "enter", instance);
+ Map env = builder.environment();
+ env.put("PWD", workDir);
+ env.put("TEST_WORKDIR", workDir);
+ env.put("COLUMNS", "256");
+ builder.redirectErrorStream(true);
+ builder.directory(new File(workDir));
+
+ try {
+ process = builder.start();
+ } catch (IOException e) {
+ throw new RuntimeException("environment failure", e);
+ }
+ reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+ writer = new OutputStreamWriter(process.getOutputStream());
+
+ write("enter " + instance);
+ Matcher m = expect(CONNECTED_PATTERN);
+ name = m.group(1);
+ prompt = Pattern.compile(Pattern.quote(name + "> "));
+ }
+
+ @Override
+ protected void suppressPrompt() {
+ expect(prompt);
+ }
+
+ @Override
+ protected void suppressEcho(String expr) {
+ expect(Pattern.compile(Pattern.quote(expr)));
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ process.destroy();
+ }
+ }
+}
diff --git a/src/test/java/org/tarantool/TarantoolControl.java b/src/test/java/org/tarantool/TarantoolControl.java
new file mode 100644
index 00000000..e13b89e7
--- /dev/null
+++ b/src/test/java/org/tarantool/TarantoolControl.java
@@ -0,0 +1,173 @@
+package org.tarantool;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.Map;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * Wrapper around tarantoolctl utility.
+ */
+public class TarantoolControl {
+ protected static final String tntCtlWorkDir = System.getProperty("tntCtlWorkDir",
+ new File("testroot").getAbsolutePath());
+ protected static final String instanceDir = new File("src/test").getAbsolutePath();
+ protected static final String tarantoolCtlConfig = new File("src/test/.tarantoolctl").getAbsolutePath();
+ protected static final int RESTART_TIMEOUT = 2000;
+
+ // Based on https://stackoverflow.com/a/779529
+ private void rmdir(File f) throws IOException {
+ if (f.isDirectory()) {
+ for (File c : f.listFiles())
+ rmdir(c);
+ }
+ f.delete();
+ }
+
+ private void rmdir(String f) throws IOException {
+ rmdir(new File(f));
+ }
+
+ private void mkdir(File f) throws IOException {
+ f.mkdirs();
+ }
+
+ private void mkdir(String f) throws IOException {
+ mkdir(new File(f));
+ }
+
+ private static void copyFile(File source, File dest) throws IOException {
+ if (dest.isDirectory())
+ dest = new File(dest, source.getName());
+ FileChannel sourceChannel = null;
+ FileChannel destChannel = null;
+ try {
+ sourceChannel = new FileInputStream(source).getChannel();
+ destChannel = new FileOutputStream(dest).getChannel();
+ destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
+ } finally {
+ sourceChannel.close();
+ destChannel.close();
+ }
+ }
+
+ private static void copyFile(String source, String dest) throws IOException {
+ copyFile(new File(source), new File(dest));
+ }
+
+ private static void copyFile(File source, String dest) throws IOException {
+ copyFile(source, new File(dest));
+ }
+
+ private static void copyFile(String source, File dest) throws IOException {
+ copyFile(new File(source), dest);
+ }
+
+ private static String loadStream(InputStream s) throws IOException {
+ BufferedReader br = new BufferedReader(new InputStreamReader(s));
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null)
+ sb.append(line).append("\n");
+ return sb.toString();
+ }
+
+ protected void setupWorkDirectory() throws IOException {
+ rmdir(tntCtlWorkDir);
+ mkdir(tntCtlWorkDir);
+ for (File c : new File(instanceDir).listFiles())
+ if (c.getName().endsWith(".lua"))
+ copyFile(c, tntCtlWorkDir);
+ copyFile(tarantoolCtlConfig, tntCtlWorkDir);
+ }
+
+ TarantoolControl() {
+ try {
+ setupWorkDirectory();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Control the given tarantool instance via tarantoolctl utility.
+ *
+ * @param command A tarantoolctl utility command.
+ * @param instanceName Name of tarantool instance to control.
+ */
+ protected void executeCommand(String command, String instanceName) {
+ ProcessBuilder builder = new ProcessBuilder("env", "tarantoolctl", command, instanceName);
+ builder.directory(new File(tntCtlWorkDir));
+ Map env = builder.environment();
+ env.put("PWD", tntCtlWorkDir);
+ env.put("TEST_WORKDIR", tntCtlWorkDir);
+
+ final Process process;
+ try {
+ process = builder.start();
+ } catch (IOException e) {
+ throw new RuntimeException("environment failure", e);
+ }
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ // The thread below is necessary to organize timed wait on the process.
+ // We cannot use Process.waitFor(long, TimeUnit) because we on java 6.
+ Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ process.waitFor();
+ } catch (InterruptedException e) {
+ // No-op.
+ }
+ latch.countDown();
+ }
+ });
+
+ thread.start();
+
+ boolean res;
+ try {
+ res = latch.await(RESTART_TIMEOUT, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("wait interrupted", e);
+ }
+
+ if (!res) {
+ thread.interrupt();
+ process.destroy();
+
+ throw new RuntimeException("timeout");
+ }
+
+ int code = process.exitValue();
+
+ if (code != 0) {
+ String stdout = "";
+ String stderr = "";
+ try {
+ stdout = loadStream(process.getInputStream());
+ stderr = loadStream(process.getErrorStream());
+ } catch (IOException e) {
+ /* No-op. */
+ }
+ throw new RuntimeException("returned exitcode " + code + "\n" +
+ "[stdout]\n" + stdout + "\n[stderr]\n" + stderr);
+ }
+ }
+
+ public void start(String instanceName) {
+ executeCommand("start", instanceName);
+ }
+
+ public void stop(String instanceName) {
+ executeCommand("stop", instanceName);
+ }
+}
diff --git a/src/test/java/org/tarantool/TestSocketChannelProvider.java b/src/test/java/org/tarantool/TestSocketChannelProvider.java
new file mode 100644
index 00000000..924097a6
--- /dev/null
+++ b/src/test/java/org/tarantool/TestSocketChannelProvider.java
@@ -0,0 +1,40 @@
+package org.tarantool;
+
+import java.net.InetSocketAddress;
+import java.nio.channels.SocketChannel;
+
+/**
+ * Socket channel provider to be used throughout the tests.
+ */
+public class TestSocketChannelProvider implements SocketChannelProvider {
+ String host;
+ int port;
+ int restart_timeout;
+
+ public TestSocketChannelProvider(String host, int port, int restart_timeout) {
+ this.host = host;
+ this.port = port;
+ this.restart_timeout = restart_timeout;
+ }
+
+ @Override
+ public SocketChannel get(int retryNumber, Throwable lastError) {
+ long budget = System.currentTimeMillis() + restart_timeout;
+ while (!Thread.currentThread().isInterrupted()) {
+ try {
+ return SocketChannel.open(new InetSocketAddress(host, port));
+ } catch (Exception e) {
+ if (budget < System.currentTimeMillis())
+ throw new RuntimeException(e);
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ex) {
+ // No-op.
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ throw new RuntimeException("Test failure due to invalid environment. " +
+ "Timeout connecting to " + host + ":" + port);
+ }
+}
diff --git a/src/test/instance.lua b/src/test/jdk-testing.lua
similarity index 63%
rename from src/test/instance.lua
rename to src/test/jdk-testing.lua
index fe8a80bc..fa6615d9 100644
--- a/src/test/instance.lua
+++ b/src/test/jdk-testing.lua
@@ -1,7 +1,5 @@
-time = require('clock').time
-
box.cfg {
- listen = '0.0.0.0:3301',
+ listen = 3301,
}
box.once('init', function()
@@ -16,7 +14,11 @@ box.once('init', function()
box.schema.user.create('test_ordin', { password = '2HWRXHfa' })
box.schema.user.create('test_admin', { password = '4pWBZmLEgkmKK5WP' })
- box.schema.user.grant('test_ordin', 'read,write', 'user')
- box.schema.user.grant('test_admin', 'execute', 'super')
+ box.schema.user.grant('test_ordin', 'read,write', 'space', 'user')
+ box.schema.user.grant('test_admin', 'super')
end)
+-- Java has no internal support for unix domain sockets,
+-- so we will use tcp for console communication.
+console = require('console')
+console.listen(3313)
diff --git a/src/test/travis.pre.sh b/src/test/travis.pre.sh
index 86ecdd3b..cdc7ca3e 100755
--- a/src/test/travis.pre.sh
+++ b/src/test/travis.pre.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-set -e
+set -e
curl http://download.tarantool.org/tarantool/1.9/gpgkey | sudo apt-key add -
release=`lsb_release -c -s`
@@ -14,6 +14,4 @@ EOF
sudo apt-get update
sudo apt-get -y install tarantool tarantool-common
-sudo cp src/test/instance.lua /etc/tarantool/instances.enabled/jdk-testing.lua
-sudo tarantoolctl stop example
-sudo tarantoolctl start jdk-testing
+sudo tarantoolctl stop example