Skip to content

Commit

Permalink
Comply with html reporter format (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugo-vrijswijk committed Mar 20, 2019
1 parent b579b13 commit 231ce57
Show file tree
Hide file tree
Showing 18 changed files with 533 additions and 33 deletions.
1 change: 0 additions & 1 deletion core/src/main/scala/stryker4s/config/Config.scala
@@ -1,7 +1,6 @@
package stryker4s.config

import better.files._
import org.apache.logging.log4j.Level
import pureconfig.ConfigWriter

case class Config(mutate: Seq[String] = Seq("**/main/scala/**/*.scala"),
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/scala/stryker4s/config/ReporterType.scala
Expand Up @@ -7,3 +7,7 @@ sealed trait ReporterType {
case object ConsoleReporterType extends ReporterType {
override val name: String = "console"
}

case object HtmlReporterType extends ReporterType {
override val name: String = "html"
}
Expand Up @@ -4,7 +4,7 @@ import java.nio.file.Path

import better.files.File
import pureconfig.ConfigReader
import stryker4s.config.{ConsoleReporterType, ExcludedMutations, ReporterType}
import stryker4s.config.{ConsoleReporterType, ExcludedMutations, HtmlReporterType, ReporterType}

trait ConfigReaderImplicits {

Expand All @@ -17,6 +17,7 @@ trait ConfigReaderImplicits {
private[config] implicit val toReporterList: ConfigReader[ReporterType] =
ConfigReader[String] map {
case ConsoleReporterType.name => ConsoleReporterType
case HtmlReporterType.name => HtmlReporterType
}

private[config] implicit val exclusions: ConfigReader[ExcludedMutations] =
Expand Down
Expand Up @@ -4,7 +4,6 @@ import java.nio.file.Path

import better.files.File
import com.typesafe.config.ConfigRenderOptions
import org.apache.logging.log4j.Level
import pureconfig.ConfigWriter
import stryker4s.config.{ExcludedMutations, ReporterType}

Expand Down
34 changes: 34 additions & 0 deletions core/src/main/scala/stryker4s/report/HtmlReporter.scala
@@ -0,0 +1,34 @@
package stryker4s.report
import better.files.Resource
import stryker4s.config.Config
import stryker4s.model.MutantRunResults
import stryker4s.report.mapper.MutantRunResultMapper

class HtmlReporter(implicit config: Config) extends FinishedRunReporter with MutantRunResultMapper {

private val reportVersion = "1.0.1"

def indexHtml(json: String): String = {
val mutationTestElementsScript = Resource.getAsString(
s"META-INF/resources/webjars/mutation-testing-elements/$reportVersion/dist/mutation-test-elements.js")

s"""<!DOCTYPE html>
|<html>
|<body>
| <mutation-test-report-app title-postfix="Stryker4s report"></mutation-test-report-app>
| <script>
| document.querySelector('mutation-test-report-app').report = $json
| </script>
| <script>
| $mutationTestElementsScript
| </script>
|</body>
|</html>""".stripMargin
}

override def reportRunFinished(runResults: MutantRunResults): Unit = {
val mapped = toReport(runResults).toJson

println(mapped)
}
}
3 changes: 2 additions & 1 deletion core/src/main/scala/stryker4s/report/Reporter.scala
@@ -1,13 +1,14 @@
package stryker4s.report

import stryker4s.config.{Config, ConsoleReporterType}
import stryker4s.config.{Config, ConsoleReporterType, HtmlReporterType}
import stryker4s.model.{Mutant, MutantRunResult, MutantRunResults}

class Reporter(implicit config: Config) extends FinishedRunReporter with ProgressReporter {

def reporters: Seq[MutationRunReporter] = {
config.reporters collect {
case ConsoleReporterType => new ConsoleReporter()
case HtmlReporterType => new HtmlReporter()
}
}

Expand Down
@@ -0,0 +1,61 @@
package stryker4s.report.mapper
import java.nio.file.Path

import stryker4s.config.{Config, Thresholds => ConfigThresholds}
import stryker4s.model._
import stryker4s.report.model.MutantStatus.MutantStatus
import stryker4s.report.model._

trait MutantRunResultMapper {

private val schemaVersion = "1"

def toReport(mutantRunResults: MutantRunResults)(implicit config: Config): MutationTestReport = MutationTestReport(
schemaVersion,
toThresholds(config.thresholds),
toMutationTestResultMap(mutantRunResults.results.toSeq)
)

private def toThresholds(thresholds: ConfigThresholds): Thresholds =
Thresholds(high = thresholds.high, low = thresholds.low)

private def toMutationTestResultMap(results: Seq[MutantRunResult])(
implicit config: Config): Map[String, MutationTestResult] =
results groupBy (_.fileSubPath) map {
case (path, runResults) => path.toString.replace('\\', '/') -> toMutationTestResult(runResults)
}

private def toMutationTestResult(runResults: Seq[MutantRunResult])(implicit config: Config): MutationTestResult =
MutationTestResult(
fileContentAsString(runResults.head.fileSubPath),
runResults.map(toMutantResult)
)

private def toMutantResult(runResult: MutantRunResult): MutantResult = {
val mutant = runResult.mutant
MutantResult(
mutant.id.toString,
mutant.mutationType.mutationName,
mutant.mutated.syntax,
toLocation(mutant.original.pos),
toMutantStatus(runResult)
)
}

private def toLocation(pos: scala.meta.inputs.Position): Location = Location(
start = Position(line = pos.startLine + 1, column = pos.startColumn + 1),
end = Position(line = pos.endLine + 1, column = pos.endColumn + 1)
)

private def toMutantStatus(mutant: MutantRunResult): MutantStatus = mutant match {
case _: Survived => MutantStatus.Survived
case _: Killed => MutantStatus.Killed
case _: NoCoverage => MutantStatus.NoCoverage
case _: TimedOut => MutantStatus.Timeout
case _: Error => MutantStatus.CompileError
}

private def fileContentAsString(path: Path)(implicit config: Config): String =
(config.baseDir / path.toString).contentAsString

}
@@ -0,0 +1,35 @@
package stryker4s.report.model
import io.circe.Encoder
import stryker4s.report.model.MutantStatus.MutantStatus

final case class MutationTestReport(schemaVersion: String,
thresholds: Thresholds,
files: Map[String, MutationTestResult]) {

private implicit val encoder: Encoder[MutantStatus] = Encoder.enumEncoder(MutantStatus)

def toJson: String = {
import io.circe.generic.auto._
import io.circe.syntax._
this.asJson.noSpaces
}
}

final case class MutationTestResult(source: String, mutants: Seq[MutantResult], language: String = "scala")

final case class MutantResult(id: String,
mutatorName: String,
replacement: String,
location: Location,
status: MutantStatus)

final case class Location(start: Position, end: Position)

final case class Position(line: Int, column: Int)

final case class Thresholds(high: Int, low: Int)

object MutantStatus extends Enumeration {
type MutantStatus = Value
val Killed, Survived, NoCoverage, CompileError, Timeout = Value
}
189 changes: 189 additions & 0 deletions core/src/test/resources/mutation-testing-report-schema.json
@@ -0,0 +1,189 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://stryker-mutator.io/report.schema.json",
"title": "Schema for a mutation testing report.",
"type": "object",
"required": [
"schemaVersion",
"thresholds",
"files"
],
"properties": {
"schemaVersion": {
"type": "string",
"pattern": "^1(\\.\\d*)?$",
"title": "Major version of this report. Used for compatibility.",
"examples": [
"1"
]
},
"thresholds": {
"type": "object",
"title": "Thresholds for the status of the reported application.",
"required": [
"high",
"low"
],
"properties": {
"high": {
"type": "integer",
"title": "Higher bound threshold.",
"minimum": 0,
"maximum": 100,
"examples": [
80
]
},
"low": {
"type": "integer",
"title": "Lower bound threshold.",
"minimum": 0,
"maximum": 100,
"examples": [
60
]
}
}
},
"files": {
"type": "object",
"title": "All mutated files.",
"definitions": {
"position": {
"$id": "#/definitions/position",
"type": "object",
"title": "Position of a mutation. Both line and column start at one.",
"required": [
"line",
"column"
],
"properties": {
"line": {
"type": "integer",
"minimum": 1,
"examples": [
4
]
},
"column": {
"type": "integer",
"minimum": 1,
"examples": [
3
]
}
}
}
},
"additionalProperties": {
"type": "object",
"title": "Mutated file, with the relative path of the file as the key.",
"required": [
"language",
"source",
"mutants"
],
"properties": {
"language": {
"type": "string",
"title": "Programming language that is used. Used for code highlighting, see https://highlightjs.org/static/demo/.",
"examples": [
"javascript",
"typescript",
"cs",
"scala"
]
},
"source": {
"type": "string",
"title": "Full source code of the mutated file, this is used for highlighting.",
"examples": [
"using System; using....."
]
},
"mutants": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "object",
"title": "Single mutation.",
"required": [
"id",
"mutatorName",
"replacement",
"location",
"status"
],
"properties": {
"id": {
"type": "string",
"title": "Unique id, can be used to correlate this mutant with other reports.",
"examples": [
"321321"
]
},
"mutatorName": {
"type": "string",
"title": "Category of the mutation.",
"examples": [
"ConditionalExpression",
"EqualityOperator",
"LogicalOperator"
]
},
"replacement": {
"type": "string",
"title": "Actual mutation that has been applied.",
"examples": [
"-",
"+",
"&&",
"||"
]
},
"location": {
"type": "object",
"title": "Location of the applied mutation.",
"required": [
"start",
"end"
],
"properties": {
"start": {
"$ref": "#/properties/files/definitions/position",
"title": "Start position of the mutation. Inclusive."
},
"end": {
"$ref": "#/properties/files/definitions/position",
"title": "End position of the mutation. Exclusive."
}
}
},
"status": {
"type": "string",
"title": "Result of the mutation.",
"enum": [
"Killed",
"Survived",
"NoCoverage",
"CompileError",
"RuntimeError",
"Timeout"
],
"examples": [
"Killed",
"Survived",
"NoCoverage",
"CompileError",
"RuntimeError",
"Timeout"
]
}
}
}
}
}
}
}
}
}

0 comments on commit 231ce57

Please sign in to comment.