Skip to content

Commit cb5abca

Browse files
author
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 10db28c commit cb5abca

File tree

40 files changed

+450
-20
lines changed

40 files changed

+450
-20
lines changed

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,13 @@ Any code between two such comments will not be instrumented or included in the c
102102
Based on minimum coverage, you can fail the build with the following keys
103103

104104
```scala
105-
coverageMinimum := 80
105+
coverageMinimum := 95
106106
coverageFailOnMinimum := true
107+
coverageMinimumBranchTotal := 90
108+
coverageMinimumStmtPerPackage := 90
109+
coverageMinimumBranchPerPackage := 85
110+
coverageMinimumStmtPerFile := 85
111+
coverageMinimumBranchPerFile := 80
107112
```
108113

109114
These settings will be enforced when the reports are generated.

src/main/scala/scoverage/ScoverageKeys.scala

+34-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ object ScoverageKeys {
88
lazy val coverageAggregate = taskKey[Unit]("aggregate reports from subprojects")
99
lazy val coverageExcludedPackages = settingKey[String]("regex for excluded packages")
1010
lazy val coverageExcludedFiles = settingKey[String]("regex for excluded file paths")
11-
lazy val coverageMinimum = settingKey[Double]("scoverage-minimum-coverage")
11+
lazy val coverageMinimum = settingKey[Double]("scoverage-minimum-coverage-stmt-total")
12+
lazy val coverageMinimumBranchTotal = settingKey[Double]("scoverage-minimum-coverage-branch-total")
13+
lazy val coverageMinimumStmtPerPackage = settingKey[Double]("scoverage-minimum-coverage-stmt-per-package")
14+
lazy val coverageMinimumBranchPerPackage = settingKey[Double]("scoverage-minimum-coverage-branch-per-package")
15+
lazy val coverageMinimumStmtPerFile = settingKey[Double]("scoverage-minimum-coverage-stmt-per-file")
16+
lazy val coverageMinimumBranchPerFile = settingKey[Double]("scoverage-minimum-coverage-branch-per-file")
1217
lazy val coverageFailOnMinimum = settingKey[Boolean]("if coverage is less than this value then fail build")
1318
lazy val coverageHighlighting = settingKey[Boolean]("enables range positioning for highlighting")
1419
lazy val coverageOutputCobertura = settingKey[Boolean]("enables cobertura XML report generation")
@@ -18,4 +23,32 @@ object ScoverageKeys {
1823
lazy val coverageCleanSubprojectFiles = settingKey[Boolean]("removes subproject data after an aggregation")
1924
lazy val coverageOutputTeamCity = settingKey[Boolean]("turn on teamcity reporting")
2025
lazy val coverageScalacPluginVersion = settingKey[String]("version of scalac-scoverage-plugin to use")
26+
27+
def coverageMinima = Def.setting {
28+
CoverageMinima(
29+
total = CoverageMinimum(
30+
statement = coverageMinimum.value,
31+
branch = coverageMinimumBranchTotal.value),
32+
perPackage = CoverageMinimum(
33+
statement = coverageMinimumStmtPerPackage.value,
34+
branch = coverageMinimumBranchPerPackage.value
35+
),
36+
perFile = CoverageMinimum(
37+
statement = coverageMinimumStmtPerFile.value,
38+
branch = coverageMinimumBranchPerFile.value
39+
)
40+
)
41+
}
42+
43+
case class CoverageMinimum(
44+
statement: Double,
45+
branch: Double
46+
)
47+
48+
case class CoverageMinima(
49+
total: CoverageMinimum,
50+
perPackage: CoverageMinimum,
51+
perFile: CoverageMinimum
52+
)
53+
2154
}

src/main/scala/scoverage/ScoverageSbtPlugin.scala

+42-14
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ object ScoverageSbtPlugin extends AutoPlugin {
2828
coverageExcludedPackages := "",
2929
coverageExcludedFiles := "",
3030
coverageMinimum := 0, // default is no minimum
31+
coverageMinimumBranchTotal := 0,
32+
coverageMinimumStmtPerPackage := 0,
33+
coverageMinimumBranchPerPackage := 0,
34+
coverageMinimumStmtPerFile := 0,
35+
coverageMinimumBranchPerFile := 0,
3136
coverageFailOnMinimum := false,
3237
coverageHighlighting := true,
3338
coverageOutputXML := true,
@@ -123,7 +128,7 @@ object ScoverageSbtPlugin extends AutoPlugin {
123128
sourceEncoding((scalacOptions in (Compile)).value),
124129
log)
125130

126-
checkCoverage(cov, log, coverageMinimum.value, coverageFailOnMinimum.value)
131+
checkCoverage(cov, log, coverageMinima.value, coverageFailOnMinimum.value)
127132
case None => log.warn("No coverage data, skipping reports")
128133
}
129134
}
@@ -150,7 +155,7 @@ object ScoverageSbtPlugin extends AutoPlugin {
150155
val cfmt = cov.statementCoverageFormatted
151156
log.info(s"Aggregation complete. Coverage was [$cfmt]")
152157

153-
checkCoverage(cov, log, coverageMinimum.value, coverageFailOnMinimum.value)
158+
checkCoverage(cov, log, coverageMinima.value, coverageFailOnMinimum.value)
154159
case None =>
155160
log.info("No subproject data to aggregate, skipping reports")
156161
}
@@ -244,28 +249,51 @@ object ScoverageSbtPlugin extends AutoPlugin {
244249

245250
private def checkCoverage(coverage: Coverage,
246251
log: Logger,
247-
min: Double,
252+
min: CoverageMinima,
248253
failOnMin: Boolean): Unit = {
254+
val ok: Boolean = checkCoverage(coverage, "Total", log, min.total) &&
255+
coverage.packages.forall(x => checkCoverage(x, s"Package:${x.name}", log, min.perPackage)) &&
256+
coverage.files.forall(x => checkCoverage(x, s"File:${x.filename}", log, min.perFile))
257+
258+
if (!ok && failOnMin)
259+
throw new RuntimeException("Coverage minimum was not reached")
260+
261+
log.info(s"All done. Coverage was [${coverage.statementCoverageFormatted}%]")
262+
}
249263

250-
val cper = coverage.statementCoveragePercent
251-
val cfmt = coverage.statementCoverageFormatted
264+
private def checkCoverage(metrics: CoverageMetrics,
265+
metric: String,
266+
log: Logger,
267+
min: CoverageMinimum): Boolean = {
268+
checkCoverage(s"Branch:$metric", log, min.branch, metrics.branchCoveragePercent) &&
269+
checkCoverage(s"Stmt:$metric", log, min.statement, metrics.statementCoveragePercent)
270+
}
252271

272+
private def checkCoverage(metric: String,
273+
log: Logger,
274+
min: Double,
275+
cper: Double): Boolean = {
253276
// check for default minimum
254-
if (min > 0) {
277+
if (min <= 0) {
278+
true
279+
} else {
255280
def is100(d: Double) = Math.abs(100 - d) <= 0.00001
256281

257282
if (is100(min) && is100(cper)) {
258-
log.info(s"100% Coverage !")
259-
} else if (min > cper) {
260-
log.error(s"Coverage is below minimum [$cfmt% < $min%]")
261-
if (failOnMin)
262-
throw new RuntimeException("Coverage minimum was not reached")
283+
log.info(s"100% Coverage: $metric")
284+
true
263285
} else {
264-
log.info(s"Coverage is above minimum [$cfmt% > $min%]")
286+
val ok: Boolean = min <= cper
287+
val minfmt = scoverage.DoubleFormat.twoFractionDigits(min)
288+
val cfmt = scoverage.DoubleFormat.twoFractionDigits(cper)
289+
if (ok) {
290+
log.info(s"Coverage is above minimum [$cfmt% > $minfmt%]: $metric")
291+
} else {
292+
log.error(s"Coverage is below minimum [$cfmt% < $minfmt%]: $metric")
293+
}
294+
ok
265295
}
266296
}
267-
268-
log.info(s"All done. Coverage was [$cfmt%]")
269297
}
270298

271299
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.10.4"
4+
5+
libraryDependencies += "org.specs2" %% "specs2" % "2.3.13" % "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,17 @@
1+
import org.specs2.mutable._
2+
3+
/**
4+
* Created by tbarke001c on 7/8/14.
5+
*/
6+
class BadCoverageSpec extends Specification {
7+
8+
"BadCoverage" should {
9+
"sum two numbers" in {
10+
one.BadCoverage.sum(1, 2) mustEqual 3
11+
one.BadCoverage.sum(0, 3) mustEqual 3
12+
one.BadCoverage.sum(3, 0) mustEqual 3
13+
two.BadCoverage.sum(1, 2) mustEqual 3
14+
two.BadCoverage.sum(3, 0) mustEqual 3
15+
}
16+
}
17+
}
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.10.4"
4+
5+
libraryDependencies += "org.specs2" %% "specs2" % "2.3.13" % "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,16 @@
1+
import org.specs2.mutable._
2+
3+
/**
4+
* Created by tbarke001c on 7/8/14.
5+
*/
6+
class BadCoverageSpec extends Specification {
7+
8+
"BadCoverage" should {
9+
"sum two numbers" in {
10+
one.BadCoverage.sum(1, 2) mustEqual 3
11+
one.BadCoverage.sum(0, 3) mustEqual 3
12+
one.BadCoverage.sum(3, 0) mustEqual 3
13+
two.BadCoverage.sum(1, 2) mustEqual 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.10.4"
4+
5+
libraryDependencies += "org.specs2" %% "specs2" % "2.3.13" % "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,17 @@
1+
import org.specs2.mutable._
2+
3+
/**
4+
* Created by tbarke001c on 7/8/14.
5+
*/
6+
class BadCoverageSpec extends Specification {
7+
8+
"BadCoverage" should {
9+
"sum two numbers" in {
10+
one.BadCoverage.sum(1, 2) mustEqual 3
11+
one.BadCoverage.sum(0, 3) mustEqual 3
12+
one.BadCoverage.sum(3, 0) mustEqual 3
13+
two.BadCoverage.sum(1, 2) mustEqual 3
14+
two.BadCoverage.sum(0, 3) mustEqual 3
15+
}
16+
}
17+
}
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.10.4"
4+
5+
libraryDependencies += "org.specs2" %% "specs2" % "2.3.13" % "test"
6+
7+
coverageMinimumStmtPerPackage := 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+
}

0 commit comments

Comments
 (0)