diff options
Diffstat (limited to 'contrib')
8 files changed, 376 insertions, 1 deletions
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) + } +} |