Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3047847
Starting to add the validation for coverage minimums.
monksy Oct 20, 2025
411175c
Added more definition and added a check to see if at least one min wa…
monksy Oct 20, 2025
ba780eb
Got everything up and compiling.
monksy Oct 21, 2025
311e755
Added todo
monksy Oct 21, 2025
d7ea5f6
Fixed formatting.
monksy Oct 21, 2025
e6336ef
Check or throws exception.
monksy Oct 21, 2025
9a9ec5c
Cleanup unused args and unused code.
monksy Oct 21, 2025
8068b3f
Added Documentation.
monksy Oct 21, 2025
45cee73
fixed formatting
monksy Oct 21, 2025
569abd8
The commit adds a test ensuring `validateCoverageMinimums` fails when…
monksy Oct 22, 2025
83de44e
Added new test to validate a failure condition that when a coverage m…
monksy Oct 22, 2025
7dd636a
test: add test for validateCoverageMinimums with statement coverage min
monksy Oct 22, 2025
d984392
Merge branch 'main' into feature/UpdateScalaCoverage-Metrics
monksy Oct 22, 2025
618f785
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 22, 2025
1986930
Merge branch 'main' into feature/UpdateScalaCoverage-Metrics
monksy Oct 22, 2025
06d5d1f
Added documentation about the new option.
monksy Oct 22, 2025
4b93095
Merge branch 'main' into feature/UpdateScalaCoverage-Metrics
monksy Oct 25, 2025
ef98e9e
Merge branch 'main' into feature/UpdateScalaCoverage-Metrics
monksy Oct 25, 2025
79866ef
Merge remote-tracking branch 'origin/feature/UpdateScalaCoverage-Metr…
monksy Oct 25, 2025
039e6ea
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 25, 2025
9244c5d
Added a call to the validate coverage minimum.
monksy Oct 27, 2025
c9423e4
Merge remote-tracking branch 'origin/feature/UpdateScalaCoverage-Metr…
monksy Oct 27, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ public String folderName() {

void report(ReportType reportType, Path[] sources, Path[] dataDirs, Path sourceRoot, Ctx ctx);

void validateCoverageMinimums(
Path[] dataDirs,
Path sourceRoot,
Double statementCoverageMin,
Double branchCoverageMin,
Ctx ctx);

static void makeAllDirs(Path path) throws IOException {
// Replicate behavior of `os.makeDir.all(path)`
if (Files.isDirectory(path) && Files.isSymbolicLink(path)) {
Expand Down
2 changes: 2 additions & 0 deletions contrib/scoverage/readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ modules introduce a few new tasks and changes the behavior of an existing one.
> 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
> mill foo.scoverage.xmlCoberturaReport # uses the metrics collected by a previous test run to generate a coverage report in Cobertura's xml format
> mill foo.scoverage.validateCoverageMinimums # This allows for you to use the metrics collected by a previous test run to validate if the coverage minimums have been set. To use this, define the functions `branchCoverageMin` and/or `statementCoverageMin` in the ScoverageModule.

----

The measurement data is by default available at `out/foo/scoverage/data/dest`,
Expand Down
37 changes: 32 additions & 5 deletions contrib/scoverage/src/mill/contrib/scoverage/ScoverageModule.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package mill.contrib.scoverage

import coursier.Repository
import mill._
import mill.api.{PathRef}
import mill.api.BuildCtx
import mill.api.{Result}
import mill.*
import mill.api.{BuildCtx, PathRef, Result}
import mill.contrib.scoverage.api.ScoverageReportWorkerApi2.ReportType
import mill.util.BuildInfo
import mill.javalib.api.JvmWorkerUtil
import mill.scalalib.{Dep, DepSyntax, JavaModule, ScalaModule}
import mill.util.BuildInfo

/**
* Adds tasks to a [[mill.scalalib.ScalaModule]] to create test coverage reports.
Expand Down Expand Up @@ -47,6 +45,8 @@ import mill.scalalib.{Dep, DepSyntax, JavaModule, ScalaModule}
* - 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
*
* - mill foo.scoverage.validateCoverageMinimums # This allows for you to use the metrics collected by a previous test run to validate if the coverage minimums have been set. To use this, define the functions `branchCoverageMin` and/or `statementCoverageMin` in the ScoverageModule.
*
* 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/`.
Expand All @@ -58,6 +58,9 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
*/
def scoverageVersion: T[String]

def branchCoverageMin: T[Option[Double]] = Task { None }
def statementCoverageMin: T[Option[Double]] = Task { None }

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

def scoverageRuntimeDeps: T[Seq[Dep]] = Task {
Expand Down Expand Up @@ -138,6 +141,21 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
.report(reportType, allSources().map(_.path), Seq(data().path), BuildCtx.workspaceRoot)
}

def validateCoverageMin(
statementCoverageMin: Option[Double],
branchCoverageMin: Option[Double]
): Task[Unit] = Task.Anon {
ScoverageReportWorker
.scoverageReportWorker()
.bridge(scoverageToolsClasspath())
.validateCoverageMinimums(
Seq(data().path),
BuildCtx.workspaceRoot,
statementCoverageMin.getOrElse(0.0),
branchCoverageMin.getOrElse(0.0)
)
}

/**
* The persistent data dir used to store scoverage coverage data.
* Use to store coverage data at compile-time and by the various report tasks.
Expand Down Expand Up @@ -190,6 +208,15 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
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)() }
def validateCoverageMinimums(): Command[Unit] = Task.Command {
Task.log.info("Validating the coverage minimums")
List(statementCoverageMin(), branchCoverageMin()).exists(_.isDefined) match {
case true => validateCoverageMin(statementCoverageMin(), branchCoverageMin())
case _ => Task.fail(
"Either statementCoverageMin or branchCoverageMin must be set in order to call the validateCoverageMinimums task."
)
}
}

override def skipIdea = true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package mill.contrib.scoverage

import mill.Task
import mill.api.{TaskCtx, PathRef}
import mill.api.{Discover, ExternalModule, PathRef, TaskCtx}
import mill.contrib.scoverage.ScoverageReportWorker.ScoverageReportWorkerApiBridge
import mill.contrib.scoverage.api.ScoverageReportWorkerApi2
import mill.api.{Discover, ExternalModule}

import ScoverageReportWorker.ScoverageReportWorkerApiBridge
import ScoverageReportWorkerApi2.ReportType
import ScoverageReportWorkerApi2.{Logger => ApiLogger}
import ScoverageReportWorkerApi2.{Ctx => ApiCtx}
import mill.contrib.scoverage.api.ScoverageReportWorkerApi2.{
ReportType,
Ctx as ApiCtx,
Logger as ApiLogger
}
import os.Path

class ScoverageReportWorker {

Expand All @@ -27,6 +28,21 @@ class ScoverageReportWorker {
}

new ScoverageReportWorkerApiBridge {
private def innerWorker[T](worker: ScoverageReportWorkerApi2 => T) = {
mill.util.Jvm.withClassLoader(
classpath.map(_.path).toVector,
getClass.getClassLoader
) { cl =>
val a = cl
.loadClass("mill.contrib.scoverage.worker.ScoverageReportWorkerImpl")
.getDeclaredConstructor()
.newInstance()
.asInstanceOf[api.ScoverageReportWorkerApi2]

worker(a)
}
}

override def report(
reportType: ReportType,
sources: Seq[os.Path],
Expand All @@ -35,30 +51,29 @@ class ScoverageReportWorker {
)(using
ctx: TaskCtx
): Unit = {
mill.util.Jvm.withClassLoader(
classpath.map(_.path).toVector,
getClass.getClassLoader
) { cl =>

val worker =
cl
.loadClass("mill.contrib.scoverage.worker.ScoverageReportWorkerImpl")
.getDeclaredConstructor()
.newInstance()
.asInstanceOf[api.ScoverageReportWorkerApi2]

worker.report(
reportType,
sources.map(_.toNIO).toArray,
dataDirs.map(_.toNIO).toArray,
sourceRoot.toNIO,
ctx0
)
}
innerWorker(_.report(
reportType,
sources.map(_.toNIO).toArray,
dataDirs.map(_.toNIO).toArray,
sourceRoot.toNIO,
ctx0
))
}

override def validateCoverageMinimums(
dataDirs: Seq[Path],
sourceRoot: Path,
statementCoverageMin: Double,
branchCoverageMin: Double
)(using ctx: TaskCtx): Unit = innerWorker(_.validateCoverageMinimums(
dataDirs.map(_.toNIO).toArray,
sourceRoot.toNIO,
statementCoverageMin,
branchCoverageMin,
ctx0
))
}
}

}

object ScoverageReportWorker extends ExternalModule {
Expand All @@ -73,6 +88,15 @@ object ScoverageReportWorker extends ExternalModule {
)(using
ctx: TaskCtx
): Unit

def validateCoverageMinimums(
dataDirs: Seq[os.Path],
sourceRoot: os.Path,
statementCoverageMin: Double,
branchCoverageMin: Double
)(using
ctx: TaskCtx
): Unit
}

def scoverageReportWorker: Task.Worker[ScoverageReportWorker] =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package mill.contrib.scoverage

import mill.*
import mill.api.Discover
import mill.api.daemon.ExecResult.Failure
import mill.contrib.buildinfo.BuildInfo
import mill.scalalib.{DepSyntax, ScalaModule, TestModule}
import mill.testkit.{TestRootModule, UnitTester}
import utest.*

trait HelloWorldInvalidValidateCoverageTest extends utest.TestSuite {
def testScalaVersion: String
def testScoverageVersion: String
def testScalatestVersion: String = "3.2.13"

val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "hello-world"

object InvalidCoverageCheck extends TestRootModule {
object core extends ScoverageModule with BuildInfo {
def scalaVersion = testScalaVersion
def scoverageVersion = testScoverageVersion

// Intentionally leave coverage minimums empty
def statementCoverageMin = None
def branchCoverageMin = None

override def moduleDeps = Seq.empty

def buildInfoPackageName = "bar"

override def buildInfoMembers = Seq(
BuildInfo.Value("scoverageVersion", scoverageVersion())
)
object test extends ScoverageTests with TestModule.ScalaTest {
override def mvnDeps = Seq(mvn"org.scalatest::scalatest:${testScalatestVersion}")
}
}

lazy val millDiscover = Discover[this.type]
}

def tests: utest.Tests = utest.Tests {
test("HelloWorldInvalidValidateCoverageTest") {
test("core") {
test("validateCoverageMinimums fails with no minimums") - UnitTester(
InvalidCoverageCheck,
resourcePath
).scoped { eval =>
val Left(Failure(msg)) =
eval.apply(InvalidCoverageCheck.core.scoverage.validateCoverageMinimums()): @unchecked

assert(
msg.equals(
"Either statementCoverageMin or branchCoverageMin must be set in order to call the validateCoverageMinimums task."
)
)
}
}
}
}
}

object HelloWorldInvalidValidateCoverageTest extends HelloWorldInvalidValidateCoverageTest {
override def testScalaVersion: String = sys.props.getOrElse("TEST_SCALA_VERSION", ???)
override def testScoverageVersion = sys.props.getOrElse("MILL_SCOVERAGE_VERSION", ???)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package mill.contrib.scoverage

import mill.*
import mill.api.Discover
import mill.contrib.buildinfo.BuildInfo
import mill.scalalib.{DepSyntax, ScalaModule, TestModule}
import mill.testkit.{TestRootModule, UnitTester}
import utest.*

trait HelloWorldStatementMinCoverageTest extends utest.TestSuite {
def testScalaVersion: String
def testScoverageVersion: String
def testScalatestVersion: String = "3.2.13"

val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "hello-world"

object ValidCoverageCheck extends TestRootModule {
object core extends ScoverageModule with BuildInfo {
def scalaVersion = testScalaVersion
def scoverageVersion = testScoverageVersion

// Set statement coverage minimum to 15%
def statementCoverageMin = Some(15.0)
def branchCoverageMin = None

override def moduleDeps = Seq.empty

def buildInfoPackageName = "bar"
override def buildInfoMembers = Seq(
BuildInfo.Value("scoverageVersion", scoverageVersion())
)
object test extends ScoverageTests with TestModule.ScalaTest {
override def mvnDeps = Seq(mvn"org.scalatest::scalatest:${testScalatestVersion}")
}
}

lazy val millDiscover = Discover[this.type]
}

def tests: utest.Tests = utest.Tests {
test("HelloWorldStatementMinCoverageTest") {
test("core") {
test("validateCoverageMinimums passes with statementCoverageMin set") - UnitTester(
ValidCoverageCheck,
resourcePath
).scoped { eval =>
val Right(result) =
eval.apply(ValidCoverageCheck.core.scoverage.validateCoverageMinimums()): @unchecked

// The task should pass since we've set the minimum coverage
assert(result.evalCount > 0)
}
}
}
}
}

object HelloWorldStatementMinCoverageTest extends HelloWorldStatementMinCoverageTest {
override def testScalaVersion: String = sys.props.getOrElse("TEST_SCALA_VERSION", ???)
override def testScoverageVersion = sys.props.getOrElse("MILL_SCOVERAGE_VERSION", ???)
}
Loading
Loading