Skip to content

Commit 6d5ab4e

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 db352b8 commit 6d5ab4e

File tree

47 files changed

+578
-50
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+578
-50
lines changed

README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,14 @@ 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 // this is the average of stmt and branch total
9393
coverageFailOnMinimum := true
94+
coverageMinimumStmtTotal := 90
95+
coverageMinimumBranchTotal := 90
96+
coverageMinimumStmtPerPackage := 90
97+
coverageMinimumBranchPerPackage := 85
98+
coverageMinimumStmtPerFile := 85
99+
coverageMinimumBranchPerFile := 80
94100
```
95101

96102
These settings will be enforced when the reports are generated. If you generate
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package scoverage
2+
3+
import sbt._
4+
import scoverage.DoubleFormat.twoFractionDigits
5+
6+
case class CoverageMinimum(
7+
statement: Double,
8+
branch: Double
9+
) {
10+
def checkCoverage(
11+
metrics: CoverageMetrics,
12+
metric: String
13+
)(implicit log: Logger): Boolean = {
14+
CoverageMinimum.checkCoverage(
15+
s"Branch:$metric",
16+
branch,
17+
metrics.branchCoveragePercent
18+
) &&
19+
CoverageMinimum.checkCoverage(
20+
s"Stmt:$metric",
21+
statement,
22+
metrics.statementCoveragePercent
23+
)
24+
}
25+
26+
}
27+
28+
object CoverageMinimum {
29+
30+
def checkCoverage(
31+
metric: String,
32+
min: Double,
33+
cper: Double
34+
)(implicit log: Logger): Boolean = {
35+
// check for default minimum
36+
min <= 0 || {
37+
def is100(d: Double) = Math.abs(100 - d) <= 0.00001
38+
39+
if (is100(min) && is100(cper)) {
40+
log.debug(s"100% Coverage: $metric")
41+
true
42+
} else {
43+
val ok: Boolean = min <= cper
44+
def minfmt = twoFractionDigits(min)
45+
def cfmt = twoFractionDigits(cper)
46+
if (ok) {
47+
log.debug(s"Coverage is above minimum [$cfmt% >= $minfmt%]: $metric")
48+
} else {
49+
log.error(s"Coverage is below minimum [$cfmt% < $minfmt%]: $metric")
50+
}
51+
ok
52+
}
53+
}
54+
}
55+
56+
case class All(
57+
overall: Double,
58+
total: CoverageMinimum,
59+
perPackage: CoverageMinimum,
60+
perFile: CoverageMinimum
61+
) {
62+
def checkCoverage(
63+
coverage: Coverage,
64+
failOnMin: Boolean
65+
)(implicit log: Logger): Unit = {
66+
val averagePercentage =
67+
(coverage.statementCoveragePercent + coverage.branchCoveragePercent) / 2
68+
val ok: Boolean = total.checkCoverage(coverage, "Total") &&
69+
coverage.packages.forall(pkg =>
70+
perPackage.checkCoverage(pkg, s"Package:${pkg.name}")
71+
) &&
72+
coverage.files.forall(file =>
73+
perFile.checkCoverage(file, s"File:${file.filename}")
74+
) &&
75+
CoverageMinimum.checkCoverage("Overall", overall, averagePercentage)
76+
77+
if (!ok && failOnMin)
78+
throw new RuntimeException("Coverage minimum was not reached")
79+
80+
log.info(
81+
s"All done. Coverage was" +
82+
s" overall=[${twoFractionDigits(averagePercentage)}]" +
83+
s" stmt=[${coverage.statementCoverageFormatted}%]" +
84+
s" branch=[${coverage.branchCoverageFormatted}]"
85+
)
86+
}
87+
88+
}
89+
90+
def all = Def.setting {
91+
import ScoverageKeys._
92+
All(
93+
overall = coverageMinimum.value,
94+
total = CoverageMinimum(
95+
statement = coverageMinimumStmtTotal.value,
96+
branch = coverageMinimumBranchTotal.value
97+
),
98+
perPackage = CoverageMinimum(
99+
statement = coverageMinimumStmtPerPackage.value,
100+
branch = coverageMinimumBranchPerPackage.value
101+
),
102+
perFile = CoverageMinimum(
103+
statement = coverageMinimumStmtPerFile.value,
104+
branch = coverageMinimumBranchPerFile.value
105+
)
106+
)
107+
}
108+
109+
}

src/main/scala/scoverage/ScoverageKeys.scala

+17-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ 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")
15-
lazy val coverageFailOnMinimum = settingKey[Boolean]("if coverage is less than this value then fail build")
1614
lazy val coverageHighlighting = settingKey[Boolean]("enables range positioning for highlighting")
1715
lazy val coverageOutputCobertura = settingKey[Boolean]("enables cobertura XML report generation")
1816
lazy val coverageOutputXML = settingKey[Boolean]("enables xml report generation")
@@ -21,4 +19,21 @@ object ScoverageKeys {
2119
lazy val coverageOutputTeamCity = settingKey[Boolean]("turn on teamcity reporting")
2220
lazy val coverageScalacPluginVersion = settingKey[String]("version of scalac-scoverage-plugin to use")
2321
// format: on
22+
23+
lazy val coverageMinimum =
24+
settingKey[Double]("scoverage minimum coverage: statement and branch total")
25+
lazy val coverageMinimumStmtTotal =
26+
settingKey[Double]("scoverage minimum coverage: statement total")
27+
lazy val coverageMinimumBranchTotal =
28+
settingKey[Double]("scoverage minimum coverage: branch total")
29+
lazy val coverageMinimumStmtPerPackage =
30+
settingKey[Double]("scoverage minimum coverage: statement per package")
31+
lazy val coverageMinimumBranchPerPackage =
32+
settingKey[Double]("scoverage minimum coverage: branch per package")
33+
lazy val coverageMinimumStmtPerFile =
34+
settingKey[Double]("scoverage minimum coverage: statement per file")
35+
lazy val coverageMinimumBranchPerFile =
36+
settingKey[Double]("scoverage minimum coverage: branch per file")
37+
lazy val coverageFailOnMinimum =
38+
settingKey[Boolean]("if coverage is less than minimum then fail build")
2439
}

src/main/scala/scoverage/ScoverageSbtPlugin.scala

+12-42
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ object ScoverageSbtPlugin extends AutoPlugin {
3535
coverageExcludedPackages := "",
3636
coverageExcludedFiles := "",
3737
coverageMinimum := 0, // default is no minimum
38+
coverageMinimumStmtTotal := 0,
39+
coverageMinimumBranchTotal := 0,
40+
coverageMinimumStmtPerPackage := 0,
41+
coverageMinimumBranchPerPackage := 0,
42+
coverageMinimumStmtPerFile := 0,
43+
coverageMinimumBranchPerFile := 0,
3844
coverageFailOnMinimum := false,
3945
coverageHighlighting := true,
4046
coverageOutputXML := true,
@@ -131,7 +137,7 @@ object ScoverageSbtPlugin extends AutoPlugin {
131137

132138
private lazy val coverageReport0 = Def.task {
133139
val target = crossTarget.value
134-
val log = streams.value.log
140+
implicit val log = streams.value.log
135141

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

156-
checkCoverage(
157-
cov,
158-
log,
159-
coverageMinimum.value,
160-
coverageFailOnMinimum.value
161-
)
162+
CoverageMinimum.all.value
163+
.checkCoverage(cov, coverageFailOnMinimum.value)
162164
case None => log.warn("No coverage data, skipping reports")
163165
}
164166
}
165167

166168
private lazy val coverageAggregate0 = Def.task {
167-
val log = streams.value.log
169+
implicit val log = streams.value.log
168170
log.info(s"Aggregating coverage from subprojects...")
169171

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

190-
checkCoverage(
191-
cov,
192-
log,
193-
coverageMinimum.value,
194-
coverageFailOnMinimum.value
195-
)
192+
CoverageMinimum.all.value
193+
.checkCoverage(cov, coverageFailOnMinimum.value)
196194
case None =>
197195
log.info("No subproject data to aggregate, skipping reports")
198196
}
@@ -326,34 +324,6 @@ object ScoverageSbtPlugin extends AutoPlugin {
326324
}
327325
}
328326

329-
private def checkCoverage(
330-
coverage: Coverage,
331-
log: Logger,
332-
min: Double,
333-
failOnMin: Boolean
334-
): Unit = {
335-
336-
val cper = coverage.statementCoveragePercent
337-
val cfmt = coverage.statementCoverageFormatted
338-
339-
// check for default minimum
340-
if (min > 0) {
341-
def is100(d: Double) = Math.abs(100 - d) <= 0.00001
342-
343-
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")
349-
} else {
350-
log.info(s"Coverage is above minimum [$cfmt% > $min%]")
351-
}
352-
}
353-
354-
log.info(s"All done. Coverage was [$cfmt%]")
355-
}
356-
357327
private def sourceEncoding(scalacOptions: Seq[String]): Option[String] = {
358328
val i = scalacOptions.indexOf("-encoding") + 1
359329
if (i > 0 && i < scalacOptions.length) Some(scalacOptions(i)) else None
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

0 commit comments

Comments
 (0)