Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Scoverage for Scala 3 #2016

Merged
merged 4 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 80 additions & 26 deletions contrib/scoverage/src/ScoverageModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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}
import mill.api.Result

/**
* Adds targets to a [[mill.scalalib.ScalaModule]] to create test coverage reports.
Expand Down Expand Up @@ -59,35 +60,61 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>

private def isScoverage2: Task[Boolean] = T.task { scoverageVersion().startsWith("2.") }

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

private def isScala2: Task[Boolean] = T.task { !isScala3() }

/** 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
T.log.error(
"scoverageRuntimeDep is no longer used. To customize your module, use scoverageRuntimeDeps."
)
val result: Result[Dep] = if (isScala3()) {
Result.Failure("When using Scala 3 there is no external runtime dependency")
} else {
scoverageRuntimeDeps().toIndexedSeq.head
}
result
}

def scoverageRuntimeDeps: T[Agg[Dep]] = T {
Agg(ivy"org.scoverage::scalac-scoverage-runtime:${outer.scoverageVersion()}")
if (isScala3()) {
Agg.empty
} else {
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
T.log.error(
"scoveragePluginDep is no longer used. To customize your module, use scoverageRuntimeDeps."
)
val result: Result[Dep] = if (isScala3()) {
Result.Failure("When using Scala 3 there is no external plugin dependency")
} else {
scoveragePluginDeps().toIndexedSeq.head
}
result
}

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}"
)
if (isScala3()) {
Agg.empty
} else {
Agg(ivy"org.scoverage:::scalac-scoverage-plugin:${sv}")
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}")
}
}
}

Expand All @@ -102,17 +129,22 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
// 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}"
)

val baseDeps = 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}"
)

val pluginDep =
Agg(ivy"org.scoverage:scalac-scoverage-plugin_${mill.BuildInfo.scalaVersion}:${sv}")

if (isScala3() && isScoverage2()) {
baseDeps
} else if (isScoverage2()) {
baseDeps ++ pluginDep
} else {
Agg(
ivy"org.scoverage:scalac-scoverage-plugin_${mill.BuildInfo.scalaVersion}:${sv}"
)
pluginDep
}
})()
}
Expand Down Expand Up @@ -141,8 +173,23 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
}

val scoverage: ScoverageData = new ScoverageData(implicitly)

class ScoverageData(ctx0: mill.define.Ctx) extends Module()(ctx0) with ScalaModule {

/** Coverage in the Scala 3 compilier is only supported in the 3.2.x series and above. */
private def isSupportedScala: Task[Boolean] = T.task {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'm having a hard time with is figuring out where to put this check. At first I thought it'd be better to put it up above and in every task that computes something I could use like

    private def ensureValidScala3[A](value: A): Task[A] = T.task {
      val scalaVersion = outer.scalaVersion()
      scalaVersion.split('.') match {
        case Array(major, minor, patch) if major == "3" && minor.toIntOption.exists(_ >= 2) => value
        case Array(major, minor, patch) if major == "3" =>
          val msg =
            s"Detected Scala version: ${scalaVersion}. However, to use Coverage with Scala 3 you must be at least on Scala 3.2.0."
          Result.Failure(msg)
        case _ => value
      }
    }

So then I could just do like:

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

But that fell apart when I hit scoveragteToolsClasspath and couldn't get it to work.

Then I moved this down below into ScoverageData and I thought about adding an assert in here to check for an invalid Scala 3, but can't really access the scalaVersion in the assert. I'm not really sure where to do this check. Any input on where you'd think this would be best and how to do it?

Copy link
Contributor Author

@ckipp01 ckipp01 Sep 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moreover this should also check that if this is a valid Scala 3 version then the user must also be using scoverage 2.x. It'd be nice to do this check in one place.

Copy link
Member

@lefou lefou Sep 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check is best placed in scoverageToolsClasspath. It has access to all required data and is run before any worker action. Also, as there can be no valid classpath created in case we have an incompatible Scala version or an inappropriate scoverage version, this is the best place for this validation.

val scalaVersion = outer.scalaVersion()
scalaVersion.split('.') match {
case Array(major, minor, patch) if major == "3" && minor.toIntOption.exists(_ >= 2) => true
case Array(major, minor, patch) if major == "3" =>
T.log.error(
s"Detected Scala version: ${scalaVersion}. However, to use Coverage with Scala 3 you must be at least on Scala 3.2.0."
)
false
case _ => true
}
}

