Skip to content

Commit

Permalink
Coverage minima: add more fine-grained control
Browse files Browse the repository at this point in the history
Along with existing statement minimum, add branch minimum. Also, include
this pair of control at the package and file level.
  • Loading branch information
Albert Meltzer committed May 8, 2021
1 parent db352b8 commit 6d5ab4e
Show file tree
Hide file tree
Showing 47 changed files with 578 additions and 50 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,14 @@ coverage report.
Based on minimum coverage, you can fail the build with the following keys:

```scala
coverageMinimum := 80
coverageMinimum := 95 // this is the average of stmt and branch total
coverageFailOnMinimum := true
coverageMinimumStmtTotal := 90
coverageMinimumBranchTotal := 90
coverageMinimumStmtPerPackage := 90
coverageMinimumBranchPerPackage := 85
coverageMinimumStmtPerFile := 85
coverageMinimumBranchPerFile := 80
```

These settings will be enforced when the reports are generated. If you generate
Expand Down
109 changes: 109 additions & 0 deletions src/main/scala/scoverage/CoverageMinimum.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package scoverage

import sbt._
import scoverage.DoubleFormat.twoFractionDigits

case class CoverageMinimum(
statement: Double,
branch: Double
) {
def checkCoverage(
metrics: CoverageMetrics,
metric: String
)(implicit log: Logger): Boolean = {
CoverageMinimum.checkCoverage(
s"Branch:$metric",
branch,
metrics.branchCoveragePercent
) &&
CoverageMinimum.checkCoverage(
s"Stmt:$metric",
statement,
metrics.statementCoveragePercent
)
}

}

object CoverageMinimum {

def checkCoverage(
metric: String,
min: Double,
cper: Double
)(implicit log: Logger): Boolean = {
// check for default minimum
min <= 0 || {
def is100(d: Double) = Math.abs(100 - d) <= 0.00001

if (is100(min) && is100(cper)) {
log.debug(s"100% Coverage: $metric")
true
} else {
val ok: Boolean = min <= cper
def minfmt = twoFractionDigits(min)
def cfmt = twoFractionDigits(cper)
if (ok) {
log.debug(s"Coverage is above minimum [$cfmt% >= $minfmt%]: $metric")
} else {
log.error(s"Coverage is below minimum [$cfmt% < $minfmt%]: $metric")
}
ok
}
}
}

case class All(
overall: Double,
total: CoverageMinimum,
perPackage: CoverageMinimum,
perFile: CoverageMinimum
) {
def checkCoverage(
coverage: Coverage,
failOnMin: Boolean
)(implicit log: Logger): Unit = {
val averagePercentage =
(coverage.statementCoveragePercent + coverage.branchCoveragePercent) / 2
val ok: Boolean = total.checkCoverage(coverage, "Total") &&
coverage.packages.forall(pkg =>
perPackage.checkCoverage(pkg, s"Package:${pkg.name}")
) &&
coverage.files.forall(file =>
perFile.checkCoverage(file, s"File:${file.filename}")
) &&
CoverageMinimum.checkCoverage("Overall", overall, averagePercentage)

if (!ok && failOnMin)
throw new RuntimeException("Coverage minimum was not reached")

log.info(
s"All done. Coverage was" +
s" overall=[${twoFractionDigits(averagePercentage)}]" +
s" stmt=[${coverage.statementCoverageFormatted}%]" +
s" branch=[${coverage.branchCoverageFormatted}]"
)
}

}

def all = Def.setting {
import ScoverageKeys._
All(
overall = coverageMinimum.value,
total = CoverageMinimum(
statement = coverageMinimumStmtTotal.value,
branch = coverageMinimumBranchTotal.value
),
perPackage = CoverageMinimum(
statement = coverageMinimumStmtPerPackage.value,
branch = coverageMinimumBranchPerPackage.value
),
perFile = CoverageMinimum(
statement = coverageMinimumStmtPerFile.value,
branch = coverageMinimumBranchPerFile.value
)
)
}

}
19 changes: 17 additions & 2 deletions src/main/scala/scoverage/ScoverageKeys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ object ScoverageKeys {
lazy val coverageAggregate = taskKey[Unit]("aggregate reports from subprojects")
lazy val coverageExcludedPackages = settingKey[String]("regex for excluded packages")
lazy val coverageExcludedFiles = settingKey[String]("regex for excluded file paths")
lazy val coverageMinimum = settingKey[Double]("scoverage-minimum-coverage")
lazy val coverageFailOnMinimum = settingKey[Boolean]("if coverage is less than this value then fail build")
lazy val coverageHighlighting = settingKey[Boolean]("enables range positioning for highlighting")
lazy val coverageOutputCobertura = settingKey[Boolean]("enables cobertura XML report generation")
lazy val coverageOutputXML = settingKey[Boolean]("enables xml report generation")
Expand All @@ -21,4 +19,21 @@ object ScoverageKeys {
lazy val coverageOutputTeamCity = settingKey[Boolean]("turn on teamcity reporting")
lazy val coverageScalacPluginVersion = settingKey[String]("version of scalac-scoverage-plugin to use")
// format: on

lazy val coverageMinimum =
settingKey[Double]("scoverage minimum coverage: statement and branch total")
lazy val coverageMinimumStmtTotal =
settingKey[Double]("scoverage minimum coverage: statement total")
lazy val coverageMinimumBranchTotal =
settingKey[Double]("scoverage minimum coverage: branch total")
lazy val coverageMinimumStmtPerPackage =
settingKey[Double]("scoverage minimum coverage: statement per package")
lazy val coverageMinimumBranchPerPackage =
settingKey[Double]("scoverage minimum coverage: branch per package")
lazy val coverageMinimumStmtPerFile =
settingKey[Double]("scoverage minimum coverage: statement per file")
lazy val coverageMinimumBranchPerFile =
settingKey[Double]("scoverage minimum coverage: branch per file")
lazy val coverageFailOnMinimum =
settingKey[Boolean]("if coverage is less than minimum then fail build")
}
54 changes: 12 additions & 42 deletions src/main/scala/scoverage/ScoverageSbtPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ object ScoverageSbtPlugin extends AutoPlugin {
coverageExcludedPackages := "",
coverageExcludedFiles := "",
coverageMinimum := 0, // default is no minimum
coverageMinimumStmtTotal := 0,
coverageMinimumBranchTotal := 0,
coverageMinimumStmtPerPackage := 0,
coverageMinimumBranchPerPackage := 0,
coverageMinimumStmtPerFile := 0,
coverageMinimumBranchPerFile := 0,
coverageFailOnMinimum := false,
coverageHighlighting := true,
coverageOutputXML := true,
Expand Down Expand Up @@ -131,7 +137,7 @@ object ScoverageSbtPlugin extends AutoPlugin {

private lazy val coverageReport0 = Def.task {
val target = crossTarget.value
val log = streams.value.log
implicit val log = streams.value.log

log.info(s"Waiting for measurement data to sync...")
Thread.sleep(
Expand All @@ -153,18 +159,14 @@ object ScoverageSbtPlugin extends AutoPlugin {
log
)

checkCoverage(
cov,
log,
coverageMinimum.value,
coverageFailOnMinimum.value
)
CoverageMinimum.all.value
.checkCoverage(cov, coverageFailOnMinimum.value)
case None => log.warn("No coverage data, skipping reports")
}
}

private lazy val coverageAggregate0 = Def.task {
val log = streams.value.log
implicit val log = streams.value.log
log.info(s"Aggregating coverage from subprojects...")

val dataDirs = crossTarget
Expand All @@ -187,12 +189,8 @@ object ScoverageSbtPlugin extends AutoPlugin {
val cfmt = cov.statementCoverageFormatted
log.info(s"Aggregation complete. Coverage was [$cfmt]")

checkCoverage(
cov,
log,
coverageMinimum.value,
coverageFailOnMinimum.value
)
CoverageMinimum.all.value
.checkCoverage(cov, coverageFailOnMinimum.value)
case None =>
log.info("No subproject data to aggregate, skipping reports")
}
Expand Down Expand Up @@ -326,34 +324,6 @@ object ScoverageSbtPlugin extends AutoPlugin {
}
}

private def checkCoverage(
coverage: Coverage,
log: Logger,
min: Double,
failOnMin: Boolean
): Unit = {

val cper = coverage.statementCoveragePercent
val cfmt = coverage.statementCoverageFormatted

// check for default minimum
if (min > 0) {
def is100(d: Double) = Math.abs(100 - d) <= 0.00001

if (is100(min) && is100(cper)) {
log.info(s"100% Coverage !")
} else if (min > cper) {
log.error(s"Coverage is below minimum [$cfmt% < $min%]")
if (failOnMin)
throw new RuntimeException("Coverage minimum was not reached")
} else {
log.info(s"Coverage is above minimum [$cfmt% > $min%]")
}
}

log.info(s"All done. Coverage was [$cfmt%]")
}

private def sourceEncoding(scalacOptions: Seq[String]): Option[String] = {
val i = scalacOptions.indexOf("-encoding") + 1
if (i > 0 && i < scalacOptions.length) Some(scalacOptions(i)) else None
Expand Down
14 changes: 14 additions & 0 deletions src/sbt-test/scoverage/bad-coverage-file-branch/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version := "0.1"

scalaVersion := "2.13.5"

libraryDependencies += "org.scalameta" %% "munit" % "0.7.25" % Test

coverageMinimumBranchPerFile := 80

coverageFailOnMinimum := true

resolvers ++= {
if (sys.props.get("plugin.version").map(_.endsWith("-SNAPSHOT")).getOrElse(false)) Seq(Resolver.sonatypeRepo("snapshots"))
else Seq.empty
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
val pluginVersion = sys.props.getOrElse(
"plugin.version",
throw new RuntimeException(
"""|The system property 'plugin.version' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin))

addSbtPlugin("org.scoverage" % "sbt-scoverage" % pluginVersion)

resolvers ++= {
if (pluginVersion.endsWith("-SNAPSHOT"))
Seq(Resolver.sonatypeRepo("snapshots"))
else
Seq.empty
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package one

object BadCoverage {

def sum(num1: Int, num2: Int) = {
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package two

object BadCoverage {

def sum(num1: Int, num2: Int) = {
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import munit.FunSuite

class BadCoverageSpec extends FunSuite {

test("one.BadCoverage should sum two numbers") {
assertEquals(one.BadCoverage.sum(1, 2), 3)
assertEquals(one.BadCoverage.sum(0, 3), 3)
assertEquals(one.BadCoverage.sum(3, 0), 3)
}

test("two.BadCoverage should sum two numbers") {
assertEquals(two.BadCoverage.sum(1, 2), 3)
assertEquals(two.BadCoverage.sum(0, 3), 3)
}

}
5 changes: 5 additions & 0 deletions src/sbt-test/scoverage/bad-coverage-file-branch/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# run scoverage
> clean
> coverage
> test
-> coverageReport
14 changes: 14 additions & 0 deletions src/sbt-test/scoverage/bad-coverage-file-stmt/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version := "0.1"

scalaVersion := "2.13.5"

libraryDependencies += "org.scalameta" %% "munit" % "0.7.25" % Test

coverageMinimumStmtPerFile := 90

coverageFailOnMinimum := true

resolvers ++= {
if (sys.props.get("plugin.version").map(_.endsWith("-SNAPSHOT")).getOrElse(false)) Seq(Resolver.sonatypeRepo("snapshots"))
else Seq.empty
}
14 changes: 14 additions & 0 deletions src/sbt-test/scoverage/bad-coverage-file-stmt/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
val pluginVersion = sys.props.getOrElse(
"plugin.version",
throw new RuntimeException(
"""|The system property 'plugin.version' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin))

addSbtPlugin("org.scoverage" % "sbt-scoverage" % pluginVersion)

resolvers ++= {
if (pluginVersion.endsWith("-SNAPSHOT"))
Seq(Resolver.sonatypeRepo("snapshots"))
else
Seq.empty
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package one

object BadCoverage {

def sum(num1: Int, num2: Int) = {
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package two

object BadCoverage {

def sum(num1: Int, num2: Int) = {
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import munit.FunSuite

class BadCoverageSpec extends FunSuite {

test("one.BadCoverage should sum two numbers") {
assertEquals(one.BadCoverage.sum(1, 2), 3)
assertEquals(one.BadCoverage.sum(0, 3), 3)
assertEquals(one.BadCoverage.sum(3, 0), 3)
}

test("two.BadCoverage should sum two numbers") {
assertEquals(two.BadCoverage.sum(1, 2), 3)
}

}
5 changes: 5 additions & 0 deletions src/sbt-test/scoverage/bad-coverage-file-stmt/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# run scoverage
> clean
> coverage
> test
-> coverageReport
Loading

0 comments on commit 6d5ab4e

Please sign in to comment.