Skip to content

Commit

Permalink
Refactor dashboard reporter and providers
Browse files Browse the repository at this point in the history
  • Loading branch information
hugo-vrijswijk committed Nov 24, 2019
1 parent 8a9b407 commit 90a5e19
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 147 deletions.
9 changes: 9 additions & 0 deletions core/src/main/scala/stryker4s/env/EnvProvider.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package stryker4s.env

trait Environment {
def getEnvVariable(key: String): Option[String]
}

object SystemEnvironment extends Environment {
override def getEnvVariable(key: String): Option[String] = sys.env.get(key)
}
28 changes: 0 additions & 28 deletions core/src/main/scala/stryker4s/http/webIO.scala

This file was deleted.

118 changes: 51 additions & 67 deletions core/src/main/scala/stryker4s/report/DashboardReporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,66 @@ package stryker4s.report

import grizzled.slf4j.Logging
import mutationtesting.{MetricsResult, MutationTestReport}
import scalaj.http.HttpResponse
import stryker4s.http.WebIO
import stryker4s.report.dashboard.Providers._
import stryker4s.report.dashboard.DashboardConfigProvider
import stryker4s.report.model._
import stryker4s.config.Config
import stryker4s.config.Full
import stryker4s.config.MutationScoreOnly
import io.circe.parser.decode
import io.circe.Decoder
class DashboardReporter(webIO: WebIO, ciEnvironment: Option[CiEnvironment])(implicit config: Config)
extends FinishedRunReporter
import sttp.client._
import sttp.client.circe._
import sttp.model.MediaType
import sttp.model.StatusCode

