From 6d22950129f2b57de8bccc2f5af2af67bdb5ffa7 Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Sun, 29 Dec 2019 01:56:02 +1030 Subject: [PATCH] Add a simple mock server for testing network code --- test/meson.build | 1 + test/test-server.vala | 222 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 test/test-server.vala diff --git a/test/meson.build b/test/meson.build index 38a3aae2..f410f914 100644 --- a/test/meson.build +++ b/test/meson.build @@ -3,6 +3,7 @@ subdir('data') geary_test_lib_sources = [ 'mock-object.vala', 'test-case.vala', + 'test-server.vala', ] geary_test_engine_sources = [ diff --git a/test/test-server.vala b/test/test-server.vala new file mode 100644 index 00000000..39e99e70 --- /dev/null +++ b/test/test-server.vala @@ -0,0 +1,222 @@ +/* + * Copyright 2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A simple mock server for testing network connections. + * + * To use it, unit tests should construct an instance as a fixture in + * set up, specify a test script by adding lines and then check the + * result, before stopping the server in tear down. + */ +public class TestServer : GLib.Object { + + + /** Possible actions a script may take. */ + public enum Action { + /** + * The implicit first action. + * + * This does not need to be specified as a script action, it + * will always be taken when a client connects. + */ + CONNECTED, + + /** Send a line to the client. */ + SEND_LINE, + + /** Receive a line from the client. */ + RECEIVE_LINE, + + /** Wait for the client to disconnect. */ + WAIT_FOR_DISCONNECT, + + /** Disconnect immediately. */ + DISCONNECT; + } + + + /** A line of the server's script. */ + public struct Line { + + /** The action to take for this line. */ + public Action action; + + /** + * The value for the action. + * + * If sending, this string will be sent. If receiving, the + * expected line. + */ + public string value; + + } + + /** The result of executing a script line. */ + public struct Result { + + /** The expected action. */ + public Line line; + + /** Was the expected action successful. */ + public bool succeeded; + + /** The actual string sent by a client when not as expected. */ + public string? actual; + + /** In case of an error being thrown, the error itself. */ + public GLib.Error? error; + + } + + + private GLib.DataStreamNewlineType line_ending; + private uint16 port; + private GLib.ThreadedSocketService service = + new GLib.ThreadedSocketService(10); + private GLib.Cancellable running = new GLib.Cancellable(); + private Gee.List script = new Gee.ArrayList(); + private GLib.AsyncQueue completion_queue = + new GLib.AsyncQueue(); + + + public TestServer(GLib.DataStreamNewlineType line_ending = CR_LF) + throws GLib.Error { + this.line_ending = line_ending; + this.port = this.service.add_any_inet_port(null); + this.service.run.connect((conn) => { + handle_connection(conn); + return true; + }); + this.service.start(); + } + + public GLib.SocketConnectable get_client_address() { + return new GLib.NetworkAddress("localhost", this.port); + } + + public void add_script_line(Action action, string value) { + this.script.add({ action, value }); + } + + public Result wait_for_script(GLib.MainContext loop) { + Result? result = null; + while (result == null) { + loop.iteration(false); + result = this.completion_queue.try_pop(); + } + return result; + } + + public void stop() { + this.service.stop(); + this.running.cancel(); + } + + private void handle_connection(GLib.SocketConnection connection) { + debug("Connected"); + var input = new GLib.DataInputStream( + connection.input_stream + ); + input.set_newline_type(this.line_ending); + + var output = new GLib.DataOutputStream( + connection.output_stream + ); + + Line connected_line = { CONNECTED, "" }; + Result result = { connected_line, true, null, null }; + foreach (var line in this.script) { + result.line = line; + switch (line.action) { + case SEND_LINE: + debug("Sending: %s", line.value); + try { + output.put_string(line.value); + switch (this.line_ending) { + case CR: + output.put_byte('\r'); + break; + case LF: + output.put_byte('\n'); + break; + default: + output.put_byte('\r'); + output.put_byte('\n'); + break; + } + } catch (GLib.Error err) { + result.succeeded = false; + result.error = err; + } + break; + + case RECEIVE_LINE: + debug("Waiting for: %s", line.value); + try { + size_t len; + string? received = input.read_line(out len, this.running); + if (received == null || received != line.value) { + result.succeeded = false; + result.actual = received; + } + } catch (GLib.Error err) { + result.succeeded = false; + result.error = err; + } + break; + + case WAIT_FOR_DISCONNECT: + debug("Waiting for disconnect"); + var socket = connection.get_socket(); + try { + uint8 buffer[4096]; + while (socket.receive_with_blocking(buffer, true) > 0) { } + } catch (GLib.Error err) { + result.succeeded = false; + result.error = err; + } + break; + + case DISCONNECT: + debug("Disconnecting"); + try { + connection.close(this.running); + } catch (GLib.Error err) { + result.succeeded = false; + result.error = err; + } + break; + } + + if (!result.succeeded) { + break; + } + } + if (result.succeeded) { + debug("Done"); + } else if (result.error != null) { + warning("Error: %s", result.error.message); + } else if (result.line.action == RECEIVE_LINE) { + warning("Received unexpected line: %s", result.actual ?? "(null)"); + } else { + warning("Failed for unknown reason"); + } + + if (connection.is_connected()) { + try { + connection.close(this.running); + } catch (GLib.Error err) { + warning( + "Error closing test server connection: %s", err.message + ); + } + } + + this.completion_queue.push(result); + } + +}