/*
* Copyright 2015 Julien Viet
*
* Licensed 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 io.termd.core.tty;
import io.termd.core.TestBase;
import io.termd.core.util.Helper;
import org.junit.Test;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
*/
public abstract class TtyTestBase extends TestBase {
protected Charset charset = StandardCharsets.UTF_8;
protected abstract void assertConnect(String term) throws Exception;
/**
* Assert we can read a string of a specified length in bytes.
*
* @param len the lenght in bytes
* @return the string
* @throws Exception
*/
protected abstract String assertReadString(int len) throws Exception;
protected abstract void assertWrite(String s) throws Exception;
protected abstract void assertWriteln(String s) throws Exception;
protected abstract void server(Consumer<TtyConnection> onConnect);
protected abstract void resize(int width, int height) throws Exception;
protected void assertDisconnect(boolean clean) throws Exception {
throw new UnsupportedOperationException();
}
protected void assertConnect() throws Exception {
assertConnect(null);
}
protected final void assertWrite(int... codePoints) throws Exception {
assertWrite(Helper.fromCodePoints(codePoints));
}
@Test
public void testWrite() throws Exception {
final AtomicInteger requestCount = new AtomicInteger();
server(conn -> {
requestCount.incrementAndGet();
conn.stdoutHandler().accept(new int[]{'%', ' '});
});
assertConnect();
assertEquals("% ", assertReadString(2));
assertEquals(1, requestCount.get());
}
@Test
public void testRead() throws Exception {
final ArrayBlockingQueue<int[]> queue = new ArrayBlockingQueue<>(1);
server(conn -> conn.setStdinHandler(data -> {
queue.add(data);
conn.stdoutHandler().accept(new int[]{'h', 'e', 'l', 'l', 'o'});
}));
assertConnect();
assertWriteln("");
int[] data = queue.poll(10, TimeUnit.SECONDS);
assertTrue(Arrays.equals(new int[]{'\r'}, data));
assertEquals("hello", assertReadString(5));
}
@Test
public void testSignalInterleaving() throws Exception {
StringBuilder buffer = new StringBuilder();
AtomicInteger count = new AtomicInteger();
server(conn -> {
conn.setStdinHandler(event -> {
Helper.appendCodePoints(event, buffer);
});
conn.setEventHandler((event, cp) -> {
if (event == TtyEvent.INTR) {
switch (count.get()) {
case 0:
assertEquals("hello", buffer.toString());
buffer.setLength(0);
count.set(1);
break;
case 1:
assertEquals("bye", buffer.toString());
count.set(2);
testComplete();
break;
default:
fail("Not expected");
}
}
});
});
assertConnect();
assertWrite('h', 'e', 'l', 'l', 'o', 3, 'b', 'y', 'e', 3);
await();
}
@Test
public void testSignals() throws Exception {
StringBuilder buffer = new StringBuilder();
AtomicInteger count = new AtomicInteger();
server(conn -> {
conn.setStdinHandler(event -> Helper.appendCodePoints(event, buffer));
conn.setEventHandler((event, cp) -> {
switch (count.get()) {
case 0:
assertEquals(TtyEvent.INTR, event);
count.set(1);
break;
case 1:
assertEquals(TtyEvent.EOF, event);
count.set(2);
break;
case 2:
assertEquals(TtyEvent.SUSP, event);
count.set(3);
testComplete();
break;
default:
fail("Not expected");
}
});
});
assertConnect();
assertWrite(3, 4, 26);
await();
}
@Test
public void testServerDisconnect() throws Exception {
CountDownLatch closedLatch = new CountDownLatch(1);
server(conn -> {
conn.setStdinHandler(bytes -> {
conn.close();
});
conn.setCloseHandler(v -> {
closedLatch.countDown();
});
});
assertConnect();
assertWrite("whatever");
assertTrue(closedLatch.await(10, TimeUnit.SECONDS));
}
@Test
public void testClientDisconnectClean() throws Exception {
testClientDisconnect(true);
}
@Test
public void testClientDisconnect() throws Exception {
testClientDisconnect(false);
}
private void testClientDisconnect(boolean clean) throws Exception {
CountDownLatch disconnectLatch = new CountDownLatch(1);
CountDownLatch closedLatch = new CountDownLatch(1);
server(conn -> {
disconnectLatch.countDown();
conn.setCloseHandler(v -> {
closedLatch.countDown();
});
});
assertConnect();
assertTrue(disconnectLatch.await(10, TimeUnit.SECONDS));
assertDisconnect(clean);
assertTrue(closedLatch.await(10, TimeUnit.SECONDS));
}
@Test
public void testTerminalType() throws Exception {
Consumer<String> assertTerm = term -> {
if (term.equals("xterm") || term.equals("vt100")) {
testComplete();
} else {
fail("Unexpected term " + term);
}
};
server(conn -> {
if (conn.terminalType() != null) {
assertTerm.accept(conn.terminalType());
} else {
conn.setTerminalTypeHandler(assertTerm);
}
});
assertConnect("xterm");
assertWrite("bye");
await();
}
@Test
public void testSize() throws Exception {
server(conn -> {
if (conn.size() != null) {
assertEquals(80, conn.size().x());
assertEquals(24, conn.size().y());
testComplete();
} else {
conn.setSizeHandler(size -> {
assertEquals(80, conn.size().x());
assertEquals(24, conn.size().y());
testComplete();
});
}
}
);
assertConnect();
await();
}
@Test
public void testResize() throws Exception {
server(conn -> {
conn.setSizeHandler(size -> {
assertEquals(40, conn.size().x());
assertEquals(12, conn.size().y());
testComplete();
});
}
);
assertConnect();
resize(40, 12);
await();
}
@Test
public void testConnectionClose() throws Exception {
AtomicInteger closeCount = new AtomicInteger();
server(conn -> {
conn.setCloseHandler(v -> {
if (closeCount.incrementAndGet() > 1) {
fail("Closed call several times");
} else {
testComplete();
}
});
conn.setStdinHandler(text -> conn.close());
});
assertConnect();
assertWrite("bye");
await();
}
/**
* Check if the client is disconnected, this affects the connection, so this should not be used if the
* connection needs to be used after.
*
* @return if the client is disconnected
*/
public boolean checkDisconnected() {
throw new UnsupportedOperationException();
}
@Test
public void testConnectionCloseImmediatly() throws Exception {
server(conn -> {
conn.setCloseHandler(v -> {
new Thread() {
@Override
public void run() {
for (int i = 0;i < 100;i++) {
if (checkDisconnected()) {
testComplete();
return;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
fail(e);
}
}
}
}.start();
});
conn.close();
});
assertConnect();
await();
}
@Test
public void testScheduleThread() throws Exception {
server(conn -> {
Thread connThread = Thread.currentThread();
conn.execute(() -> {
Thread schedulerThread = Thread.currentThread();
try {
assertThreading(connThread, schedulerThread);
} catch (Exception e) {
fail(e);
}
testComplete();
});
});
assertConnect();
await();
}
protected void assertThreading(Thread connThread, Thread schedulerThread) throws Exception {
}
@Test
public void testLastAccessedTime() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicInteger count = new AtomicInteger();
long connTime = System.currentTimeMillis();
server(conn -> {
assertTrue(conn.lastAccessedTime() >= connTime);
long openTime = System.currentTimeMillis();
conn.setStdinHandler(cp -> {
long delta = conn.lastAccessedTime() - openTime;
switch (count.getAndIncrement()) {
case 0:
assertTrue(delta >= 0);
latch.countDown();
break;
case 1:
assertTrue(delta >= 10);
testComplete();
break;
}
});
});
assertConnect();
Thread.sleep(15);
assertWrite("hello");
awaitLatch(latch);
Thread.sleep(15);
assertWrite("byebye");
await();
}
@Test
public void testDifferentCharset() throws Exception {
charset = StandardCharsets.ISO_8859_1;
server(conn -> {
// 20AC does not exists in ISO_8859_1 and is replaced by `?`
conn.write("\u20AC");
});
assertConnect();
String s = assertReadString(1);
assertEquals("?", s);
}
}