Skip to content

Commit

Permalink
chore: upgrade sttp-client to 4 (#1718)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugo-vrijswijk authored Mar 10, 2025
1 parent 27a9b4a commit 8a6aa0a
Show file tree
Hide file tree
Showing 11 changed files with 55 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.syntax.option.*
import ciris.ConfigValue
import fs2.io.file.Path
import stryker4s.config.*
import sttp.client3.UriContext
import sttp.client4.UriContext
import sttp.model.Uri

import scala.concurrent.duration.*
Expand Down
16 changes: 10 additions & 6 deletions modules/core/src/main/scala/stryker4s/log/SttpLogWrapper.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package stryker4s.log

import cats.effect.IO
import sttp.client3.logging as sttp
import sttp.client4.logging as sttp

/** Wraps a `stryker4s.log.Logger` to a sttp Logger
*/
class SttpLogWrapper(implicit log: Logger) extends sttp.Logger[IO] {

override def apply(level: sttp.LogLevel, message: => String): IO[Unit] =
IO.delay(log.log(toLevel(level), message))

override def apply(level: sttp.LogLevel, message: => String, t: Throwable): IO[Unit] =
IO.delay(log.log(toLevel(level), message, t))
override def apply(
level: sttp.LogLevel,
message: => String,
exception: Option[Throwable],
context: Map[String, Any]
): IO[Unit] = exception match {
case None => IO.delay(log.log(toLevel(level), message))
case Some(t) => IO.delay(log.log(toLevel(level), message, t))
}

def toLevel: sttp.LogLevel => Level = {
case sttp.LogLevel.Trace => Level.Debug
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import cats.effect.{IO, Resource}
import cats.syntax.foldable.*
import fansi.Color.Red
import fansi.{Bold, Str}
import io.circe.Error
import mutationtesting.{MetricsResult, MutationTestResult}
import stryker4s.config.codec.CirceConfigEncoder
import stryker4s.config.{Config, Full, MutationScoreOnly}
import stryker4s.log.Logger
import stryker4s.report.dashboard.DashboardConfigProvider
import stryker4s.report.model.*
import sttp.client3.*
import sttp.client3.circe.{asJson, circeBodySerializer}
import sttp.client4.*
import sttp.client4.ResponseException.{DeserializationException, UnexpectedStatusCode}
import sttp.client4.circe.*
import sttp.model.{MediaType, StatusCode}

class DashboardReporter(dashboardConfigProvider: DashboardConfigProvider[IO])(implicit
log: Logger,
httpBackend: Resource[IO, SttpBackend[IO, Any]]
httpBackend: Resource[IO, Backend[IO]]
) extends Reporter
with CirceConfigEncoder {

Expand Down Expand Up @@ -49,26 +49,26 @@ class DashboardReporter(dashboardConfigProvider: DashboardConfigProvider[IO])(im
case Full =>
import mutationtesting.circe.*
request
.body(report)
.body(asJson(report))
case MutationScoreOnly =>
implicit val encoder: Encoder[ScoreOnlyReport] = Encoder.forProduct1("mutationScore")(r => r.mutationScore)
request
.body(ScoreOnlyReport(metrics.mutationScore))
.body(asJson(ScoreOnlyReport(metrics.mutationScore)))
}
}

private def logResponse(response: Response[Either[ResponseException[String, Error], DashboardPutResult]]): Unit =
private def logResponse(response: Response[Either[ResponseException[String], DashboardPutResult]]): Unit =
response.body match {
case Left(HttpError(errorBody, StatusCode.Unauthorized)) =>
case Left(UnexpectedStatusCode(errorBody, meta)) if meta.code == StatusCode.Unauthorized =>
log.error(
s"Error HTTP PUT '$errorBody'. Status code ${Red("401 Unauthorized")}. Did you provide the correct api key in the '${Bold
.On("STRYKER_DASHBOARD_API_KEY")}' environment variable?"
)
case Left(HttpError(errorBody, statusCode)) =>
case Left(UnexpectedStatusCode(errorBody, statusCode)) =>
log.error(
s"Failed to PUT report to dashboard. Response status code: ${Red(statusCode.code.toString())}. Response body: '$errorBody'"
)
case Left(DeserializationException(original, error)) =>
case Left(DeserializationException(original, error, _)) =>
log.warn(
s"Dashboard report was sent successfully, but could not decode the response: '$original'. Error:",
error
Expand Down
14 changes: 8 additions & 6 deletions modules/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import stryker4s.mutants.{Mutator, TreeTraverserImpl}
import stryker4s.report.*
import stryker4s.report.dashboard.DashboardConfigProvider
import stryker4s.run.threshold.{ScoreStatus, SuccessStatus}
import sttp.client3.SttpBackend
import sttp.client3.httpclient.fs2.HttpClientFs2Backend
import sttp.client3.logging.LoggingBackend
import sttp.client4.Backend
import sttp.client4.httpclient.fs2.HttpClientFs2Backend
import sttp.client4.logging.{LogConfig, LoggingBackend}
import sttp.model.HeaderNames

abstract class Stryker4sRunner(implicit log: Logger) {
Expand Down Expand Up @@ -60,7 +60,7 @@ abstract class Stryker4sRunner(implicit log: Logger) {
case Html => new HtmlReporter(new DiskFileIO(), new DesktopFileIO())
case Json => new JsonReporter(new DiskFileIO())
case Dashboard =>
implicit val httpBackend: Resource[IO, SttpBackend[IO, Any]] =
implicit val httpBackend: Resource[IO, Backend[IO]] =
// Catch if the user runs the dashboard on Java <11
try
HttpClientFs2Backend
Expand All @@ -69,8 +69,10 @@ abstract class Stryker4sRunner(implicit log: Logger) {
LoggingBackend(
_,
new SttpLogWrapper(),
logResponseBody = true,
sensitiveHeaders = HeaderNames.SensitiveHeaders + "X-Api-Key"
LogConfig(
logResponseBody = true,
sensitiveHeaders = HeaderNames.SensitiveHeaders + "X-Api-Key"
)
)
)
catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.syntax.all.*
import ciris.{ConfigDecoder, ConfigKey}
import stryker4s.config.*
import stryker4s.testkit.Stryker4sSuite
import sttp.client3.*
import sttp.client4.UriContext

import scala.concurrent.duration.*
import scala.meta.dialects.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import fs2.io.file.Path
import munit.Location
import stryker4s.config.*
import stryker4s.testkit.Stryker4sIOSuite
import sttp.client3.*
import sttp.client4.UriContext

import scala.concurrent.duration.*
import scala.meta.dialects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import cats.syntax.all.*
import fs2.io.file.Path
import stryker4s.config.*
import stryker4s.testkit.Stryker4sIOSuite
import sttp.client3.*
import sttp.client4.UriContext

import scala.concurrent.duration.*
import scala.meta.dialects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import stryker4s.config.codec.Hocon
import stryker4s.config.{ExcludedMutation, Html, MutationScoreOnly}
import stryker4s.testkit.{FileUtil, LogMatchers, Stryker4sIOSuite}
import stryker4s.testutil.ExampleConfigs
import sttp.client3.*
import sttp.client4.UriContext

import scala.concurrent.duration.*
import scala.meta.dialects.*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package stryker4s.report

import cats.data.NonEmptyChain
import cats.effect.{IO, Resource}
import cats.effect.IO
import cats.syntax.all.*
import fansi.Bold
import fansi.Color.Red
import fs2.io.file.Path
import io.circe.Json
import io.circe.syntax.*
import mutationtesting.*
import mutationtesting.circe.*
import stryker4s.config.codec.CirceConfigEncoder
import stryker4s.config.{Full, MutationScoreOnly}
import stryker4s.report.model.{DashboardConfig, DashboardPutResult}
import stryker4s.report.model.DashboardConfig
import stryker4s.testkit.{LogMatchers, Stryker4sIOSuite}
import stryker4s.testutil.stubs.DashboardConfigProviderStub
import sttp.client3.*
import sttp.client3.testing.SttpBackendStub
import sttp.model.{Header, MediaType, Method, StatusCode}
import sttp.client4.*
import sttp.model.{Header, MediaType, Method}

import scala.concurrent.duration.*

Expand All @@ -29,19 +31,17 @@ class DashboardReporterTest extends Stryker4sIOSuite with LogMatchers with Circe

val request = sut.buildRequest(dashConfig, report, metrics)
assertEquals(request.uri, uri"https://baseurl.com/api/reports/project/foo/version/bar")
val jsonBody = {
import mutationtesting.circe.*
import io.circe.syntax.*
report.asJson.noSpaces
}
val jsonBody = report.asJson.noSpaces

assertEquals(request.body, StringBody(jsonBody, "utf-8", MediaType.ApplicationJson))
assertEquals(request.method, Method.PUT)
assertSameElements(
request.headers,
List(
Header.acceptEncoding("gzip, deflate"),
new Header("X-Api-Key", "apiKeyHere"),
Header.contentType(MediaType.ApplicationJson)
Header.contentType(MediaType.ApplicationJson),
Header.contentLength(jsonBody.length().toLong)
)
)
}
Expand Down Expand Up @@ -76,7 +76,7 @@ class DashboardReporterTest extends Stryker4sIOSuite with LogMatchers with Circe
test("should send the request") {
implicit val backend = backendStub.map(
_.whenAnyRequest
.thenRespond(Right(DashboardPutResult("https://hrefHere.com")))
.thenRespondAdjust(Json.obj(("href", Json.fromString("https://hrefHere.com"))).noSpaces)
)
val dashConfigProvider = DashboardConfigProviderStub(baseDashConfig)
val sut = new DashboardReporter(dashConfigProvider)
Expand Down Expand Up @@ -105,7 +105,7 @@ class DashboardReporterTest extends Stryker4sIOSuite with LogMatchers with Circe
}

test("should log when a response can't be parsed to a href") {
implicit val backend = backendStub.map(_.whenAnyRequest.thenRespond("some other response"))
implicit val backend = backendStub.map(_.whenAnyRequest.thenRespondAdjust("some other response"))
val dashConfigProvider = DashboardConfigProviderStub(baseDashConfig)
val sut = new DashboardReporter(dashConfigProvider)
val runReport = baseResults
Expand All @@ -122,7 +122,7 @@ class DashboardReporterTest extends Stryker4sIOSuite with LogMatchers with Circe
test("should log when a 401 is returned by the API") {
implicit val backend = backendStub.map(
_.whenAnyRequest
.thenRespond(Response(Left(HttpError("auth required", StatusCode.Unauthorized)), StatusCode.Unauthorized))
.thenRespondUnauthorized()
)
val dashConfigProvider = DashboardConfigProviderStub(baseDashConfig)
val sut = new DashboardReporter(dashConfigProvider)
Expand All @@ -132,7 +132,7 @@ class DashboardReporterTest extends Stryker4sIOSuite with LogMatchers with Circe
.onRunFinished(runReport)
.asserting { _ =>
assertLoggedError(
s"Error HTTP PUT 'auth required'. Status code ${Red("401 Unauthorized")}. Did you provide the correct api key in the '${Bold
s"Error HTTP PUT 'Unauthorized'. Status code ${Red("401 Unauthorized")}. Did you provide the correct api key in the '${Bold
.On("STRYKER_DASHBOARD_API_KEY")}' environment variable?"
)
}
Expand All @@ -141,9 +141,7 @@ class DashboardReporterTest extends Stryker4sIOSuite with LogMatchers with Circe
test("should log when a error code is returned by the API") {
implicit val backend =
backendStub.map(
_.whenAnyRequest.thenRespond(
Response(Left(HttpError("internal error", StatusCode.InternalServerError)), StatusCode.InternalServerError)
)
_.whenAnyRequest.thenRespondServerError()
)
val dashConfigProvider = DashboardConfigProviderStub(baseDashConfig)
val sut = new DashboardReporter(dashConfigProvider)
Expand All @@ -153,14 +151,14 @@ class DashboardReporterTest extends Stryker4sIOSuite with LogMatchers with Circe
.onRunFinished(runReport)
.asserting { _ =>
assertLoggedError(
s"Failed to PUT report to dashboard. Response status code: ${Red("500")}. Response body: 'internal error'"
s"Failed to PUT report to dashboard. Response status code: ${Red("500")}. Response body: 'Internal Server Error'"
)
}
}
}

def backendStub =
Resource.pure[IO, SttpBackendStub[IO, Any]](sttp.client3.httpclient.fs2.HttpClientFs2Backend.stub[IO])
sttp.client4.httpclient.fs2.HttpClientFs2Backend.stub[IO].pure[IO].toResource

def baseResults = {
val files =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import stryker4s.config.{Config, Full, MutationScoreOnly}
import stryker4s.report.model.DashboardConfig
import stryker4s.testkit.Stryker4sSuite
import stryker4s.testutil.stubs.EnvStub
import sttp.client3.UriContext
import sttp.client4.UriContext

class DashboardConfigProviderTest extends Stryker4sSuite {

Expand Down
6 changes: 3 additions & 3 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object Dependencies {

val slf4j = "2.0.17"

val sttp = "3.10.3"
val sttp = "4.0.0-RC1"

val testInterface = "1.0"

Expand Down Expand Up @@ -72,8 +72,8 @@ object Dependencies {
val scalapbRuntime =
"com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf"
val slf4j = "org.slf4j" % "slf4j-simple" % versions.slf4j
val sttpCirce = "com.softwaremill.sttp.client3" %% "circe" % versions.sttp
val sttpFs2Backend = "com.softwaremill.sttp.client3" %% "fs2" % versions.sttp
val sttpCirce = "com.softwaremill.sttp.client4" %% "circe" % versions.sttp
val sttpFs2Backend = "com.softwaremill.sttp.client4" %% "fs2" % versions.sttp
val testInterface = "org.scala-sbt" % "test-interface" % versions.testInterface
val weaponRegeX = "io.stryker-mutator" %% "weapon-regex" % versions.weaponRegeX

Expand Down

0 comments on commit 8a6aa0a

Please sign in to comment.