Added integration test.

This commit is contained in:
Andrew Lalis 2025-06-22 22:00:14 -04:00
parent 93d983424e
commit bbd1c05e62
6 changed files with 126 additions and 9 deletions

View File

@ -17,3 +17,16 @@ jobs:
compiler: ldc-latest
- name: Build and 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

1
integration-tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
http1-test

48
integration-tests/http1-test.d Executable file
View File

@ -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;
}

View File

@ -11,16 +11,18 @@ import streams;
* Returns: The string that was read, or a stream error.
*/
Either!(string, "value", StreamError, "error") consumeUntil(S)(
ref S inputStream,
S inputStream,
string target
) if (isByteInputStream!S) {
import slf4d;
// info("consumeUntil called.");
ubyte[1024] buffer;
size_t idx;
while (true) {
auto result = inputStream.readFromStream(buffer[idx .. idx + 1]);
if (result.hasError) return Either!(string, "value", StreamError, "error")(result.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++;
if (idx >= target.length && buffer[idx - target.length .. idx] == target) {

View File

@ -23,6 +23,12 @@ class Http1Transport : HttpTransport {
private const ushort port;
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) {
assert(requestHandler !is null);
this.serverSocket = new TcpSocket();
@ -30,18 +36,33 @@ class Http1Transport : HttpTransport {
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() {
debugF!"Starting HTTP1Transport server on port %d."(port);
debugF!"Starting server on port %d."(port);
startloop();
go(() => runServer());
runFibers();
}
/**
* Stops the server. This will mark the server as no longer running, so
* no more connections will be accepted.
*/
void stop() {
debugF!"Stopping HTTP1Transport server on port %d."(port);
debugF!"Stopping server on port %d."(port);
this.running = false;
this.serverSocket.shutdown(SocketShutdown.BOTH);
this.serverSocket.close();
// Send a dummy request to cause the server's blocking accept() call to end.
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() {
@ -58,6 +79,8 @@ class Http1Transport : HttpTransport {
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.
import handy_http_primitives.address;
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);
debug_("Finished reading HTTP request from client.");
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();
return;
}
@ -182,9 +209,17 @@ alias HttpRequestParseResult = Either!(ServerHttpRequest, "request", StreamError
*/
HttpRequestParseResult readHttpRequest(S)(S inputStream, in ClientAddress addr) if (isByteInputStream!S) {
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, " ");
if (urlStr.hasError) return HttpRequestParseResult(urlStr.error);
auto versionStr = consumeUntil(inputStream, "\r\n");
if (versionStr.hasError) return HttpRequestParseResult(versionStr.error);

View File

@ -4,3 +4,21 @@ interface HttpTransport {
void start();
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;
}