Skip to content

Commit 08ae68d

Browse files
authored
Support for Scoverage 2.x (#2010)
This is the first iteration to support Scoverage 2.x in projects which already work with Scoverage 1.x It does not specifically handle Scala 3.x, which already comes with a scoverage plugin. Pull request: #2010
1 parent d944b3c commit 08ae68d

File tree

7 files changed

+209
-32
lines changed

7 files changed

+209
-32
lines changed

build.sc

+27-3
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ object Deps {
129129
val scalametaTrees = ivy"org.scalameta::trees:4.5.13"
130130
def scalaReflect(scalaVersion: String) = ivy"org.scala-lang:scala-reflect:${scalaVersion}"
131131
def scalacScoveragePlugin = ivy"org.scoverage:::scalac-scoverage-plugin:1.4.11"
132+
def scalacScoverage2Plugin = ivy"org.scoverage:::scalac-scoverage-plugin:2.0.2"
133+
def scalacScoverage2Reporter = ivy"org.scoverage::scalac-scoverage-reporter:2.0.2"
134+
def scalacScoverage2Domain = ivy"org.scoverage::scalac-scoverage-domain:2.0.2"
135+
def scalacScoverage2Serializer = ivy"org.scoverage::scalac-scoverage-serializer:2.0.2"
132136
val semanticDB = ivy"org.scalameta:::semanticdb-scalac:4.5.11"
133137
val sourcecode = ivy"com.lihaoyi::sourcecode:0.3.0"
134138
val upickle = ivy"com.lihaoyi::upickle:2.0.0"
@@ -745,7 +749,9 @@ object contrib extends MillModule {
745749
override def testArgs = T {
746750
val mapping = Map(
747751
"MILL_SCOVERAGE_REPORT_WORKER" -> worker.compile().classes.path,
748-
"MILL_SCOVERAGE_VERSION" -> Deps.scalacScoveragePlugin.dep.version
752+
"MILL_SCOVERAGE2_REPORT_WORKER" -> worker2.compile().classes.path,
753+
"MILL_SCOVERAGE_VERSION" -> Deps.scalacScoveragePlugin.dep.version,
754+
"MILL_SCOVERAGE2_VERSION" -> Deps.scalacScoverage2Plugin.dep.version
749755
)
750756
scalalib.worker.testArgs() ++
751757
scalalib.backgroundwrapper.testArgs() ++
@@ -758,18 +764,36 @@ object contrib extends MillModule {
758764
contrib.buildinfo
759765
)
760766

761-
object worker extends MillApiModule {
767+
object worker extends MillInternalModule {
762768
override def compileModuleDeps = Seq(main.api)
763769
override def moduleDeps = Seq(scoverage.api)
764770
override def compileIvyDeps = T {
765771
Agg(
766-
// compile-time only, need to provide the correct scoverage version runtime
772+
// compile-time only, need to provide the correct scoverage version at runtime
767773
Deps.scalacScoveragePlugin,
768774
// provided by mill runtime
769775
Deps.osLib
770776
)
771777
}
772778
}
779+
780+
object worker2 extends MillInternalModule {
781+
override def compileModuleDeps = Seq(main.api)
782+
783+
override def moduleDeps = Seq(scoverage.api)
784+
785+
override def compileIvyDeps = T {
786+
Agg(
787+
// compile-time only, need to provide the correct scoverage version at runtime
788+
Deps.scalacScoverage2Plugin,
789+
Deps.scalacScoverage2Reporter,
790+
Deps.scalacScoverage2Domain,
791+
Deps.scalacScoverage2Serializer,
792+
// provided by mill runtime
793+
Deps.osLib
794+
)
795+
}
796+
}
773797
}
774798

775799
object buildinfo extends MillModule {

contrib/scoverage/api/src/ScoverageReportWorkerApi.scala

+24-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,31 @@ import mill.api.Ctx
55
trait ScoverageReportWorkerApi {
66
import ScoverageReportWorkerApi._
77

8-
def report(reportType: ReportType, sources: Seq[os.Path], dataDirs: Seq[os.Path])(implicit
8+
@deprecated("Use other overload instead.", "Mill after 0.10.7")
9+
def report(
10+
reportType: ReportType,
11+
sources: Seq[os.Path],
12+
dataDirs: Seq[os.Path]
13+
)(implicit
914
ctx: Ctx
10-
): Unit
15+
): Unit = {
16+
report(reportType, sources, dataDirs, ctx.workspace)
17+
}
18+
19+
def report(
20+
reportType: ReportType,
21+
sources: Seq[os.Path],
22+
dataDirs: Seq[os.Path],
23+
sourceRoot: os.Path
24+
)(implicit
25+
ctx: Ctx
26+
): Unit = {
27+
// FIXME: We only call the deprecated version here, to preserve binary compatibility. Remove when appropriate.
28+
ctx.log.error(
29+
"Binary compatibility stub may cause infinite loops with StackOverflowError. You need to implement: def report(ReportType, Seq[Path], Seq[Path], os.Path): Unit"
30+
)
31+
report(reportType, sources, dataDirs)
32+
}
1133
}
1234

1335
object ScoverageReportWorkerApi {

contrib/scoverage/src/ScoverageModule.scala

+69-16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import mill._
55
import mill.api.{Loose, PathRef}
66
import mill.contrib.scoverage.api.ScoverageReportWorkerApi.ReportType
77
import mill.define.{Command, Persistent, Sources, Target, Task}
8+
import mill.scalalib.api.ZincWorkerUtil
89
import mill.scalalib.{Dep, DepSyntax, JavaModule, ScalaModule}
910

1011
/**
@@ -56,11 +57,38 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
5657
*/
5758
def scoverageVersion: T[String]
5859

60+
private def isScoverage2: Task[Boolean] = T.task { scoverageVersion().startsWith("2.") }
61+
62+
/** Binary compatibility shim. */
63+
@deprecated("Use scoverageRuntimeDeps instead.", "Mill after 0.10.7")
5964
def scoverageRuntimeDep: T[Dep] = T {
60-
ivy"org.scoverage::scalac-scoverage-runtime:${outer.scoverageVersion()}"
65+
T.log.error("scoverageRuntimeDep is no longer used. To customize your module, use scoverageRuntimeDeps.")
66+
scoverageRuntimeDeps().toIndexedSeq.head
67+
}
68+
69+
def scoverageRuntimeDeps: T[Agg[Dep]] = T {
70+
Agg(ivy"org.scoverage::scalac-scoverage-runtime:${outer.scoverageVersion()}")
6171
}
72+
73+
/** Binary compatibility shim. */
74+
@deprecated("Use scoveragePluginDeps instead.", "Mill after 0.10.7")
6275
def scoveragePluginDep: T[Dep] = T {
63-
ivy"org.scoverage:::scalac-scoverage-plugin:${outer.scoverageVersion()}"
76+
T.log.error("scoveragePluginDep is no longer used. To customize your module, use scoverageRuntimeDeps.")
77+
scoveragePluginDeps().toIndexedSeq.head
78+
}
79+
80+
def scoveragePluginDeps: T[Agg[Dep]] = T {
81+
val sv = scoverageVersion()
82+
if (isScoverage2()) {
83+
Agg(
84+
ivy"org.scoverage:::scalac-scoverage-plugin:${sv}",
85+
ivy"org.scoverage::scalac-scoverage-domain:${sv}",
86+
ivy"org.scoverage::scalac-scoverage-serializer:${sv}",
87+
ivy"org.scoverage::scalac-scoverage-reporter:${sv}"
88+
)
89+
} else {
90+
Agg(ivy"org.scoverage:::scalac-scoverage-plugin:${sv}")
91+
}
6492
}
6593

6694
@deprecated("Use scoverageToolsClasspath instead.", "mill after 0.10.0-M1")
@@ -71,23 +99,44 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
7199
def scoverageToolsClasspath: T[Agg[PathRef]] = T {
72100
scoverageReportWorkerClasspath() ++
73101
resolveDeps(T.task {
74-
Agg(
75-
ivy"org.scoverage:scalac-scoverage-plugin_${mill.BuildInfo.scalaVersion}:${outer.scoverageVersion()}"
76-
)
102+
// we need to resolve with same Scala version used for Mill, not the project Scala version
103+
val scalaBinVersion = ZincWorkerUtil.scalaBinaryVersion(BuildInfo.scalaVersion)
104+
val sv = scoverageVersion()
105+
if (isScoverage2()) {
106+
Agg(
107+
ivy"org.scoverage:scalac-scoverage-plugin_${mill.BuildInfo.scalaVersion}:${sv}",
108+
ivy"org.scoverage:scalac-scoverage-domain_${scalaBinVersion}:${sv}",
109+
ivy"org.scoverage:scalac-scoverage-serializer_${scalaBinVersion}:${sv}",
110+
ivy"org.scoverage:scalac-scoverage-reporter_${scalaBinVersion}:${sv}"
111+
)
112+
} else {
113+
Agg(
114+
ivy"org.scoverage:scalac-scoverage-plugin_${mill.BuildInfo.scalaVersion}:${sv}"
115+
)
116+
}
77117
})()
78118
}
79119

80120
def scoverageClasspath: T[Agg[PathRef]] = T {
81-
resolveDeps(T.task { Agg(scoveragePluginDep()) })()
121+
resolveDeps(scoveragePluginDeps)()
82122
}
83123

84124
def scoverageReportWorkerClasspath: T[Agg[PathRef]] = T {
85-
val workerKey = "MILL_SCOVERAGE_REPORT_WORKER"
125+
val isScov2 = isScoverage2()
126+
127+
val workerKey =
128+
if (isScov2) "MILL_SCOVERAGE2_REPORT_WORKER"
129+
else "MILL_SCOVERAGE_REPORT_WORKER"
130+
131+
val workerArtifact =
132+
if (isScov2) "mill-contrib-scoverage-worker2"
133+
else "mill-contrib-scoverage-worker"
134+
86135
mill.modules.Util.millProjectModule(
87136
workerKey,
88-
s"mill-contrib-scoverage-worker",
137+
workerArtifact,
89138
repositoriesTask(),
90-
resolveFilter = _.toString.contains("mill-contrib-scoverage-worker")
139+
resolveFilter = _.toString.contains(workerArtifact)
91140
)
92141
}
93142

@@ -98,7 +147,7 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
98147
ScoverageReportWorker
99148
.scoverageReportWorker()
100149
.bridge(scoverageToolsClasspath().map(_.path))
101-
.report(reportType, allSources().map(_.path), Seq(data().path))
150+
.report(reportType, allSources().map(_.path), Seq(data().path), T.workspace)
102151
}
103152

104153
/**
@@ -121,16 +170,20 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
121170
override def repositoriesTask: Task[Seq[Repository]] = T.task { outer.repositoriesTask() }
122171
override def compileIvyDeps: Target[Loose.Agg[Dep]] = T { outer.compileIvyDeps() }
123172
override def ivyDeps: Target[Loose.Agg[Dep]] =
124-
T { outer.ivyDeps() ++ Agg(outer.scoverageRuntimeDep()) }
173+
T { outer.ivyDeps() ++ outer.scoverageRuntimeDeps() }
125174
override def unmanagedClasspath: Target[Loose.Agg[PathRef]] = T { outer.unmanagedClasspath() }
126175

127176
/** Add the scoverage scalac plugin. */
128177
override def scalacPluginIvyDeps: Target[Loose.Agg[Dep]] =
129-
T { outer.scalacPluginIvyDeps() ++ Agg(outer.scoveragePluginDep()) }
178+
T { outer.scalacPluginIvyDeps() ++ outer.scoveragePluginDeps() }
130179

131180
/** Add the scoverage specific plugin settings (`dataDir`). */
132181
override def scalacOptions: Target[Seq[String]] =
133-
T { outer.scalacOptions() ++ Seq(s"-P:scoverage:dataDir:${data().path.toIO.getPath()}") }
182+
T {
183+
outer.scalacOptions() ++
184+
Seq(s"-P:scoverage:dataDir:${data().path.toIO.getPath()}") ++
185+
(if (isScoverage2()) Seq(s"-P:scoverage:sourceRoot:${T.workspace}") else Seq())
186+
}
134187

135188
def htmlReport(): Command[Unit] = T.command { doReport(ReportType.Html) }
136189
def xmlReport(): Command[Unit] = T.command { doReport(ReportType.Xml) }
@@ -142,15 +195,15 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
142195
trait ScoverageTests extends outer.Tests {
143196
override def upstreamAssemblyClasspath = T {
144197
super.upstreamAssemblyClasspath() ++
145-
resolveDeps(T.task { Agg(outer.scoverageRuntimeDep()) })()
198+
resolveDeps(outer.scoverageRuntimeDeps)()
146199
}
147200
override def compileClasspath = T {
148201
super.compileClasspath() ++
149-
resolveDeps(T.task { Agg(outer.scoverageRuntimeDep()) })()
202+
resolveDeps(outer.scoverageRuntimeDeps)()
150203
}
151204
override def runClasspath = T {
152205
super.runClasspath() ++
153-
resolveDeps(T.task { Agg(outer.scoverageRuntimeDep()) })()
206+
resolveDeps(outer.scoverageRuntimeDeps)()
154207
}
155208

156209
// Need the sources compiled with scoverage instrumentation to run.

contrib/scoverage/src/ScoverageReport.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ trait ScoverageReport extends Module {
106106
scoverageReportWorkerModule
107107
.scoverageReportWorker()
108108
.bridge(workerModule.scoverageToolsClasspath().map(_.path))
109-
.report(reportType, sourcePaths, dataPaths)
109+
.report(reportType, sourcePaths, dataPaths, T.workspace)
110110
PathRef(T.dest)
111111
}
112112
}

contrib/scoverage/test/src/HelloWorldTests.scala

+35-8
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,34 @@ trait HelloWorldTests extends utest.TestSuite {
101101
evalCount > 0
102102
)
103103
}
104-
"scalacPluginIvyDeps" - workspaceTest(HelloWorld) { eval =>
105-
val Right((result, evalCount)) =
106-
eval.apply(HelloWorld.core.scoverage.scalacPluginIvyDeps)
107-
108-
assert(
109-
result == Agg(ivy"org.scoverage:::scalac-scoverage-plugin:${testScoverageVersion}"),
110-
evalCount > 0
111-
)
104+
"scalacPluginIvyDeps" - {
105+
"scoverage1x" - workspaceTest(HelloWorld) { eval =>
106+
val Right((result, evalCount)) =
107+
eval.apply(HelloWorld.core.scoverage.scalacPluginIvyDeps)
108+
if (testScoverageVersion.startsWith("1.")) {
109+
assert(
110+
result == Agg(
111+
ivy"org.scoverage:::scalac-scoverage-plugin:${testScoverageVersion}"
112+
),
113+
evalCount > 0
114+
)
115+
} else "skipped"
116+
}
117+
"scoverage2x" - workspaceTest(HelloWorld) { eval =>
118+
val Right((result, evalCount)) =
119+
eval.apply(HelloWorld.core.scoverage.scalacPluginIvyDeps)
120+
if (testScoverageVersion.startsWith("2.")) {
121+
assert(
122+
result == Agg(
123+
ivy"org.scoverage:::scalac-scoverage-plugin:${testScoverageVersion}",
124+
ivy"org.scoverage::scalac-scoverage-domain:${testScoverageVersion}",
125+
ivy"org.scoverage::scalac-scoverage-serializer:${testScoverageVersion}",
126+
ivy"org.scoverage::scalac-scoverage-reporter:${testScoverageVersion}"
127+
),
128+
evalCount > 0
129+
)
130+
} else "skipped"
131+
}
112132
}
113133
"data" - workspaceTest(HelloWorld) { eval =>
114134
val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.data)
@@ -201,3 +221,10 @@ object HelloWorldTests_2_13 extends HelloWorldTests {
201221
override def testScoverageVersion = sys.props.getOrElse("MILL_SCOVERAGE_VERSION", ???)
202222
override def testScalatestVersion = "3.0.8"
203223
}
224+
225+
object Scoverage2Tests_2_13 extends HelloWorldTests {
226+
override def threadCount = Some(1)
227+
override def testScalaVersion: String = sys.props.getOrElse("TEST_SCALA_2_13_VERSION", ???)
228+
override def testScoverageVersion = sys.props.getOrElse("MILL_SCOVERAGE2_VERSION", ???)
229+
override def testScalatestVersion = "3.0.8"
230+
}

contrib/scoverage/worker/src/ScoverageReportWorkerImpl.scala

+7-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ import _root_.scoverage.report.{CoverageAggregator, ScoverageHtmlWriter, Scovera
55
import mill.api.Ctx
66
import mill.contrib.scoverage.api.ScoverageReportWorkerApi.ReportType
77

8+
/**
9+
* Scoverage Worker for Scoverage 1.x
10+
*/
811
class ScoverageReportWorkerImpl extends ScoverageReportWorkerApi {
912

1013
override def report(
1114
reportType: ReportType,
1215
sources: Seq[os.Path],
13-
dataDirs: Seq[os.Path]
16+
dataDirs: Seq[os.Path],
17+
// ignored in Scoverage 1.x
18+
sourceRoot: os.Path
1419
)(implicit ctx: Ctx): Unit =
1520
try {
1621
ctx.log.info(s"Processing coverage data for ${dataDirs.size} data locations")
@@ -34,7 +39,7 @@ class ScoverageReportWorkerImpl extends ScoverageReportWorkerApi {
3439
ctx.log.error(s"No coverage data found in [${dataDirs.mkString(", ")}]")
3540
}
3641
} catch {
37-
case e =>
42+
case e: Throwable =>
3843
ctx.log.error(s"Exception while building coverage report. ${e.getMessage()}")
3944
e.printStackTrace()
4045
throw e
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package mill.contrib.scoverage.worker
2+
3+
import mill.contrib.scoverage.api.ScoverageReportWorkerApi
4+
import _root_.scoverage.reporter.{CoverageAggregator, ScoverageHtmlWriter, ScoverageXmlWriter}
5+
import mill.api.Ctx
6+
import mill.contrib.scoverage.api.ScoverageReportWorkerApi.ReportType
7+
8+
/**
9+
* Scoverage Worker for Scoverage 1.x
10+
*/
11+
class ScoverageReportWorkerImpl extends ScoverageReportWorkerApi {
12+
13+
override def report(
14+
reportType: ReportType,
15+
sources: Seq[os.Path],
16+
dataDirs: Seq[os.Path],
17+
sourceRoot: os.Path
18+
)(implicit ctx: Ctx): Unit =
19+
try {
20+
ctx.log.info(s"Processing coverage data for ${dataDirs.size} data locations")
21+
CoverageAggregator.aggregate(dataDirs.map(_.toIO), sourceRoot.toIO) match {
22+
case Some(coverage) =>
23+
val sourceFolders = sources.map(_.toIO)
24+
val folder = ctx.dest
25+
os.makeDir.all(folder)
26+
reportType match {
27+
case ReportType.Html =>
28+
new ScoverageHtmlWriter(sourceFolders, folder.toIO, None)
29+
.write(coverage)
30+
case ReportType.Xml =>
31+
new ScoverageXmlWriter(sourceFolders, folder.toIO, false, None)
32+
.write(coverage)
33+
case ReportType.Console =>
34+
ctx.log.info(s"Statement coverage.: ${coverage.statementCoverageFormatted}%")
35+
ctx.log.info(s"Branch coverage....: ${coverage.branchCoverageFormatted}%")
36+
}
37+
case None =>
38+
ctx.log.error(s"No coverage data found in [${dataDirs.mkString(", ")}]")
39+
}
40+
} catch {
41+
case e: Throwable =>
42+
ctx.log.error(s"Exception while building coverage report. ${e.getMessage()}")
43+
e.printStackTrace()
44+
throw e
45+
}
46+
}

0 commit comments

Comments
 (0)