diff --git a/example/kotlinlib/linting/4-kover/bar/src/BarKotlin.kt b/example/kotlinlib/linting/4-kover/bar/src/BarKotlin.kt new file mode 100644 index 00000000000..04d5028ab8a --- /dev/null +++ b/example/kotlinlib/linting/4-kover/bar/src/BarKotlin.kt @@ -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" + } + } +} diff --git a/example/kotlinlib/linting/4-kover/bar/test/src/BarTests.kt b/example/kotlinlib/linting/4-kover/bar/test/src/BarTests.kt new file mode 100644 index 00000000000..9d68a854514 --- /dev/null +++ b/example/kotlinlib/linting/4-kover/bar/test/src/BarTests.kt @@ -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" + } +}) diff --git a/example/kotlinlib/linting/4-kover/build.mill b/example/kotlinlib/linting/4-kover/build.mill index ec85c519e34..809dfdaf753 100644 --- a/example/kotlinlib/linting/4-kover/build.mill +++ b/example/kotlinlib/linting/4-kover/build.mill @@ -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") @@ -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 @@ -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 @@ -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... + */ diff --git a/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverModule.scala b/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverModule.scala index 8c3a004a527..ba661eaf327 100644 --- a/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverModule.scala +++ b/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverModule.scala @@ -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. @@ -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/`, @@ -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), @@ -71,7 +79,6 @@ trait KoverModule extends KotlinModule { outer => koverCliClasspath().map(_.path), T.dest ) - PathRef(reportPath) } def htmlReport(): Command[PathRef] = Task.Command { doReport(Html)() } @@ -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 +} diff --git a/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverReportBaseModule.scala b/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverReportBaseModule.scala new file mode 100644 index 00000000000..03e61980bb8 --- /dev/null +++ b/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverReportBaseModule.scala @@ -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()) + } +} diff --git a/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverReportModule.scala b/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverReportModule.scala deleted file mode 100644 index ffbd8539382..00000000000 --- a/kotlinlib/src/mill/kotlinlib/contrib/kover/KoverReportModule.scala +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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.define.{Discover, ExternalModule} -import mill.eval.Evaluator -import mill.kotlinlib.contrib.kover.ReportType.Html -import mill.kotlinlib.{Dep, DepSyntax, Versions} -import mill.resolve.{Resolve, SelectMode} -import mill.scalalib.CoursierModule -import mill.scalalib.api.CompilationResult -import mill.util.Jvm -import os.Path - -import java.util.Locale - -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()) - } -} - -/** - * 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]]. Simply - * define a module that extends [[KoverReportModule]] and - * call one of the available "report all" functions. - * - * For example, define the following `kover` module and use the relevant - * reporting option to generate a report: - * {{{ - * object kover extends KoverReportModule - * }}} - * - * - mill __.test # run tests for all modules - * - mill kover.htmlReportAll # generates report in html format for all modules - * - mill kover.xmlReportAll # generates report in xml format for all modules - * - * The aggregated report will be available at either `out/kover/htmlReportAll.dest/` - * for html reports or `out/kover/xmlReportAll.dest/` for xml reports. - */ -trait KoverReportModule extends CoursierModule with KoverReportBaseModule { - - 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 - )() - } - - def koverReportTask( - evaluator: mill.eval.Evaluator, - sources: String = "__:KotlinModule:^TestModule.allSources", - compiled: String = "__:KotlinModule:^TestModule.compile", - binaryReports: String = "__.koverBinaryReport", - reportType: ReportType = Html - ): 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 - - KoverReport.runKoverCli( - sourcePaths, - compiledPaths, - binaryReportsPaths, - reportDir, - reportType, - koverCliClasspath().map(_.path), - T.dest - ) - PathRef(reportDir) - } - } - - 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]]] - } -} - -private[kover] object KoverReport extends ExternalModule with KoverReportBaseModule { - - lazy val millDiscover: Discover = Discover[this.type] - - 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): Unit = { - 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())) - args ++= Seq(s"--${reportType.toString.toLowerCase(Locale.US)}", reportPath.toString()) - Jvm.runSubprocess( - mainClass = "kotlinx.kover.cli.MainKt", - classPath = classpath, - jvmArgs = Seq.empty[String], - mainArgs = args.result(), - workingDir = workingDir - ) - } -} - -sealed trait ReportType -object ReportType { - final case object Html extends ReportType - final case object Xml extends ReportType -} diff --git a/kotlinlib/test/src/mill/kotlinlib/contrib/kover/KoverModuleTests.scala b/kotlinlib/test/src/mill/kotlinlib/contrib/kover/KoverModuleTests.scala index e1f0dbd8bad..f280d88626f 100644 --- a/kotlinlib/test/src/mill/kotlinlib/contrib/kover/KoverModuleTests.scala +++ b/kotlinlib/test/src/mill/kotlinlib/contrib/kover/KoverModuleTests.scala @@ -39,8 +39,6 @@ object KoverModuleTests extends TestSuite { def kotlinVersion = "1.9.24" object test extends KotlinModuleTests with module.KotestTestModule } - - object kover extends KoverReportModule } def tests: Tests = Tests { @@ -61,14 +59,13 @@ object KoverModuleTests extends TestSuite { ) ) - val Right(result) = eval(module.kover.xmlReportAll(eval.evaluator)) + val Right(result) = eval(Kover.xmlReportAll(eval.evaluator)) val xmlReportPath = result.value.path assert(os.exists(xmlReportPath)) - val relPath = xmlReportPath.segments.toVector.dropRight(1).takeRight(3) - assert(relPath.head == "out") - assert(relPath(1) == "kover") - assert(relPath(2) == "xmlReportAll.dest") + val relPath = xmlReportPath.segments.toVector.takeRight(2) + assert(relPath.head == "xmlReportAll.dest") + assert(relPath.last == "kover-report.xml") val xmlReport = XML.loadFile(xmlReportPath.toString) @@ -81,7 +78,7 @@ object KoverModuleTests extends TestSuite { } - test("report") { + test("report-xml") { val eval = UnitTester(module, resourcePath) @@ -91,12 +88,14 @@ object KoverModuleTests extends TestSuite { val xmlReportPath = result.value.path assert(os.exists(xmlReportPath)) + assert(os.isFile(xmlReportPath)) // drop report name - val relPath = xmlReportPath.segments.toVector.dropRight(1).takeRight(3) + val relPath = xmlReportPath.segments.toVector.takeRight(4) assert(relPath.head == "foo") assert(relPath(1) == "kover") assert(relPath(2) == "xmlReport.dest") + assert(relPath(3) == "kover-report.xml") val xmlReport = XML.loadFile(xmlReportPath.toString) @@ -108,6 +107,28 @@ object KoverModuleTests extends TestSuite { assert(packageNameChildNode(xmlReport, "qux").isEmpty) } + + test("report-html") { + + val eval = UnitTester(module, resourcePath) + + val Right(_) = eval(module.foo.test.test()) + + val Right(result) = eval(module.foo.kover.htmlReport()) + + val htmlReportPath = result.value.path + assert(os.exists(htmlReportPath)) + assert(os.isDir(htmlReportPath)) + assert(os.walk(htmlReportPath) + .exists(p => p.ext == "html")) + + // drop report name + val relPath = htmlReportPath.segments.toVector.takeRight(4) + assert(relPath.head == "foo") + assert(relPath(1) == "kover") + assert(relPath(2) == "htmlReport.dest") + assert(relPath(3) == "kover-report") + } } private def instructionsCovered = (node: Node) => {