Skip to content

Commit

Permalink
Added API for getting coverage inside a running application, instrume…
Browse files Browse the repository at this point in the history
…nted offline

- upgraded IntelliJ Coverage version to `1.0.729`
- added Kover API to get code coverage
- expanded documentation

PR #435

Co-authored-by: Vsevolod Tolstopyatov <[email protected]>
Co-authored-by: Leonid Startsev <[email protected]>
  • Loading branch information
3 people authored Jul 26, 2023
1 parent 4ab9591 commit a74f3ba
Show file tree
Hide file tree
Showing 15 changed files with 529 additions and 112 deletions.
21 changes: 21 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2000-2023 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

plugins {
kotlin("jvm") apply false
alias(libs.plugins.kotlinx.dokka) apply false
alias(libs.plugins.kotlinx.binaryCompatibilityValidator) apply false
}
111 changes: 1 addition & 110 deletions docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Java 1.6 or higher is required for execution.

### Offline instrumentation

For information about offline instrumentation, [see](#offline-instrumentation-1).
For information about offline instrumentation, [see](../offline-instrumentation#description).

`java -jar kover-cli.jar instrument [<class-file-path> ...] --dest <dir> [--exclude <class-name>] [--excludeAnnotation <annotation-name>] [--hits] [--include <class-name>]`

Expand Down Expand Up @@ -37,112 +37,3 @@ Allows you to generate HTML and XML reports from the existing binary report.
| --src <sources-path> | location of the source files root | + | + |
| --title <html-title> | title in the HTML report | | |
| --xml <xml-file-path> | generate a XML report in the specified path | | |

## Offline instrumentation

Offline instrumentation is suitable when using runtime environments that do not support Java agents.
It instruments the files located in the file system and saves the result to the specified directory.

To run classes instrumented offline, you need to add `org.jetbrains.kotlinx:kover-offline` artifact to the application's classpath.

You also need to pass the system property `kover.offline.report.path` to the application with the path where you want binary report to be saved.

Also see [Gradle example](#gradle-example)

## Examples

### Gradle example
Example of custom using Kover tool CLI in Gradle
```
plugins {
kotlin("jvm") version "1.8.0"
application
}
repositories {
mavenCentral()
}
configurations.register("koverCli") {
isVisible = false
isCanBeConsumed = false
isTransitive = true
isCanBeResolved = true
}
dependencies {
runtimeOnly("org.jetbrains.kotlinx:kover-offline-runtime:0.7.2")
add("koverCli", "org.jetbrains.kotlinx:kover-cli:0.7.2")
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(11)
}
fun cliJar(): File {
val cliConfig = configurations.getByName("koverCli")
return cliConfig.filter {it.name.startsWith("kover-cli")}.singleFile
}
tasks.compileKotlin {
doLast {
val outputDir = destinationDirectory.get().asFile
exec {
commandLine(
"java",
"-jar",
cliJar().canonicalPath,
"instrument",
outputDir,
"--dest",
outputDir,
"--hits",
)
}
}
}
val binaryReport = layout.buildDirectory.file("kover/report.ic").get().asFile
tasks.test {
// set system property for binary report path
systemProperty("kover.offline.report.path", binaryReport.absolutePath)
}
tasks.register("koverReport") {
dependsOn(tasks.test)
doLast {
val args = mutableListOf<String>()
args += "java"
args += "-jar"
args += cliJar().canonicalPath
args += "report"
args += binaryReport.absolutePath
args += "--classfiles"
args += tasks.compileKotlin.get().destinationDirectory.get().asFile.absolutePath
args += "--classfiles"
args += tasks.compileJava.get().destinationDirectory.get().asFile.absolutePath
args += "--xml"
args += layout.buildDirectory.file("reports/kover/report.xml").get().asFile.absolutePath
args += "--html"
args += layout.buildDirectory.file("reports/kover/html").get().asFile.absolutePath
sourceSets.main.get().kotlin.sourceDirectories.files.forEach { src ->
args += "--src"
args += src.canonicalPath
}
exec { commandLine(args) }
}
}
```
165 changes: 165 additions & 0 deletions docs/offline-instrumentation/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Offline instrumentation

## Description

To collect code coverage for JVM applications, Kover uses instrumentation -- modification of the bytecode in order to place entry counters in certain blocks of code.

Offline instrumentation is a transformation of the bytecode in compiled class files located somewhere in a file system.
Offline instrumentation is suitable when using runtime environments that do not support Java agents.

## Working steps

### Class instrumentation

For instrumentation, you must first build the application, then the root directories for the class files
must be passed to Kover CLI as arguments, see [Kover CLI](../cli#offline-instrumentation) for the technical detils.

### Dump coverage result

To run classes instrumented offline, you'll need to add `org.jetbrains.kotlinx:kover-offline` artifact to the application's classpath.

There are two ways to get coverage:

- Run tests to get a binary report file, then run [Kover CLI](../cli#generating-reports) to get HTML or XML report from binary report
- Call `KoverRuntime.collectByDirs` or `KoverRuntime.collect` in the same process after the tests are finished

One or both of these ways can be used at the same time.

#### Binary report file

You'll also need to pass the system property `kover.offline.report.path` to the application with the path where you want a binary report to be saved.
This binary file can be used to generate human-readable reports using [Kover CLI](../cli#generating-reports).

#### In-process reporting

Inside the same JVM process in which the tests were run, call Java static method `kotlinx.kover.offline.runtime.api.KoverRuntime.collectByDirs` or `kotlinx.kover.offline.runtime.api.KoverRuntime.collect`.

For correct generation of the report, it is necessary to pass the bytecode of the non-instrumented classes.
This can be done by specifying the directories where the class-files are stored, or a byte array with the bytecode of the application non-instrumented classes.

See [example](#example-of-using-the-api).

## Examples

### Gradle example for binary report

Example of a custom binary report production using Kover tool CLI in Gradle
```
plugins {
kotlin("jvm") version "1.8.0"
application
}
repositories {
mavenCentral()
}
configurations.register("koverCli") {
isVisible = false
isCanBeConsumed = false
isTransitive = true
isCanBeResolved = true
}
dependencies {
runtimeOnly("org.jetbrains.kotlinx:kover-offline-runtime:0.7.2")
add("koverCli", "org.jetbrains.kotlinx:kover-cli:0.7.2")
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(11)
}
fun cliJar(): File {
val cliConfig = configurations.getByName("koverCli")
return cliConfig.filter {it.name.startsWith("kover-cli")}.singleFile
}
tasks.compileKotlin {
doLast {
val outputDir = destinationDirectory.get().asFile
exec {
commandLine(
"java",
"-jar",
cliJar().canonicalPath,
"instrument",
outputDir,
"--dest",
outputDir,
"--hits",
)
}
}
}
val binaryReport = layout.buildDirectory.file("kover/report.ic").get().asFile
tasks.test {
// set system property for binary report path
systemProperty("kover.offline.report.path", binaryReport.absolutePath)
}
tasks.register("koverReport") {
dependsOn(tasks.test)
doLast {
val args = mutableListOf<String>()
args += "java"
args += "-jar"
args += cliJar().canonicalPath
args += "report"
args += binaryReport.absolutePath
args += "--classfiles"
args += tasks.compileKotlin.get().destinationDirectory.get().asFile.absolutePath
args += "--classfiles"
args += tasks.compileJava.get().destinationDirectory.get().asFile.absolutePath
args += "--xml"
args += layout.buildDirectory.file("reports/kover/report.xml").get().asFile.absolutePath
args += "--html"
args += layout.buildDirectory.file("reports/kover/html").get().asFile.absolutePath
sourceSets.main.get().kotlin.sourceDirectories.files.forEach { src ->
args += "--src"
args += src.canonicalPath
}
exec { commandLine(args) }
}
}
```

### Example of using the API
```kotlin
// the directory with class files can be transferred using the system property, any other methods are possible
val outputDir = File(System.getProperty("output.dir"))
val coverage = KoverRuntime.collectByDirs(listOf(outputDir))

// check coverage of `readState` method
assertEquals(3, coverage.size)
val coverageByClass = coverage.associateBy { cov -> cov.className }

val mainClassCoverage = coverageByClass.getValue("org.jetbrains.kotlinx.kover.MainClass")
assertEquals("Main.kt", mainClassCoverage.fileName)
assertEquals(4, mainClassCoverage.methods.size)

val coverageBySignature = mainClassCoverage.methods.associateBy { meth -> meth.signature }
val readStateCoverage = coverageBySignature.getValue("readState()Lorg/jetbrains/kotlinx/kover/DataClass;")

assertEquals(1, readStateCoverage.hit)
assertEquals(1, readStateCoverage.lines.size)
assertEquals(6, readStateCoverage.lines[0].lineNumber)
assertEquals(1, readStateCoverage.lines[0].hit)
assertEquals(0, readStateCoverage.lines[0].branches.size)
```

see [full example](https://github.com/Kotlin/kotlinx-kover/tree/main/kover-offline-runtime/examples/runtime-api)
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[versions]

intellij-coverage = "1.0.724"
intellij-coverage = "1.0.729"
junit = "5.9.0"
kotlinx-bcv = "0.13.0"
kotlinx-bcv = "0.13.2"
kotlinx-dokka = "1.8.10"
args4j = "2.33"

Expand Down
6 changes: 6 additions & 0 deletions kover-offline-runtime/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ extensions.configure<Kover_publishing_conventions_gradle.KoverPublicationExtensi
fatJar.set(true)
}

java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}

repositories {
mavenCentral()
}
Expand All @@ -39,5 +44,6 @@ tasks.jar {
exclude("OSGI-OPT/**")
exclude("META-INF/**")
exclude("LICENSE")
exclude("classpath.index")
}
}
58 changes: 58 additions & 0 deletions kover-offline-runtime/examples/runtime-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
plugins {
kotlin("jvm") version "1.8.0"
}

group = "org.jetbrains"

repositories {
mavenCentral()
}

configurations.register("koverCli") {
isVisible = false
isCanBeConsumed = false
isTransitive = true
isCanBeResolved = true
}

dependencies {
add("koverCli", "org.jetbrains.kotlinx:kover-cli:0.7.2")

implementation("org.jetbrains.kotlinx:kover-offline-runtime:0.7.2")

testImplementation(kotlin("test"))
}

tasks.test {
useJUnitPlatform()

systemProperty("output.dir", tasks.compileKotlin.get().destinationDirectory.get().asFile.absolutePath)
}

kotlin {
jvmToolchain(8)
}

fun cliJar(): File {
val cliConfig = configurations.getByName("koverCli")
return cliConfig.filter {it.name.startsWith("kover-cli")}.singleFile
}

tasks.compileKotlin {
doLast {
val outputDir = destinationDirectory.get().asFile

exec {
commandLine(
"java",
"-jar",
cliJar().canonicalPath,
"instrument",
outputDir,
"--dest",
outputDir,
"--hits",
)
}
}
}
Loading

0 comments on commit a74f3ba

Please sign in to comment.