class DashboardReporter(dashboardConfigProvider: DashboardConfigProvider)(
implicit httpBackend: SttpBackend[Identity, Nothing, NothingT]
) extends FinishedRunReporter
with Logging {
def buildUrl: Option[String] =
for {
project <- config.dashboard.project.orElse(ciEnvironment.map(_.project))
version <- config.dashboard.version.orElse(ciEnvironment.map(_.version))
baseUrl = config.dashboard.baseUrl
url = s"$baseUrl/api/reports/$project/$version"
} yield config.dashboard.module match {
case Some(module) => s"$url?module=$module"
case None => url
override def reportRunFinished(report: MutationTestReport, metrics: MetricsResult): Unit =
dashboardConfigProvider.resolveConfig() match {
case Left(configKey) => warn(s"Could not resolve dashboard configuration key '$configKey', not sending report")
case Right(dashboardConfig) =>
val request = buildRequest(dashboardConfig, report, metrics)
val response = request.send()
logResponse(response)
}

def buildBody(report: MutationTestReport, metrics: MetricsResult): StrykerDashboardReport = {
config.dashboard.reportType match {
case Full => FullDashboardReport(report)
case MutationScoreOnly => ScoreOnlyReport(metrics.mutationScore)
def buildRequest(dashConfig: DashboardConfig, report: MutationTestReport, metrics: MetricsResult) = {
import io.circe.{Decoder, Encoder}
implicit val decoder: Decoder[DashboardPutResult] = Decoder.forProduct1("href")(DashboardPutResult.apply)
val uri =
uri"${dashConfig.baseUrl}/api/reports/${dashConfig.project}/${dashConfig.version}?module=${dashConfig.module}"
val request = basicRequest
.header("X-Api-Key", dashConfig.apiKey)
.response(asJson[DashboardPutResult])
.contentType(MediaType.ApplicationJson)
.put(uri)
dashConfig.reportType match {
case Full =>
import mutationtesting.MutationReportEncoder._
request
.body(report)
case MutationScoreOnly =>
implicit val encoder: Encoder[ScoreOnlyReport] = Encoder.forProduct1("mutationScore")(r => r.mutationScore)
request
.body(ScoreOnlyReport(metrics.mutationScore))
}
}

def writeReportToDashboard(url: String, body: StrykerDashboardReport, apiKey: String): HttpResponse[String] = {
webIO.putRequest(url, StrykerDashboardReport.toJson(body), Map("X-Api-Key" -> apiKey))
}

override def reportRunFinished(report: MutationTestReport, metrics: MetricsResult): Unit = {
buildUrl match {
case None => info("Could not resolve dashboard configuration, not sending report")
case Some(url) =>
val body = buildBody(report, metrics)
val response = writeReportToDashboard(url, body, ciEnvironment.get.apikey)

if (response.code == 200) {
if (config.dashboard.reportType == Full) {
implicit val decoder: Decoder[Href] = Decoder.forProduct1("href")(Href.apply)
val href = decode[Href](response.body) match {
case Left(error) => throw error
case Right(Href(value)) => value
}
info(s"Sent report to dashboard: $href")
} else {
info(s"Sent report to dashboard: $url")
}
} else {
error(s"Failed to send report to dashboard.")
error(s"Expected status code 200, but was ${response.code}. Body: '${response.body}'")
def logResponse(response: Response[Either[ResponseError[io.circe.Error], DashboardPutResult]]): Unit =
response.body match {
case Left(HttpError(errorBody)) =>
response.code match {
case StatusCode.Unauthorized =>
error(
s"Error HTTP PUT $errorBody. Unauthorized. Did you provide the correct api key in the 'STRYKER_DASHBOARD_API_KEY' environment variable?"
)
case statusCode =>
error(
s"Failed to PUT report to dashboard. Response status code: ${statusCode.code}. Response body: '${errorBody}'"
)
}
case Left(DeserializationError(original, error)) =>
warn(s"Dashboard report was sent successfully, but could not decode the response $original:", error)
case Right(href) =>
info(s"Sent report to dashboard. Available at $href")
}
}
}

object DashboardReporter {
def resolveCiEnvironment(): Option[CiEnvironment] =
tryResolveEnv(TravisProvider) orElse
tryResolveEnv(CircleProvider)

def tryResolveEnv(provider: CiProvider): Option[CiEnvironment] =
if (provider.isPullRequest) None
else
for {
apiKey <- provider.determineApiKey()
project <- provider.determineProject()
withGitHub = s"github.com/$project"
version <- provider.determineVersion()
} yield CiEnvironment(apiKey, withGitHub, version)
}

case class CiEnvironment(apikey: String, project: String, version: String)

case class Href(href: String)
15 changes: 9 additions & 6 deletions core/src/main/scala/stryker4s/report/Reporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import mutationtesting.{MetricsResult, MutationTestReport}
import stryker4s.config._
import stryker4s.files.DiskFileIO
import stryker4s.model.{Mutant, MutantRunResult}

import stryker4s.report.dashboard.DashboardConfigProvider
import scala.util.{Failure, Try}
import stryker4s.http.HttpClient
import sttp.client.HttpURLConnectionBackend
import stryker4s.env.SystemEnvironment

class Reporter(implicit config: Config) extends FinishedRunReporter with ProgressReporter with Logging {
lazy val reporters: Seq[MutationRunReporter] = config.reporters map {
case Console => new ConsoleReporter()
case Html => new HtmlReporter(DiskFileIO)
case Json => new JsonReporter(DiskFileIO)
case Dashboard => new DashboardReporter(HttpClient, DashboardReporter.resolveCiEnvironment())
case Console => new ConsoleReporter()
case Html => new HtmlReporter(DiskFileIO)
case Json => new JsonReporter(DiskFileIO)
case Dashboard =>
implicit val backend = HttpURLConnectionBackend()
new DashboardReporter(new DashboardConfigProvider(SystemEnvironment))
}

private[this] val progressReporters = reporters collect { case r: ProgressReporter => r }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package stryker4s.report.dashboard
import stryker4s.report.model.DashboardConfig
import stryker4s.config.Config
import stryker4s.report.dashboard.Providers._
import stryker4s.env.Environment

class DashboardConfigProvider(env: Environment)(implicit config: Config) {
def resolveConfig(): Either[String, DashboardConfig] =
for {
apiKey <- resolveapiKey()
project <- resolveproject()
version <- resolveversion()
baseUrl = config.dashboard.baseUrl
reportType = config.dashboard.reportType
module = config.dashboard.module
} yield DashboardConfig(
apiKey = apiKey,
project = project,
version = version,
baseUrl = baseUrl,
reportType = reportType,
module = module
)

private def resolveapiKey() =
env
.getEnvVariable("STRYKER_DASHBOARD_API_KEY")
.toRight("STRYKER_DASHBOARD_API_KEY")

private def resolveproject() =
config.dashboard.project
.orElse(byCiProvider(_.determineProject()))
.toRight("dashboard.project")

private def resolveversion() =
config.dashboard.version
.orElse(byCiProvider(_.determineVersion()))
.toRight("dashboard.version")

private def byCiProvider[T](f: CiProvider => Option[T])() = Providers.determineCiProvider(env).flatMap(f)
}

final case class DashboardConfigError(message: String)
46 changes: 25 additions & 21 deletions core/src/main/scala/stryker4s/report/dashboard/Providers.scala
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
package stryker4s.report.dashboard
import grizzled.slf4j.Logging
import stryker4s.env.Environment

object Providers extends Logging {
def determineCiProvider(env: Environment): Option[CiProvider] =
if (env.getEnvVariable("TRAVIS").isDefined) {
Some(new TravisProvider(env))
} else if (env.getEnvVariable("CIRCLECI").isDefined) {
Some(new CircleProvider(env))
} else {
None
}

trait CiProvider {
def isPullRequest: Boolean
def determineProject(): Option[String]
def determineVersion(): Option[String]
def determineApiKey(): Option[String] = readEnvironmentVariableOrLog("STRYKER_DASHBOARD_API_KEY")

protected def readEnvironmentVariableOrLog(name: String): Option[String] = {
val environmentOption = sys.env.get(name).filter(_.nonEmpty)
if (environmentOption.isEmpty) {
warn(
s"Missing environment variable $name, not initializing ${this.getClass.getSimpleName} for dashboard reporter."
)
}
environmentOption
}
}

object TravisProvider extends CiProvider {
override def isPullRequest: Boolean = !readEnvironmentVariableOrLog("TRAVIS_PULL_REQUEST").forall(_ == "false")
override def determineProject(): Option[String] = readEnvironmentVariableOrLog("TRAVIS_REPO_SLUG")
override def determineVersion(): Option[String] = readEnvironmentVariableOrLog("TRAVIS_BRANCH")
private def readEnvironmentVariable(name: String, env: Environment): Option[String] =
env.getEnvVariable(name).filter(_.nonEmpty)

class TravisProvider(env: Environment) extends CiProvider {
override def determineProject(): Option[String] =
readEnvironmentVariable("TRAVIS_REPO_SLUG", env)

override def determineVersion(): Option[String] =
readEnvironmentVariable("TRAVIS_BRANCH", env)
}

object CircleProvider extends CiProvider {
override def isPullRequest: Boolean = !readEnvironmentVariableOrLog("CIRCLE_PULL_REQUEST").forall(_ == "false")
class CircleProvider(env: Environment) extends CiProvider {
override def determineProject(): Option[String] =
for {
username <- readEnvironmentVariableOrLog("CIRCLE_PROJECT_USERNAME")
repoName <- readEnvironmentVariableOrLog("CIRCLE_PROJECT_REPONAME")
username <- readEnvironmentVariable("CIRCLE_PROJECT_USERNAME", env)
repoName <- readEnvironmentVariable("CIRCLE_PROJECT_REPONAME", env)
} yield s"$username/$repoName"
override def determineVersion(): Option[String] = readEnvironmentVariableOrLog("CIRCLE_BRANCH")

override def determineVersion(): Option[String] =
readEnvironmentVariable("CIRCLE_BRANCH", env)
}
}
12 changes: 12 additions & 0 deletions core/src/main/scala/stryker4s/report/model/DashboardConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package stryker4s.report.model

import stryker4s.config.DashboardReportType

case class DashboardConfig(
apiKey: String,
baseUrl: String,
reportType: DashboardReportType,
project: String,
version: String,
module: Option[String]
)
3 changes: 3 additions & 0 deletions core/src/main/scala/stryker4s/report/model/Href.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package stryker4s.report.model

case class DashboardPutResult(href: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package stryker4s.report.model

case class ScoreOnlyReport(mutationScore: Double)

This file was deleted.

5 changes: 3 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ object Dependencies {
val circe = "0.12.3"
val mutationTestingElements = "1.2.0"
val mutationTestingMetrics = "1.2.0"
val scalajHttp = "2.4.2"
val sttp = "2.0.0-RC2"
}

object test {
Expand All @@ -32,7 +32,8 @@ object Dependencies {
val grizzledSlf4j = "org.clapper" %% "grizzled-slf4j" % versions.grizzledSlf4j
val catsCore = "org.typelevel" %% "cats-core" % versions.cats
val circeCore = "io.circe" %% "circe-core" % versions.circe
val scalajHttp = "org.scalaj" %% "scalaj-http" % versions.scalajHttp
val sttp = "com.softwaremill.sttp.client" %% "core" % versions.sttp
val sttpCirce = "com.softwaremill.sttp.client" %% "circe" % versions.sttp
val mutationTestingElements = "io.stryker-mutator" % "mutation-testing-elements" % versions.mutationTestingElements
val mutationTestingMetrics = "io.stryker-mutator" %% "mutation-testing-metrics-circe" % versions.mutationTestingMetrics
}
3 changes: 2 additions & 1 deletion project/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ object Settings {
Dependencies.grizzledSlf4j,
Dependencies.log4jslf4jImpl % Test, // Logging tests need a slf4j implementation
Dependencies.circeCore,
Dependencies.scalajHttp,
Dependencies.sttp,
Dependencies.sttpCirce,
Dependencies.mutationTestingElements,
Dependencies.mutationTestingMetrics
)
Expand Down

0 comments on commit 90a5e19

Please sign in to comment.