From 1eb1fca214b8768e2e9031e8a30ba521a7fa141d Mon Sep 17 00:00:00 2001 From: Sergey Shanshin Date: Wed, 15 Nov 2023 21:44:25 +0300 Subject: [PATCH] Introduced Offline Runtime API for saving binary report - 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 Resolves #503 PR #500 --- docs/offline-instrumentation/index.md | 58 +++++++++++++--- gradle/libs.versions.toml | 2 +- .../kover/gradle/plugin/dsl/KoverVersions.kt | 2 +- .../runtime-api/src/test/kotlin/Tests.kt | 15 +++++ .../kover/offline/runtime/KoverInit.java | 49 ++++++++++++++ .../offline/runtime/api/KoverRuntime.java | 66 ++++++++++++++++++- 6 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 kover-offline-runtime/src/main/java/kotlinx/kover/offline/runtime/KoverInit.java diff --git a/docs/offline-instrumentation/index.md b/docs/offline-instrumentation/index.md index a1b836fb..888d851f 100644 --- a/docs/offline-instrumentation/index.md +++ b/docs/offline-instrumentation/index.md @@ -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 @@ -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)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17664fa8..7c9b717f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/dsl/KoverVersions.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/dsl/KoverVersions.kt index 13c5e163..8d9e188c 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/dsl/KoverVersions.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/dsl/KoverVersions.kt @@ -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. diff --git a/kover-offline-runtime/examples/runtime-api/src/test/kotlin/Tests.kt b/kover-offline-runtime/examples/runtime-api/src/test/kotlin/Tests.kt index daaf8645..7d1b420e 100644 --- a/kover-offline-runtime/examples/runtime-api/src/test/kotlin/Tests.kt +++ b/kover-offline-runtime/examples/runtime-api/src/test/kotlin/Tests.kt @@ -2,7 +2,9 @@ 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 { @@ -10,6 +12,19 @@ class Tests { 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)) diff --git a/kover-offline-runtime/src/main/java/kotlinx/kover/offline/runtime/KoverInit.java b/kover-offline-runtime/src/main/java/kotlinx/kover/offline/runtime/KoverInit.java new file mode 100644 index 00000000..d387c719 --- /dev/null +++ b/kover-offline-runtime/src/main/java/kotlinx/kover/offline/runtime/KoverInit.java @@ -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. + *

+ * 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 } 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 + } +} diff --git a/kover-offline-runtime/src/main/java/kotlinx/kover/offline/runtime/api/KoverRuntime.java b/kover-offline-runtime/src/main/java/kotlinx/kover/offline/runtime/api/KoverRuntime.java index 6a3cd965..c89c9db1 100644 --- a/kover-offline-runtime/src/main/java/kotlinx/kover/offline/runtime/api/KoverRuntime.java +++ b/kover-offline-runtime/src/main/java/kotlinx/kover/offline/runtime/api/KoverRuntime.java @@ -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; @@ -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. + *

+ * 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. + */ + 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. + * + *

+ * 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. *

@@ -42,6 +67,45 @@ public static List collect(List 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. + *

+ * 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 + *

+ * 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 convertClasses(List origins) { ArrayList result = new ArrayList<>(origins.size()); for (com.intellij.rt.coverage.offline.api.ClassCoverage classCoverage : origins) {