def doReport(reportType: ReportType): Task[Unit] = T.task {
ScoverageReportWorker
.scoverageReportWorker()
Expand Down Expand Up @@ -180,9 +227,16 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
/** 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()}") ++
(if (isScoverage2()) Seq(s"-P:scoverage:sourceRoot:${T.workspace}") else Seq())
val extras =
if (isScala3()) {
Seq(s"-coverage-out:${data().path.toIO.getPath()}")
} else {
val base = s"-P:scoverage:dataDir:${data().path.toIO.getPath()}"
if (isScoverage2()) Seq(base, s"-P:scoverage:sourceRoot:${T.workspace}")
else Seq(base)
}

outer.scalacOptions() ++ extras
}

def htmlReport(): Command[Unit] = T.command { doReport(ReportType.Html) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import org.scalatest._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

class GreetSpec extends WordSpec with Matchers {
class GreetSpec extends AnyWordSpec with Matchers {
"Greet" should {
"work" in {
Greet.greet("Nik", None) shouldBe ("Hello, Nik!")
Expand Down
90 changes: 68 additions & 22 deletions contrib/scoverage/test/src/HelloWorldTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package mill.contrib.scoverage
import mill._
import mill.contrib.buildinfo.BuildInfo
import mill.scalalib.{DepSyntax, ScalaModule, TestModule}
import mill.scalalib.api.ZincWorkerUtil
import mill.util.{TestEvaluator, TestUtil}
import utest._
import utest.framework.TestPath
Expand Down Expand Up @@ -96,8 +97,13 @@ trait HelloWorldTests extends utest.TestSuite {
val Right((result, evalCount)) =
eval.apply(HelloWorld.core.scoverage.ivyDeps)

val expected = if (ZincWorkerUtil.isScala3(testScalaVersion)) Agg.empty
else Agg(
ivy"org.scoverage::scalac-scoverage-runtime:${testScoverageVersion}"
)

assert(
result == Agg(ivy"org.scoverage::scalac-scoverage-runtime:${testScoverageVersion}"),
result == expected,
evalCount > 0
)
}
Expand All @@ -117,14 +123,19 @@ trait HelloWorldTests extends utest.TestSuite {
"scoverage2x" - workspaceTest(HelloWorld) { eval =>
val Right((result, evalCount)) =
eval.apply(HelloWorld.core.scoverage.scalacPluginIvyDeps)

val expected = if (ZincWorkerUtil.isScala3(testScalaVersion)) Agg.empty
else
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}"
)

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}"
),
result == expected,
evalCount > 0
)
} else "skipped"
Expand Down Expand Up @@ -161,27 +172,55 @@ trait HelloWorldTests extends utest.TestSuite {
val Right((result, evalCount)) =
eval.apply(HelloWorld.core.scoverage.upstreamAssemblyClasspath)

assert(
result.map(_.toString).iterator.exists(_.contains("scalac-scoverage-runtime")),
evalCount > 0
)
val runtimeExistsOnClasspath =
result.map(_.toString).iterator.exists(_.contains("scalac-scoverage-runtime"))
if (ZincWorkerUtil.isScala3(testScalaVersion)) {
assert(
!runtimeExistsOnClasspath,
evalCount > 0
)
} else {
assert(
runtimeExistsOnClasspath,
evalCount > 0
)
}
}
"compileClasspath" - workspaceTest(HelloWorld) { eval =>
val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.compileClasspath)

assert(
result.map(_.toString).iterator.exists(_.contains("scalac-scoverage-runtime")),
evalCount > 0
)
val runtimeExistsOnClasspath =
result.map(_.toString).iterator.exists(_.contains("scalac-scoverage-runtime"))
if (ZincWorkerUtil.isScala3(testScalaVersion)) {
assert(
!runtimeExistsOnClasspath,
evalCount > 0
)
} else {
assert(
runtimeExistsOnClasspath,
evalCount > 0
)
}
}
// TODO: document why we disable for Java9+
"runClasspath" - workspaceTest(HelloWorld) { eval =>
val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.runClasspath)

assert(
result.map(_.toString).exists(_.contains("scalac-scoverage-runtime")),
evalCount > 0
)
val runtimeExistsOnClasspath =
result.map(_.toString).iterator.exists(_.contains("scalac-scoverage-runtime"))

if (ZincWorkerUtil.isScala3(testScalaVersion)) {
assert(
!runtimeExistsOnClasspath,
evalCount > 0
)
} else {
assert(
runtimeExistsOnClasspath,
evalCount > 0
)
}
}
}
}
Expand Down Expand Up @@ -212,19 +251,26 @@ object HelloWorldTests_2_12 extends HelloWorldTests {
override def threadCount = Some(1)
override def testScalaVersion: String = sys.props.getOrElse("MILL_SCALA_2_12_VERSION", ???)
override def testScoverageVersion = sys.props.getOrElse("MILL_SCOVERAGE_VERSION", ???)
override def testScalatestVersion = "3.0.8"
override def testScalatestVersion = "3.2.13"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up having to bump all these because 3.0.8 doesn't support Scala 3.

}

object HelloWorldTests_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_SCOVERAGE_VERSION", ???)
override def testScalatestVersion = "3.0.8"
override def testScalatestVersion = "3.2.13"
}

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"
override def testScalatestVersion = "3.2.13"
}

object Scoverage2Tests_3 extends HelloWorldTests {
override def threadCount = Some(1)
override def testScalaVersion: String = "3.2.0"
override def testScoverageVersion = sys.props.getOrElse("MILL_SCOVERAGE2_VERSION", ???)
override def testScalatestVersion = "3.2.13"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import mill.api.Ctx
import mill.contrib.scoverage.api.ScoverageReportWorkerApi.ReportType

/**
* Scoverage Worker for Scoverage 1.x
* Scoverage Worker for Scoverage 2.x
*/
class ScoverageReportWorkerImpl extends ScoverageReportWorkerApi {

Expand Down