From fd37f95f7ea0131ad8498705960bdfdac673f976 Mon Sep 17 00:00:00 2001 From: David Roden Date: Sat, 25 Jan 2025 10:18:20 +0100 Subject: [PATCH] =?utf8?q?=E2=9C=A8=20Add=20basic=20MSTA=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- client/pom.xml | 7 +++ .../main/java/de/qsheltier/msta/client/Client.java | 73 ++++++++++++++++++++++ .../java/de/qsheltier/msta/client/ClientTest.java | 56 +++++++++++++++++ .../de/qsheltier/msta/client/FakeTcpServer.java | 52 +++++++++++++++ .../java/de/qsheltier/msta/client/TextSocket.java | 48 ++++++++++++++ pom.xml | 5 ++ 6 files changed, 241 insertions(+) create mode 100644 client/src/main/java/de/qsheltier/msta/client/Client.java create mode 100644 client/src/test/java/de/qsheltier/msta/client/ClientTest.java create mode 100644 client/src/test/java/de/qsheltier/msta/client/FakeTcpServer.java create mode 100644 client/src/test/java/de/qsheltier/msta/client/TextSocket.java diff --git a/client/pom.xml b/client/pom.xml index 4cd574c..758aef7 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -14,4 +14,11 @@ MSTA – Client Manual Software Testing Avoidance – Client Component + + + com.fasterxml.jackson.core + jackson-databind + + + diff --git a/client/src/main/java/de/qsheltier/msta/client/Client.java b/client/src/main/java/de/qsheltier/msta/client/Client.java new file mode 100644 index 0000000..2830534 --- /dev/null +++ b/client/src/main/java/de/qsheltier/msta/client/Client.java @@ -0,0 +1,73 @@ +package de.qsheltier.msta.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * An MSTA {@code Client} connects to an MSTA server, can send commands, + * and notifies listeners of received events. + */ +public class Client { + + public Client(String hostname, int port) { + this.hostname = hostname; + this.port = port; + socket = new Socket(); + } + + public void onConnect(Runnable connectListener) { + connectListeners.add(connectListener); + } + + public void connect() throws IOException { + socket.connect(new InetSocketAddress(hostname, port)); + outputWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), UTF_8)); + new Thread(() -> { + try (Socket socket = this.socket) { + handleConnection(socket.getInputStream()); + } catch (IOException e) { + /* ignore. */ + } + }).start(); + } + + public void sendCommand(String command) throws IOException { + outputWriter.write(command + "\r\n"); + outputWriter.flush(); + } + + private void handleConnection(InputStream inputStream) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, UTF_8))) { + while (true) { + var line = reader.readLine(); + if (line == null) { + break; + } + var reply = new ObjectMapper().readTree(line); + if (reply.has("event") && reply.get("event").asText().equalsIgnoreCase("connected")) { + connectListeners.forEach(Runnable::run); + } + } + } + } + + private final Socket socket; + private Writer outputWriter; + private final String hostname; + private final int port; + + private final List connectListeners = new ArrayList<>(); + +} diff --git a/client/src/test/java/de/qsheltier/msta/client/ClientTest.java b/client/src/test/java/de/qsheltier/msta/client/ClientTest.java new file mode 100644 index 0000000..6053bbd --- /dev/null +++ b/client/src/test/java/de/qsheltier/msta/client/ClientTest.java @@ -0,0 +1,56 @@ +package de.qsheltier.msta.client; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Timeout.ThreadMode.SEPARATE_THREAD; + +public class ClientTest { + + @Test + @Timeout(value = 5, unit = SECONDS) + public void clientCanConnectToAServer() throws Exception { + var client = new Client("localhost", server.getPort()); + client.connect(); + server.connect().get(); + } + + @Test + @Timeout(value = 5, unit = SECONDS, threadMode = SEPARATE_THREAD) + public void listenerIsNotifiedOnConnectEvent() throws Exception { + var client = new Client("localhost", server.getPort()); + var connectListenerCalled = new AtomicBoolean(false); + client.onConnect(() -> connectListenerCalled.set(true)); + client.connect(); + server.connect().get(); + server.writeLine("{\"event\": \"connected\"}"); + while (!connectListenerCalled.get()) { + Thread.yield(); + } + } + + @Test + @Timeout(value = 5, unit = SECONDS, threadMode = SEPARATE_THREAD) + public void clientCanSendCommandToServer() throws Exception { + var client = new Client("localhost", server.getPort()); + client.connect(); + server.connect().get(); + server.writeLine("{\"event\": \"connected\"}"); + client.sendCommand("shutdown"); + var line = server.readLine(); + assertThat(line, equalTo("shutdown")); + } + + public ClientTest() throws IOException { + } + + private final FakeTcpServer server = new FakeTcpServer(Executors.newCachedThreadPool()); + +} diff --git a/client/src/test/java/de/qsheltier/msta/client/FakeTcpServer.java b/client/src/test/java/de/qsheltier/msta/client/FakeTcpServer.java new file mode 100644 index 0000000..d4d94b1 --- /dev/null +++ b/client/src/test/java/de/qsheltier/msta/client/FakeTcpServer.java @@ -0,0 +1,52 @@ +package de.qsheltier.msta.client; + +import java.io.Closeable; +import java.io.IOException; +import java.net.ServerSocket; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; +import org.hamcrest.Matcher; + +public class FakeTcpServer implements Closeable { + + private final ExecutorService executorService; + private final ServerSocket serverSocket = new ServerSocket(0); + private final AtomicReference clientSocket = new AtomicReference<>(); + + public FakeTcpServer(ExecutorService executorService) throws IOException { + this.executorService = executorService; + } + + public int getPort() { + return serverSocket.getLocalPort(); + } + + public Future connect() { + return executorService.submit(() -> { + try { + clientSocket.set(new TextSocket(serverSocket.accept())); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public void writeLine(String... lines) throws IOException { + var textSocket = clientSocket.get(); + for (String line : lines) { + textSocket.writeLine(line); + } + } + + public String readLine() throws IOException { + return clientSocket.get().readLine(); + } + + @Override + public void close() throws IOException { + clientSocket.get().close(); + } + +} diff --git a/client/src/test/java/de/qsheltier/msta/client/TextSocket.java b/client/src/test/java/de/qsheltier/msta/client/TextSocket.java new file mode 100644 index 0000000..dc2019b --- /dev/null +++ b/client/src/test/java/de/qsheltier/msta/client/TextSocket.java @@ -0,0 +1,48 @@ +package de.qsheltier.msta.client; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import org.hamcrest.Matcher; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Wrapper around a [Socket] that handles text. + */ +public class TextSocket implements Closeable { + + private final Socket socket; + private final BufferedReader inputReader; + private final Writer outputWriter; + + public TextSocket(Socket socket) throws IOException { + this.socket = socket; + inputReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF_8)); + outputWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), UTF_8)); + } + + public String readLine() throws IOException { + return inputReader.readLine(); + } + + public void writeLine(String line) throws IOException { + outputWriter.write(line + "\r\n"); + outputWriter.flush(); + } + + @Override + public void close() throws IOException { + outputWriter.close(); + inputReader.close(); + socket.close(); + } + +} diff --git a/pom.xml b/pom.xml index 7584c79..ecfc5b6 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,11 @@ 3.0 test + + com.fasterxml.jackson.core + jackson-databind + 2.18.1 + -- 2.7.4