Add a simple mock server for testing network code

This commit is contained in:
Michael Gratton 2019-12-29 01:56:02 +10:30 committed by Michael James Gratton
parent 4285cafa01
commit 6d22950129
2 changed files with 223 additions and 0 deletions

222
test/test-server.vala Normal file
View file

@ -0,0 +1,222 @@
/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* 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<Line?> script = new Gee.ArrayList<Line?>();
private GLib.AsyncQueue<Result?> completion_queue =
new GLib.AsyncQueue<Result?>();
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);
}
}