Skip to content

Commit 34490a3

Browse files
authored
Merge pull request #253 from kitbellew/covmin
Coverage minima: add more fine-grained control
2 parents 550b3e8 + fdc20aa commit 34490a3

File tree

47 files changed

+574
-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

+574
-50
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
9392
coverageFailOnMinimum := true
93+
coverageMinimumStmtTotal := 90
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
total: CoverageMinimum,
58+
perPackage: CoverageMinimum,
59+
perFile: CoverageMinimum
60+
) {
61+
def checkCoverage(
62+
coverage: Coverage,
63+
failOnMin: Boolean
64+
)(implicit log: Logger): Unit = {
65+
val ok: Boolean = total.checkCoverage(coverage, "Total") &&
66+
coverage.packages.forall(pkg =>
67+
perPackage.checkCoverage(pkg, s"Package:${pkg.name}")
68+
) &&
69+
coverage.files.forall(file =>
70+
perFile.checkCoverage(file, s"File:${file.filename}")
71+
)
72+
73+
if (!ok && failOnMin)
74+
throw new RuntimeException("Coverage minimum was not reached")
75+
76+
log.info(
77+
s"All done. Coverage was" +
78+
s" stmt=[${coverage.statementCoverageFormatted}%]" +
79+
s" branch=[${coverage.branchCoverageFormatted}]"
80+
)
81+
}
82+
83+
}
84+
85+
def all = Def.setting {
86+
import ScoverageKeys._
87+
val stmtTotal =
88+
math.max(coverageMinimum.value, coverageMinimumStmtTotal.value)
89+
All(
90+
total = CoverageMinimum(
91+
statement = stmtTotal,
92+
branch = coverageMinimumBranchTotal.value
93+
),
94+
perPackage = CoverageMinimum(
95+
statement = coverageMinimumStmtPerPackage.value,
96+
branch = coverageMinimumBranchPerPackage.value
97+
),
98+
perFile = CoverageMinimum(
99+
statement = coverageMinimumStmtPerFile.value,
100+
branch = coverageMinimumBranchPerFile.value
101+
)
102+
)
103+
}
104+
105+
}

src/main/scala/scoverage/ScoverageKeys.scala

+18-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,22 @@ 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+
@deprecated("Use coverageMinimumStmtTotal instead", "v1.8.0")
24+
lazy val coverageMinimum =
25+
settingKey[Double]("see coverageMinimumStmtTotal")
26+
lazy val coverageMinimumStmtTotal =
27+
settingKey[Double]("scoverage minimum coverage: statement total")
28+
lazy val coverageMinimumBranchTotal =
29+
settingKey[Double]("scoverage minimum coverage: branch total")
30+
lazy val coverageMinimumStmtPerPackage =
31+
settingKey[Double]("scoverage minimum coverage: statement per package")
32+
lazy val coverageMinimumBranchPerPackage =
33+
settingKey[Double]("scoverage minimum coverage: branch per package")
34+
lazy val coverageMinimumStmtPerFile =
35+
settingKey[Double]("scoverage minimum coverage: statement per file")
36+
lazy val coverageMinimumBranchPerFile =
37+
settingKey[Double]("scoverage minimum coverage: branch per file")
38+
lazy val coverageFailOnMinimum =
39+
settingKey[Boolean]("if coverage is less than minimum then fail build")
2440
}

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)