Added integration test.
This commit is contained in:
parent
93d983424e
commit
bbd1c05e62
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue