diff options
author | Nik Vanderhoof <nikolasrvanderhoof@gmail.com> | 2019-05-18 23:22:13 -0400 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2019-05-19 11:22:13 +0800 |
commit | c4a65baab92890d2d5682329a87469bec605fe5d (patch) | |
tree | 1d8caffb159d847e7cafa211572204164b3eabc2 | |
parent | 6c86609a6474e952e3d4972852f032af944631dc (diff) | |
download | mill-c4a65baab92890d2d5682329a87469bec605fe5d.tar.gz mill-c4a65baab92890d2d5682329a87469bec605fe5d.tar.bz2 mill-c4a65baab92890d2d5682329a87469bec605fe5d.zip |
Add support for Scoverage (#571)
* Add initial work for ScoverageModule
* style: Move package scoverage from lib to contrib
Suggested by @lefou
> I think, it would be better to add under `mill.contrib.scoverage`.
Pull request: #571
* Initial changes to non-hardcoded scoverage version
* Using task context api to specify dataDir
Now measurement data will be written to:
PROJECT_ROOT/out/MODULE/scoverage/data/
and the html report will be written to:
PROJECT_ROOT/out/MODULE/scoverage/data/htmlReport/
* Remove wild card imports in scoverage
Also remove scoverage dependency from build.sc
* Move htmlReport into worker
Based on what I've seen in
scalalib, scalajslib, scalanativelib, playlib, and twirllib modules.
Still need to add tests
* Add basic docs + tests for scoverage
I still am working on testing the actual generation of
reports.
* Use cross-module for scoverage worker
Now we can support multiple versions of scoverage
by adding them to the crossmodule list.
Also now running the local publish script succeeds.
* Add scoverage to ci tests
* Add detailed ScoverageModule documentation
* Test scoverage dataDir
* Remove <pre> tags in scaladoc
* Add scoverage dependency in less hacky way
* Modify scoverage tests to check classpaths
* Put docs in alphabetical order
* Test classpaths for scoverage runtime
* Remove abstract def test: ScoverageTests
* Construct classloader differently
* Revert "Construct classloader differently"
This reverts commit fccf9a94cc38fb9e2be58a9ff90b00b65f339db6.
* Revert "Construct classloader differently"
Also fixes unfound error in html report
This reverts commit fccf9a94cc38fb9e2be58a9ff90b00b65f339db6.
* Fix classpath for scoverage worker
-rwxr-xr-x | build.sc | 27 | ||||
-rwxr-xr-x | ci/test-mill-0.sh | 3 | ||||
-rwxr-xr-x | ci/test-mill-bootstrap.sh | 2 | ||||
-rw-r--r-- | contrib/playlib/src/mill/playlib/RouterModule.scala | 2 | ||||
-rw-r--r-- | contrib/scoverage/api/src/ScoverageReportWorkerApi.scala | 7 | ||||
-rw-r--r-- | contrib/scoverage/src/ScoverageModule.scala | 122 | ||||
-rw-r--r-- | contrib/scoverage/src/ScoverageReportWorker.scala | 39 | ||||
-rw-r--r-- | contrib/scoverage/test/resources/hello-world/core/src/Greet.scala | 6 | ||||
-rw-r--r-- | contrib/scoverage/test/src/HelloWorldTests.scala | 107 | ||||
-rw-r--r-- | contrib/scoverage/worker/1.3.1/src/ScoverageReportWorkerImpl.scala | 21 | ||||
-rw-r--r-- | docs/pages/9 - Contrib Modules.md | 44 |
11 files changed, 375 insertions, 5 deletions
@@ -287,6 +287,31 @@ object contrib extends MillModule { def moduleDeps = Seq(scalalib) } + object scoverage extends MillModule { + def moduleDeps = Seq(scalalib, scoverage.api) + + def testArgs = T { + val mapping = Map( + "MILL_SCOVERAGE_REPORT_WORKER_1_3_1" -> worker("1.3.1").compile().classes.path + ) + scalalib.worker.testArgs() ++ + scalalib.backgroundwrapper.testArgs() ++ + (for ((k, v) <- mapping) yield s"-D$k=$v") + } + + object api extends MillApiModule { + def moduleDeps = Seq(scalalib) + } + + object worker extends Cross[WorkerModule]("1.3.1") + + class WorkerModule(scoverageVersion: String) extends MillApiModule { + def moduleDeps = Seq(scoverage.api) + + def ivyDeps = Agg(ivy"org.scoverage::scalac-scoverage-plugin:${scoverageVersion}") + } + } + object buildinfo extends MillModule { def moduleDeps = Seq(scalalib) // why do I need this? @@ -436,7 +461,7 @@ def launcherScript(shellJvmArgs: Seq[String], } object dev extends MillModule{ - def moduleDeps = Seq(scalalib, scalajslib, scalanativelib, contrib.scalapblib, contrib.tut) + def moduleDeps = Seq(scalalib, scalajslib, scalanativelib, contrib.scalapblib, contrib.tut, contrib.scoverage) def forkArgs = ( diff --git a/ci/test-mill-0.sh b/ci/test-mill-0.sh index fa8d7604..83c361b1 100755 --- a/ci/test-mill-0.sh +++ b/ci/test-mill-0.sh @@ -6,4 +6,5 @@ set -eux git clean -xdf # Run tests -mill -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,main.client,contrib.scalapblib,contrib.flyway}.test + +mill -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,main.client,contrib.scalapblib,contrib.flyway,contrib.scoverage}.test
\ No newline at end of file diff --git a/ci/test-mill-bootstrap.sh b/ci/test-mill-bootstrap.sh index f95c0646..80086df2 100755 --- a/ci/test-mill-bootstrap.sh +++ b/ci/test-mill-bootstrap.sh @@ -27,4 +27,4 @@ git clean -xdf rm -rf ~/.mill # Use second build to run tests using Mill -~/mill-2 -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,contrib.scalapblib}.test +~/mill-2 -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,contrib.scalapblib,contrib.scoverage}.test diff --git a/contrib/playlib/src/mill/playlib/RouterModule.scala b/contrib/playlib/src/mill/playlib/RouterModule.scala index abf3082b..62a4c16b 100644 --- a/contrib/playlib/src/mill/playlib/RouterModule.scala +++ b/contrib/playlib/src/mill/playlib/RouterModule.scala @@ -97,4 +97,4 @@ trait RouterModule extends ScalaModule with Version { override def generatedSources = T { super.generatedSources() ++ routerClasses() } -}
\ No newline at end of file +} diff --git a/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala b/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala new file mode 100644 index 00000000..d74e1275 --- /dev/null +++ b/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala @@ -0,0 +1,7 @@ +package mill.contrib.scoverage.api + +import mill.eval.PathRef + +trait ScoverageReportWorkerApi { + def htmlReport(sources: Seq[PathRef], dataDir: String, selfDir: String): Unit +} diff --git a/contrib/scoverage/src/ScoverageModule.scala b/contrib/scoverage/src/ScoverageModule.scala new file mode 100644 index 00000000..b96afa34 --- /dev/null +++ b/contrib/scoverage/src/ScoverageModule.scala @@ -0,0 +1,122 @@ +package mill +package contrib +package scoverage + +import coursier.{Cache, MavenRepository} +import mill.api.Result +import mill.eval.PathRef +import mill.util.Ctx +import mill.scalalib.{DepSyntax, JavaModule, Lib, ScalaModule, TestModule, Dep} +import mill.moduledefs.Cacher + + +/** Adds targets to a [[mill.scalalib.ScalaModule]] to create test coverage reports. + * + * This module allows you to generate code coverage reports for Scala projects with + * [[https://github.com/scoverage Scoverage]] via the + * [[https://github.com/scoverage/scalac-scoverage-plugin scoverage compiler plugin]]. + * + * To declare a module for which you want to generate coverage reports you can + * Extends the `mill.contrib.scoverage.ScoverageModule` trait when defining your + * Module. Additionally, you must define a submodule that extends the + * `ScoverageTests` trait that belongs to your instance of `ScoverageModule`. + * + * {{{ + * // You have to replace VERSION + * import $ivy.`com.lihaoyi::mill-contrib-buildinfo:VERSION` + * import mill.contrib.scoverage.ScoverageModule + * + * Object foo extends ScoverageModule { + * def scalaVersion = "2.11.8" + * def scoverageVersion = "1.3.1" + * + * object test extends ScoverageTests { + * def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.0.5") + * def testFrameworks = Seq("org.scalatest.tools.Framework") + * } + * } + * }}} + * + * In addition to the normal tasks available to your Scala module, Scoverage + * Modules introduce a few new tasks and changes the behavior of an existing one. + * + * - mill foo.scoverage.compile # compiles your module with test instrumentation + * # (you don't have to run this manually, running the test task will force its invocation) + * + * - mill foo.test # tests your project and collects metrics on code coverage + * - mill foo.scoverage.htmlReport # uses the metrics collected by a previous test run to generate a coverage report in html format + * + * The measurement data is available at `out/foo/scoverage/data/`, + * And the html report is saved in `out/foo/scoverage/htmlReport/`. + */ +trait ScoverageModule extends ScalaModule { outer: ScalaModule => + def scoverageVersion: T[String] + private def scoverageRuntimeDep = T { + ivy"org.scoverage::scalac-scoverage-runtime:${outer.scoverageVersion()}" + } + private def scoveragePluginDep = T { + ivy"org.scoverage::scalac-scoverage-plugin:${outer.scoverageVersion()}" + } + + private def toolsClasspath = T { + scoverageReportWorkerClasspath() ++ scoverageClasspath() + } + + def scoverageClasspath = T { + Lib.resolveDependencies( + Seq(Cache.ivy2Local, MavenRepository("https://repo1.maven.org/maven2")), + Lib.depToDependency(_, outer.scalaVersion()), + Seq(scoveragePluginDep()), + ctx = Some(implicitly[mill.util.Ctx.Log]) + ) + } + + def scoverageReportWorkerClasspath = T { + val workerKey = "MILL_SCOVERAGE_REPORT_WORKER_" + scoverageVersion().replace(".", "_") + mill.modules.Util.millProjectModule( + workerKey, + s"mill-contrib-scoverage-worker-${outer.scoverageVersion()}", + repositories, + resolveFilter = _.toString.contains("mill-contrib-scoverage-worker") + ) + } + + object scoverage extends ScalaModule { + def selfDir = T { T.ctx().dest / os.up / os.up } + def dataDir = T { selfDir() / "data" } + + def sources = outer.sources + def resources = outer.resources + def scalaVersion = outer.scalaVersion() + def compileIvyDeps = outer.compileIvyDeps() + def ivyDeps = outer.ivyDeps() ++ Agg(outer.scoverageRuntimeDep()) + def scalacPluginIvyDeps = outer.scalacPluginIvyDeps() ++ Agg(outer.scoveragePluginDep()) + def scalacOptions = outer.scalacOptions() ++ + Seq(s"-P:scoverage:dataDir:${dataDir()}") + + def htmlReport() = T.command { + ScoverageReportWorkerApi + .scoverageReportWorker() + .bridge(toolsClasspath().map(_.path)) + .htmlReport(sources(), dataDir().toString, selfDir().toString) + } + } + + trait ScoverageTests extends outer.Tests { + override def upstreamAssemblyClasspath = T { + super.upstreamAssemblyClasspath() ++ + resolveDeps(T.task{Agg(outer.scoverageRuntimeDep())})() + } + override def compileClasspath = T { + super.compileClasspath() ++ + resolveDeps(T.task{Agg(outer.scoverageRuntimeDep())})() + } + override def runClasspath = T { + super.runClasspath() ++ + resolveDeps(T.task{Agg(outer.scoverageRuntimeDep())})() + } + + // Need the sources compiled with scoverage instrumentation to run. + override def moduleDeps: Seq[JavaModule] = Seq(outer.scoverage) + } +} diff --git a/contrib/scoverage/src/ScoverageReportWorker.scala b/contrib/scoverage/src/ScoverageReportWorker.scala new file mode 100644 index 00000000..1aaa31ad --- /dev/null +++ b/contrib/scoverage/src/ScoverageReportWorker.scala @@ -0,0 +1,39 @@ +package mill.contrib.scoverage + +import mill.{Agg, T} +import mill.api.{ClassLoader, Ctx, Result} +import mill.define.{Discover, ExternalModule, Worker} +import mill.eval.PathRef + +class ScoverageReportWorker { + private var scoverageInstanceCache = Option.empty[(Long, api.ScoverageReportWorkerApi)] + + def bridge(classpath: Agg[os.Path]) + (implicit ctx: Ctx) = { + val classloaderSig = + classpath.map(p => p.toString().hashCode + os.mtime(p)).sum + scoverageInstanceCache match { + case Some((sig, bridge)) if sig == classloaderSig => bridge + case _ => + val toolsClassPath = classpath.map(_.toIO.toURI.toURL).toVector + ctx.log.debug("Loading classes from\n"+toolsClassPath.mkString("\n")) + val cl = ClassLoader.create( + toolsClassPath, + getClass.getClassLoader + ) + val bridge = cl + .loadClass("mill.contrib.scoverage.worker.ScoverageReportWorkerImpl") + .getDeclaredConstructor() + .newInstance() + .asInstanceOf[api.ScoverageReportWorkerApi] + scoverageInstanceCache = Some((classloaderSig, bridge)) + bridge + } + } +} + +object ScoverageReportWorkerApi extends ExternalModule { + + def scoverageReportWorker = T.worker { new ScoverageReportWorker() } + lazy val millDiscover = Discover[this.type] +} diff --git a/contrib/scoverage/test/resources/hello-world/core/src/Greet.scala b/contrib/scoverage/test/resources/hello-world/core/src/Greet.scala new file mode 100644 index 00000000..608becc9 --- /dev/null +++ b/contrib/scoverage/test/resources/hello-world/core/src/Greet.scala @@ -0,0 +1,6 @@ +object Greet { + def greet(name: String, prefix: Option[String]): String = prefix match { + case Some(p) => s"Hello, ${p} ${name}!" + case None => s"Hello, ${name}!" + } +} diff --git a/contrib/scoverage/test/src/HelloWorldTests.scala b/contrib/scoverage/test/src/HelloWorldTests.scala new file mode 100644 index 00000000..98a4201c --- /dev/null +++ b/contrib/scoverage/test/src/HelloWorldTests.scala @@ -0,0 +1,107 @@ +package mill.contrib.scoverage + +import mill._ +import mill.api.Result +import mill.scalalib._ +import mill.util.{TestEvaluator, TestUtil} +import utest._ +import utest.framework.TestPath + +object HelloWorldTests extends utest.TestSuite { + val resourcePath = os.pwd / 'contrib / 'scoverage / 'test / 'resources / "hello-world" + trait HelloBase extends TestUtil.BaseModule { + def millSourcePath = TestUtil.getSrcPathBase() / millOuterCtx.enclosing.split('.') + } + + object HelloWorld extends HelloBase { + object core extends ScoverageModule { + def scalaVersion = "2.12.4" + def scoverageVersion = "1.3.1" + + object test extends ScoverageTests { + override def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.0.5") + def testFrameworks = Seq("org.scalatest.tools.Framework") + } + } + } + + def workspaceTest[T](m: TestUtil.BaseModule, resourcePath: os.Path = resourcePath) + (t: TestEvaluator => T) + (implicit tp: TestPath): T = { + val eval = new TestEvaluator(m) + os.remove.all(m.millSourcePath) + os.remove.all(eval.outPath) + os.makeDir.all(m.millSourcePath / os.up) + os.copy(resourcePath, m.millSourcePath) + t(eval) + } + + def tests: utest.Tests = utest.Tests { + "HelloWorld" - { + "core" - { + "scoverageVersion" - workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverageVersion) + + assert( + result == "1.3.1", + evalCount > 0 + ) + } + "scoverage" - { + "ivyDeps" - workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = + eval.apply(HelloWorld.core.scoverage.ivyDeps) + + assert( + result == Agg(ivy"org.scoverage::scalac-scoverage-runtime:1.3.1"), + 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:1.3.1"), + evalCount > 0 + ) + } + "dataDir" - workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.dataDir) + + assert( + result.toString.endsWith("mill/target/workspace/mill/contrib/scoverage/HelloWorldTests/eval/HelloWorld/core/scoverage/dataDir/core/scoverage/data"), + evalCount > 0 + ) + } + } + "test" - { + "upstreamAssemblyClasspath" - workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.upstreamAssemblyClasspath) + + assert( + result.map(_.toString).exists(_.contains("scalac-scoverage-runtime")), + evalCount > 0 + ) + } + "compileClasspath" - workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.compileClasspath) + + assert( + result.map(_.toString).exists(_.contains("scalac-scoverage-runtime")), + evalCount > 0 + ) + } + "runClasspath" - TestUtil.disableInJava9OrAbove(workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.runClasspath) + + assert( + result.map(_.toString).exists(_.contains("scalac-scoverage-runtime")), + evalCount > 0 + ) + }) + } + } + } + } +} diff --git a/contrib/scoverage/worker/1.3.1/src/ScoverageReportWorkerImpl.scala b/contrib/scoverage/worker/1.3.1/src/ScoverageReportWorkerImpl.scala new file mode 100644 index 00000000..44f506f7 --- /dev/null +++ b/contrib/scoverage/worker/1.3.1/src/ScoverageReportWorkerImpl.scala @@ -0,0 +1,21 @@ +package mill.contrib.scoverage.worker + +import mill.contrib.scoverage.api.ScoverageReportWorkerApi +import mill.eval.PathRef +import _root_.scoverage.Serializer.{ coverageFile, deserialize } +import _root_.scoverage.IOUtils.{ findMeasurementFiles, invoked } +import _root_.scoverage.report.ScoverageHtmlWriter + +class ScoverageReportWorkerImpl extends ScoverageReportWorkerApi { + def htmlReport(sources: Seq[PathRef], dataDir: String, selfDir: String) = { + val coverageFileObj = coverageFile(dataDir) + val coverage = deserialize(coverageFileObj) + coverage(invoked(findMeasurementFiles(dataDir))) + val Seq(PathRef(sourceFolderPath, _, _)) = sources + val sourceFolders = Seq(sourceFolderPath.toIO) + val htmlFolder = new java.io.File(s"${selfDir}/htmlReport") + htmlFolder.mkdir() + new ScoverageHtmlWriter(sourceFolders, htmlFolder, None) + .write(coverage) + } +} diff --git a/docs/pages/9 - Contrib Modules.md b/docs/pages/9 - Contrib Modules.md index eca61be3..c3d4751a 100644 --- a/docs/pages/9 - Contrib Modules.md +++ b/docs/pages/9 - Contrib Modules.md @@ -543,6 +543,49 @@ object example extends ScalaPBModule { } ``` + +## Scoverage + +This module allows you to generate code coverage reports for Scala projects with +[Scoverage](https://github.com/scoverage) via the +[scalac-scoverage-plugin](https://github.com/scoverage/scalac-scoverage-plugin). + +To declare a module for which you want to generate coverage reports you can +extends the `mill.contrib.scoverage.ScoverageModule` trait when defining your +module. Additionally, you must define a submodule that extends the +`ScoverageTests` trait that belongs to your instance of `ScoverageModule`. + +```scala +// You have to replace VERSION +import $ivy.`com.lihaoyi::mill-contrib-buildinfo:VERSION` +import mill.contrib.scoverage.ScoverageModule + +object foo extends ScoverageModule { + def scalaVersion = "2.11.8" + def scoverageVersion = "1.3.1" + + object test extends ScoverageTests { + def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.0.5") + def testFrameworks = Seq("org.scalatest.tools.Framework") + } +} +``` + +In addition to the normal tasks available to your Scala module, Scoverage +modules introduce a few new tasks and changes the behavior of an existing one. + +``` +mill foo.scoverage.compile # compiles your module with test instrumentation + # (you don't have to run this manually, running the test task will force its invocation) + +mill foo.test # tests your project and collects metrics on code coverage +mill foo.scoverage.htmlReport # uses the metrics collected by a previous test run to generate a coverage report in html format +``` + +The measurement data is available at `out/foo/scoverage/data/`, +and the html report is saved in `out/foo/scoverage/htmlReport/`. + + ## TestNG Provides support for [TestNG](https://testng.org/doc/index.html). @@ -722,4 +765,3 @@ These imports will always be added to every template. You don't need to list th ### Example There's an [example project](https://github.com/lihaoyi/cask/tree/master/example/twirl) - |