Try to make HTTP tests a bit more robust

These unfortunately rely on the limiting/measuring real time, so can be
a little flakey if the CPU is overloaded.

 - Increase timeout of tests. We add a new runWithServer helper to make
   this a bit easier to do.
 - Reduce parallelism of junit tests. Not sure how much this helps —
   Gradle will still try to do other things — but hopefully a bit.
This commit is contained in:
Jonathan Coates
2026-10-16 17:21:01 +01:00
parent 832800babe
commit 2eec02fd64
2 changed files with 94 additions and 97 deletions
@@ -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<LuaException> {
for (i in 0 until 10_000) {
websocket.send(Coerced(LuaValues.encode("Hello")), Optional.of(false))
}
val error = assertThrows<LuaException> {
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<LuaException>("Throws an exception when sending") {
websocket.send(Coerced(LuaValues.encode("hello")), Optional.of(false))
}
Reference.reachabilityFence(websocket)
assertThrows<LuaException>("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
}
}
@@ -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