summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2019-05-20 07:17:49 +0800
committerLi Haoyi <haoyi.sg@gmail.com>2019-05-20 07:17:49 +0800
commitd12a0d762193d83f83c837eb4affad389744a6cd (patch)
tree3c26c7dfff35b27b843cdc6175fe05c53f930b76
parent827c72af63fdfd3ee6e4ed0e18a3e5a42e2c0c1c (diff)
parent1cb439fce4af7a61166f13a3c5045fa7c73b25de (diff)
downloadmill-d12a0d762193d83f83c837eb4affad389744a6cd.tar.gz
mill-d12a0d762193d83f83c837eb4affad389744a6cd.tar.bz2
mill-d12a0d762193d83f83c837eb4affad389744a6cd.zip
Merge branch '599'
-rwxr-xr-xbuild.sc33
-rwxr-xr-xci/test-mill-0.sh3
-rwxr-xr-xci/test-mill-bootstrap.sh2
-rw-r--r--contrib/docker/src/DockerModule.scala73
-rw-r--r--contrib/playlib/src/mill/playlib/RouterModule.scala2
-rw-r--r--contrib/scoverage/api/src/ScoverageReportWorkerApi.scala7
-rw-r--r--contrib/scoverage/src/ScoverageModule.scala122
-rw-r--r--contrib/scoverage/src/ScoverageReportWorker.scala39
-rw-r--r--contrib/scoverage/test/resources/hello-world/core/src/Greet.scala6
-rw-r--r--contrib/scoverage/test/src/HelloWorldTests.scala107
-rw-r--r--contrib/scoverage/worker/1.3.1/src/ScoverageReportWorkerImpl.scala21
-rw-r--r--docs/pages/9 - Contrib Modules.md88
12 files changed, 497 insertions, 6 deletions
diff --git a/build.sc b/build.sc
index 4ed072a1..c70aaf71 100755
--- a/build.sc
+++ b/build.sc
@@ -289,6 +289,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?
@@ -309,6 +334,11 @@ object contrib extends MillModule {
def ivyDeps = Agg(ivy"org.flywaydb:flyway-core:5.2.4")
}
+
+ object docker extends MillModule {
+ def moduleDeps = Seq(scalalib)
+ }
+
object bloop extends MillModule {
def moduleDeps = Seq(scalalib)
def ivyDeps = Agg(
@@ -316,7 +346,6 @@ object contrib extends MillModule {
ivy"com.lihaoyi::ujson-circe:0.7.4"
)
}
-
}
@@ -439,7 +468,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/docker/src/DockerModule.scala b/contrib/docker/src/DockerModule.scala
new file mode 100644
index 00000000..bbf4d926
--- /dev/null
+++ b/contrib/docker/src/DockerModule.scala
@@ -0,0 +1,73 @@
+import mill.T
+import mill.scalalib.JavaModule
+import os.Shellable.IterableShellable
+
+import scala.collection.immutable._
+
+trait DockerModule { outer: JavaModule =>
+
+ trait DockerConfig extends mill.Module {
+ /**
+ * Tags that should be applied to the built image
+ * In the standard registry/repository:tag format
+ */
+ def tags: T[Seq[String]] = T(List(outer.artifactName()))
+ def labels: T[Map[String, String]] = Map.empty[String, String]
+ def baseImage: T[String] = "gcr.io/distroless/java:latest"
+ def pullBaseImage: T[Boolean] = T(baseImage().endsWith(":latest"))
+ private def baseImageCacheBuster: T[(Boolean, Double)] = T.input {
+ val pull = pullBaseImage()
+ if(pull) (pull, Math.random()) else (pull, 0d)
+ }
+
+ def dockerfile: T[String] = T {
+ val jarName = assembly().path.last
+ val labelRhs = labels()
+ .map { case (k, v) =>
+ val lineBrokenValue = v
+ .replace("\r\n", "\\\r\n")
+ .replace("\n", "\\\n")
+ .replace("\r", "\\\r")
+ s""""$k"="$lineBrokenValue""""
+ }
+ .mkString(" ")
+
+ val labelLine = if(labels().isEmpty) "" else s"LABEL $labelRhs"
+
+ s"""
+ |FROM ${baseImage()}
+ |$labelLine
+ |COPY $jarName /$jarName
+ |ENTRYPOINT ["java", "-jar", "/$jarName"]
+ """.stripMargin
+ }
+
+ final def build = T {
+ val dest = T.ctx().dest
+
+ val asmPath = outer.assembly().path
+ os.copy(asmPath, dest / asmPath.last)
+
+ os.write(dest / "Dockerfile", dockerfile())
+
+ val log = T.ctx().log
+
+ val tagArgs = tags().flatMap(t => List("-t", t))
+
+ val (pull, _) = baseImageCacheBuster()
+ val pullLatestBase = IterableShellable(if(pull) Some("--pull") else None)
+
+ val result = os
+ .proc("docker", "build", tagArgs, pullLatestBase, dest)
+ .call(stdout = os.Inherit, stderr = os.Inherit)
+
+ log.info(s"Docker build completed ${if(result.exitCode == 0) "successfully" else "unsuccessfully"} with ${result.exitCode}")
+ tags()
+ }
+
+ final def push() = T.command {
+ val tags = build()
+ tags.foreach(t => os.proc("docker", "push", t).call(stdout = os.Inherit, stderr = os.Inherit))
+ }
+ }
+} \ No newline at end of file
diff --git a/contrib/playlib/src/mill/playlib/RouterModule.scala b/contrib/playlib/src/mill/playlib/RouterModule.scala
index 5af87839..c02bd7e1 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..36eb40ef 100644
--- a/docs/pages/9 - Contrib Modules.md
+++ b/docs/pages/9 - Contrib Modules.md
@@ -99,6 +99,50 @@ object project extends BuildInfo {
* `def buildInfoPackageName: Option[String]`, default: `None`
The package name of the object.
+### Docker
+
+Automatically build docker images from your mill project.
+
+Requires the docker CLI to be installed.
+
+In the simplest configuration just extend `DockerModule` and declare a `DockerConfig` object.
+
+```scala
+import mill._, scalalib._
+
+import ivy`com.lihaoyi::mill-contrib-docker:VERSION`
+import contrib.docker.DockerModule
+
+object foo extends JavaModule with DockerModule {
+ object docker extends DockerConfig
+}
+```
+
+Then
+
+```
+$ mill foo.docker.build
+$ docker run foo
+```
+
+#### Configuration
+
+Configure the image by overriding tasks in the `DockerConfig` object
+
+```scala
+object docker extends DockerConfig {
+ // Override tags to set the output image name
+ def tags = List("aws_account_id.dkr.ecr.region.amazonaws.com/hello-repository")
+
+ def baseImage = "openjdk:11"
+
+ // Configure whether the docker build should check the remote registry for a new version of the base image before building.
+ // By default this is true if the base image is using a latest tag
+ def pullBaseImage = true
+}
+```
+
+Run mill in interactive mode to see the docker client output, like `mill -i foo.docker.build`.
## Flyway
@@ -543,6 +587,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 +809,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)
-