/* * 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); } }