<name>MSTA – Client</name>
<description>Manual Software Testing Avoidance – Client Component</description>
+ <dependencies>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+ </dependencies>
+
</project>
--- /dev/null
+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<Runnable> connectListeners = new ArrayList<>();
+
+}
--- /dev/null
+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());
+
+}
--- /dev/null
+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<TextSocket> 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();
+ }
+
+}
--- /dev/null
+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();
+ }
+
+}
<version>3.0</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>2.18.1</version>
+ </dependency>
</dependencies>
</dependencyManagement>