I am having issues testing my first ZIO+HTTP4S application. The test hangs and does not finish.
The code for my App (simplified) is
object Main extends App {
def server: ZIO[AppEnvironment, Throwable, Unit] =
for {
(...)
fullApp <- ZIO.runtime[AppEnvironment].flatMap { implicit rts =>
BlazeServerBuilder[AppTask](ec)
.bindHttp(api.port, api.endpoint)
.withHttpApp(CORS(httpApp))
.serve
.compile[AppTask, AppTask, CatsExitCode]
.drain
}
} yield fullApp
override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = {
server.provideSomeLayer[ZEnv](appEnvironment).tapError(err => putStrLn(s"Execution failed with: $err")).exitCode
}
}
This is my test code. Note that it is basically copypasted from THIS OTHER STACKOVERFLOW QUESTION
object ApiTest extends DefaultRunnableSpec {
val ec: ExecutionContext = ExecutionContext.global
def httpServer = Main.run(List()).forkManaged.toLayer
val clockDuration = ofSeconds(1)
val blocker = Blocker.liftExecutionContext(ec)
//did the httpserver start listening on 8080?
private def isLocalPortInUse(port: Int): ZIO[Clock, Throwable, Unit] = {
IO.effect {
println("checking for local port in use")
new Socket("0.0.0.0", port).close()
}
.retry(Schedule.linear(clockDuration) && Schedule.recurs(10))
}
override def spec: ZSpec[Environment, Failure] =
suite("MainTest")(
testM("Health check") {
for {
_ <- TestClock.adjust(clockDuration).fork
_ = println("1")
_ <- isLocalPortInUse(8080)
_ = println("2")
client <- Task(JavaNetClientBuilder[Task](blocker).create)
_ = println("3")
response <- client.expect[String]("http://localhost:8080/healthcheck")
_ = println("4")
} yield assert(response)(equalTo(""))
}
).provideCustomLayerShared(httpServer)
}
The problem is that as soon as the server starts, the test stops running and are not executed. The output is
1
checking for local port in use
checking for local port in use
<server init message>
In Suite "MainTest", test "Health check" has taken more than 1 m to execute. If this is not expected, consider using TestAspect.timeout to timeout runaway tests for faster diagnostics.
So as you can see, the tests run OK until the server starts and then does not continue the execution.
Also, how could I perform a POST call instead of a GET one? I'm a bit lost in the HTTP4S/ZIO ecosystem with http4sClients, ZHTTP, BlazeClientBuilders and the like. What would be the easiest way of doing a POST call to my server in a test like the previous one?
Cheers!
edit: I’ve checked that the server works fine while hanging in here, I can do CURL calls from a terminal and it responds correctly. So it seems clear that the problem is that as soon as the server is up it stays in the front, not background, and the tests do not have the chance to finish the execution.
You are advancing the clock by 1 second but your application might require more time to run. Also, your particular test will require infinite time to run because, while unit tests are instantaneous in ZIO, integration tests are not.
Advancing the time of a unit test by 1 second requires theoretically 0 seconds. This might not be enough for the port to become free.
Since you are trying to create an integration test, you should use a real Clock and not the one provided by the test kit.
Related
I am trying to figure out the idiom for writing an integration test for an Http4s app that supports two end points.
I'm starting the Main app class in a ZManaged by forking it on a new fiber and then doing interruptFork on release of the ZManaged.
I then convert this to a ZLayer and pass it via provideCustomLayerShared() on the whole suite that has multiple testMs .
Am I on the right track here?
It doesn't behave as I expect it to :
Although the httpserver managed in the way mentioned is provided to the suite that encloses both the tests, it is released after the first test and thus the second test fails
The test suite never finishes and just hangs after executing both tests
Apologies for the half baked nature of the code below.
object MainTest extends DefaultRunnableSpec {
def httpServer =
ZManaged
.make(Main.run(List()).fork)(fiber => {
//fiber.join or Fiber.interrupt will not work here, hangs the test
fiber.interruptFork.map(
ex => println(s"stopped with exitCode: $ex")
)
})
.toLayer
val clockDuration = 1.second
//did the httpserver start listening on 8080?
private def isLocalPortInUse(port: Int): ZIO[Clock, Throwable, Unit] = {
IO.effect(new Socket("0.0.0.0", port).close()).retry(Schedule.exponential(clockDuration) && Schedule.recurs(10))
}
override def spec: ZSpec[Environment, Failure] =
suite("MainTest")(
testM("Health check") {
for {
_ <- TestClock.adjust(clockDuration).fork
_ <- isLocalPortInUse(8080)
client <- Task(JavaNetClientBuilder[Task](blocker).create)
response <- client.expect[HealthReplyDTO]("http://localhost:8080/health")
expected = HealthReplyDTO("OK")
} yield assert(response) {
equalTo(expected)
}
},
testM("Distances endpoint check") {
for {
_ <- TestClock.adjust(clockDuration).fork
_ <- isLocalPortInUse(8080)
client <- Task(JavaNetClientBuilder[Task](blocker).create)
response <- client.expect[DistanceReplyDTO](
Request[Task](method = Method.GET, uri = uri"http://localhost:8080/distances")
.withEntity(DistanceRequestDTO(List("JFK", "LHR")))
)
expected = DistanceReplyDTO(5000)
} yield assert(response) {
equalTo(expected)
}
}
).provideCustomLayerShared(httpServer)
}
Output of the test is that the second test fails while the first succeeds.
And I debugged enough to see that the HTTPServer is already brought down before the second test.
stopped with exitCode: ()
- MainTest
+ Health check
- Distances endpoint check
Fiber failed.
A checked error was not handled.
org.http4s.client.UnexpectedStatus: unexpected HTTP status: 404 Not Found
And whether I run the tests from Intellij on sbt testOnly, the test process keeps hung after all this and I have to manually terminate it.
I think there are two things here:
ZManaged and acquire
The first parameter of ZManaged.make is the acquire function which creates the resource. The problem is that resource acquisition (as well as releasing them) is done uninterruptible. And whenever you do a .fork, the forked fiber inherits its interruptibility from its parent fiber. So the Main.run() part can actually never be interrupted.
Why does it seem to work when you do fiber.interruptFork? interruptFork doesn't actually wait for the fiber to be interrupted. Only interrupt will do that, which is why it will hang the test.
Luckily there is a method which will do exactly what you want: Main.run(List()).forkManaged. This will generate a ZManaged which will start the main function and interrupt it when the resource is released.
Here is some code which demonstrates the problem nicely:
import zio._
import zio.console._
import zio.duration._
object Main extends App {
override def run(args: List[String]): URIO[ZEnv, ExitCode] = for {
// interrupting after normal fork
fiberNormal <- liveASecond("normal").fork
_ <- fiberNormal.interrupt
// forking in acquire, interrupting in relase
_ <- ZManaged.make(liveASecond("acquire").fork)(fiber => fiber.interrupt).use(_ => ZIO.unit)
// fork into a zmanaged
_ <- liveASecond("forkManaged").forkManaged.use(_ => ZIO.unit)
_ <- ZIO.sleep(5.seconds)
} yield ExitCode.success
def liveASecond(name: String) = (for {
_ <- putStrLn(s"born: $name")
_ <- ZIO.sleep(1.seconds)
_ <- putStrLn(s"lived one second: $name")
_ <- putStrLn(s"died: $name")
} yield ()).onInterrupt(putStrLn(s"interrupted: $name"))
}
This will give the output:
born: normal
interrupted: normal
born: acquire
lived one second: acquire
died: acquire
born: forkManaged
interrupted: forkManaged
As you can see, both normal as well as forkManaged get interrupted immediately. But the one forked within acquire runs to completion.
The second test
The second test seems to fail not because the server is down, but because the server seems to be missing the "distances" route on the http4s side. I noticed that you get a 404, which is a HTTP Status Code. If the server were down, you would probably get something like Connection Refused. When you get a 404, some HTTP server is actually answering.
So here my guess would be that the route really is missing. Maybe check for typos in the route definition or maybe the route is just not composed into the main route.
In the end #felher 's Main.run(List()).forkManaged helped solve the first problem.
The second problem about the GET with a body being rejected from inside the integration test was worked-around by changing the method to POST. I did not look further into why the GET was being rejected from inside the test but not when done with a normal curl to the running app.
I'm using http4s BlazeServer 0.21, how can I graceful shutdown? I want to reject all upcoming requests, and keep process unfinished requests and response back, within a hard shutdown time.
I tried starting server with serveWhile and set a shutdownHook SignallingRef. The server stream & middleware defer as expected (so our metrics & log middleware still log this response)
//serverStream
for {
signal <- fs2.Stream.eval(SignallingRef[F, Boolean](false))
exitCode <- fs2.Stream.eval(Ref[F].of(ExitCode.Success))
_ <- fs2.Stream.eval(shutdown(signal))
server <- BlazeServerBuilder[F]
.bindHttp(8080, "0.0.0.0")
.withHttpApp(httpApp)
.serveWhile(signal, exitCode)
} yield server
def shutdown[F[_]: Effect](interrupter: SignallingRef[F, Boolean]): F[Unit] = {
LiftIO[F].liftIO(IO {
sys.addShutdownHook {
...
interrupter.set(true)
}
})
}
object Server extends IOApp {
def run(args: List[String]): IO[ExitCode] =
serverStream[IO].compile.drain.as(ExitCode.Success)
}
but the http server doesn't work as I expect, seems like http4s's internal ServerChannel has its own shutdownHook and cancel all the responses already.
any suggestion/workaround? or maybe just a way to hold and don't kill requests for x seconds is also appreciated.
The server is hooked for SIGTERM as a matter of convenience.
As a convenience, cats-effect provides an cats.effect.IOApp trait with
an abstract run method that returns a IO[ExitCode]. An IOApp runs the
process and adds a JVM shutdown hook to interrupt the infinite process
and gracefully shut down your server when a SIGTERM is received.
And if you want to shutdown using an URL e.g. http://localhost:8080/ops/shutdown/true, it works too.
The code is pretty straight forward,
class SysOpsEndpoints[F[_]: Sync](signal: SignallingRef[F, Boolean]) extends Http4sDsl[F] {
private def shutdown: HttpRoutes[F] =
HttpRoutes.of[F] {
case GET -> Root / "shutdown" / shutdown =>
for {
_ <- signal.set(Try(shutdown.toBoolean).getOrElse(false))
result <- Ok(s"Shutdown: $shutdown")
} yield result
}
}
object SysOpsEndpoints {
def endpoints[F[_]: Sync](signal: SignallingRef[F, Boolean]): HttpRoutes[F] =
new SysOpsEndpoints(signal).shutdown
}
and the setup for the server for-comprehension is similar to yours,
for {
signal <- fs2.Stream.eval(SignallingRef[F, Boolean](false))
exitCode <- fs2.Stream.eval(Ref[F].of(ExitCode.Success))
httpApp = Router(
"/ops" -> SysOpsEndpoints.endpoints(signal)
).orNotFound
server <- BlazeServerBuilder[F](serverEc)
.bindHttp(8080, "0.0.0.0")
.withHttpApp(httpApp)
.serveWhile(signal, exitCode)
} yield server
The Scenario
In an application I am currently writing I am using cats-effect's IO monad in an IOApp.
If started with a command line argument 'debug', I am delegeting my program flow into a debug loop that waits for user input and executes all kinds of debugging-relevant methods. As soon as the developer presses enter without any input, the application will exit the debug loop and exit the main method, thus closing the application.
The main method of this application looks roughly like this:
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor}
import cats.effect.{ExitCode, IO, IOApp}
import cats.implicits._
object Main extends IOApp {
val BlockingFileIO: ExecutionContextExecutor = ExecutionContext.fromExecutor(blockingIOCachedThreadPool)
def run(args: List[String]): IO[ExitCode] = for {
_ <- IO { println ("Running with args: " + args.mkString(","))}
debug = args.contains("debug")
// do all kinds of other stuff like initializing a webserver, file IO etc.
// ...
_ <- if(debug) debugLoop else IO.unit
} yield ExitCode.Success
def debugLoop: IO[Unit] = for {
_ <- IO(println("Debug mode: exit application be pressing ENTER."))
_ <- IO.shift(BlockingFileIO) // readLine might block for a long time so we shift to another thread
input <- IO(StdIn.readLine()) // let it run until user presses return
_ <- IO.shift(ExecutionContext.global) // shift back to main thread
_ <- if(input == "b") {
// do some debug relevant stuff
IO(Unit) >> debugLoop
} else {
shutDown()
}
} yield Unit
// shuts down everything
def shutDown(): IO[Unit] = ???
}
Now, I want to test if e.g. my run method behaves like expected in my ScalaTests:
import org.scalatest.FlatSpec
class MainSpec extends FlatSpec{
"Main" should "enter the debug loop if args contain 'debug'" in {
val program: IO[ExitCode] = Main.run("debug" :: Nil)
// is there some way I can 'search through the IO monad' and determine if my program contains the statements from the debug loop?
}
}
My Question
Can I somehow 'search/iterate through the IO monad' and determine if my program contains the statements from the debug loop? Do I have to call program.unsafeRunSync() on it to check that?
You could implement the logic of run inside your own method, and test that instead, where you aren't restricted in the return type and forward run to your own implementation. Since run forces your hand to IO[ExitCode], there's not much you can express from the return value. In general, there's no way to "search" an IO value as it just a value that describes a computation that has a side effect. If you want to inspect it's underlying value, you do so by running it in the end of the world (your main method), or for your tests, you unsafeRunSync it.
For example:
sealed trait RunResult extends Product with Serializable
case object Run extends RunResult
case object Debug extends RunResult
def run(args: List[String]): IO[ExitCode] = {
run0(args) >> IO.pure(ExitCode.Success)
}
def run0(args: List[String]): IO[RunResult] = {
for {
_ <- IO { println("Running with args: " + args.mkString(",")) }
debug = args.contains("debug")
runResult <- if (debug) debugLoop else IO.pure(Run)
} yield runResult
}
def debugLoop: IO[Debug.type] =
for {
_ <- IO(println("Debug mode: exit application be pressing ENTER."))
_ <- IO.shift(BlockingFileIO) // readLine might block for a long time so we shift to another thread
input <- IO(StdIn.readLine()) // let it run until user presses return
_ <- IO.shift(ExecutionContext.global) // shift back to main thread
_ <- if (input == "b") {
// do some debug relevant stuff
IO(Unit) >> debugLoop
} else {
shutDown()
}
} yield Debug
// shuts down everything
def shutDown(): IO[Unit] = ???
}
And then in your test:
import org.scalatest.FlatSpec
class MainSpec extends FlatSpec {
"Main" should "enter the debug loop if args contain 'debug'" in {
val program: IO[RunResult] = Main.run0("debug" :: Nil)
program.unsafeRunSync() match {
case Debug => // do stuff
case Run => // other stuff
}
}
}
To search through some monad expression, it would have to be values, not statements, aka reified. That is the core idea behind the (in)famous Free monad. If you were to go through the hassle of expressing your code in some "algebra" as they call (think DSL) it and lift it into monad expression nesting via Free, then yes you would be able to search through it. There are plenty of resources that explain Free monads better than I could google is your friend here.
My general suggestion would be that the general principles of good testing apply everywhere. Isolate the side-effecting part and inject it into the main piece of logic, so that you can inject a fake implementation in testing to allow all sorts of assertions.
Extending StreamApp asks you to provide the stream def. It has a requestShutdown parameter.
def stream(args: List[String], requestShutdown: F[Unit]): Stream[F, ExitCode]
I provide the implementation for this and understand that args is passed in as command line arguments. I'm unsure however, what supplies the requestShutdown parameter and what I can do with it.
Specifically, I'd like to invoke a graceful shutdown on a Stream[IO, ExitCode] which is starting a Http4s server (which blocks forever).
It looks like a Signal is needed and must be set? The underlying stream that I'm trying to 'get at' looks like this:
for {
scheduler <- Scheduler[IO](corePoolSize = 1)
exitCode <- BlazeBuilder[IO]
.bindHttp(port, "0.0.0.0")
.mountService(services(scheduler), "/")
.serve
} yield exitCode
My stream def is here and StreamAppSpec from the fs2 project has something in the StreamAppSpec but I can't work out how I'd adapt it.
You can think of the requestShutdown parameter that is supplied to the stream function as meaning an action that, when executed, will request the termination of the program.
Executing it will consequently result in it ending the program.
Here is an example use:
override def stream(args: List[String], requestShutdown: IO[Unit]): Stream[IO, ExitCode] =
for {
scheduler <- Scheduler[IO](corePoolSize = 1)
exitStream = scheduler.sleep[IO](10 seconds)
.evalMap(_ => requestShutdown)
.map(_ => ExitCode.Success)
serverStream = BlazeBuilder[IO]
.bindHttp(port, "0.0.0.0")
.mountService(services(scheduler), "/")
.serve
result <- Stream.emits(List(exitStream, serverStream)).joinUnbounded
} yield result
In this scenario, we create two streams:
The first will wait for 10 seconds before triggering the effect of
terminating the app.
The second will run the http4s server.
We then join these two streams so that they run concurrently meaning that the web server will run for 10 seconds before the other stream signals that the program should terminate.
I've got the following Play! 2 Action.async controller:
def complexAlgorithm(graphId: String) = Action.async { implicit request =>
val f = future {
val data = new ComplexAlgorithm(GraphContext.get(graphId))
data.evaluate
data
}
f.map { result => Ok(Json.generate(result.getRankings.toList.filterNot(o => o.rankScore == 0))) }
}
I've realized that in some cases this computation is actually going to take more than an hour. Since this will be redesigned for production use, I'm OK with waiting for the result in the browser since it's logged anyways.
So my question is does the future above val f get killed if the browser request is interrupted? Say for instance, if the internet disconnects while waiting for a response? Or will it actually complete its calculation (even if hours later)?
Thanks!