Tom Koptel
04/03/2024, 2:02 PMsrc/main
source sets and jacocoTestReport
reports 0% coverage. Any ideas why this happens and how to address?
I am relying on jacoco-gradle-testkit-plugin which follows the same strategy suggested by @Vampire on Jacoco & Gradle Test Kit with Java thread. During the test execution the file called gradle.properties
is created under the GradleRunner.projectDir
.
#Generated by pl.droidsonroids.jacoco.testkit
org.gradle.jvmargs="-javaagent\:/Users/user/.gradle/caches/modules-2/files-2.1/org.jacoco/org.jacoco.agent/0.8.11/d5287cced3d0afd0cfa0be7f84773ea811836f21/org.jacoco.agent-0.8.11-runtime.jar\=destfile\=/Users/user/work/myproject/build-logic/android/build/jacoco/test.exec"
I was trying to figure out what might have caused regression between 8.6 and 8.7. So far I suspect [#27328] - Use artifact transforms to instrument TestKit injected classpath.
My build.gradle.kts
plugins {
`kotlin-dsl`
id("java-gradle-plugin")
id("groovy-gradle-plugin")
jacoco
id("pl.droidsonroids.jacoco.testkit")
}
tasks.test {
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport {
reports {
xml.required.set(true)
csv.required.set(false)
}
dependsOn(tasks.test)
}
tasks.jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = "0.49".toBigDecimal()
}
}
}
dependsOn(tasks.jacocoTestReport)
}
Util method to inject gradle.properties
import org.gradle.testkit.runner.GradleRunner
import java.io.File
import java.io.InputStream
object GradleRunnerExt {
fun GradleRunner.withJaCoCo(): GradleRunner {
// creates gradle.properties in the project under test with org.gradle.jvmargs="-javaagent:..."
javaClass.classLoader.getResourceAsStream("testkit-gradle.properties")
?.toFile(File(projectDir, "gradle.properties"))
return this
}
private fun InputStream.toFile(file: File) {
use { input ->
file.outputStream().use { input.copyTo(it) }
}
}
}
Then in the tests I do call
GradleRunner.create().withPluginClasspath()
.withProjectDir(root)
.withJaCoCo()
.withArguments(":app:mergeDebugResources")
.build()
cc @Anze SodjaTom Koptel
04/03/2024, 2:03 PMAnze Sodja
04/03/2024, 2:31 PMwithDebug(true)
is required to force Gradle in in-process mode. Is that something you are missing?Tom Koptel
04/03/2024, 2:45 PM"pl.droidsonroids.jacoco.testkit"
plugin + withDebug(true)
Jacoco throws following warning.
> Task :android:jacocoTestReport
[ant:jacocoReport] Classes in bundle 'android' do not match with execution data. For report generation the same class files must be used as at runtime.
Tom Koptel
04/03/2024, 3:13 PMpl.droidsonroids.jacoco.testkit
, withDebug(false)
• jacoco/test.exec does not include information from the src/main
Gradle 8.6, with pl.droidsonroids.jacoco.testkit
, withDebug(false)
• jacoco/test.exec does include information from the src/main
Gradle 8.7, with pl.droidsonroids.jacoco.testkit
, withDebug(false)
• jacoco/test.exec does not include information from the src/main
Gradle 8.6 and Gradle 8.7, without pl.droidsonroids.jacoco.testkit
, withDebug(true)
• jacoco/test.exec does include information from the src/main
• throws ‘For report generation the same class files must be used as at runtime.’
Judging by this observations smth happened to the list of classes used to be instrumented during the Gradle Test Kit execution. On Gradle 8.7 the classes from src/main
are not included into the the .exec
file and leads to the empty coverage reports.Vampire
04/03/2024, 3:45 PMI am relying on jacoco-gradle-testkit-plugin which follows the same strategy suggested by @Vampire on Jacoco & Gradle Test Kit with Java thread.It's similar, not the same. I started trying to use that plugin, found it to only be working sometimes and only on a very narrow happy-path. Thus I made it properly in my plugin build. Afair it is still like written in that thread.
Vampire
04/03/2024, 3:46 PMTom Koptel
04/03/2024, 4:01 PMJacocoDumper
? Should I add this to the project that I prepare during the test and pass to GradleTestkit.withProjectDir(root)
? Or to my source project?Vampire
04/03/2024, 4:06 PMVampire
04/03/2024, 4:07 PMJacocoDumper
is in the testee's settings script, as described in that threadVampire
04/03/2024, 4:07 PMAnd then in the settings script of the projects under test I always add
Vampire
04/03/2024, 4:07 PMTom Koptel
04/03/2024, 4:09 PMVampire
04/03/2024, 4:09 PMTom Koptel
04/03/2024, 4:11 PMVampire
04/03/2024, 4:11 PMTom Koptel
04/03/2024, 8:14 PMdevelop
uses Gradle 8.6 and does produce 100% coverage. Switching to https://github.com/tomkoptel/jacoco-gradle-testkit/tree/gradle-8.7 and executing ./gradlew :jacocoTestReport
gives 0% coverage. Maybe, I’ve missed smth from your setup. Can you take a look, please?Vampire
04/03/2024, 8:15 PMTom Koptel
04/03/2024, 8:15 PMVampire
04/03/2024, 8:39 PMfinalizedBy(tasks.jacocoTestReport)
twice, and needlessly redo some conventional defaults like configuring the execution data of the jacoco report task.
But that's of course off-topic to the question.
• you miss to do escape backslashes in the two paths, that breaks it on windows
• you add a backslash in front of the two paths, which is just useless as on Linux it will then be \/
in the properties file and on Windows \<drive letter>
which both just resolves to the character after the backslash as neither is a special escape sequence
• you enclose the jvmargs entry in the properties file with double quotes which my instructions don't, but I'm not sure whether it makes a difference (this cannot replace the backslash escapes iirc if that was the intention)
Other than that it looks correct I'd say, so it should probably work on *nix as intendedVampire
04/03/2024, 9:09 PMNote that in the linked thread it's mentioned that@Anze Sodja With my tactic both should work, but neither does, with the difference that with debug the report complains about different class files while without debug there is no complaint but just 0 coverage.is required to force Gradle in in-process mode. Is that something you are missing?withDebug(true)
Vampire
04/03/2024, 9:20 PMVampire
04/03/2024, 9:41 PMorg.gradle.internal.instrumentation.agent=false
then it complains about the class mismatch both with and without debugAnze Sodja
04/03/2024, 9:41 PMVampire
04/03/2024, 9:44 PMAnze Sodja
04/03/2024, 9:45 PMVampire
04/04/2024, 12:05 AMwithDebug
you do not get warnings about trying to instrument already instrumented classes.Vampire
04/04/2024, 12:19 AMdiff --git a/build.gradle.kts b/build.gradle.kts
index 8fb0479..ad39200 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,8 +1,10 @@
+import org.gradle.plugin.devel.tasks.PluginUnderTestMetadata.IMPLEMENTATION_CLASSPATH_PROP_KEY
+import org.gradle.plugin.devel.tasks.PluginUnderTestMetadata.METADATA_FILE_NAME
+import org.jetbrains.kotlin.konan.file.use
import java.io.Serializable
import java.nio.channels.FileChannel
-import java.nio.file.OpenOption
import java.nio.file.StandardOpenOption
-import java.nio.file.attribute.FileAttribute
+import java.util.Properties
plugins {
`kotlin-dsl`
@@ -23,7 +25,6 @@ dependencies {
}
tasks.test {
- finalizedBy(tasks.jacocoTestReport)
finalizedBy(tasks.jacocoTestReport)
val testRuns = layout.buildDirectory.dir("testRuns")
systemProperty("testEnv.workDir", LazyString(testRuns.map { it.asFile.apply { mkdirs() }.absolutePath }))
@@ -43,8 +44,40 @@ tasks.test {
}
}
-tasks.jacocoTestReport {
- executionData(tasks.test.map { test -> test.the<JacocoTaskExtension>().destinationFile })
+jacoco {
+ toolVersion = "0.8.12"
+}
+
+tasks.test {
+ the<JacocoTaskExtension>().excludes = listOf("*")
+}
+
+val jacocoAnt by configurations.existing
+tasks.pluginUnderTestMetadata {
+ actions.clear()
+ doLast {
+ val instrumentedPluginClasspath = temporaryDir.resolve("instrumentedPluginClasspath")
+ instrumentedPluginClasspath.deleteRecursively()
+ ant.withGroovyBuilder {
+ "taskdef"("name" to "instrument",
+ "classname" to "org.jacoco.ant.InstrumentTask",
+ "classpath" to jacocoAnt.get().asPath)
+ "instrument"("destdir" to instrumentedPluginClasspath) {
+ pluginClasspath.asFileTree.addToAntBuilder(ant, "resources")
+ }
+ }
+
+ val properties = Properties();
+ if (!pluginClasspath.isEmpty) {
+ properties.setProperty(
+ IMPLEMENTATION_CLASSPATH_PROP_KEY,
+ instrumentedPluginClasspath.absoluteFile.invariantSeparatorsPath
+ )
+ }
+ outputDirectory.file(METADATA_FILE_NAME).get().asFile.outputStream().use {
+ properties.store(it, null)
+ }
+ }
}
tasks.jacocoTestReport {
diff --git a/src/test/kotlin/my/sample/MySamplePluginTest.kt b/src/test/kotlin/my/sample/MySamplePluginTest.kt
index 5e01907..c012ef5 100644
--- a/src/test/kotlin/my/sample/MySamplePluginTest.kt
+++ b/src/test/kotlin/my/sample/MySamplePluginTest.kt
@@ -16,7 +16,7 @@ class MySamplePluginTest {
}
private val jacocoAgentJar: String get() = System.getProperty("jacocoAgentJar")!!
- private val jacocoDestfile: String get() = System.getProperty("jacocoDestfile")!!
+ private val jacocoDestfile: String get() = System.getProperty("jacocoDestfile")!!.replace("""\""", """\\""")
@Test
fun `task is executed`() {
@@ -50,11 +50,17 @@ class MySamplePluginTest {
)
testProjectDir.newFile("gradle.properties").writeText(
"""
- org.gradle.jvmargs="-javaagent:\$jacocoAgentJar=destfile=\$jacocoDestfile,append=true,dumponexit=false,jmx=true"
+ systemProp.jacoco-agent.destfile=$jacocoDestfile
+ systemProp.jacoco-agent.append=true
+ systemProp.jacoco-agent.dumponexit=false
+ systemProp.jacoco-agent.jmx=true
""".trimIndent()
)
GradleRunner.create()
.withPluginClasspath()
+ .run {
+ withPluginClasspath(pluginClasspath + File(jacocoAgentJar))
+ }
.withProjectDir(testProjectDir.root)
.withArguments("mySampleTask")
.build()
With 8.8 one could probably use pluginClasspath.replace...
instead of replacing the whole task actions.Vampire
04/04/2024, 12:22 AMTom Koptel
04/04/2024, 8:52 AMTom Koptel
04/04/2024, 8:59 AMVampire
04/04/2024, 9:00 AMAnze Sodja
04/04/2024, 10:00 AMTom Koptel
04/04/2024, 12:09 PMError while instrumenting /jacoco-gradle-testkit/build/classes/kotlin/main/my/sample/MySamplePlugin$apply$1$2$invoke$inlined$the$1.class
Is there any ideas how to properly configura Jacoco in case of Kotlin inlined classes?Vampire
04/04/2024, 12:23 PMtasks.withType<X> { ... }
which breaks task-configuration avoidance for all tasks of type X
but always tasks.withType<X>().configureEach { ... }
, what is the actual error that happened?Vampire
04/04/2024, 12:51 PM...invoke$inlined...
while the file is called ...invoke$$inlined...
.
This was once fixed for https://issues.gradle.org/browse/GRADLE-3511 with commit b6503cb2462ab84ce65ae51761eec49efd3160ae for Gradle 3.0, but either the fix was lost, or it was not fixed for all cases.Vampire
04/04/2024, 12:51 PMVampire
04/04/2024, 12:55 PMVampire
04/04/2024, 1:56 PMaddToAntBuilder
is doing with only using public API and work-arounding the Gradle bug:
"instrument"("destdir" to instrumentedPluginClasspath) {
pluginClasspath.asFileTree.visit {
"gradleFileResource"(
"file" to file.absolutePath.replace("$", "$$"),
"name" to relativePath.pathString.replace("$", "$$")
)
}
}
Tom Koptel
04/04/2024, 2:57 PMTom Koptel
04/04/2024, 4:07 PMPluginUnderTestMetadata.METADATA_FILE_NAME
https://github.com/tomkoptel/jacoco-gradle-testkit/commit/dccec7b010d1880718ab50c8f1fc4170bb9d881b Without it the project under test was not able to apply plugins used in the main project. On the private repo we use Android Gradle Plugin and it was not applied to the project under test. Not sure, if what I did is a correct way, but it works.Vampire
04/04/2024, 4:13 PMinstrumentedClasses
directory and thus are not used or something like that.Tom Koptel
04/04/2024, 4:34 PMVampire
04/04/2024, 4:36 PMVampire
04/04/2024, 4:42 PMinputs.property("jacocoAntPath", objects.fileCollection().from(jacocoAnt))
also most probably is not what you wantVampire
04/04/2024, 4:42 PMdiff --git a/build.gradle.kts b/build.gradle.kts
index 709ac89..638be72 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -53,19 +53,17 @@ tasks.test {
val jacocoAnt by configurations.existing
tasks.pluginUnderTestMetadata {
- inputs.property("jacocoAntPath", objects.fileCollection().from(jacocoAnt))
+ inputs.files(jacocoAnt).withPropertyName("jacocoAntPath").withNormalizer(ClasspathNormalizer::class.java)
+ val jacocoAntPath = jacocoAnt.get().asPath
actions.clear()
doLast {
- val collection = inputs.properties["jacocoAntPath"] as FileCollection
- val classpath = collection.asPath
-
val instrumentedPluginClasspath = temporaryDir.resolve("instrumentedPluginClasspath")
instrumentedPluginClasspath.deleteRecursively()
ant.withGroovyBuilder {
"taskdef"(
"name" to "instrument",
"classname" to "org.jacoco.ant.InstrumentTask",
- "classpath" to classpath
+ "classpath" to jacocoAntPath
)
"instrument"("destdir" to instrumentedPluginClasspath) {
pluginClasspath.asFileTree.visit {
@@ -119,4 +117,3 @@ class LazyString(private val source: Lazy<String>) : Serializable {
override fun toString() = source.value
}
-
Tom Koptel
04/04/2024, 6:00 PMjacocoAntPath
property, so that we can add configuration cache compatible input. I found that it is not possible get jacocoAntPath
back with doLast { val collection = inputs.properties["jacocoAntPath"] as FileCollection }
as mentioned in Add ability to get named Task file inputs and outputs #21456.
So what I did is following. Which seems to work, but again I have no idea if that is a correct way to consume jacocoAntPath
(inputs.properties["jacocoAntPath"]
just returns null
).
inputs.files(jacocoAnt).withPropertyName("jacocoAntPath").withNormalizer(ClasspathNormalizer::class.java)
actions.clear()
doLast {
val classpath = inputs.files.asPath
https://github.com/tomkoptel/jacoco-gradle-testkit/commit/32966b6bc10039ec017f36d9dcf8f253d68a256dVampire
04/04/2024, 6:29 PMVampire
04/04/2024, 10:31 PMdiff --git a/build.gradle.kts b/build.gradle.kts
index 709ac89..22796da 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -18,6 +18,7 @@ jacocoAgentJar.isCanBeResolved = true
dependencies {
compileOnly(gradleApi())
+ implementation("commons-io:commons-io:+")
testImplementation("junit:junit:4.13.2")
testImplementation(gradleTestKit())
jacocoAgentJar("org.jacoco:org.jacoco.agent:0.8.12:runtime")
@@ -53,19 +54,17 @@ tasks.test {
val jacocoAnt by configurations.existing
tasks.pluginUnderTestMetadata {
- inputs.property("jacocoAntPath", objects.fileCollection().from(jacocoAnt))
+ inputs.files(jacocoAnt).withPropertyName("jacocoAntPath").withNormalizer(ClasspathNormalizer::class.java)
+ val jacocoAntPath = jacocoAnt.get().asPath
actions.clear()
doLast {
- val collection = inputs.properties["jacocoAntPath"] as FileCollection
- val classpath = collection.asPath
-
val instrumentedPluginClasspath = temporaryDir.resolve("instrumentedPluginClasspath")
instrumentedPluginClasspath.deleteRecursively()
ant.withGroovyBuilder {
"taskdef"(
"name" to "instrument",
"classname" to "org.jacoco.ant.InstrumentTask",
- "classpath" to classpath
+ "classpath" to jacocoAntPath
)
"instrument"("destdir" to instrumentedPluginClasspath) {
pluginClasspath.asFileTree.visit {
@@ -79,12 +78,17 @@ tasks.pluginUnderTestMetadata {
val properties = Properties()
if (!pluginClasspath.isEmpty) {
- val originalClasspath = pluginClasspath.joinToString(File.pathSeparator) {
- it.absoluteFile.invariantSeparatorsPath
- }
properties.setProperty(
IMPLEMENTATION_CLASSPATH_PROP_KEY,
- instrumentedPluginClasspath.absoluteFile.invariantSeparatorsPath + File.pathSeparator + originalClasspath
+ listOf(
+ instrumentedPluginClasspath
+ .absoluteFile
+ .invariantSeparatorsPath,
+ *instrumentedPluginClasspath
+ .listFiles { dir, name -> name.endsWith(".jar") }!!
+ .map { it.absoluteFile.invariantSeparatorsPath }
+ .toTypedArray()
+ ).joinToString(File.pathSeparator)
)
}
outputDirectory.file(PluginUnderTestMetadata.METADATA_FILE_NAME).get().asFile.outputStream().use {
diff --git a/src/main/kotlin/my/sample/MySampleTask.kt b/src/main/kotlin/my/sample/MySampleTask.kt
index 831a7a3..31be2e8 100644
--- a/src/main/kotlin/my/sample/MySampleTask.kt
+++ b/src/main/kotlin/my/sample/MySampleTask.kt
@@ -1,5 +1,6 @@
package my.sample
+import org.apache.commons.io.IOUtil
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
@@ -16,6 +17,7 @@ abstract class MySampleTask : DefaultTask() {
@TaskAction
fun run() {
+ println(IOUtil::class)
output.get().asFile.writeText(input.get())
}
}
diff --git a/src/test/kotlin/my/sample/MySamplePluginTest.kt b/src/test/kotlin/my/sample/MySamplePluginTest.kt
index 5bc8fb8..d2b76da 100644
--- a/src/test/kotlin/my/sample/MySamplePluginTest.kt
+++ b/src/test/kotlin/my/sample/MySamplePluginTest.kt
@@ -62,7 +62,7 @@ class MySamplePluginTest {
withPluginClasspath(pluginClasspath + File(jacocoAgentJar))
}
.withProjectDir(testProjectDir.root)
- .withArguments("mySampleTask")
+ .withArguments("mySampleTask", "-d")
.build()
val output = SimpleDateFormat("yyyy-MM-dd").format(Date())
val outputContents = testProjectDir.root.resolve("build/mySampleTaskOutput.txt").readText()
Tom Koptel
04/08/2024, 6:13 PMAnze Sodja
04/08/2024, 6:44 PM