diff --git a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt index d6eb9ca30..25072a6e7 100644 --- a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt +++ b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt @@ -11,7 +11,6 @@ import dan200.computercraft.api.lua.ObjectArguments import dan200.computercraft.core.apis.HTTPAPI import dan200.computercraft.core.apis.IAPIEnvironment import dan200.computercraft.core.apis.handles.ReadHandle -import dan200.computercraft.core.apis.http.HttpServer.Companion.runServer import dan200.computercraft.core.apis.http.TestHttpApi.Companion.INSTANT_DURATION import dan200.computercraft.core.apis.http.TestHttpApi.Companion.LIMITED_DURATION import dan200.computercraft.core.apis.http.options.Action @@ -29,11 +28,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.lang.ref.Reference import java.nio.ByteBuffer -import java.time.Duration import java.util.* import java.util.concurrent.TimeUnit import java.util.random.RandomGenerator import java.util.random.RandomGeneratorFactory +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds class TestHttpApi { @@ -59,21 +60,28 @@ class TestHttpApi { NettyHttp.DEFAULT_CONFIG.proxy, ) + /** + * Start an HTTP server, then run a test within a [LuaTaskRunner]. + * + * This test runs with a longer timeout, as these tests are a little more timing-sensitive than most. + */ + private fun runWithServer(closeWebsocket: Boolean = true, timeout: Duration = 10.seconds, fn: suspend LuaTaskRunner.(HttpServer) -> Unit) { + HttpServer.runServer(closeWebsocket = closeWebsocket) { server -> LuaTaskRunner.runTest(timeout) { fn(server) } } + } + @Test fun `Connects to a HTTP server`() { - runServer { server -> - LuaTaskRunner.runTest { - val url = "http://127.0.0.1:${server.port}" - val httpApi = addApi(createHttpApi(environment, server.port)) - assertThat("http.request succeeded", httpApi.request(ObjectArguments(url)), array(equalTo(true))) + runWithServer { server -> + val url = "http://127.0.0.1:${server.port}" + val httpApi = addApi(createHttpApi(environment, server.port)) + assertThat("http.request succeeded", httpApi.request(ObjectArguments(url)), array(equalTo(true))) - val result = pullEvent("http_success") - assertThat(result, array(equalTo("http_success"), equalTo(url), isA(HttpResponseHandle::class.java))) + val result = pullEvent("http_success") + assertThat(result, array(equalTo("http_success"), equalTo(url), isA(HttpResponseHandle::class.java))) - val handle = result[2] as HttpResponseHandle - val reader = handle.extra.iterator().next() as ReadHandle - assertThat(reader.readAll(), array(equalTo("Hello, world!".toByteArray()))) - } + val handle = result[2] as HttpResponseHandle + val reader = handle.extra.iterator().next() as ReadHandle + assertThat(reader.readAll(), array(equalTo("Hello, world!".toByteArray()))) } } @@ -89,106 +97,96 @@ class TestHttpApi { @Test fun `Connects to websocket`() { - runServer { server -> - LuaTaskRunner.runTest { - val httpApi = addApi(createHttpApi(environment, server.port)) - val (url, websocket) = websocket(server, httpApi) + runWithServer { server -> + val httpApi = addApi(createHttpApi(environment, server.port)) + val (url, websocket) = websocket(server, httpApi) - websocket.send(Coerced(LuaValues.encode("Hello")), Optional.of(false)) + websocket.send(Coerced(LuaValues.encode("Hello")), Optional.of(false)) - val message = websocket.receive(Optional.empty()).await() - assertThat("Received a return message", message, array(equalTo("HELLO".toByteArray()), equalTo(false))) + val message = websocket.receive(Optional.empty()).await() + assertThat("Received a return message", message, array(equalTo("HELLO".toByteArray()), equalTo(false))) - websocket.close() + websocket.close() - val closeEvent = pullEvent() - assertThat("Received a close event", closeEvent, array(equalTo("websocket_closed"), equalTo(url), nullValue(), equalTo(1000))) + val closeEvent = pullEvent() + assertThat("Received a close event", closeEvent, array(equalTo("websocket_closed"), equalTo(url), nullValue(), equalTo(1000))) - assertFalse(server.lastRequest.headers.contains(HttpHeaderNames.ORIGIN), "HTTP Headers should not contain Origin") + assertFalse(server.lastRequest.headers.contains(HttpHeaderNames.ORIGIN), "HTTP Headers should not contain Origin") - Reference.reachabilityFence(websocket) - } + Reference.reachabilityFence(websocket) } } @Test fun `Errors if too many websocket messages are sent`() { - runServer { server -> - LuaTaskRunner.runTest { - val httpApi = addApi(createHttpApi(environment, server.port)) - val (_, websocket) = websocket(server, httpApi) + runWithServer { server -> + val httpApi = addApi(createHttpApi(environment, server.port)) + val (_, websocket) = websocket(server, httpApi) - val error = assertThrows { - for (i in 0 until 10_000) { - websocket.send(Coerced(LuaValues.encode("Hello")), Optional.of(false)) - } + val error = assertThrows { + for (i in 0 until 10_000) { + websocket.send(Coerced(LuaValues.encode("Hello")), Optional.of(false)) } - - websocket.close() - - assertThat(error.message, equalTo("Too many ongoing websocket messages")) } + + websocket.close() + + assertThat(error.message, equalTo("Too many ongoing websocket messages")) } } @Test fun `Closes if a websocket message is too large`() { - runServer { server -> - LuaTaskRunner.runTest { - val httpApi = addApi(createHttpApi(environment, server.port)) - val (url, websocket) = websocket(server, httpApi) + runWithServer { server -> + val httpApi = addApi(createHttpApi(environment, server.port)) + val (url, websocket) = websocket(server, httpApi) - val out = ByteArray(AddressRule.WEBSOCKET_MESSAGE + 1) - Random(0xDEADBEEF).nextBytes(out) - server.broadcast(BinaryWebSocketFrame(Unpooled.wrappedBuffer(out))) + val out = ByteArray(AddressRule.WEBSOCKET_MESSAGE + 1) + Random(0xDEADBEEF).nextBytes(out) + server.broadcast(BinaryWebSocketFrame(Unpooled.wrappedBuffer(out))) - val closeEvent = pullEvent() - assertThat(closeEvent, array(equalTo("websocket_closed"), equalTo(url), equalTo("Received a too-large message"), nullValue())) + val closeEvent = pullEvent() + assertThat(closeEvent, array(equalTo("websocket_closed"), equalTo(url), equalTo("Received a too-large message"), nullValue())) - Reference.reachabilityFence(websocket) - } + Reference.reachabilityFence(websocket) } } @Test fun `Queues an event when the socket is externally closed`() { - runServer { server -> - LuaTaskRunner.runTest { - val httpApi = addApi(createHttpApi(environment, server.port)) - val (url, websocket) = websocket(server, httpApi) + runWithServer { server -> + val httpApi = addApi(createHttpApi(environment, server.port)) + val (url, websocket) = websocket(server, httpApi) - server.stop() + server.stop() - val closeEvent = pullEvent("websocket_closed") - assertThat( - "Websocket was closed", - closeEvent, - array(equalTo("websocket_closed"), equalTo(url), equalTo("Connection closed"), equalTo(null)), - ) + val closeEvent = pullEvent("websocket_closed") + assertThat( + "Websocket was closed", + closeEvent, + array(equalTo("websocket_closed"), equalTo(url), equalTo("Connection closed"), equalTo(null)), + ) - assertThrows("Throws an exception when sending") { - websocket.send(Coerced(LuaValues.encode("hello")), Optional.of(false)) - } - - Reference.reachabilityFence(websocket) + assertThrows("Throws an exception when sending") { + websocket.send(Coerced(LuaValues.encode("hello")), Optional.of(false)) } + + Reference.reachabilityFence(websocket) } } @Test fun `Closes websocket if the server doesn't respond`() { - runServer(closeWebsocket = false) { server -> - LuaTaskRunner.runTest(timeout = 10.seconds) { - val httpApi = addApi(createHttpApi(environment, server.port)) - val (url, websocket) = websocket(server, httpApi) + runWithServer(closeWebsocket = false) { server -> + val httpApi = addApi(createHttpApi(environment, server.port)) + val (url, websocket) = websocket(server, httpApi) - websocket.close() + websocket.close() - val closeEvent = pullEvent() - assertThat("Received a close event", closeEvent, array(equalTo("websocket_closed"), equalTo(url), nullValue(), equalTo(1006))) + val closeEvent = pullEvent() + assertThat("Received a close event", closeEvent, array(equalTo("websocket_closed"), equalTo(url), nullValue(), equalTo(1006))) - Reference.reachabilityFence(websocket) - } + Reference.reachabilityFence(websocket) } } @@ -214,36 +212,34 @@ class TestHttpApi { return ByteBuffer.wrap(message).asReadOnlyBuffer() } - runServer { server -> - LuaTaskRunner.runTest(timeout = 10.seconds) { - val httpApi = addApi(createHttpApi(environment, server.port, config)) - val (_, websocket) = websocket(server, httpApi) + runWithServer { server -> + val httpApi = addApi(createHttpApi(environment, server.port, config)) + val (_, websocket) = websocket(server, httpApi) - // Send/receive one message before timing. Read bandwidth limits get applied retroactively (we disable - // auto-read after hitting the limit), so we need to ensure that happens first. + // Send/receive one message before timing. Read bandwidth limits get applied retroactively (we disable + // auto-read after hitting the limit), so we need to ensure that happens first. + websocket.send(Coerced(randomMessage()), Optional.of(true)) + websocket.receive(Optional.empty()).await() + + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2) + do { + val sent = System.nanoTime() websocket.send(Coerced(randomMessage()), Optional.of(true)) websocket.receive(Optional.empty()).await() + val clientReceived = System.nanoTime() + val serverReceived = server.lastRequest.lastMessageTime - val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2) - do { - val sent = System.nanoTime() - websocket.send(Coerced(randomMessage()), Optional.of(true)) - websocket.receive(Optional.empty()).await() - val clientReceived = System.nanoTime() - val serverReceived = server.lastRequest.lastMessageTime + count += 1 + send += (serverReceived - sent).nanoseconds + receive += (clientReceived - serverReceived).nanoseconds + } while (sent < deadline) - count += 1 - send += Duration.ofNanos(serverReceived - sent) - receive += Duration.ofNanos(clientReceived - serverReceived) - } while (sent < deadline) - - websocket.close() - } + websocket.close() } assertThat("Ran multiple tests", count, greaterThan(1)) - return DelayStats(send = send.dividedBy(count.toLong()), receive = receive.dividedBy(count.toLong())) + return DelayStats(send = send / count, receive = receive / count) } @Test @@ -288,9 +284,9 @@ class TestHttpApi { private const val BANDWIDTH_SCALE: Int = 2 /** The duration when we've not hit the bandwidth limit and the message is sent "instantly". */ - private val INSTANT_DURATION: Duration = Duration.ofMillis(50) + private val INSTANT_DURATION: Duration = 50.milliseconds /** The duration when we've hit the bandwidth limit. */ - private val LIMITED_DURATION: Duration = Duration.ofMillis(800).dividedBy(BANDWIDTH_SCALE.toLong()) + private val LIMITED_DURATION: Duration = 800.milliseconds / BANDWIDTH_SCALE } } diff --git a/projects/core/src/test/resources/junit-platform.properties b/projects/core/src/test/resources/junit-platform.properties index 527dde601..3f398218d 100644 --- a/projects/core/src/test/resources/junit-platform.properties +++ b/projects/core/src/test/resources/junit-platform.properties @@ -3,3 +3,4 @@ # SPDX-License-Identifier: CC0-1.0 junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent +junit.jupiter.execution.parallel.config.dynamic.factor=0.5