Skip to content

Commit

Permalink
Added Gradle configuration cache support
Browse files Browse the repository at this point in the history
Resolves #142
- all inputs for Gradle beans (tasks, actions, extensions, etc.) are serializable
- there is no direct access to the Project instance during the task execution, only a call from providers is allowed
  • Loading branch information
shanshin committed Feb 22, 2022
1 parent 3cd1d5b commit e1f3fbc
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 113 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package kotlinx.kover.test.functional.cases

import kotlinx.kover.test.functional.core.*
import kotlin.test.*

internal class ConfigurationCacheTests: BaseGradleScriptTest() {
@Test
fun testConfigCache() {
builder("Testing configuration cache support")
.sources("simple")
.build()
.run("build", "koverMergedReport", "koverMergedVerify", "koverReport", "koverVerify", "--configuration-cache")
}
}
104 changes: 59 additions & 45 deletions src/main/kotlin/kotlinx/kover/KoverPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.gradle.api.provider.*
import org.gradle.api.tasks.*
import org.gradle.api.tasks.testing.*
import org.gradle.process.*
import java.io.File
import kotlin.reflect.*

class KoverPlugin : Plugin<Project> {
Expand Down Expand Up @@ -160,23 +161,25 @@ class KoverPlugin : Plugin<Project> {
providers: BuildProviders,
block: (T) -> Unit
): T {
return tasks.create(taskName, type.java) { task ->
task.group = VERIFICATION_GROUP
val task = tasks.create(taskName, type.java)

providers.projects.forEach { (projectName, m) ->
task.binaryReportFiles.put(projectName, NestedFiles(task.project.objects, m.reports))
task.srcDirs.put(projectName, NestedFiles(task.project.objects, m.sources))
task.outputDirs.put(projectName, NestedFiles(task.project.objects, m.output))
}
task.group = VERIFICATION_GROUP

task.coverageEngine.set(providers.engine)
task.classpath.set(providers.classpath)
task.dependsOn(providers.merged.tests)
providers.projects.forEach { (projectName, m) ->
task.binaryReportFiles.put(projectName, NestedFiles(task.project.objects, m.reports))
task.srcDirs.put(projectName, NestedFiles(task.project.objects, m.sources))
task.outputDirs.put(projectName, NestedFiles(task.project.objects, m.output))
}

task.onlyIf { !providers.merged.disabled.get() }
task.coverageEngine.set(providers.engine)
task.classpath.set(providers.classpath)
task.dependsOn(providers.merged.tests)

block(task)
}
val disabledProvider = providers.merged.disabled
task.onlyIf { !disabledProvider.get() }

block(task)
return task
}

private fun Project.createCollectingTask() {
Expand Down Expand Up @@ -214,23 +217,25 @@ class KoverPlugin : Plugin<Project> {
throw GradleException("Kover task '$taskName' already exist. Plugin should not be applied in child project if it has already been applied in one of the parent projects.")
}

return tasks.create(taskName, type.java) { task ->
task.group = VERIFICATION_GROUP
val task = tasks.create(taskName, type.java)
task.group = VERIFICATION_GROUP

task.coverageEngine.set(providers.engine)
task.classpath.set(providers.classpath)
task.srcDirs.set(projectProviders.sources)
task.outputDirs.set(projectProviders.output)
task.coverageEngine.set(providers.engine)
task.classpath.set(providers.classpath)
task.srcDirs.set(projectProviders.sources)
task.outputDirs.set(projectProviders.output)

// it is necessary to read all binary reports because project's classes can be invoked in another project
task.binaryReportFiles.set(projectProviders.reports)
task.dependsOn(projectProviders.tests)
// it is necessary to read all binary reports because project's classes can be invoked in another project
task.binaryReportFiles.set(projectProviders.reports)
task.dependsOn(projectProviders.tests)

task.onlyIf { !projectProviders.disabled.get() }
task.onlyIf { !task.binaryReportFiles.get().isEmpty }
val disabledProvider = projectProviders.disabled
task.onlyIf { !disabledProvider.get() }
task.onlyIf { !(it as KoverProjectTask).binaryReportFiles.get().isEmpty }

block(task)
}
block(task)

return task
}

private fun Project.createKoverExtension(): KoverExtension {
Expand All @@ -249,26 +254,33 @@ class KoverPlugin : Plugin<Project> {
val taskExtension = extensions.create(TASK_EXTENSION_NAME, KoverTaskExtension::class.java, project.objects)

taskExtension.isDisabled = false
taskExtension.binaryReportFile.set(this.project.provider {
taskExtension.binaryReportFile.set(project.provider {
val koverExtension = providers.koverExtension.get()
val suffix = if (koverExtension.coverageEngine.get() == CoverageEngine.INTELLIJ) ".ic" else ".exec"
project.layout.buildDirectory.get().file("kover/$name$suffix").asFile
})

val pluginContainer = project.plugins
val excludeAndroidPackages =
project.provider { project.androidPluginIsApplied && !providers.koverExtension.get().instrumentAndroidPackage }
project.provider { pluginContainer.androidPluginIsApplied && !providers.koverExtension.get().instrumentAndroidPackage }

jvmArgumentProviders.add(
CoverageArgumentProvider(
this,
agents,
taskExtension,
providers.koverExtension,
excludeAndroidPackages
)
)

doFirst(BinaryReportCleanupAction(providers.koverExtension, taskExtension))
doLast(IntellijErrorLogCopyAction(taskExtension))
val sourceErrorProvider = project.provider {
File(taskExtension.binaryReportFile.get().parentFile, "coverage-error.log")
}
val targetErrorProvider = project.layout.buildDirectory.file("kover/errors/$name.log").map { it.asFile }

doFirst(BinaryReportCleanupAction(project.name, providers.koverExtension, taskExtension))
doLast(MoveIntellijErrorLogAction(sourceErrorProvider, targetErrorProvider))
}
}

Expand All @@ -277,6 +289,7 @@ class KoverPlugin : Plugin<Project> {
For this reason, before starting the tests, it is necessary to clear the file from the results of previous runs.
*/
private class BinaryReportCleanupAction(
private val projectName: String,
private val koverExtensionProvider: Provider<KoverExtension>,
private val taskExtension: KoverTaskExtension
) : Action<Task> {
Expand All @@ -289,7 +302,7 @@ private class BinaryReportCleanupAction(

if (!taskExtension.isDisabled
&& !koverExtension.isDisabled
&& !koverExtension.disabledProjects.contains(task.project.name)
&& !koverExtension.disabledProjects.contains(projectName)
&& koverExtension.coverageEngine.get() == CoverageEngine.INTELLIJ
) {
// IntelliJ engine expected empty file for parallel test execution.
Expand All @@ -299,26 +312,28 @@ private class BinaryReportCleanupAction(
}
}

private class IntellijErrorLogCopyAction(private val taskExtension: KoverTaskExtension) : Action<Task> {
private class MoveIntellijErrorLogAction(
private val sourceFile: Provider<File>,
private val targetFile: Provider<File>
) : Action<Task> {
override fun execute(task: Task) {
task.project.copyIntellijErrorLog(
task.project.layout.buildDirectory.get().file("kover/errors/${task.name}.log").asFile,
taskExtension.binaryReportFile.get().parentFile
)
val origin = sourceFile.get()
if (origin.exists() && origin.isFile) {
origin.copyTo(targetFile.get(), true)
origin.delete()
}
}
}

private class CoverageArgumentProvider(
private val task: Task,
private val agents: Map<CoverageEngine, CoverageAgent>,
@get:Nested val taskExtension: KoverTaskExtension,
@get:Nested val koverExtension: Provider<KoverExtension>,
@get:Input val excludeAndroidPackage: Provider<Boolean>
) : CommandLineArgumentProvider, Named {

@get:Nested
val taskExtension: Provider<KoverTaskExtension> = task.project.provider {
task.extensions.getByType(KoverTaskExtension::class.java)
}
private val projectName: String = task.project.name

@Internal
override fun getName(): String {
Expand All @@ -327,11 +342,10 @@ private class CoverageArgumentProvider(

override fun asArguments(): MutableIterable<String> {
val koverExtensionValue = koverExtension.get()
val taskExtensionValue = taskExtension.get()

if (taskExtensionValue.isDisabled
if (taskExtension.isDisabled
|| koverExtensionValue.isDisabled
|| koverExtensionValue.disabledProjects.contains(task.project.name)
|| koverExtensionValue.disabledProjects.contains(projectName)
) {
return mutableListOf()
}
Expand All @@ -346,9 +360,9 @@ private class CoverageArgumentProvider(
FIXME Remove this code if the IntelliJ Agent stops changing project classes during instrumentation
*/
taskExtensionValue.excludes = taskExtensionValue.excludes + "android.*" + "com.android.*"
taskExtension.excludes = taskExtension.excludes + "android.*" + "com.android.*"
}

return agents.getFor(koverExtensionValue.coverageEngine.get()).buildCommandLineArgs(task, taskExtensionValue)
return agents.getFor(koverExtensionValue.coverageEngine.get()).buildCommandLineArgs(task, taskExtension)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
package kotlinx.kover.adapters

import kotlinx.kover.adapters.api.*
import org.gradle.api.*
import org.gradle.api.file.*
import java.io.*
import org.gradle.api.plugins.PluginContainer

internal fun createAdapters(): List<CompilationPluginAdapter> {
return listOf(
Expand All @@ -18,7 +16,7 @@ internal fun createAdapters(): List<CompilationPluginAdapter> {
)
}

val Project.androidPluginIsApplied: Boolean
val PluginContainer.androidPluginIsApplied: Boolean
get() {
return plugins.findPlugin("android") != null || plugins.findPlugin("kotlin-android") != null
return findPlugin("android") != null || findPlugin("kotlin-android") != null
}
7 changes: 6 additions & 1 deletion src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ package kotlinx.kover.engines.commons

import java.io.*

internal class Report(val files: List<File>, val projects: List<ProjectInfo>)
internal class Report(
val files: List<File>,
val projects: List<ProjectInfo>,
val includes: List<String> = emptyList(),
val excludes: List<String> = emptyList()
)
internal class ProjectInfo(val sources: Iterable<File>, val outputs: Iterable<File>)

private val regexMetacharactersSet = "<([{\\^-=$!|]})+.>".toSet()
Expand Down
11 changes: 6 additions & 5 deletions src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,30 @@ import kotlinx.kover.engines.commons.CoverageAgent
import org.gradle.api.*
import org.gradle.api.artifacts.*
import org.gradle.api.file.*
import org.gradle.api.provider.Provider
import java.io.*


internal fun Project.createIntellijAgent(koverExtension: KoverExtension): CoverageAgent {
val intellijConfig = createIntellijConfig(koverExtension)
return IntellijAgent(intellijConfig)
val jarProvider = provider { intellijConfig.fileCollection { it.name == "intellij-coverage-agent" }.singleFile }
return IntellijAgent(intellijConfig, jarProvider)
}

private class IntellijAgent(private val config: Configuration): CoverageAgent {
private class IntellijAgent(override val classpath: FileCollection, private val jarProvider: Provider<File>): CoverageAgent {
private val trackingPerTest = false // a flag to enable tracking per test coverage
private val calculateForUnloadedClasses = false // a flag to calculate coverage for unloaded classes
private val appendToDataFile = true // a flag to use data file as initial coverage
private val samplingMode = false //a flag to run coverage in sampling mode or in tracing mode otherwise

override val engine: CoverageEngine = CoverageEngine.INTELLIJ
override val classpath: FileCollection = config

override fun buildCommandLineArgs(task: Task, extension: KoverTaskExtension): MutableList<String> {
val argsFile = File(task.temporaryDir, "intellijagent.args")
argsFile.writeArgsToFile(extension)
val jarFile = config.fileCollection { it.name == "intellij-coverage-agent" }.singleFile

return mutableListOf(
"-javaagent:${jarFile.canonicalPath}=${argsFile.canonicalPath}",
"-javaagent:${jarProvider.get().canonicalPath}=${argsFile.canonicalPath}",
"-Didea.new.sampling.coverage=true",
"-Didea.new.tracing.coverage=true",
"-Didea.coverage.log.level=error",
Expand Down
30 changes: 8 additions & 22 deletions src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import kotlinx.kover.engines.commons.*
import kotlinx.kover.engines.commons.Report
import org.gradle.api.*
import org.gradle.api.file.*
import org.gradle.process.ExecOperations
import java.io.*
import java.util.*

internal fun Task.intellijReport(
exec: ExecOperations,
report: Report,
xmlFile: File?,
htmlDir: File?,
includes: List<String>,
excludes: List<String>,
classpath: FileCollection
) {
xmlFile?.let {
Expand All @@ -30,29 +30,14 @@ internal fun Task.intellijReport(

val argsFile = File(temporaryDir, "intellijreport.json")
argsFile.printWriter().use { pw ->
pw.writeReportsJson(report, xmlFile, htmlDir, includes, excludes)
pw.writeReportsJson(report, xmlFile, htmlDir)
}

project.javaexec { e ->
exec.javaexec { e ->
e.mainClass.set("com.intellij.rt.coverage.report.Main")
e.classpath = classpath
e.args = mutableListOf(argsFile.canonicalPath)
}

project.copyIntellijErrorLog(project.layout.buildDirectory.get().file("kover/errors/$name.log").asFile)
}

internal fun Project.copyIntellijErrorLog(toFile: File, customDirectory: File? = null) {
var errorLog = customDirectory?.let { File(it, "coverage-error.log") }

if (errorLog == null || !errorLog.exists()) {
errorLog = File(projectDir, "coverage-error.log")
}

if (errorLog.exists() && errorLog.isFile) {
errorLog.copyTo(toFile, true)
errorLog.delete()
}
}

/*
Expand Down Expand Up @@ -97,9 +82,7 @@ JSON example:
private fun Writer.writeReportsJson(
report: Report,
xmlFile: File?,
htmlDir: File?,
includes: List<String>,
excludes: List<String>,
htmlDir: File?
) {
appendLine("{")

Expand All @@ -116,6 +99,9 @@ private fun Writer.writeReportsJson(
appendLine(""" "html": ${it.jsonString},""")
}

val includes = report.includes
val excludes = report.excludes

if (includes.isNotEmpty()) {
appendLine(""" "include": {""")
appendLine(includes.joinToString(", ", """ "classes": [""", "]") { i -> i.wildcardsToRegex().jsonString })
Expand Down

0 comments on commit e1f3fbc

Please sign in to comment.