Skip to content

Commit 3668412

Browse files
adamwclaude
andcommitted
Make Vert.x test server Resource repeatable so the flaky-test retry works
The three Vert.x test interpreters created the HTTP server eagerly, outside the Resource.make acquire, capturing a single server instance. The flaky-WS- test retry (TestSuite.retries, added in #5410) re-uses the test's server Resource, which re-runs listen() on that same instance; Vert.x 5's CleanableHttpServer.listen throws IllegalStateException when listen() is called twice on one server ("Starting server failed because of null"). So every retry of a Vert.x test failed instantly on server startup, defeating the retry. Create the server inside the acquire instead, so each acquire (and thus each retry) gets a fresh server. Add ServerResourceRepeatableSpec, which fails on the old eager code and passes now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f31bde1 commit 3668412

4 files changed

Lines changed: 65 additions & 16 deletions

File tree

server/vertx-server/cats/src/test/scala/sttp/tapir/server/vertx/cats/CatsVertxTestServerInterpreter.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,17 @@ class CatsVertxTestServerInterpreter(vertx: Vertx, dispatcher: Dispatcher[IO])
3030
route: Router => Route,
3131
gracefulShutdownTimeout: Option[FiniteDuration]
3232
): Resource[IO, Port] = {
33-
val router = Router.router(vertx)
34-
route(router)
35-
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
36-
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
37-
val listenIO = ioFromVFuture(server.listen(0))
33+
// The server is created inside the Resource's acquire (not eagerly), so that the Resource can be safely re-run:
34+
// the flaky-WebSocket-test retry re-uses this Resource, and Vert.x's CleanableHttpServer.listen throws
35+
// IllegalStateException if listen() is called a second time on the same server instance.
36+
val listenIO = IO
37+
.delay {
38+
val router = Router.router(vertx)
39+
val _ = route(router)
40+
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
41+
vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
42+
}
43+
.flatMap(server => ioFromVFuture(server.listen(0)))
3844
// Vertx doesn't offer graceful shutdown with timeout OOTB
3945
Resource.make(listenIO)(s => ioFromVFuture(s.close).void).map(_.actualPort())
4046
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package sttp.tapir.server.vertx.cats
2+
3+
import cats.effect.IO
4+
import cats.effect.std.Dispatcher
5+
import cats.effect.unsafe.implicits.global
6+
import io.vertx.core.Vertx
7+
import io.vertx.ext.web.{Route, Router}
8+
import org.scalatest.funsuite.AnyFunSuite
9+
10+
// Guards that the per-test server Resource can be acquired more than once. The flaky-WebSocket-test retry
11+
// (TestSuite.retries) re-runs the whole test, hence re-uses this Resource; if the server were created eagerly
12+
// (outside the acquire) rather than inside it, the second listen() would hit Vert.x's CleanableHttpServer guard
13+
// and throw IllegalStateException, defeating the retry. See CatsVertxTestServerInterpreter.server.
14+
class ServerResourceRepeatableSpec extends AnyFunSuite {
15+
test("server Resource can be acquired twice without IllegalStateException") {
16+
val vertx = Vertx.vertx()
17+
try {
18+
Dispatcher
19+
.parallel[IO]
20+
.use { dispatcher =>
21+
val interpreter = new CatsVertxTestServerInterpreter(vertx, dispatcher)
22+
val trivialRoute: Router => Route = router => router.route()
23+
val res = interpreter.server(trivialRoute, None)
24+
// first use = original run; second use = the retry
25+
res.use(_ => IO.unit) >> res.use(port => IO(assert(port > 0)))
26+
}
27+
.unsafeRunSync()
28+
} finally { val _ = vertx.close() }
29+
}
30+
}

server/vertx-server/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,18 @@ class VertxTestServerInterpreter(vertx: Vertx)
2828
route: Router => Route,
2929
gracefulShutdownTimeout: Option[FiniteDuration]
3030
): Resource[IO, Port] = {
31-
val router = Router.router(vertx)
32-
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
33-
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
34-
val listenIO = vertxFutureToIo(server.listen(0))
35-
route(router)
31+
// The server is created inside the Resource's acquire (not eagerly), so that the Resource can be safely re-run:
32+
// the flaky-WebSocket-test retry re-uses this Resource, and Vert.x's CleanableHttpServer.listen throws
33+
// IllegalStateException if listen() is called a second time on the same server instance.
34+
val listenIO = IO
35+
.delay {
36+
val router = Router.router(vertx)
37+
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
38+
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
39+
val _ = route(router)
40+
server
41+
}
42+
.flatMap(server => vertxFutureToIo(server.listen(0)))
3643
// Vertx doesn't offer graceful shutdown with timeout OOTB
3744
Resource.make(listenIO)(s => vertxFutureToIo(s.close()).void).map(s => s.actualPort())
3845
}

server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxTestServerInterpreter.scala

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ class ZioVertxTestServerInterpreter(vertx: Vertx)
2727
route: Router => Route,
2828
gracefulShutdownTimeout: Option[FiniteDuration]
2929
): Resource[IO, Port] = {
30-
val router = Router.router(vertx)
31-
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
32-
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
33-
route(router)
34-
35-
val listenIO = VertxTestServerInterpreter.vertxFutureToIo(server.listen(0))
30+
// The server is created inside the Resource's acquire (not eagerly), so that the Resource can be safely re-run:
31+
// the flaky-WebSocket-test retry re-uses this Resource, and Vert.x's CleanableHttpServer.listen throws
32+
// IllegalStateException if listen() is called a second time on the same server instance.
33+
val listenIO = IO
34+
.delay {
35+
val router = Router.router(vertx)
36+
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
37+
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
38+
val _ = route(router)
39+
server
40+
}
41+
.flatMap(server => VertxTestServerInterpreter.vertxFutureToIo(server.listen(0)))
3642
// Vertx doesn't offer graceful shutdown with timeout OOTB
3743
Resource.make(listenIO)(server => VertxTestServerInterpreter.vertxFutureToIo(server.close).void).map(_.actualPort())
3844
}

0 commit comments

Comments
 (0)