Skip to content

Commit

Permalink
Support aggregating Kover reports without a need for a dedicated module
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnm committed Sep 29, 2024
1 parent e7e144d commit 1330e2f
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 186 deletions.
17 changes: 17 additions & 0 deletions example/kotlinlib/linting/4-kover/bar/src/BarKotlin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package bar

fun action(one: Boolean, two: Boolean): String {
return if (one) {
if (two) {
"one, two"
} else {
"one"
}
} else {
if (two) {
"two"
} else {
"nothing"
}
}
}
10 changes: 10 additions & 0 deletions example/kotlinlib/linting/4-kover/bar/test/src/BarTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package bar

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class BarTests : FunSpec({
test("kotlin - success") {
action(true, true) shouldBe "one, two"
}
})
27 changes: 22 additions & 5 deletions example/kotlinlib/linting/4-kover/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import kotlinlib.contrib.kover.KoverModule

object `package` extends RootModule with KotlinModule with KoverModule {

def kotlinVersion = "1.9.24"

override def koverVersion = "0.8.3"

object test extends KotlinModuleTests with TestModule.Junit5 with KoverTests {
trait KotestTests extends TestModule.Junit5 {
override def forkArgs: T[Seq[String]] = Task {
super.forkArgs() ++ Seq("-Dkotest.framework.classpath.scanning.autoscan.disable=true")

Expand All @@ -18,6 +14,17 @@ object `package` extends RootModule with KotlinModule with KoverModule {
ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1"
)
}

def kotlinVersion = "1.9.24"

override def koverVersion = "0.8.3"

object test extends KotlinModuleTests with KotestTests with KoverTests

object bar extends KotlinModule with KoverModule {
def kotlinVersion = "1.9.24"
object test extends KotlinModuleTests with KotestTests with KoverTests
}
}

// This is a basic Mill build for a single `KotlinModule`, enhanced with
Expand All @@ -32,6 +39,8 @@ object `package` extends RootModule with KotlinModule with KoverModule {
// must first run the test.
// `./mill test` then `./mill show kover.htmlReport` and get your
// coverage in HTML format.
// Also reports for all modules can be collected in a single place by
// running `./mill show mill.kotlinlib.contrib.kover.Kover/htmlReportAll`.

/** Usage

Expand All @@ -54,4 +63,12 @@ kover.xmlReport
...
...Kover HTML Report: Overall Coverage Summary...

> ./mill show mill.kotlinlib.contrib.kover.Kover/htmlReportAll # collect reports from all modules
...
...out/mill/kotlinlib/contrib/kover/Kover/htmlReportAll.dest/kover-report...

> cat out/mill/kotlinlib/contrib/kover/Kover/htmlReportAll.dest/kover-report/index.html
...
...Kover HTML Report: Overall Coverage Summary...

*/
134 changes: 128 additions & 6 deletions kotlinlib/src/mill/kotlinlib/contrib/kover/KoverModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@
package mill
package kotlinlib.contrib.kover

import mill.api.PathRef
import mill.api.{Loose, PathRef}
import mill.api.Result.Success
import mill.define.{Discover, ExternalModule}
import mill.eval.Evaluator
import mill.kotlinlib.contrib.kover.ReportType.{Html, Xml}
import mill.kotlinlib.{Dep, DepSyntax, KotlinModule, TestModule, Versions}
import mill.resolve.{Resolve, SelectMode}
import mill.scalalib.api.CompilationResult
import mill.util.Jvm
import os.Path

import java.util.Locale

/**
* Adds targets to a [[mill.kotlinlib.KotlinModule]] to create test coverage reports.
Expand All @@ -34,9 +42,9 @@ import mill.kotlinlib.{Dep, DepSyntax, KotlinModule, TestModule, Versions}
* In addition to the normal tasks available to your Kotlin module, Kover
* Module introduce a few new tasks and changes the behavior of an existing one.
*
* - mill foo.test # tests your project and collects metrics on code coverage
* - mill foo.kover.htmlReport # uses the metrics collected by a previous test run to generate a coverage report in html format
* - mill foo.kover.xmlReport # uses the metrics collected by a previous test run to generate a coverage report in xml format
* - ./mill foo.test # tests your project and collects metrics on code coverage
* - ./mill foo.kover.htmlReport # uses the metrics collected by a previous test run to generate a coverage report in html format
* - ./mill foo.kover.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/kover/koverDataDir.dest/`,
* the html report is saved in `out/foo/kover/htmlReport.dest/`,
Expand All @@ -62,7 +70,7 @@ trait KoverModule extends KotlinModule { outer =>
reportType: ReportType
): Task[PathRef] = Task.Anon {
val reportPath = PathRef(T.dest).path / reportName
KoverReport.runKoverCli(
Kover.runKoverCli(
sourcePaths = outer.allSources().map(_.path),
compiledPaths = Seq(outer.compile().classes.path),
binaryReportsPaths = Seq(outer.koverBinaryReport().path),
Expand All @@ -71,7 +79,6 @@ trait KoverModule extends KotlinModule { outer =>
koverCliClasspath().map(_.path),
T.dest
)
PathRef(reportPath)
}

def htmlReport(): Command[PathRef] = Task.Command { doReport(Html)() }
Expand Down Expand Up @@ -105,3 +112,118 @@ trait KoverModule extends KotlinModule { outer =>
}
}
}

/**
* Allows the aggregation of coverage reports across multi-module projects.
*
* Once tests have been run across all modules, this collects reports from
* all modules that extend [[KoverModule]].
*
* - ./mill __.test # run tests for all modules
* - ./mill mill.kotlinlib.contrib.kover.Kover/htmlReportAll # generates report in html format for all modules
* - ./mill mill.kotlinlib.contrib.kover.Kover/xmlReportAll # generates report in xml format for all modules
*
* The aggregated report will be available at either `out/mill/kotlinlib/contrib/kover/Kover/htmlReportAll.dest/`
* for html reports or `out/mill/kotlinlib/contrib/kover/Kover/xmlReportAll.dest/` for xml reports.
*/
object Kover extends ExternalModule with KoverReportBaseModule {

lazy val millDiscover: Discover = Discover[this.type]

def htmlReportAll(evaluator: Evaluator): Command[PathRef] = Task.Command {
koverReportTask(
evaluator = evaluator,
reportType = ReportType.Html
)()
}

def xmlReportAll(evaluator: Evaluator): Command[PathRef] = Task.Command {
koverReportTask(
evaluator = evaluator,
reportType = ReportType.Xml
)()
}

private def koverReportTask(
evaluator: mill.eval.Evaluator,
sources: String = "__:KotlinModule:^TestModule.allSources",
compiled: String = "__:KotlinModule:^TestModule.compile",
binaryReports: String = "__.koverBinaryReport",
reportType: ReportType
): Task[PathRef] = {
val sourcesTasks: Seq[Task[Seq[PathRef]]] = resolveTasks(sources, evaluator)
val compiledTasks: Seq[Task[CompilationResult]] = resolveTasks(compiled, evaluator)
val binaryReportTasks: Seq[Task[PathRef]] = resolveTasks(binaryReports, evaluator)

Task.Anon {

val sourcePaths: Seq[Path] =
T.sequence(sourcesTasks)().flatten.map(_.path).filter(
os.exists
)
val compiledPaths: Seq[Path] =
T.sequence(compiledTasks)().map(_.classes.path).filter(
os.exists
)
val binaryReportsPaths: Seq[Path] =
T.sequence(binaryReportTasks)().map(_.path).filter(
os.exists
)

val reportDir = PathRef(T.dest).path / reportName

runKoverCli(
sourcePaths,
compiledPaths,
binaryReportsPaths,
reportDir,
reportType,
koverCliClasspath().map(_.path),
T.dest
)
}
}

private[kover] def runKoverCli(
sourcePaths: Seq[Path],
compiledPaths: Seq[Path],
binaryReportsPaths: Seq[Path],
// will be treated as a dir in case of HTML, and as file in case of XML
reportPath: Path,
reportType: ReportType,
classpath: Loose.Agg[Path],
workingDir: os.Path
)(implicit ctx: api.Ctx): PathRef = {
val args = Seq.newBuilder[String]
args += "report"
args ++= binaryReportsPaths.map(_.toString())

args ++= sourcePaths.flatMap(path => Seq("--src", path.toString()))
args ++= compiledPaths.flatMap(path => Seq("--classfiles", path.toString()))
val output = if (reportType == Xml) {
s"${reportPath.toString()}.xml"
} else reportPath.toString()
args ++= Seq(s"--${reportType.toString.toLowerCase(Locale.US)}", output)
Jvm.runSubprocess(
mainClass = "kotlinx.kover.cli.MainKt",
classPath = classpath,
jvmArgs = Seq.empty[String],
mainArgs = args.result(),
workingDir = workingDir
)
PathRef(os.Path(output))
}

private def resolveTasks[T](tasks: String, evaluator: Evaluator): Seq[Task[T]] =
if (tasks.trim().isEmpty) Seq.empty
else Resolve.Tasks.resolve(evaluator.rootModule, Seq(tasks), SelectMode.Multi) match {
case Left(err) => throw new Exception(err)
case Right(tasks) => tasks.asInstanceOf[Seq[Task[T]]]
}
}

sealed trait ReportType
object ReportType {
final case object Html extends ReportType
final case object Xml extends ReportType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Some parts of this code are taken from lefou/mill-jacoco. Copyright 2021-Present Tobias Roeser.
*/

package mill
package kotlinlib.contrib.kover

import mill.api.Result.Success
import mill.api.{Loose, PathRef}
import mill.kotlinlib.{Dep, DepSyntax, Versions}
import mill.scalalib.CoursierModule

trait KoverReportBaseModule extends CoursierModule {

private[kover] val reportName = "kover-report"

/**
* Reads the Kover version from system environment variable `KOVER_VERSION` or defaults to a hardcoded version.
*/
def koverVersion: T[String] = Task.Input {
Success[String](T.env.getOrElse("KOVER_VERSION", Versions.koverVersion))
}

def koverCliDep: Target[Agg[Dep]] = T {
Agg(ivy"org.jetbrains.kotlinx:kover-cli:${koverVersion()}")
}

/**
* Classpath for running Kover.
*/
def koverCliClasspath: T[Loose.Agg[PathRef]] = T {
defaultResolver().resolveDeps(koverCliDep())
}
}
Loading

0 comments on commit 1330e2f

Please # to comment.