From 1c4d5ad3a431acbf9315ca2ce488a38b5715f4a9 Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Mon, 29 Aug 2022 10:57:53 +0200 Subject: [PATCH 1/3] Support for Scoverage 2.x 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. --- build.sc | 28 ++++++- .../api/src/ScoverageReportWorkerApi.scala | 7 +- contrib/scoverage/src/ScoverageModule.scala | 74 ++++++++++++++----- contrib/scoverage/src/ScoverageReport.scala | 2 +- .../scoverage/test/src/HelloWorldTests.scala | 43 +++++++++-- .../src/ScoverageReportWorkerImpl.scala | 9 ++- .../src/ScoverageReportWorkerImpl.scala | 46 ++++++++++++ 7 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 contrib/scoverage/worker2/src/ScoverageReportWorkerImpl.scala diff --git a/build.sc b/build.sc index 3ca9a05a06a..4abbb303c3f 100755 --- a/build.sc +++ b/build.sc @@ -129,6 +129,10 @@ object Deps { val scalametaTrees = ivy"org.scalameta::trees:4.5.13" def scalaReflect(scalaVersion: String) = ivy"org.scala-lang:scala-reflect:${scalaVersion}" def scalacScoveragePlugin = ivy"org.scoverage:::scalac-scoverage-plugin:1.4.11" + def scalacScoverage2Plugin = ivy"org.scoverage:::scalac-scoverage-plugin:2.0.2" + def scalacScoverage2Reporter = ivy"org.scoverage::scalac-scoverage-reporter:2.0.2" + def scalacScoverage2Domain = ivy"org.scoverage::scalac-scoverage-domain:2.0.2" + def scalacScoverage2Serializer = ivy"org.scoverage::scalac-scoverage-serializer:2.0.2" val semanticDB = ivy"org.scalameta:::semanticdb-scalac:4.5.11" val sourcecode = ivy"com.lihaoyi::sourcecode:0.3.0" val upickle = ivy"com.lihaoyi::upickle:2.0.0" @@ -745,7 +749,9 @@ object contrib extends MillModule { override def testArgs = T { val mapping = Map( "MILL_SCOVERAGE_REPORT_WORKER" -> worker.compile().classes.path, - "MILL_SCOVERAGE_VERSION" -> Deps.scalacScoveragePlugin.dep.version + "MILL_SCOVERAGE2_REPORT_WORKER" -> worker2.compile().classes.path, + "MILL_SCOVERAGE_VERSION" -> Deps.scalacScoveragePlugin.dep.version, + "MILL_SCOVERAGE2_VERSION" -> Deps.scalacScoverage2Plugin.dep.version ) scalalib.worker.testArgs() ++ scalalib.backgroundwrapper.testArgs() ++ @@ -763,13 +769,31 @@ object contrib extends MillModule { override def moduleDeps = Seq(scoverage.api) override def compileIvyDeps = T { Agg( - // compile-time only, need to provide the correct scoverage version runtime + // compile-time only, need to provide the correct scoverage version at runtime Deps.scalacScoveragePlugin, // provided by mill runtime Deps.osLib ) } } + + object worker2 extends MillApiModule { + override def compileModuleDeps = Seq(main.api) + + override def moduleDeps = Seq(scoverage.api) + + override def compileIvyDeps = T { + Agg( + // compile-time only, need to provide the correct scoverage version at runtime + Deps.scalacScoverage2Plugin, + Deps.scalacScoverage2Reporter, + Deps.scalacScoverage2Domain, + Deps.scalacScoverage2Serializer, + // provided by mill runtime + Deps.osLib + ) + } + } } object buildinfo extends MillModule { diff --git a/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala b/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala index 338ba907804..4319bd282b2 100644 --- a/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala +++ b/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala @@ -5,7 +5,12 @@ import mill.api.Ctx trait ScoverageReportWorkerApi { import ScoverageReportWorkerApi._ - def report(reportType: ReportType, sources: Seq[os.Path], dataDirs: Seq[os.Path])(implicit + def report( + reportType: ReportType, + sources: Seq[os.Path], + dataDirs: Seq[os.Path], + sourceRoot: os.Path + )(implicit ctx: Ctx ): Unit } diff --git a/contrib/scoverage/src/ScoverageModule.scala b/contrib/scoverage/src/ScoverageModule.scala index f82a1b7377d..da72304928c 100644 --- a/contrib/scoverage/src/ScoverageModule.scala +++ b/contrib/scoverage/src/ScoverageModule.scala @@ -5,6 +5,7 @@ import mill._ import mill.api.{Loose, PathRef} import mill.contrib.scoverage.api.ScoverageReportWorkerApi.ReportType import mill.define.{Command, Persistent, Sources, Target, Task} +import mill.scalalib.api.ZincWorkerUtil import mill.scalalib.{Dep, DepSyntax, JavaModule, ScalaModule} /** @@ -56,11 +57,23 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule => */ def scoverageVersion: T[String] - def scoverageRuntimeDep: T[Dep] = T { - ivy"org.scoverage::scalac-scoverage-runtime:${outer.scoverageVersion()}" + private def isScoverage2: Task[Boolean] = T.task { scoverageVersion().startsWith("2.") } + + def scoverageRuntimeDeps: T[Agg[Dep]] = T { + Agg(ivy"org.scoverage::scalac-scoverage-runtime:${outer.scoverageVersion()}") } - def scoveragePluginDep: T[Dep] = T { - ivy"org.scoverage:::scalac-scoverage-plugin:${outer.scoverageVersion()}" + def scoveragePluginDeps: T[Agg[Dep]] = T { + val sv = scoverageVersion() + if (isScoverage2()) { + Agg( + ivy"org.scoverage:::scalac-scoverage-plugin:${sv}", + ivy"org.scoverage::scalac-scoverage-domain:${sv}", + ivy"org.scoverage::scalac-scoverage-serializer:${sv}", + ivy"org.scoverage::scalac-scoverage-reporter:${sv}" + ) + } else { + Agg(ivy"org.scoverage:::scalac-scoverage-plugin:${sv}") + } } @deprecated("Use scoverageToolsClasspath instead.", "mill after 0.10.0-M1") @@ -71,23 +84,44 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule => def scoverageToolsClasspath: T[Agg[PathRef]] = T { scoverageReportWorkerClasspath() ++ resolveDeps(T.task { - Agg( - ivy"org.scoverage:scalac-scoverage-plugin_${mill.BuildInfo.scalaVersion}:${outer.scoverageVersion()}" - ) + // we need to resolve with same Scala version used for Mill, not the project Scala version + val scalaBinVersion = ZincWorkerUtil.scalaBinaryVersion(BuildInfo.scalaVersion) + val sv = scoverageVersion() + if (isScoverage2()) { + Agg( + ivy"org.scoverage:scalac-scoverage-plugin_${mill.BuildInfo.scalaVersion}:${sv}", + ivy"org.scoverage:scalac-scoverage-domain_${scalaBinVersion}:${sv}", + ivy"org.scoverage:scalac-scoverage-serializer_${scalaBinVersion}:${sv}", + ivy"org.scoverage:scalac-scoverage-reporter_${scalaBinVersion}:${sv}" + ) + } else { + Agg( + ivy"org.scoverage:scalac-scoverage-plugin_${mill.BuildInfo.scalaVersion}:${sv}" + ) + } })() } def scoverageClasspath: T[Agg[PathRef]] = T { - resolveDeps(T.task { Agg(scoveragePluginDep()) })() + resolveDeps(scoveragePluginDeps)() } def scoverageReportWorkerClasspath: T[Agg[PathRef]] = T { - val workerKey = "MILL_SCOVERAGE_REPORT_WORKER" + val isScov2 = isScoverage2() + + val workerKey = + if (isScov2) "MILL_SCOVERAGE2_REPORT_WORKER" + else "MILL_SCOVERAGE_REPORT_WORKER" + + val workerArtifact = + if (isScov2) "mill-contrib-scoverage-worker2" + else "mill-contrib-scoverage-worker" + mill.modules.Util.millProjectModule( workerKey, - s"mill-contrib-scoverage-worker", + workerArtifact, repositoriesTask(), - resolveFilter = _.toString.contains("mill-contrib-scoverage-worker") + resolveFilter = _.toString.contains(workerArtifact) ) } @@ -98,7 +132,7 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule => ScoverageReportWorker .scoverageReportWorker() .bridge(scoverageToolsClasspath().map(_.path)) - .report(reportType, allSources().map(_.path), Seq(data().path)) + .report(reportType, allSources().map(_.path), Seq(data().path), T.workspace) } /** @@ -121,16 +155,20 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule => override def repositoriesTask: Task[Seq[Repository]] = T.task { outer.repositoriesTask() } override def compileIvyDeps: Target[Loose.Agg[Dep]] = T { outer.compileIvyDeps() } override def ivyDeps: Target[Loose.Agg[Dep]] = - T { outer.ivyDeps() ++ Agg(outer.scoverageRuntimeDep()) } + T { outer.ivyDeps() ++ outer.scoverageRuntimeDeps() } override def unmanagedClasspath: Target[Loose.Agg[PathRef]] = T { outer.unmanagedClasspath() } /** Add the scoverage scalac plugin. */ override def scalacPluginIvyDeps: Target[Loose.Agg[Dep]] = - T { outer.scalacPluginIvyDeps() ++ Agg(outer.scoveragePluginDep()) } + T { outer.scalacPluginIvyDeps() ++ outer.scoveragePluginDeps() } /** Add the scoverage specific plugin settings (`dataDir`). */ override def scalacOptions: Target[Seq[String]] = - T { outer.scalacOptions() ++ Seq(s"-P:scoverage:dataDir:${data().path.toIO.getPath()}") } + T { + outer.scalacOptions() ++ + Seq(s"-P:scoverage:dataDir:${data().path.toIO.getPath()}") ++ + (if(isScoverage2()) Seq(s"-P:scoverage:sourceRoot:${T.workspace}") else Seq()) + } def htmlReport(): Command[Unit] = T.command { doReport(ReportType.Html) } def xmlReport(): Command[Unit] = T.command { doReport(ReportType.Xml) } @@ -142,15 +180,15 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule => trait ScoverageTests extends outer.Tests { override def upstreamAssemblyClasspath = T { super.upstreamAssemblyClasspath() ++ - resolveDeps(T.task { Agg(outer.scoverageRuntimeDep()) })() + resolveDeps(outer.scoverageRuntimeDeps)() } override def compileClasspath = T { super.compileClasspath() ++ - resolveDeps(T.task { Agg(outer.scoverageRuntimeDep()) })() + resolveDeps(outer.scoverageRuntimeDeps)() } override def runClasspath = T { super.runClasspath() ++ - resolveDeps(T.task { Agg(outer.scoverageRuntimeDep()) })() + resolveDeps(outer.scoverageRuntimeDeps)() } // Need the sources compiled with scoverage instrumentation to run. diff --git a/contrib/scoverage/src/ScoverageReport.scala b/contrib/scoverage/src/ScoverageReport.scala index d5ca109bbdd..603d951e0c9 100644 --- a/contrib/scoverage/src/ScoverageReport.scala +++ b/contrib/scoverage/src/ScoverageReport.scala @@ -106,7 +106,7 @@ trait ScoverageReport extends Module { scoverageReportWorkerModule .scoverageReportWorker() .bridge(workerModule.scoverageToolsClasspath().map(_.path)) - .report(reportType, sourcePaths, dataPaths) + .report(reportType, sourcePaths, dataPaths, T.workspace) PathRef(T.dest) } } diff --git a/contrib/scoverage/test/src/HelloWorldTests.scala b/contrib/scoverage/test/src/HelloWorldTests.scala index b072944f520..42db6e9de26 100644 --- a/contrib/scoverage/test/src/HelloWorldTests.scala +++ b/contrib/scoverage/test/src/HelloWorldTests.scala @@ -101,14 +101,34 @@ trait HelloWorldTests extends utest.TestSuite { evalCount > 0 ) } - "scalacPluginIvyDeps" - workspaceTest(HelloWorld) { eval => - val Right((result, evalCount)) = - eval.apply(HelloWorld.core.scoverage.scalacPluginIvyDeps) - - assert( - result == Agg(ivy"org.scoverage:::scalac-scoverage-plugin:${testScoverageVersion}"), - evalCount > 0 - ) + "scalacPluginIvyDeps" - { + "scoverage1x" - workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = + eval.apply(HelloWorld.core.scoverage.scalacPluginIvyDeps) + if (testScoverageVersion.startsWith("1.")) { + assert( + result == Agg( + ivy"org.scoverage:::scalac-scoverage-plugin:${testScoverageVersion}" + ), + evalCount > 0 + ) + } else "skipped" + } + "scoverage2x" - workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = + eval.apply(HelloWorld.core.scoverage.scalacPluginIvyDeps) + if (testScoverageVersion.startsWith("2.")) { + assert( + result == Agg( + ivy"org.scoverage:::scalac-scoverage-plugin:${testScoverageVersion}", + ivy"org.scoverage::scalac-scoverage-domain:${testScoverageVersion}", + ivy"org.scoverage::scalac-scoverage-serializer:${testScoverageVersion}", + ivy"org.scoverage::scalac-scoverage-reporter:${testScoverageVersion}" + ), + evalCount > 0 + ) + } else "skipped" + } } "data" - workspaceTest(HelloWorld) { eval => val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.data) @@ -201,3 +221,10 @@ object HelloWorldTests_2_13 extends HelloWorldTests { override def testScoverageVersion = sys.props.getOrElse("MILL_SCOVERAGE_VERSION", ???) override def testScalatestVersion = "3.0.8" } + +object Scoverage2Tests_2_13 extends HelloWorldTests { + override def threadCount = Some(1) + override def testScalaVersion: String = sys.props.getOrElse("TEST_SCALA_2_13_VERSION", ???) + override def testScoverageVersion = sys.props.getOrElse("MILL_SCOVERAGE2_VERSION", ???) + override def testScalatestVersion = "3.0.8" +} diff --git a/contrib/scoverage/worker/src/ScoverageReportWorkerImpl.scala b/contrib/scoverage/worker/src/ScoverageReportWorkerImpl.scala index ac948ec9969..602f3c3a293 100644 --- a/contrib/scoverage/worker/src/ScoverageReportWorkerImpl.scala +++ b/contrib/scoverage/worker/src/ScoverageReportWorkerImpl.scala @@ -5,12 +5,17 @@ import _root_.scoverage.report.{CoverageAggregator, ScoverageHtmlWriter, Scovera import mill.api.Ctx import mill.contrib.scoverage.api.ScoverageReportWorkerApi.ReportType +/** + * Scoverage Worker for Scoverage 1.x + */ class ScoverageReportWorkerImpl extends ScoverageReportWorkerApi { override def report( reportType: ReportType, sources: Seq[os.Path], - dataDirs: Seq[os.Path] + dataDirs: Seq[os.Path], + // ignored in Scoverage 1.x + sourceRoot: os.Path )(implicit ctx: Ctx): Unit = try { ctx.log.info(s"Processing coverage data for ${dataDirs.size} data locations") @@ -34,7 +39,7 @@ class ScoverageReportWorkerImpl extends ScoverageReportWorkerApi { ctx.log.error(s"No coverage data found in [${dataDirs.mkString(", ")}]") } } catch { - case e => + case e: Throwable => ctx.log.error(s"Exception while building coverage report. ${e.getMessage()}") e.printStackTrace() throw e diff --git a/contrib/scoverage/worker2/src/ScoverageReportWorkerImpl.scala b/contrib/scoverage/worker2/src/ScoverageReportWorkerImpl.scala new file mode 100644 index 00000000000..c0cdfc8be62 --- /dev/null +++ b/contrib/scoverage/worker2/src/ScoverageReportWorkerImpl.scala @@ -0,0 +1,46 @@ +package mill.contrib.scoverage.worker + +import mill.contrib.scoverage.api.ScoverageReportWorkerApi +import _root_.scoverage.reporter.{CoverageAggregator, ScoverageHtmlWriter, ScoverageXmlWriter} +import mill.api.Ctx +import mill.contrib.scoverage.api.ScoverageReportWorkerApi.ReportType + +/** + * Scoverage Worker for Scoverage 1.x + */ +class ScoverageReportWorkerImpl extends ScoverageReportWorkerApi { + + override def report( + reportType: ReportType, + sources: Seq[os.Path], + dataDirs: Seq[os.Path], + sourceRoot: os.Path + )(implicit ctx: Ctx): Unit = + try { + ctx.log.info(s"Processing coverage data for ${dataDirs.size} data locations") + CoverageAggregator.aggregate(dataDirs.map(_.toIO), sourceRoot.toIO) match { + case Some(coverage) => + val sourceFolders = sources.map(_.toIO) + val folder = ctx.dest + os.makeDir.all(folder) + reportType match { + case ReportType.Html => + new ScoverageHtmlWriter(sourceFolders, folder.toIO, None) + .write(coverage) + case ReportType.Xml => + new ScoverageXmlWriter(sourceFolders, folder.toIO, false, None) + .write(coverage) + case ReportType.Console => + ctx.log.info(s"Statement coverage.: ${coverage.statementCoverageFormatted}%") + ctx.log.info(s"Branch coverage....: ${coverage.branchCoverageFormatted}%") + } + case None => + ctx.log.error(s"No coverage data found in [${dataDirs.mkString(", ")}]") + } + } catch { + case e: Throwable => + ctx.log.error(s"Exception while building coverage report. ${e.getMessage()}") + e.printStackTrace() + throw e + } +} From 8c0e1da410c9341c01f7d1d1b235923b4e187049 Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Mon, 29 Aug 2022 21:19:53 +0200 Subject: [PATCH 2/3] Don't run MiMa checks on worker modules --- build.sc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sc b/build.sc index 4abbb303c3f..d964a83732d 100755 --- a/build.sc +++ b/build.sc @@ -764,7 +764,7 @@ object contrib extends MillModule { contrib.buildinfo ) - object worker extends MillApiModule { + object worker extends MillInternalModule { override def compileModuleDeps = Seq(main.api) override def moduleDeps = Seq(scoverage.api) override def compileIvyDeps = T { @@ -777,7 +777,7 @@ object contrib extends MillModule { } } - object worker2 extends MillApiModule { + object worker2 extends MillInternalModule { override def compileModuleDeps = Seq(main.api) override def moduleDeps = Seq(scoverage.api) From 61d01cf55ca3053a08ad1af17332cae063cacc7d Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Tue, 30 Aug 2022 08:51:42 +0200 Subject: [PATCH 3/3] Restored binary compatibility --- .../api/src/ScoverageReportWorkerApi.scala | 19 ++++++++++++++++++- contrib/scoverage/src/ScoverageModule.scala | 17 ++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala b/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala index 4319bd282b2..6003bed659e 100644 --- a/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala +++ b/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala @@ -5,6 +5,17 @@ import mill.api.Ctx trait ScoverageReportWorkerApi { import ScoverageReportWorkerApi._ + @deprecated("Use other overload instead.", "Mill after 0.10.7") + def report( + reportType: ReportType, + sources: Seq[os.Path], + dataDirs: Seq[os.Path] + )(implicit + ctx: Ctx + ): Unit = { + report(reportType, sources, dataDirs, ctx.workspace) + } + def report( reportType: ReportType, sources: Seq[os.Path], @@ -12,7 +23,13 @@ trait ScoverageReportWorkerApi { sourceRoot: os.Path )(implicit ctx: Ctx - ): Unit + ): Unit = { + // FIXME: We only call the deprecated version here, to preserve binary compatibility. Remove when appropriate. + ctx.log.error( + "Binary compatibility stub may cause infinite loops with StackOverflowError. You need to implement: def report(ReportType, Seq[Path], Seq[Path], os.Path): Unit" + ) + report(reportType, sources, dataDirs) + } } object ScoverageReportWorkerApi { diff --git a/contrib/scoverage/src/ScoverageModule.scala b/contrib/scoverage/src/ScoverageModule.scala index da72304928c..b6a73fdc6f9 100644 --- a/contrib/scoverage/src/ScoverageModule.scala +++ b/contrib/scoverage/src/ScoverageModule.scala @@ -59,9 +59,24 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule => private def isScoverage2: Task[Boolean] = T.task { scoverageVersion().startsWith("2.") } + /** Binary compatibility shim. */ + @deprecated("Use scoverageRuntimeDeps instead.", "Mill after 0.10.7") + def scoverageRuntimeDep: T[Dep] = T { + T.log.error("scoverageRuntimeDep is no longer used. To customize your module, use scoverageRuntimeDeps.") + scoverageRuntimeDeps().toIndexedSeq.head + } + def scoverageRuntimeDeps: T[Agg[Dep]] = T { Agg(ivy"org.scoverage::scalac-scoverage-runtime:${outer.scoverageVersion()}") } + + /** Binary compatibility shim. */ + @deprecated("Use scoveragePluginDeps instead.", "Mill after 0.10.7") + def scoveragePluginDep: T[Dep] = T { + T.log.error("scoveragePluginDep is no longer used. To customize your module, use scoverageRuntimeDeps.") + scoveragePluginDeps().toIndexedSeq.head + } + def scoveragePluginDeps: T[Agg[Dep]] = T { val sv = scoverageVersion() if (isScoverage2()) { @@ -167,7 +182,7 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule => T { outer.scalacOptions() ++ Seq(s"-P:scoverage:dataDir:${data().path.toIO.getPath()}") ++ - (if(isScoverage2()) Seq(s"-P:scoverage:sourceRoot:${T.workspace}") else Seq()) + (if (isScoverage2()) Seq(s"-P:scoverage:sourceRoot:${T.workspace}") else Seq()) } def htmlReport(): Command[Unit] = T.command { doReport(ReportType.Html) }