Skip to content

Commit

Permalink
Introduced Offline Runtime API for saving binary report
Browse files Browse the repository at this point in the history
- implemented API method to get binary report as a byte array
- implemented API method to save binary report
- bump Intellij Coverage Library to `1.0.740`

Co-authored-by: Leonid Startsev <sandwwraith@users.noreply.github.com>

Resolves #503
PR #500
  • Loading branch information
shanshin committed Nov 15, 2023
1 parent b3ca174 commit 1eb1fca
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 11 deletions.
58 changes: 50 additions & 8 deletions docs/offline-instrumentation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,56 @@ must be passed to Kover CLI as arguments, see [Kover CLI](../cli#offline-instrum

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:
There are several 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
- [Save binary report file when the JVM is shut down](#save-binary-report-on-shut-down)
- [Save binary report in runtime by Kover API](#save-binary-report-in-runtime)
- [Get binary report in runtime by Kover API](#get-binary-report-in-runtime)
- [Get coverage details in runtime by Kover API](#get-coverage-details-in-runtime)

One or both of these ways can be used at the same time.
Binary reports are presented in `ic` format, and can later be used in the [Kover CLI](../cli#generating-reports) to generate HTML or XML reports.

#### Binary report file
#### Save binary report on shut down

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).
You'll 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.

#### In-process reporting
If this property is specified, then at the end of the JVM process,
the binary coverage report will be saved to a file at the path passed in the parameter value.

If the file does not exist, it will be created. If a file with that name already exists, it will be overwritten.

#### Save binary report in runtime

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

If the file does not exist, it will be created. If a file already exists, it will be overwritten.

Calling this method is allowed only after all tests are completed. If the method is called in parallel with the execution of the measured code, the coverage value is unpredictable.

#### Get binary report in runtime

Inside the same JVM process in which the tests were run, call Java static method `kotlinx.kover.offline.runtime.api.KoverRuntime.getReport`.
This method will return byte array with a binary coverage report, which can be saved to a file later.
It is important that this byte array cannot be appended to an already existing file, and must be saved to a separate file.

Calling this method is allowed only after all tests are completed. If the method is called in parallel with the execution of the measured code, the coverage value is unpredictable.

#### Get coverage details in runtime

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.

Calling these methods is allowed only after all tests are completed. If the method is called in parallel with the execution of the measured code, the coverage value is unpredictable.

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

## Logging
`org.jetbrains.kotlinx:kover-offline` has its own logging system.

By default, error messages are saved to a file in the working directory with the name `kover-offline.log`. To change the path to this file, pass the `kover.offline.log.file.path` system property with new path.

## Examples

### Gradle example for binary report
Expand Down Expand Up @@ -140,6 +169,19 @@ tasks.register("koverReport") {

### Example of using the API
```kotlin
val reportFile = Files.createTempFile("kover-report-", ".ic").toFile()

// save binary report to file
KoverRuntime.saveReport(reportFile)

// get binary report as byte array
val bytes = KoverRuntime.getReport()

// check reports are same
val bytesFromFile = reportFile.readBytes()
assertContentEquals(bytesFromFile, bytes)


// 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))
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]

intellij-coverage = "1.0.738"
intellij-coverage = "1.0.740"
junit = "5.9.0"
kotlinx-bcv = "0.13.2"
kotlinx-dokka = "1.8.10"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public object KoverVersions {
/**
* Kover coverage tool version.
*/
public const val KOVER_TOOL_VERSION = "1.0.738"
public const val KOVER_TOOL_VERSION = "1.0.740"

/**
* JaCoCo coverage tool version used by default.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@ package org.jetbrains.kotlinx.kover

import kotlinx.kover.offline.runtime.api.KoverRuntime
import java.io.File
import java.nio.file.Files
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals

class Tests {
@Test
fun test() {
MainClass().readState()

val reportFile = Files.createTempFile("kover-report-", ".ic").toFile()

// save binary report to file
KoverRuntime.saveReport(reportFile)

// get binary report as byte array
val bytes = KoverRuntime.getReport()

// check reports are same
val bytesFromFile = reportFile.readBytes()
assertContentEquals(bytesFromFile, bytes)


val outputDir = File(System.getProperty("output.dir"))
val coverage = KoverRuntime.collectByDirs(listOf(outputDir))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package kotlinx.kover.offline.runtime;

import com.intellij.rt.coverage.offline.api.CoverageRuntime;
import kotlinx.kover.offline.runtime.api.KoverRuntime;

import java.io.File;

import static kotlinx.kover.offline.runtime.api.KoverRuntime.LOG_FILE_PROPERTY_NAME;
import static kotlinx.kover.offline.runtime.api.KoverRuntime.REPORT_PROPERTY_NAME;

/**
* Class for initializing the Kover offline instrumentation runtime.
* <p>
* This class is initialized by the instrumented code by class name when any of the instrumented method is executed for the first time.
* Therefore, all initialization code must be placed in the {@code <clinit>} method.
*/
class KoverInit {

static {
String reportNameSavedOnExitProp = System.getProperty(REPORT_PROPERTY_NAME);
String logFileProp = System.getProperty(LOG_FILE_PROPERTY_NAME);

if (logFileProp != null) {
CoverageRuntime.setLogPath(new File(LOG_FILE_PROPERTY_NAME));
} else {
CoverageRuntime.setLogPath(new File(KoverRuntime.DEFAULT_LOG_FILE_NAME));
}

if (reportNameSavedOnExitProp != null) {
saveOnExit(reportNameSavedOnExitProp);
}
}

private static void saveOnExit(final String fileName) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
try {
KoverRuntime.saveReport(new File(fileName));
} catch (Throwable e) {
System.err.println("Kover error: failed to save report file '" + fileName +"': " + e.getMessage());
}
}
}));
}

private KoverInit() {
// no instances
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import com.intellij.rt.coverage.offline.api.CoverageRuntime;

import java.io.File;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -16,6 +16,31 @@
*/
public class KoverRuntime {

/**
* Default name of file with Kover offline logs.
*
* Can be overridden using the {@link KoverRuntime#LOG_FILE_PROPERTY_NAME} property.
*/
public static final String DEFAULT_LOG_FILE_NAME = "kover-offline.log";

/**
* JVM property name used to define the path where the offline report will be stored.
* <p>
* If this property is specified, then at the end of the JVM process,
* the binary coverage report will be saved to a file at the path passed in the parameter value.
* <p>
* If the file does not exist, it will be created. If a file with that name already exists, it will be overwritten.
*/
public static final String REPORT_PROPERTY_NAME = "kover.offline.report.path";

/**
* JVM property name used to define the path to the file with Kover offline logs.
*
*<p>
* If this property is not specified, the logs are saved to the {@link KoverRuntime#DEFAULT_LOG_FILE_NAME} file located in the current directory.
*/
public static final String LOG_FILE_PROPERTY_NAME = "kover.offline.log.file.path";

/**
* Get classes coverage. For the correct collection of coverage, an analysis of the class-files is required.
* <p/>
Expand All @@ -42,6 +67,45 @@ public static List<ClassCoverage> collect(List<byte[]> classFiles) {
return convertClasses(CoverageRuntime.collectClassfileData(classFiles));
}

/**
* Save coverage binary report in file with ic format.
* If the file does not exist, it will be created. If a file already exists, it will be overwritten.
* <p/>
* Calling this method is allowed only after all tests are completed. If the method is called in parallel with the execution of the measured code, the coverage value is unpredictable.
*
*
* @param file the file to save binary report
* @throws IOException in case of any error working with files
*/
public static void saveReport(File file) throws IOException {
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}

try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file)); DataOutputStream outputStream = new DataOutputStream(out)) {
CoverageRuntime.dumpIcReport(outputStream);
}
}

/**
* Get content of the coverage binary report with ic format.
* The resulting byte array can be directly saved to an ic file for working with the CLI
* <p/>
* Calling this method is allowed only after all tests are completed. If the method is called in parallel with the execution of the measured code, the coverage value is unpredictable.
*
*
* @return byte array with binary report in ic format
* @throws IOException in case of any error working with files
*/
public static byte[] getReport() throws IOException {
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
try (DataOutputStream outputStream = new DataOutputStream(byteArrayStream)) {
CoverageRuntime.dumpIcReport(outputStream);
}
return byteArrayStream.toByteArray();
}

private static List<ClassCoverage> convertClasses(List<com.intellij.rt.coverage.offline.api.ClassCoverage> origins) {
ArrayList<ClassCoverage> result = new ArrayList<>(origins.size());
for (com.intellij.rt.coverage.offline.api.ClassCoverage classCoverage : origins) {
Expand Down

0 comments on commit 1eb1fca

Please sign in to comment.