Skip to content

Commit 905a220

Browse files
Albert MeltzerAlbert Meltzer
Albert Meltzer
authored and
Albert Meltzer
committed
Coverage minima: add more fine-grained control
Along with existing statement minimum, add branch minimum. Also, include this pair of control at the package and file level.
1 parent db352b8 commit 905a220

File tree

40 files changed

+472
-21
lines changed

40 files changed

+472
-21
lines changed

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,13 @@ coverage report.
8989
Based on minimum coverage, you can fail the build with the following keys:
9090

9191
```scala
92-
coverageMinimum := 80
92+
coverageMinimum := 95
9393
coverageFailOnMinimum := true
94+
coverageMinimumBranchTotal := 90
95+
coverageMinimumStmtPerPackage := 90
96+
coverageMinimumBranchPerPackage := 85
97+
coverageMinimumStmtPerFile := 85
98+
coverageMinimumBranchPerFile := 80
9499
```
95100

96101
These settings will be enforced when the reports are generated. If you generate

src/main/scala/scoverage/ScoverageKeys.scala

+35-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ object ScoverageKeys {
1111
lazy val coverageAggregate = taskKey[Unit]("aggregate reports from subprojects")
1212
lazy val coverageExcludedPackages = settingKey[String]("regex for excluded packages")
1313
lazy val coverageExcludedFiles = settingKey[String]("regex for excluded file paths")
14-
lazy val coverageMinimum = settingKey[Double]("scoverage-minimum-coverage")
14+
lazy val coverageMinimum = settingKey[Double]("scoverage-minimum-coverage-stmt-total")
15+
lazy val coverageMinimumBranchTotal = settingKey[Double]("scoverage-minimum-coverage-branch-total")
16+
lazy val coverageMinimumStmtPerPackage = settingKey[Double]("scoverage-minimum-coverage-stmt-per-package")
17+
lazy val coverageMinimumBranchPerPackage = settingKey[Double]("scoverage-minimum-coverage-branch-per-package")
18+
lazy val coverageMinimumStmtPerFile = settingKey[Double]("scoverage-minimum-coverage-stmt-per-file")
19+
lazy val coverageMinimumBranchPerFile = settingKey[Double]("scoverage-minimum-coverage-branch-per-file")
1520
lazy val coverageFailOnMinimum = settingKey[Boolean]("if coverage is less than this value then fail build")
1621
lazy val coverageHighlighting = settingKey[Boolean]("enables range positioning for highlighting")
1722
lazy val coverageOutputCobertura = settingKey[Boolean]("enables cobertura XML report generation")
@@ -21,4 +26,33 @@ object ScoverageKeys {
2126
lazy val coverageOutputTeamCity = settingKey[Boolean]("turn on teamcity reporting")
2227
lazy val coverageScalacPluginVersion = settingKey[String]("version of scalac-scoverage-plugin to use")
2328
// format: on
29+
30+
def coverageMinima = Def.setting {
31+
CoverageMinima(
32+
total = CoverageMinimum(
33+
statement = coverageMinimum.value,
34+
branch = coverageMinimumBranchTotal.value
35+
),
36+
perPackage = CoverageMinimum(
37+
statement = coverageMinimumStmtPerPackage.value,
38+
branch = coverageMinimumBranchPerPackage.value
39+
),
40+
perFile = CoverageMinimum(
41+
statement = coverageMinimumStmtPerFile.value,
42+
branch = coverageMinimumBranchPerFile.value
43+
)
44+
)
45+
}
46+
47+
case class CoverageMinimum(
48+
statement: Double,
49+
branch: Double
50+
)
51+
52+
case class CoverageMinima(
53+
total: CoverageMinimum,
54+
perPackage: CoverageMinimum,
55+
perFile: CoverageMinimum
56+
)
57+
2458
}

src/main/scala/scoverage/ScoverageSbtPlugin.scala

+62-14
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ object ScoverageSbtPlugin extends AutoPlugin {
3535
coverageExcludedPackages := "",
3636
coverageExcludedFiles := "",
3737
coverageMinimum := 0, // default is no minimum
38+
coverageMinimumBranchTotal := 0,
39+
coverageMinimumStmtPerPackage := 0,
40+
coverageMinimumBranchPerPackage := 0,
41+
coverageMinimumStmtPerFile := 0,
42+
coverageMinimumBranchPerFile := 0,
3843
coverageFailOnMinimum := false,
3944
coverageHighlighting := true,
4045
coverageOutputXML := true,
@@ -156,7 +161,7 @@ object ScoverageSbtPlugin extends AutoPlugin {
156161
checkCoverage(
157162
cov,
158163
log,
159-
coverageMinimum.value,
164+
coverageMinima.value,
160165
coverageFailOnMinimum.value
161166
)
162167
case None => log.warn("No coverage data, skipping reports")
@@ -190,7 +195,7 @@ object ScoverageSbtPlugin extends AutoPlugin {
190195
checkCoverage(
191196
cov,
192197
log,
193-
coverageMinimum.value,
198+
coverageMinima.value,
194199
coverageFailOnMinimum.value
195200
)
196201
case None =>
@@ -329,29 +334,72 @@ object ScoverageSbtPlugin extends AutoPlugin {
329334
private def checkCoverage(
330335
coverage: Coverage,
331336
log: Logger,
332-
min: Double,
337+
min: CoverageMinima,
333338
failOnMin: Boolean
334339
): Unit = {
340+
val ok: Boolean = checkCoverage(coverage, "Total", log, min.total) &&
341+
coverage.packages.forall(x =>
342+
checkCoverage(x, s"Package:${x.name}", log, min.perPackage)
343+
) &&
344+
coverage.files.forall(x =>
345+
checkCoverage(x, s"File:${x.filename}", log, min.perFile)
346+
)
335347

336-
val cper = coverage.statementCoveragePercent
337-
val cfmt = coverage.statementCoverageFormatted
348+
if (!ok && failOnMin)
349+
throw new RuntimeException("Coverage minimum was not reached")
338350

351+
log.info(
352+
s"All done. Coverage was [${coverage.statementCoverageFormatted}%]"
353+
)
354+
}
355+
356+
private def checkCoverage(
357+
metrics: CoverageMetrics,
358+
metric: String,
359+
log: Logger,
360+
min: CoverageMinimum
361+
): Boolean = {
362+
checkCoverage(
363+
s"Branch:$metric",
364+
log,
365+
min.branch,
366+
metrics.branchCoveragePercent
367+
) &&
368+
checkCoverage(
369+
s"Stmt:$metric",
370+
log,
371+
min.statement,
372+
metrics.statementCoveragePercent
373+
)
374+
}
375+
376+
private def checkCoverage(
377+
metric: String,
378+
log: Logger,
379+
min: Double,
380+
cper: Double
381+
): Boolean = {
339382
// check for default minimum
340-
if (min > 0) {
383+
if (min <= 0) {
384+
true
385+
} else {
341386
def is100(d: Double) = Math.abs(100 - d) <= 0.00001
342387

343388
if (is100(min) && is100(cper)) {
344-
log.info(s"100% Coverage !")
345-
} else if (min > cper) {
346-
log.error(s"Coverage is below minimum [$cfmt% < $min%]")
347-
if (failOnMin)
348-
throw new RuntimeException("Coverage minimum was not reached")
389+
log.debug(s"100% Coverage: $metric")
390+
true
349391
} else {
350-
log.info(s"Coverage is above minimum [$cfmt% > $min%]")
392+
val ok: Boolean = min <= cper
393+
val minfmt = scoverage.DoubleFormat.twoFractionDigits(min)
394+
val cfmt = scoverage.DoubleFormat.twoFractionDigits(cper)
395+
if (ok) {
396+
log.debug(s"Coverage is above minimum [$cfmt% > $minfmt%]: $metric")
397+
} else {
398+
log.error(s"Coverage is below minimum [$cfmt% < $minfmt%]: $metric")
399+
}
400+
ok
351401
}
352402
}
353-
354-
log.info(s"All done. Coverage was [$cfmt%]")
355403
}
356404

357405
private def sourceEncoding(scalacOptions: Seq[String]): Option[String] = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version := "0.1"
2+
3+
scalaVersion := "2.13.5"
4+
5+
libraryDependencies += "org.scalameta" %% "munit" % "0.7.25" % Test
6+
7+
coverageMinimumBranchPerFile := 80
8+
9+
coverageFailOnMinimum := true
10+
11+
resolvers ++= {
12+
if (sys.props.get("plugin.version").map(_.endsWith("-SNAPSHOT")).getOrElse(false)) Seq(Resolver.sonatypeRepo("snapshots"))
13+
else Seq.empty
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
val pluginVersion = sys.props.getOrElse(
2+
"plugin.version",
3+
throw new RuntimeException(
4+
"""|The system property 'plugin.version' is not defined.
5+
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin))
6+
7+
addSbtPlugin("org.scoverage" % "sbt-scoverage" % pluginVersion)
8+
9+
resolvers ++= {
10+
if (pluginVersion.endsWith("-SNAPSHOT"))
11+
Seq(Resolver.sonatypeRepo("snapshots"))
12+
else
13+
Seq.empty
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package one
2+
3+
object BadCoverage {
4+
5+
def sum(num1: Int, num2: Int) = {
6+
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
7+
}
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package two
2+
3+
object BadCoverage {
4+
5+
def sum(num1: Int, num2: Int) = {
6+
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
7+
}
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import munit.FunSuite
2+
3+
class BadCoverageSpec extends FunSuite {
4+
5+
test("one.BadCoverage should sum two numbers") {
6+
assertEquals(one.BadCoverage.sum(1, 2), 3)
7+
assertEquals(one.BadCoverage.sum(0, 3), 3)
8+
assertEquals(one.BadCoverage.sum(3, 0), 3)
9+
}
10+
11+
test("two.BadCoverage should sum two numbers") {
12+
assertEquals(two.BadCoverage.sum(1, 2), 3)
13+
assertEquals(two.BadCoverage.sum(0, 3), 3)
14+
}
15+
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# run scoverage
2+
> clean
3+
> coverage
4+
> test
5+
-> coverageReport
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version := "0.1"
2+
3+
scalaVersion := "2.13.5"
4+
5+
libraryDependencies += "org.scalameta" %% "munit" % "0.7.25" % Test
6+
7+
coverageMinimumStmtPerFile := 90
8+
9+
coverageFailOnMinimum := true
10+
11+
resolvers ++= {
12+
if (sys.props.get("plugin.version").map(_.endsWith("-SNAPSHOT")).getOrElse(false)) Seq(Resolver.sonatypeRepo("snapshots"))
13+
else Seq.empty
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
val pluginVersion = sys.props.getOrElse(
2+
"plugin.version",
3+
throw new RuntimeException(
4+
"""|The system property 'plugin.version' is not defined.
5+
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin))
6+
7+
addSbtPlugin("org.scoverage" % "sbt-scoverage" % pluginVersion)
8+
9+
resolvers ++= {
10+
if (pluginVersion.endsWith("-SNAPSHOT"))
11+
Seq(Resolver.sonatypeRepo("snapshots"))
12+
else
13+
Seq.empty
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package one
2+
3+
object BadCoverage {
4+
5+
def sum(num1: Int, num2: Int) = {
6+
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
7+
}
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package two
2+
3+
object BadCoverage {
4+
5+
def sum(num1: Int, num2: Int) = {
6+
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
7+
}
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import munit.FunSuite
2+
3+
class BadCoverageSpec extends FunSuite {
4+
5+
test("one.BadCoverage should sum two numbers") {
6+
assertEquals(one.BadCoverage.sum(1, 2), 3)
7+
assertEquals(one.BadCoverage.sum(0, 3), 3)
8+
assertEquals(one.BadCoverage.sum(3, 0), 3)
9+
}
10+
11+
test("two.BadCoverage should sum two numbers") {
12+
assertEquals(two.BadCoverage.sum(1, 2), 3)
13+
}
14+
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# run scoverage
2+
> clean
3+
> coverage
4+
> test
5+
-> coverageReport
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version := "0.1"
2+
3+
scalaVersion := "2.13.5"
4+
5+
libraryDependencies += "org.scalameta" %% "munit" % "0.7.25" % Test
6+
7+
coverageMinimumBranchPerPackage := 80
8+
9+
coverageFailOnMinimum := true
10+
11+
resolvers ++= {
12+
if (sys.props.get("plugin.version").map(_.endsWith("-SNAPSHOT")).getOrElse(false)) Seq(Resolver.sonatypeRepo("snapshots"))
13+
else Seq.empty
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
val pluginVersion = sys.props.getOrElse(
2+
"plugin.version",
3+
throw new RuntimeException(
4+
"""|The system property 'plugin.version' is not defined.
5+
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin))
6+
7+
addSbtPlugin("org.scoverage" % "sbt-scoverage" % pluginVersion)
8+
9+
resolvers ++= {
10+
if (pluginVersion.endsWith("-SNAPSHOT"))
11+
Seq(Resolver.sonatypeRepo("snapshots"))
12+
else
13+
Seq.empty
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package one
2+
3+
object BadCoverage {
4+
5+
def sum(num1: Int, num2: Int) = {
6+
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
7+
}
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package two
2+
3+
object BadCoverage {
4+
5+
def sum(num1: Int, num2: Int) = {
6+
if (0 == num1) num2 else if (0 == num2) num1 else num1 + num2
7+
}
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import munit.FunSuite
2+
3+
class BadCoverageSpec extends FunSuite {
4+
5+
test("one.BadCoverage should sum two numbers") {
6+
assertEquals(one.BadCoverage.sum(1, 2), 3)
7+
assertEquals(one.BadCoverage.sum(0, 3), 3)
8+
assertEquals(one.BadCoverage.sum(3, 0), 3)
9+
}
10+
11+
test("two.BadCoverage should sum two numbers") {
12+
assertEquals(two.BadCoverage.sum(1, 2), 3)
13+
assertEquals(two.BadCoverage.sum(0, 3), 3)
14+
}
15+
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# run scoverage
2+
> clean
3+
> coverage
4+
> test
5+
-> coverageReport

0 commit comments

Comments
 (0)