All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mill.contrib.scoverage.ScoverageModule.scala Maven / Gradle / Ivy

The newest version!
package mill.contrib.scoverage

import coursier.Repository
import mill._
import mill.api.{Loose, PathRef, Result}
import mill.contrib.scoverage.api.ScoverageReportWorkerApi2.ReportType
import mill.main.BuildInfo
import mill.scalalib.api.ZincWorkerUtil
import mill.scalalib.{Dep, DepSyntax, JavaModule, ScalaModule}
import mill.util.Util.millProjectModule

/**
 * 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
 * extend 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.13.15"
 *   def scoverageVersion = "2.1.1"
 *
 *   object test extends ScoverageTests {
 *     def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.2.19")
 *     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
 * - mill foo.scoverage.xmlReport    # uses the metrics collected by a previous test run to generate a coverage report in xml format
 *
 * The measurement data by default is available at `out/foo/scoverage/dataDir.dest/`,
 * the html report is saved in `out/foo/scoverage/htmlReport.dest/`,
 * and the xml report is saved in `out/foo/scoverage/xmlReport.dest/`.
 */
trait ScoverageModule extends ScalaModule { outer: ScalaModule =>

  /**
   * The Scoverage version to use.
   */
  def scoverageVersion: T[String]

  private def isScala3: Task[Boolean] = Task.Anon { ZincWorkerUtil.isScala3(outer.scalaVersion()) }

  def scoverageRuntimeDeps: T[Agg[Dep]] = Task {
    if (isScala3()) {
      Agg.empty
    } else {
      Agg(ivy"org.scoverage::scalac-scoverage-runtime:${outer.scoverageVersion()}")
    }
  }

  def scoveragePluginDeps: T[Agg[Dep]] = Task {
    val sv = scoverageVersion()
    if (isScala3()) {
      Agg.empty
    } else {
      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}"
      )
    }
  }

  private def checkVersions = Task.Anon {
    val sv = scalaVersion()
    val isSov2 = scoverageVersion().startsWith("2.")
    (sv.split('.'), isSov2) match {
      case (_, false) =>
        Result.Failure("Scoverage 1.x is no longer supported. Please use Scoverage 2.x")
      case (Array("3", "0" | "1", _*), _) => Result.Failure(
          "Scala 3.0 and 3.1 is not supported by Scoverage. You have to update to at least Scala 3.2"
        )
      case _ =>
    }
  }

  private def scoverageReporterIvyDeps: T[Agg[Dep]] = Task {
    checkVersions()

    val sv = scoverageVersion()
    val millScalaVersion = BuildInfo.scalaVersion

    // we need to resolve with same Scala version used for Mill, not the project Scala version
    val scalaBinVersion = ZincWorkerUtil.scalaBinaryVersion(millScalaVersion)
    // In Scoverage 2.x, the reporting API is no longer bundled in the plugin jar
    Agg(
      ivy"org.scoverage:scalac-scoverage-domain_${scalaBinVersion}:${sv}",
      ivy"org.scoverage:scalac-scoverage-serializer_${scalaBinVersion}:${sv}",
      ivy"org.scoverage:scalac-scoverage-reporter_${scalaBinVersion}:${sv}"
    )
  }

  def scoverageToolsClasspath: T[Agg[PathRef]] = Task {
    scoverageReportWorkerClasspath() ++
      defaultResolver().resolveDeps(scoverageReporterIvyDeps())
  }

  def scoverageClasspath: T[Agg[PathRef]] = Task {
    defaultResolver().resolveDeps(scoveragePluginDeps())
  }

  def scoverageReportWorkerClasspath: T[Agg[PathRef]] = Task {
    val workerArtifact = "mill-contrib-scoverage-worker2"

    millProjectModule(
      workerArtifact,
      repositoriesTask(),
      resolveFilter = _.toString.contains(workerArtifact)
    )
  }

  /** Inner worker module. This is not an `object` to allow users to override and customize it. */
  lazy val scoverage: ScoverageData = new ScoverageData {}

  trait ScoverageData extends ScalaModule {

    def doReport(reportType: ReportType): Task[Unit] = Task.Anon {
      ScoverageReportWorker
        .scoverageReportWorker()
        .bridge(scoverageToolsClasspath())
        .report(reportType, allSources().map(_.path), Seq(data().path), T.workspace)
    }

    /**
     * The persistent data dir used to store scoverage coverage data.
     * Use to store coverage data at compile-time and by the various report targets.
     */
    def data: T[PathRef] = Task(persistent = true) {
      // via the persistent target, we ensure, the dest dir doesn't get cleared
      PathRef(T.dest)
    }

    override def compileResources: T[Seq[PathRef]] = outer.compileResources
    override def generatedSources: T[Seq[PathRef]] = Task { outer.generatedSources() }
    override def allSources: T[Seq[PathRef]] = Task { outer.allSources() }
    override def moduleDeps: Seq[JavaModule] = outer.moduleDeps
    override def compileModuleDeps: Seq[JavaModule] = outer.compileModuleDeps
    override def sources: T[Seq[PathRef]] = Task { outer.sources() }
    override def resources: T[Seq[PathRef]] = Task { outer.resources() }
    override def scalaVersion = Task { outer.scalaVersion() }
    override def repositoriesTask: Task[Seq[Repository]] = Task.Anon { outer.repositoriesTask() }
    override def compileIvyDeps: T[Agg[Dep]] = Task { outer.compileIvyDeps() }
    override def ivyDeps: T[Agg[Dep]] =
      Task { outer.ivyDeps() ++ outer.scoverageRuntimeDeps() }
    override def unmanagedClasspath: T[Agg[PathRef]] = Task { outer.unmanagedClasspath() }

    /** Add the scoverage scalac plugin. */
    override def scalacPluginIvyDeps: T[Loose.Agg[Dep]] =
      Task { outer.scalacPluginIvyDeps() ++ outer.scoveragePluginDeps() }

    /** Add the scoverage specific plugin settings (`dataDir`). */
    override def scalacOptions: T[Seq[String]] =
      Task {
        val extras =
          if (isScala3()) {
            Seq(
              s"-coverage-out:${data().path.toIO.getPath()}",
              s"-sourceroot:${T.workspace}"
            )
          } else {
            Seq(
              s"-P:scoverage:dataDir:${data().path.toIO.getPath()}",
              s"-P:scoverage:sourceRoot:${T.workspace}"
            )
          }

        outer.scalacOptions() ++ extras
      }

    def htmlReport(): Command[Unit] = Task.Command { doReport(ReportType.Html)() }
    def xmlReport(): Command[Unit] = Task.Command { doReport(ReportType.Xml)() }
    def xmlCoberturaReport(): Command[Unit] = Task.Command { doReport(ReportType.XmlCobertura)() }
    def consoleReport(): Command[Unit] = Task.Command { doReport(ReportType.Console)() }

    override def skipIdea = true
  }

  trait ScoverageTests extends ScalaTests {

    /**
     * Alter classpath from upstream modules by replacing in-place outer module
     * classes folder by the outer.scoverage classes folder and adding the
     * scoverage runtime dependency.
     */
    override def runClasspath: T[Seq[PathRef]] = Task {
      val outerClassesPath = outer.compile().classes
      val outerScoverageClassesPath = outer.scoverage.compile().classes
      (super.runClasspath().map { path =>
        if (outerClassesPath == path) outerScoverageClassesPath else path
      } ++ defaultResolver().resolveDeps(outer.scoverageRuntimeDeps())).distinct
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy