diff --git a/docs/index.md b/docs/index.md index 988b5e49..70e8cbec 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,2 +1,3 @@ - [Kover Gradle Plugin](gradle-plugin) -- [Kover Command Line Interface](cli) \ No newline at end of file +- [Kover Command Line Interface](cli) +- [Kover offline instrumentation](offline-instrumentation) \ No newline at end of file diff --git a/docs/offline-instrumentation/index.md b/docs/offline-instrumentation/index.md index 8db28367..c0caf689 100644 --- a/docs/offline-instrumentation/index.md +++ b/docs/offline-instrumentation/index.md @@ -11,12 +11,32 @@ Offline instrumentation is suitable when using runtime environments that do not ### 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. +#### Instrumentation by Kover CLI +The Kover CLI is a fat jar that needs to be called and passed certain commands through arguments. + +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 details. + +#### Instrumentation by Kover Features +Kover Features is a library that provides capabilities similar to Kover CLI and Kover Gradle plugin. + +You can declare a dependency on Kover Features using following coordinates: `org.jetbrains.kotlinx:kover-features-jvm:0.7.5`. + +Then you can use the Kover Features classes to instrument the bytecode of each class: +```kotlin +import kotlinx.kover.features.jvm.KoverFeatures + // ... + + val instrumenter = KoverFeatures.createOfflineInstrumenter() + + // read class-file with name `fileName` bytes to `classBytes` + val instrumentedBytes = instrumenter.instrument(classBytes, fileName) + // save `instrumentedBytes` to file +``` ### Dump coverage result -To run classes instrumented offline, you'll need to add `org.jetbrains.kotlinx:kover-offline` artifact to the application's classpath. +To run classes instrumented offline (with CLI) or programmatically (with Kover Features), you'll need to add `org.jetbrains.kotlinx:kover-offline-runtime` artifact to the application's classpath. There are several ways to get coverage: @@ -64,16 +84,18 @@ Calling these methods is allowed only after all tests are completed. If the meth See [example](#example-of-using-the-api). ## Logging -`org.jetbrains.kotlinx:kover-offline` has its own logging system. +`org.jetbrains.kotlinx:kover-offline-runtime` has its own logging system. + +By default, warning and error messages are printed to standard error stream. -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. +It is also possible to save all log messages to a file, to do this, you need to pass the system property `kover.offline.log.file.path` with path to the log file. ## Examples ### Gradle example for binary report Example of a custom binary report production using Kover tool CLI in Gradle -``` +```kotlin plugins { kotlin("jvm") version "1.8.0" application diff --git a/kover-cli/build.gradle.kts b/kover-cli/build.gradle.kts index aa9606d5..cf245634 100644 --- a/kover-cli/build.gradle.kts +++ b/kover-cli/build.gradle.kts @@ -33,7 +33,8 @@ kotlin { } dependencies { - implementation(libs.intellij.reporter) + implementation(project(":kover-features-jvm")) + implementation(libs.args4j) testImplementation(kotlin("test")) diff --git a/kover-cli/src/main/kotlin/kotlinx/kover/cli/commands/OfflineInstrumentCommand.kt b/kover-cli/src/main/kotlin/kotlinx/kover/cli/commands/OfflineInstrumentCommand.kt index 76e3665e..247944b6 100644 --- a/kover-cli/src/main/kotlin/kotlinx/kover/cli/commands/OfflineInstrumentCommand.kt +++ b/kover-cli/src/main/kotlin/kotlinx/kover/cli/commands/OfflineInstrumentCommand.kt @@ -16,9 +16,8 @@ package kotlinx.kover.cli.commands -import com.intellij.rt.coverage.instrument.api.OfflineInstrumentationApi -import com.intellij.rt.coverage.report.api.Filters -import kotlinx.kover.cli.util.asPatterns +import kotlinx.kover.cli.util.asRegex +import kotlinx.kover.features.jvm.KoverLegacyFeatures import org.kohsuke.args4j.Argument import org.kohsuke.args4j.Option import java.io.File @@ -63,21 +62,14 @@ internal class OfflineInstrumentCommand : Command { override fun call(output: PrintWriter, errorWriter: PrintWriter): Int { - // disable ConDy for offline instrumentations - System.setProperty("coverage.condy.enable", "false") - - val outputRoots = ArrayList(roots.size) - for (i in roots.indices) { - outputRoots.add(outputDir!!) - } - val filters = Filters( - includeClasses.asPatterns(), - excludeClasses.asPatterns(), - excludeAnnotation.asPatterns() + val filters = KoverLegacyFeatures.ClassFilters( + includeClasses.asRegex().toSet(), + excludeClasses.asRegex().toSet(), + excludeAnnotation.asRegex().toSet() ) try { - OfflineInstrumentationApi.instrument(roots, outputRoots, filters, countHits) + KoverLegacyFeatures.instrument(outputDir!!, roots, filters, countHits) } catch (e: Exception) { errorWriter.println("Instrumentation failed: " + e.message) return -1 diff --git a/kover-cli/src/main/kotlin/kotlinx/kover/cli/commands/ReportCommand.kt b/kover-cli/src/main/kotlin/kotlinx/kover/cli/commands/ReportCommand.kt index 4ba34eea..fa0eba6c 100644 --- a/kover-cli/src/main/kotlin/kotlinx/kover/cli/commands/ReportCommand.kt +++ b/kover-cli/src/main/kotlin/kotlinx/kover/cli/commands/ReportCommand.kt @@ -16,9 +16,9 @@ package kotlinx.kover.cli.commands -import com.intellij.rt.coverage.report.api.Filters -import com.intellij.rt.coverage.report.api.ReportApi -import kotlinx.kover.cli.util.asPatterns +import kotlinx.kover.cli.util.asRegex +import kotlinx.kover.features.jvm.KoverLegacyFeatures +import kotlinx.kover.features.jvm.KoverLegacyFeatures.ClassFilters import org.kohsuke.args4j.Argument import org.kohsuke.args4j.Option import java.io.File @@ -77,15 +77,16 @@ internal class ReportCommand : Command { override fun call(output: PrintWriter, errorWriter: PrintWriter): Int { - val filters = Filters( - includeClasses.asPatterns(), - excludeClasses.asPatterns(), - excludeAnnotation.asPatterns() + val filters = ClassFilters( + includeClasses.asRegex().toSet(), + excludeClasses.asRegex().toSet(), + excludeAnnotation.asRegex().toSet() ) + var fail = false if (xmlFile != null) { try { - ReportApi.xmlReport(xmlFile, title ?: "Kover XML Report", binaryReports, outputRoots, sourceRoots, filters) + KoverLegacyFeatures.generateXmlReport(xmlFile, binaryReports, outputRoots, sourceRoots, title ?: "Kover XML Report", filters) } catch (e: IOException) { fail = true errorWriter.println("XML generation failed: " + e.message) @@ -93,7 +94,7 @@ internal class ReportCommand : Command { } if (htmlDir != null) { try { - ReportApi.htmlReport(htmlDir, title, null, binaryReports, outputRoots, sourceRoots, filters) + KoverLegacyFeatures.generateHtmlReport(htmlDir, binaryReports, outputRoots, sourceRoots, title ?: "Kover HTML Report", filters) } catch (e: IOException) { fail = true errorWriter.println("HTML generation failed: " + e.message) diff --git a/kover-cli/src/main/kotlin/kotlinx/kover/cli/util/KoverUtils.kt b/kover-cli/src/main/kotlin/kotlinx/kover/cli/util/KoverUtils.kt index f0445677..e0e01070 100644 --- a/kover-cli/src/main/kotlin/kotlinx/kover/cli/util/KoverUtils.kt +++ b/kover-cli/src/main/kotlin/kotlinx/kover/cli/util/KoverUtils.kt @@ -16,9 +16,7 @@ package kotlinx.kover.cli.util -import java.util.regex.Pattern - -internal fun List.asPatterns(): List = map { Pattern.compile(it.wildcardsToRegex()) } +internal fun List.asRegex(): List = map { it.wildcardsToRegex() } /** * Replaces characters `*` or `.` to `.*` and `.` regexp characters and also add escape char '\' before regexp metacharacters (see [regexMetacharactersSet]). diff --git a/kover-features-jvm/build.gradle.kts b/kover-features-jvm/build.gradle.kts new file mode 100644 index 00000000..6177bd9d --- /dev/null +++ b/kover-features-jvm/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2024 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 { + java + id("kover-publishing-conventions") +} + +extensions.configure { + description.set("Implementation of calling the main features of Kover programmatically") + fatJar.set(true) +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 +} + +repositories { + mavenCentral() +} + +tasks.processResources { + filesMatching("**/kover.version") { + filter { + it.replace("\$version", project.version.toString()) + } + } +} + +dependencies { + implementation(libs.intellij.reporter) +} diff --git a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/ConDySettings.java b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/ConDySettings.java new file mode 100644 index 00000000..3b913275 --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/ConDySettings.java @@ -0,0 +1,36 @@ +package kotlinx.kover.features.jvm; + +/** + * Internal class to control JVM ConDy settings. + */ +final class ConDySettings { + + private ConDySettings() { + // no-op + } + + private static final String CONDY_SYSTEM_PARAM_NAME = "coverage.condy.enable"; + + /** + * Disable JVM ConDy during instrumentation. + * + * @return previous value of ConDy setting + */ + static String disableConDy() { + // disable ConDy for offline instrumentations + return System.setProperty(CONDY_SYSTEM_PARAM_NAME, "false"); + } + + /** + * Restore previous value of JVM ConDy setting. + * + * @param prevValue new setting value + */ + static void restoreConDy(String prevValue) { + if (prevValue == null) { + System.clearProperty(CONDY_SYSTEM_PARAM_NAME); + } else { + System.setProperty(CONDY_SYSTEM_PARAM_NAME, prevValue); + } + } +} diff --git a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverFeatures.java b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverFeatures.java new file mode 100644 index 00000000..04ba5dde --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverFeatures.java @@ -0,0 +1,42 @@ +package kotlinx.kover.features.jvm; + +import java.io.InputStream; +import java.util.Scanner; + +/** + * A class for using features via Java calls. + */ +public class KoverFeatures { + private static final String koverVersion = readVersion(); + + /** + * Getting the Kover version. + * + * @return The version of Kover used in these utilities. + */ + public static String getVersion() { + return koverVersion; + } + + /** + * Create instance to instrument already compiled class-files. + * + * @return instrumenter for offline instrumentation. + */ + public static OfflineInstrumenter createOfflineInstrumenter() { + return new OfflineInstrumenterImpl(false); + } + + private static String readVersion() { + String version = "unrecognized"; + // read version from file in resources + try (InputStream stream = KoverFeatures.class.getClassLoader().getResourceAsStream("kover.version")) { + if (stream != null) { + version = new Scanner(stream).nextLine(); + } + } catch (Throwable e) { + // can't read + } + return version; + } +} diff --git a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverLegacyFeatures.java b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverLegacyFeatures.java new file mode 100644 index 00000000..8c091c53 --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverLegacyFeatures.java @@ -0,0 +1,134 @@ +package kotlinx.kover.features.jvm; + +import com.intellij.rt.coverage.instrument.api.OfflineInstrumentationApi; +import com.intellij.rt.coverage.report.api.Filters; +import com.intellij.rt.coverage.report.api.ReportApi; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Kover Features for support Kover capabilities in Kover CLI via outdated API. + */ +public class KoverLegacyFeatures { + + /** + * Generate modified class-files to measure the coverage. + * + * @param resultDir Directory where the instrumented class-files will be placed + * @param originalDirs Root directories where the original files are located, the coverage of which needs to be measured + * @param filters Filters to limit the classes that will be displayed in the report + * @param countHits Flag indicating whether to count the number of executions to each block of code. {@code false} if it is enough to register only the fact of at least one execution + */ + public static void instrument(File resultDir, + List originalDirs, + ClassFilters filters, + boolean countHits + ) { + ArrayList outputs = new ArrayList<>(originalDirs.size()); + for (int i = 0; i < originalDirs.size(); i++) { + outputs.add(resultDir); + } + + String previousConDySetting = ConDySettings.disableConDy(); + try { + OfflineInstrumentationApi.instrument(originalDirs, outputs, convertFilters(filters), countHits); + } finally { + ConDySettings.restoreConDy(previousConDySetting); + } + } + + /** + * Generate Kover XML report, compatible with JaCoCo XML. + * + * @param xmlFile path to the generated XML report + * @param binaryReports list of coverage binary reports in IC format + * @param classfileDirs list of root directories for compiled class-files + * @param sourceDirs list of root directories for Java and Kotlin source files + * @param title Title for header + * @param filters Filters to limit the classes that will be displayed in the report + * @throws IOException In case of a report generation error + */ + public static void generateXmlReport( + File xmlFile, + List binaryReports, + List classfileDirs, + List sourceDirs, + String title, + ClassFilters filters + ) throws IOException { + ReportApi.xmlReport(xmlFile, title, binaryReports, classfileDirs, sourceDirs, convertFilters(filters)); + } + + /** + * Generate Kover HTML report. + * + * @param htmlDir output directory with result HTML report + * @param binaryReports list of coverage binary reports in IC format + * @param classfileDirs list of root directories for compiled class-files + * @param sourceDirs list of root directories for Java and Kotlin source files + * @param title Title for header + * @param filters Filters to limit the classes that will be displayed in the report. + * @throws IOException In case of a report generation error + */ + public static void generateHtmlReport( + File htmlDir, + List binaryReports, + List classfileDirs, + List sourceDirs, + String title, + ClassFilters filters + ) throws IOException { + ReportApi.htmlReport(htmlDir, title, null, binaryReports, classfileDirs, sourceDirs, convertFilters(filters)); + } + + /** + * Class filters. + */ + public static class ClassFilters { + /** + * If specified, only the classes specified in this field are filtered. + */ + public final Set includeClasses; + + /** + * The classes specified in this field are not filtered. + */ + public final Set excludeClasses; + + /** + * Classes that have at least one of the annotations specified in this field are not filtered. + */ + public final Set excludeAnnotation; + + public ClassFilters(Set includeClasses, + Set excludeClasses, + Set excludeAnnotation) { + this.includeClasses = includeClasses; + this.excludeClasses = excludeClasses; + this.excludeAnnotation = excludeAnnotation; + } + } + + private static Filters convertFilters(ClassFilters filters) { + return new Filters( + convert(filters.includeClasses), + convert(filters.excludeClasses), + convert(filters.excludeAnnotation) + ); + } + + private static List convert(Set regexes) { + ArrayList patterns = new ArrayList<>(regexes.size()); + for (String regex : regexes) { + patterns.add(Pattern.compile(regex)); + } + return patterns; + } + + +} diff --git a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/OfflineInstrumenter.java b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/OfflineInstrumenter.java new file mode 100644 index 00000000..411910ba --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/OfflineInstrumenter.java @@ -0,0 +1,19 @@ +package kotlinx.kover.features.jvm; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Class for instrumentation of JVM byte code of already compiled class-files. + */ +public interface OfflineInstrumenter { + /** + * Modify byte code of single class-file to measure the coverage of this class. + * + * @param originalClass input stream with byte code of original class-file + * @param debugName name of the class or class-file, which is used in the error message + * @return instrumented byte code + * @throws IOException in case of any instrumentation error + */ + byte[] instrument(InputStream originalClass, String debugName) throws IOException; +} \ No newline at end of file diff --git a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/OfflineInstrumenterImpl.java b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/OfflineInstrumenterImpl.java new file mode 100644 index 00000000..b48583fc --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/OfflineInstrumenterImpl.java @@ -0,0 +1,34 @@ +package kotlinx.kover.features.jvm; + +import com.intellij.rt.coverage.instrument.api.OfflineInstrumentationApi; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Implementation of {@link OfflineInstrumenter}. + * The class should not be explicitly used from the outside. + */ +class OfflineInstrumenterImpl implements OfflineInstrumenter { + private final boolean countHits; + + OfflineInstrumenterImpl(boolean countHits) { + this.countHits = countHits; + } + + @Override + public byte[] instrument(InputStream originalClass, String debugName) throws IOException { + String previousConDySetting = ConDySettings.disableConDy(); + + try { + return OfflineInstrumentationApi.instrument(originalClass, countHits); + } catch (Throwable e) { + throw new IOException( + String.format("Error while instrumenting '%s' with Kover instrumenter version '%s'", + debugName, KoverFeatures.getVersion()), e); + } finally { + ConDySettings.restoreConDy(previousConDySetting); + } + } + +} diff --git a/kover-features-jvm/src/main/resources/kover.version b/kover-features-jvm/src/main/resources/kover.version new file mode 100644 index 00000000..03f59033 --- /dev/null +++ b/kover-features-jvm/src/main/resources/kover.version @@ -0,0 +1 @@ +$version \ No newline at end of file diff --git a/kover-offline-runtime/build.gradle.kts b/kover-offline-runtime/build.gradle.kts index d8873744..74638369 100644 --- a/kover-offline-runtime/build.gradle.kts +++ b/kover-offline-runtime/build.gradle.kts @@ -33,13 +33,17 @@ repositories { mavenCentral() } +val fatJarDependency = "fatJar" +val fatJarConfiguration = configurations.create(fatJarDependency) + dependencies { - implementation(libs.intellij.offline) + compileOnly(libs.intellij.offline) + fatJarConfiguration(libs.intellij.offline) } tasks.jar { from( - configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) } + fatJarConfiguration.map { if (it.isDirectory) it else zipTree(it) } ) { exclude("OSGI-OPT/**") exclude("META-INF/**") 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 index d387c719..6f569933 100644 --- 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 @@ -1,6 +1,7 @@ package kotlinx.kover.offline.runtime; import com.intellij.rt.coverage.offline.api.CoverageRuntime; +import com.intellij.rt.coverage.util.ErrorReporter; import kotlinx.kover.offline.runtime.api.KoverRuntime; import java.io.File; @@ -17,16 +18,21 @@ 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)); + if (logFileProp == null) { + // by default, we do not create a file, because we do not know what rights our application is running with + // and whether it can create files in the current directory or next to the binary report file + CoverageRuntime.setLogPath(null); } else { - CoverageRuntime.setLogPath(new File(KoverRuntime.DEFAULT_LOG_FILE_NAME)); + CoverageRuntime.setLogPath(new File(logFileProp)); } + // setting the logging level in the "standard" error output stream + CoverageRuntime.setLogLevel(ErrorReporter.WARNING); + + String reportNameSavedOnExitProp = System.getProperty(REPORT_PROPERTY_NAME); if (reportNameSavedOnExitProp != null) { + // if a parameter is passed, then use the shutdown hook to save the binary report to a file saveOnExit(reportNameSavedOnExitProp); } } 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 c89c9db1..25031c1c 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 @@ -16,13 +16,6 @@ */ 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. *

@@ -37,7 +30,7 @@ public class KoverRuntime { * 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. + * If this property is not specified, the logs file will not be created. */ public static final String LOG_FILE_PROPERTY_NAME = "kover.offline.log.file.path"; diff --git a/settings.gradle.kts b/settings.gradle.kts index 6001d9a9..73afbc01 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,7 @@ dependencyResolutionManagement { } } +include(":kover-features-jvm") include(":kover-gradle-plugin") include(":kover-cli") include(":kover-offline-runtime")