Added integration test.
This commit is contained in:
parent
93d983424e
commit
bbd1c05e62
|
@ -17,3 +17,16 @@ jobs:
|
||||||
compiler: ldc-latest
|
compiler: ldc-latest
|
||||||
- name: Build and Test
|
- name: Build and Test
|
||||||
run: dub -q test
|
run: dub -q test
|
||||||
|
|
||||||
|
integration-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup DLang
|
||||||
|
uses: dlang-community/setup-dlang@v2
|
||||||
|
with:
|
||||||
|
compiler: ldc-latest
|
||||||
|
|
||||||
|
- name: http1-test
|
||||||
|
working-directory: integration-tests
|
||||||
|
run: dub run --single http1-test.d
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
http1-test
|
|
@ -0,0 +1,48 @@
|
||||||
|
/+ dub.sdl:
|
||||||
|
dependency "handy-http-transport" path="../"
|
||||||
|
dependency "requests" version="~>2.1"
|
||||||
|
dependency "streams" path="/home/andrew/Code/github-andrewlalis/streams"
|
||||||
|
dependency "slf4d" path="/home/andrew/Code/github-andrewlalis/slf4d"
|
||||||
|
+/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This tests the basic HTTP functionality of the Http1Transport implementation
|
||||||
|
* by starting a server, sending a request, and checking the response.
|
||||||
|
*/
|
||||||
|
module integration_tests.http1_test;
|
||||||
|
|
||||||
|
import handy_http_primitives;
|
||||||
|
import handy_http_transport;
|
||||||
|
import slf4d;
|
||||||
|
import slf4d.default_provider;
|
||||||
|
import requests;
|
||||||
|
|
||||||
|
import core.thread;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
auto loggingProvider = new shared DefaultProvider(true, Levels.INFO);
|
||||||
|
configureLoggingProvider(loggingProvider);
|
||||||
|
|
||||||
|
HttpTransport transport = new Http1Transport(HttpRequestHandler.of((ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
|
response.headers.add("Content-Type", "text/plain");
|
||||||
|
response.headers.add("Content-Length", "13");
|
||||||
|
response.outputStream.writeToStream(cast(ubyte[]) "Hello, world!");
|
||||||
|
}));
|
||||||
|
Thread thread = transport.startInNewThread();
|
||||||
|
scope(exit) {
|
||||||
|
transport.stop();
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
info("Started server in another thread.");
|
||||||
|
Thread.sleep(msecs(100));
|
||||||
|
|
||||||
|
auto content = getContent("http://localhost:8080");
|
||||||
|
ubyte[] data = content.data;
|
||||||
|
if (data.length != 13 || (cast(string) data) != "Hello, world!") {
|
||||||
|
error("Received unexpected content: " ~ cast(string) data);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info("Test completed successfully.");
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -11,16 +11,18 @@ import streams;
|
||||||
* Returns: The string that was read, or a stream error.
|
* Returns: The string that was read, or a stream error.
|
||||||
*/
|
*/
|
||||||
Either!(string, "value", StreamError, "error") consumeUntil(S)(
|
Either!(string, "value", StreamError, "error") consumeUntil(S)(
|
||||||
ref S inputStream,
|
S inputStream,
|
||||||
string target
|
string target
|
||||||
) if (isByteInputStream!S) {
|
) if (isByteInputStream!S) {
|
||||||
|
import slf4d;
|
||||||
|
// info("consumeUntil called.");
|
||||||
ubyte[1024] buffer;
|
ubyte[1024] buffer;
|
||||||
size_t idx;
|
size_t idx;
|
||||||
while (true) {
|
while (true) {
|
||||||
auto result = inputStream.readFromStream(buffer[idx .. idx + 1]);
|
auto result = inputStream.readFromStream(buffer[idx .. idx + 1]);
|
||||||
if (result.hasError) return Either!(string, "value", StreamError, "error")(result.error);
|
if (result.hasError) return Either!(string, "value", StreamError, "error")(result.error);
|
||||||
if (result.count != 1) return Either!(string, "value", StreamError, "error")(
|
if (result.count != 1) return Either!(string, "value", StreamError, "error")(
|
||||||
StreamError("Failed to read a single element", 1)
|
StreamError("Failed to read a single element", 0)
|
||||||
);
|
);
|
||||||
idx++;
|
idx++;
|
||||||
if (idx >= target.length && buffer[idx - target.length .. idx] == target) {
|
if (idx >= target.length && buffer[idx - target.length .. idx] == target) {
|
||||||
|
|
|
@ -23,6 +23,12 @@ class Http1Transport : HttpTransport {
|
||||||
private const ushort port;
|
private const ushort port;
|
||||||
private bool running = false;
|
private bool running = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new Http1Transport server instance.
|
||||||
|
* Params:
|
||||||
|
* requestHandler = The request handler to use for all requests.
|
||||||
|
* port = The port to bind to.
|
||||||
|
*/
|
||||||
this(HttpRequestHandler requestHandler, ushort port = 8080) {
|
this(HttpRequestHandler requestHandler, ushort port = 8080) {
|
||||||
assert(requestHandler !is null);
|
assert(requestHandler !is null);
|
||||||
this.serverSocket = new TcpSocket();
|
this.serverSocket = new TcpSocket();
|
||||||
|
@ -30,18 +36,33 @@ class Http1Transport : HttpTransport {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server. Internally, this starts the Photon event loop and
|
||||||
|
* accepts incoming connections in a separate fiber. Then, clients are
|
||||||
|
* handled in their own separate fiber (think "coroutine").
|
||||||
|
*/
|
||||||
void start() {
|
void start() {
|
||||||
debugF!"Starting HTTP1Transport server on port %d."(port);
|
debugF!"Starting server on port %d."(port);
|
||||||
startloop();
|
startloop();
|
||||||
go(() => runServer());
|
go(() => runServer());
|
||||||
runFibers();
|
runFibers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the server. This will mark the server as no longer running, so
|
||||||
|
* no more connections will be accepted.
|
||||||
|
*/
|
||||||
void stop() {
|
void stop() {
|
||||||
debugF!"Stopping HTTP1Transport server on port %d."(port);
|
debugF!"Stopping server on port %d."(port);
|
||||||
this.running = false;
|
this.running = false;
|
||||||
this.serverSocket.shutdown(SocketShutdown.BOTH);
|
// Send a dummy request to cause the server's blocking accept() call to end.
|
||||||
this.serverSocket.close();
|
try {
|
||||||
|
Socket dummySocket = new TcpSocket(this.serverSocket.localAddress());
|
||||||
|
dummySocket.shutdown(SocketShutdown.BOTH);
|
||||||
|
dummySocket.close();
|
||||||
|
} catch (SocketOSException e) {
|
||||||
|
warn("Failed to send empty request to stop server.", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runServer() {
|
private void runServer() {
|
||||||
|
@ -58,6 +79,8 @@ class Http1Transport : HttpTransport {
|
||||||
warn("Failed to accept socket connection.", e);
|
warn("Failed to accept socket connection.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.serverSocket.shutdown(SocketShutdown.BOTH);
|
||||||
|
this.serverSocket.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,10 +99,14 @@ void handleClient(Socket clientSocket, HttpRequestHandler requestHandler) {
|
||||||
// Get remote address from the socket.
|
// Get remote address from the socket.
|
||||||
import handy_http_primitives.address;
|
import handy_http_primitives.address;
|
||||||
ClientAddress addr = getAddress(clientSocket);
|
ClientAddress addr = getAddress(clientSocket);
|
||||||
traceF!"Handling client request from %s."(addr.toString());
|
debugF!"Handling client request from %s."(addr.toString());
|
||||||
auto result = readHttpRequest(&bufferedInput, addr);
|
auto result = readHttpRequest(&bufferedInput, addr);
|
||||||
|
debug_("Finished reading HTTP request from client.");
|
||||||
if (result.hasError) {
|
if (result.hasError) {
|
||||||
warnF!"Failed to read HTTP request: %s"(result.error.message);
|
if (result.error.code != -1) {
|
||||||
|
// Only warn if we didn't read an empty request.
|
||||||
|
warnF!"Failed to read HTTP request: %s"(result.error.message);
|
||||||
|
}
|
||||||
inputStream.closeStream();
|
inputStream.closeStream();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -182,9 +209,17 @@ alias HttpRequestParseResult = Either!(ServerHttpRequest, "request", StreamError
|
||||||
*/
|
*/
|
||||||
HttpRequestParseResult readHttpRequest(S)(S inputStream, in ClientAddress addr) if (isByteInputStream!S) {
|
HttpRequestParseResult readHttpRequest(S)(S inputStream, in ClientAddress addr) if (isByteInputStream!S) {
|
||||||
auto methodStr = consumeUntil(inputStream, " ");
|
auto methodStr = consumeUntil(inputStream, " ");
|
||||||
if (methodStr.hasError) return HttpRequestParseResult(methodStr.error);
|
if (methodStr.hasError) {
|
||||||
|
if (methodStr.error.code == 0) {
|
||||||
|
// Set a custom code to indicate an empty request.
|
||||||
|
return HttpRequestParseResult(StreamError(methodStr.error.message, -1));
|
||||||
|
}
|
||||||
|
return HttpRequestParseResult(methodStr.error);
|
||||||
|
}
|
||||||
|
|
||||||
auto urlStr = consumeUntil(inputStream, " ");
|
auto urlStr = consumeUntil(inputStream, " ");
|
||||||
if (urlStr.hasError) return HttpRequestParseResult(urlStr.error);
|
if (urlStr.hasError) return HttpRequestParseResult(urlStr.error);
|
||||||
|
|
||||||
auto versionStr = consumeUntil(inputStream, "\r\n");
|
auto versionStr = consumeUntil(inputStream, "\r\n");
|
||||||
if (versionStr.hasError) return HttpRequestParseResult(versionStr.error);
|
if (versionStr.hasError) return HttpRequestParseResult(versionStr.error);
|
||||||
|
|
||||||
|
|
|
@ -4,3 +4,21 @@ interface HttpTransport {
|
||||||
void start();
|
void start();
|
||||||
void stop();
|
void stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import core.thread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a new thread to run an HTTP transport implementation in, separate
|
||||||
|
* from the calling thread. This is useful for running a server in the
|
||||||
|
* background, like for integration tests.
|
||||||
|
* Params:
|
||||||
|
* transport = The transport implementation to start.
|
||||||
|
* Returns: The thread that was started.
|
||||||
|
*/
|
||||||
|
Thread startInNewThread(HttpTransport transport) {
|
||||||
|
Thread t = new Thread(() {
|
||||||
|
transport.start();
|
||||||
|
});
|
||||||
|
t.start();
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue