Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coverage minima: add more fine-grained control #253

Merged
merged 1 commit into from
May 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,13 @@ coverage report.
Based on minimum coverage, you can fail the build with the following keys:

```scala
coverageMinimum := 80
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
105 changes: 105 additions & 0 deletions src/main/scala/scoverage/CoverageMinimum.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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(
total: CoverageMinimum,
perPackage: CoverageMinimum,
perFile: CoverageMinimum
) {
def checkCoverage(
coverage: Coverage,
failOnMin: Boolean
)(implicit log: Logger): Unit = {
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}")
)

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

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

}

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

}
20 changes: 18 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,22 @@ 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

@deprecated("Use coverageMinimumStmtTotal instead", "v1.8.0")
lazy val coverageMinimum =
settingKey[Double]("see coverageMinimumStmtTotal")
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