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 247944b6..86187a12 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,7 +16,7 @@ package kotlinx.kover.cli.commands -import kotlinx.kover.cli.util.asRegex +import kotlinx.kover.features.jvm.ClassFilters import kotlinx.kover.features.jvm.KoverLegacyFeatures import org.kohsuke.args4j.Argument import org.kohsuke.args4j.Option @@ -62,10 +62,10 @@ internal class OfflineInstrumentCommand : Command { override fun call(output: PrintWriter, errorWriter: PrintWriter): Int { - val filters = KoverLegacyFeatures.ClassFilters( - includeClasses.asRegex().toSet(), - excludeClasses.asRegex().toSet(), - excludeAnnotation.asRegex().toSet() + val filters = ClassFilters( + includeClasses.toSet(), + excludeClasses.toSet(), + excludeAnnotation.toSet() ) try { 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 fa0eba6c..7f11be76 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,8 @@ package kotlinx.kover.cli.commands -import kotlinx.kover.cli.util.asRegex +import kotlinx.kover.features.jvm.ClassFilters 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 @@ -78,15 +77,15 @@ internal class ReportCommand : Command { override fun call(output: PrintWriter, errorWriter: PrintWriter): Int { val filters = ClassFilters( - includeClasses.asRegex().toSet(), - excludeClasses.asRegex().toSet(), - excludeAnnotation.asRegex().toSet() + includeClasses.toSet(), + excludeClasses.toSet(), + excludeAnnotation.toSet() ) var fail = false if (xmlFile != null) { try { - KoverLegacyFeatures.generateXmlReport(xmlFile, binaryReports, outputRoots, sourceRoots, title ?: "Kover XML Report", filters) + KoverLegacyFeatures.generateXmlReport(xmlFile!!, binaryReports, outputRoots, sourceRoots, title ?: "Kover XML Report", filters) } catch (e: IOException) { fail = true errorWriter.println("XML generation failed: " + e.message) @@ -94,7 +93,7 @@ internal class ReportCommand : Command { } if (htmlDir != null) { try { - KoverLegacyFeatures.generateHtmlReport(htmlDir, binaryReports, outputRoots, sourceRoots, title ?: "Kover HTML Report", filters) + KoverLegacyFeatures.generateHtmlReport(htmlDir!!, null, 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 deleted file mode 100644 index e0e01070..00000000 --- a/kover-cli/src/main/kotlin/kotlinx/kover/cli/util/KoverUtils.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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. - */ - -package kotlinx.kover.cli.util - -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]). - */ -internal fun String.wildcardsToRegex(): String { - // in most cases, the characters `*` or `.` will be present therefore, we increase the capacity in advance - val builder = StringBuilder(length * 2) - - forEach { char -> - when (char) { - in regexMetacharactersSet -> builder.append('\\').append(char) - '*' -> builder.append('.').append("*") - '?' -> builder.append('.') - else -> builder.append(char) - } - } - - return builder.toString() -} - -private val regexMetacharactersSet = "<([{\\^-=$!|]})+.>".toSet() - diff --git a/kover-features-jvm/api/kover-features-jvm.api b/kover-features-jvm/api/kover-features-jvm.api new file mode 100644 index 00000000..b325d422 --- /dev/null +++ b/kover-features-jvm/api/kover-features-jvm.api @@ -0,0 +1,136 @@ +public final class kotlinx/kover/features/jvm/AggregationType : java/lang/Enum { + public static final field COVERED_COUNT Lkotlinx/kover/features/jvm/AggregationType; + public static final field COVERED_PERCENTAGE Lkotlinx/kover/features/jvm/AggregationType; + public static final field MISSED_COUNT Lkotlinx/kover/features/jvm/AggregationType; + public static final field MISSED_PERCENTAGE Lkotlinx/kover/features/jvm/AggregationType; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/kover/features/jvm/AggregationType; + public static fun values ()[Lkotlinx/kover/features/jvm/AggregationType; +} + +public final class kotlinx/kover/features/jvm/Bound { + public fun (Ljava/math/BigDecimal;Ljava/math/BigDecimal;Lkotlinx/kover/features/jvm/CoverageUnit;Lkotlinx/kover/features/jvm/AggregationType;)V + public final fun component1 ()Ljava/math/BigDecimal; + public final fun component2 ()Ljava/math/BigDecimal; + public final fun component3 ()Lkotlinx/kover/features/jvm/CoverageUnit; + public final fun component4 ()Lkotlinx/kover/features/jvm/AggregationType; + public final fun copy (Ljava/math/BigDecimal;Ljava/math/BigDecimal;Lkotlinx/kover/features/jvm/CoverageUnit;Lkotlinx/kover/features/jvm/AggregationType;)Lkotlinx/kover/features/jvm/Bound; + public static synthetic fun copy$default (Lkotlinx/kover/features/jvm/Bound;Ljava/math/BigDecimal;Ljava/math/BigDecimal;Lkotlinx/kover/features/jvm/CoverageUnit;Lkotlinx/kover/features/jvm/AggregationType;ILjava/lang/Object;)Lkotlinx/kover/features/jvm/Bound; + public fun equals (Ljava/lang/Object;)Z + public final fun getAggregationForGroup ()Lkotlinx/kover/features/jvm/AggregationType; + public final fun getCoverageUnits ()Lkotlinx/kover/features/jvm/CoverageUnit; + public final fun getMaxValue ()Ljava/math/BigDecimal; + public final fun getMinValue ()Ljava/math/BigDecimal; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/kover/features/jvm/BoundViolation { + public fun (Lkotlinx/kover/features/jvm/Bound;ZLjava/math/BigDecimal;Ljava/lang/String;)V + public final fun component1 ()Lkotlinx/kover/features/jvm/Bound; + public final fun component2 ()Z + public final fun component3 ()Ljava/math/BigDecimal; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Lkotlinx/kover/features/jvm/Bound;ZLjava/math/BigDecimal;Ljava/lang/String;)Lkotlinx/kover/features/jvm/BoundViolation; + public static synthetic fun copy$default (Lkotlinx/kover/features/jvm/BoundViolation;Lkotlinx/kover/features/jvm/Bound;ZLjava/math/BigDecimal;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/kover/features/jvm/BoundViolation; + public fun equals (Ljava/lang/Object;)Z + public final fun getBound ()Lkotlinx/kover/features/jvm/Bound; + public final fun getEntityName ()Ljava/lang/String; + public final fun getValue ()Ljava/math/BigDecimal; + public fun hashCode ()I + public final fun isMax ()Z + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/kover/features/jvm/ClassFilters { + public fun (Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;)V + public final fun component1 ()Ljava/util/Set; + public final fun component2 ()Ljava/util/Set; + public final fun component3 ()Ljava/util/Set; + public final fun copy (Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;)Lkotlinx/kover/features/jvm/ClassFilters; + public static synthetic fun copy$default (Lkotlinx/kover/features/jvm/ClassFilters;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ILjava/lang/Object;)Lkotlinx/kover/features/jvm/ClassFilters; + public fun equals (Ljava/lang/Object;)Z + public final fun getExcludeAnnotation ()Ljava/util/Set; + public final fun getExcludeClasses ()Ljava/util/Set; + public final fun getIncludeClasses ()Ljava/util/Set; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/kover/features/jvm/CoverageUnit : java/lang/Enum { + public static final field BRANCH Lkotlinx/kover/features/jvm/CoverageUnit; + public static final field INSTRUCTION Lkotlinx/kover/features/jvm/CoverageUnit; + public static final field LINE Lkotlinx/kover/features/jvm/CoverageUnit; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/kover/features/jvm/CoverageUnit; + public static fun values ()[Lkotlinx/kover/features/jvm/CoverageUnit; +} + +public final class kotlinx/kover/features/jvm/CoverageValue { + public fun (Ljava/lang/String;Ljava/math/BigDecimal;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/math/BigDecimal; + public final fun copy (Ljava/lang/String;Ljava/math/BigDecimal;)Lkotlinx/kover/features/jvm/CoverageValue; + public static synthetic fun copy$default (Lkotlinx/kover/features/jvm/CoverageValue;Ljava/lang/String;Ljava/math/BigDecimal;ILjava/lang/Object;)Lkotlinx/kover/features/jvm/CoverageValue; + public fun equals (Ljava/lang/Object;)Z + public final fun getEntityName ()Ljava/lang/String; + public final fun getValue ()Ljava/math/BigDecimal; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/kover/features/jvm/GroupingBy : java/lang/Enum { + public static final field APPLICATION Lkotlinx/kover/features/jvm/GroupingBy; + public static final field CLASS Lkotlinx/kover/features/jvm/GroupingBy; + public static final field PACKAGE Lkotlinx/kover/features/jvm/GroupingBy; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/kover/features/jvm/GroupingBy; + public static fun values ()[Lkotlinx/kover/features/jvm/GroupingBy; +} + +public final class kotlinx/kover/features/jvm/KoverFeatures { + public static final field INSTANCE Lkotlinx/kover/features/jvm/KoverFeatures; + public final fun createOfflineInstrumenter ()Lkotlinx/kover/features/jvm/OfflineInstrumenter; + public final fun getVersion ()Ljava/lang/String; + public final fun koverWildcardToRegex (Ljava/lang/String;)Ljava/lang/String; +} + +public final class kotlinx/kover/features/jvm/KoverLegacyFeatures { + public static final field INSTANCE Lkotlinx/kover/features/jvm/KoverLegacyFeatures; + public final fun aggregateIc (Ljava/io/File;Lkotlinx/kover/features/jvm/ClassFilters;Ljava/io/File;Ljava/util/List;Ljava/util/List;)V + public final fun evalCoverage (Lkotlinx/kover/features/jvm/GroupingBy;Lkotlinx/kover/features/jvm/CoverageUnit;Lkotlinx/kover/features/jvm/AggregationType;Ljava/io/File;Lkotlinx/kover/features/jvm/ClassFilters;Ljava/util/List;Ljava/util/List;)Ljava/util/List; + public final fun generateHtmlReport (Ljava/io/File;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Lkotlinx/kover/features/jvm/ClassFilters;)V + public final fun generateXmlReport (Ljava/io/File;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Lkotlinx/kover/features/jvm/ClassFilters;)V + public final fun instrument (Ljava/io/File;Ljava/util/List;Lkotlinx/kover/features/jvm/ClassFilters;Z)V + public final fun verify (Ljava/util/List;Ljava/io/File;Lkotlinx/kover/features/jvm/ClassFilters;Ljava/util/List;Ljava/util/List;)Ljava/util/List; +} + +public abstract interface class kotlinx/kover/features/jvm/OfflineInstrumenter { + public abstract fun instrument (Ljava/io/InputStream;Ljava/lang/String;)[B +} + +public final class kotlinx/kover/features/jvm/Rule { + public fun (Ljava/lang/String;Lkotlinx/kover/features/jvm/GroupingBy;Ljava/util/List;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlinx/kover/features/jvm/GroupingBy; + public final fun component3 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Lkotlinx/kover/features/jvm/GroupingBy;Ljava/util/List;)Lkotlinx/kover/features/jvm/Rule; + public static synthetic fun copy$default (Lkotlinx/kover/features/jvm/Rule;Ljava/lang/String;Lkotlinx/kover/features/jvm/GroupingBy;Ljava/util/List;ILjava/lang/Object;)Lkotlinx/kover/features/jvm/Rule; + public fun equals (Ljava/lang/Object;)Z + public final fun getBounds ()Ljava/util/List; + public final fun getGroupBy ()Lkotlinx/kover/features/jvm/GroupingBy; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/kover/features/jvm/RuleViolations { + public fun (Lkotlinx/kover/features/jvm/Rule;Ljava/util/List;)V + public final fun component1 ()Lkotlinx/kover/features/jvm/Rule; + public final fun component2 ()Ljava/util/List; + public final fun copy (Lkotlinx/kover/features/jvm/Rule;Ljava/util/List;)Lkotlinx/kover/features/jvm/RuleViolations; + public static synthetic fun copy$default (Lkotlinx/kover/features/jvm/RuleViolations;Lkotlinx/kover/features/jvm/Rule;Ljava/util/List;ILjava/lang/Object;)Lkotlinx/kover/features/jvm/RuleViolations; + public fun equals (Ljava/lang/Object;)Z + public final fun getRule ()Lkotlinx/kover/features/jvm/Rule; + public final fun getViolations ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/kover-features-jvm/build.gradle.kts b/kover-features-jvm/build.gradle.kts index 0d53c9b8..b6a0b18e 100644 --- a/kover-features-jvm/build.gradle.kts +++ b/kover-features-jvm/build.gradle.kts @@ -1,3 +1,7 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + /* * Copyright 2000-2024 JetBrains s.r.o. * @@ -15,7 +19,8 @@ */ plugins { - java + kotlin("jvm") + alias(libs.plugins.kotlinx.binaryCompatibilityValidator) id("kover-publishing-conventions") } @@ -24,8 +29,28 @@ extensions.configure().configureEach { + compilerOptions { + allWarningsAsErrors.set(true) + jvmTarget.set(JvmTarget.JVM_1_8) + languageVersion.set(KotlinVersion.KOTLIN_1_5) + apiVersion.set(KotlinVersion.KOTLIN_1_5) + freeCompilerArgs.addAll("-Xsuppress-version-warnings", "-Xjdk-release=1.8") + } + } } repositories { 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 deleted file mode 100644 index 04ba5dde..00000000 --- a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverFeatures.java +++ /dev/null @@ -1,42 +0,0 @@ -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/KoverFeatures.kt b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverFeatures.kt new file mode 100644 index 00000000..4ec40bb4 --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverFeatures.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.features.jvm + +import kotlinx.kover.features.jvm.impl.OfflineInstrumenterImpl +import kotlinx.kover.features.jvm.impl.wildcardsToRegex +import java.util.* + +/** + * A class for using features via Java calls. + */ +public object KoverFeatures { + /** + * Getting version of Kover used in these utilities. + */ + public val version: String = readVersion() + + /** + * Converts a Kover [template] string to a regular expression string. + *ё + * Replaces characters `*` to `.*`, `#` to `[^.]*` and `?` to `.` regexp characters. + * All special characters of regular expressions are also escaped. + */ + public fun koverWildcardToRegex(template: String): String { + return template.wildcardsToRegex() + } + + /** + * Create instance to instrument already compiled class-files. + * + * @return instrumenter for offline instrumentation. + */ + public fun createOfflineInstrumenter(): OfflineInstrumenter { + return OfflineInstrumenterImpl(false) + } + + private fun readVersion(): String { + var version = "unrecognized" + // read version from file in resources + try { + KoverFeatures::class.java.classLoader.getResourceAsStream("kover.version").use { stream -> + if (stream != null) { + version = Scanner(stream).nextLine() + } + } + } catch (e: Throwable) { + // can't read + } + return version + } +} \ No newline at end of file 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 deleted file mode 100644 index 8c091c53..00000000 --- a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverLegacyFeatures.java +++ /dev/null @@ -1,134 +0,0 @@ -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/KoverLegacyFeatures.kt b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverLegacyFeatures.kt new file mode 100644 index 00000000..a5c53c58 --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/KoverLegacyFeatures.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.features.jvm + +import com.intellij.rt.coverage.aggregate.api.AggregatorApi +import com.intellij.rt.coverage.aggregate.api.Request +import com.intellij.rt.coverage.instrument.api.OfflineInstrumentationApi +import com.intellij.rt.coverage.report.api.ReportApi +import com.intellij.rt.coverage.util.ErrorReporter +import kotlinx.kover.features.jvm.impl.ConDySettings +import kotlinx.kover.features.jvm.impl.LegacyVerification +import kotlinx.kover.features.jvm.impl.convert +import java.io.File +import java.io.IOException +import java.math.BigDecimal + +/** + * Kover Features for support Kover capabilities in Kover CLI via outdated API. + */ +public object KoverLegacyFeatures { + private const val FREE_MARKER_LOGGER_PROPERTY_NAME = "org.freemarker.loggerLibrary" + + /** + * 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. `false` if it is enough to register only the fact of at least one execution + */ + public fun instrument( + resultDir: File, originalDirs: List, filters: ClassFilters, countHits: Boolean + ) { + val outputs = ArrayList(originalDirs.size) + for (i in originalDirs.indices) { + outputs.add(resultDir) + } + + val previousConDySetting = ConDySettings.disableConDy() + try { + OfflineInstrumentationApi.instrument(originalDirs, outputs, filters.convert(), 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 + */ + @Throws(IOException::class) + public fun generateXmlReport( + xmlFile: File, + binaryReports: List, + classfileDirs: List, + sourceDirs: List, + title: String, + filters: ClassFilters + ) { + ReportApi.xmlReport(xmlFile, title, binaryReports, classfileDirs, sourceDirs, filters.convert()) + } + + /** + * Generate Kover HTML report. + * + * @param htmlDir Output directory with result HTML report + * @param charsetName Name of charset used in 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 + */ + @Throws(IOException::class) + public fun generateHtmlReport( + htmlDir: File, + charsetName: String?, + binaryReports: List, + classfileDirs: List, + sourceDirs: List, + title: String, + filters: ClassFilters + ) { + // print to stdout only critical errors + ErrorReporter.setLogLevel(ErrorReporter.ERROR) + + // disable freemarker logging to stdout for the time of report generation + val oldFreemarkerLogger = System.setProperty(FREE_MARKER_LOGGER_PROPERTY_NAME, "none") + try { + ReportApi.htmlReport( + htmlDir, title, charsetName, binaryReports, classfileDirs, sourceDirs, filters.convert() + ) + } finally { + if (oldFreemarkerLogger == null) { + System.clearProperty(FREE_MARKER_LOGGER_PROPERTY_NAME) + } else { + System.setProperty(FREE_MARKER_LOGGER_PROPERTY_NAME, oldFreemarkerLogger) + } + } + } + + + /** + * Verify coverage by specified verification rules. + * + * @param rules List of the verification rules to check + * @param tempDir Directory to create temporary files + * @param filters Filters to limit the classes that will be verified + * @param binaryReports List of coverage binary reports in IC format + * @param classfileDirs List of root directories for compiled class-files + * @return List of rule violation errors, empty list if there is no verification errors. + */ + public fun verify( + rules: List, tempDir: File, filters: ClassFilters, binaryReports: List, classfileDirs: List + ): List { + try { + return LegacyVerification.verify(rules, tempDir, filters, binaryReports, classfileDirs) + } catch (e: IOException) { + throw RuntimeException("Kover features exception occurred while verification", e) + } + } + + /** + * Merge several IC binaryReports into one file. + * + * @param icFile Target IC report file + * @param filters Filters to limit the classes that will be placed into result file + * @param tempDir Directory to create temporary files + * @param binaryReports List of coverage binary reports in IC format + * @param classfileDirs List of root directories for compiled class-files + */ + public fun aggregateIc( + icFile: File, filters: ClassFilters, tempDir: File, binaryReports: List, classfileDirs: List + ) { + val smapFile = tempDir.resolve("report.smap") + + val request = Request(filters.convert(), icFile, smapFile) + AggregatorApi.aggregate(listOf(request), binaryReports, classfileDirs) + } + + /** + * Get coverage values from binary reports. + * + * @param groupBy Code unit for which coverage will be aggregated + * @param coverageUnit Specify which units to measure coverage for (line, branch, etc.) + * @param aggregationForGroup Aggregation function that will be calculated over all the elements of the same group + * @param tempDir Directory to create temporary files + * @param filters Filters to limit the classes that will be placed into result coverage + * @param binaryReports List of coverage binary reports in IC format + * @param classfileDirs List of root directories for compiled class-files + * @return List of coverage values. + */ + public fun evalCoverage( + groupBy: GroupingBy, + coverageUnit: CoverageUnit, + aggregationForGroup: AggregationType, + tempDir: File, + filters: ClassFilters, + binaryReports: List, + classfileDirs: List + ): List { + val bound = Bound(LegacyVerification.ONE_HUNDRED, BigDecimal.ZERO, coverageUnit, aggregationForGroup) + val rule = Rule("", groupBy, listOf(bound)) + + val violations = verify(listOf(rule), tempDir, filters, binaryReports, classfileDirs) + val result = ArrayList() + + for (violation in violations) { + for (boundViolation in violation.violations) { + result.add(CoverageValue(boundViolation.entityName, boundViolation.value)) + } + } + + return result + } +} + +/** + * Class filters. + */ +public data class ClassFilters( + /** + * If specified, only the classes specified in this field are filtered. + */ + public val includeClasses: Set, + /** + * The classes specified in this field are not filtered. + */ + public val excludeClasses: Set, + /** + * Classes that have at least one of the annotations specified in this field are not filtered. + */ + public val excludeAnnotation: Set +) 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.kt similarity index 61% rename from kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/OfflineInstrumenter.java rename to kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/OfflineInstrumenter.kt index 411910ba..301739ed 100644 --- 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.kt @@ -1,7 +1,11 @@ -package kotlinx.kover.features.jvm; +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.features.jvm -import java.io.IOException; -import java.io.InputStream; +import java.io.IOException +import java.io.InputStream /** * Class for instrumentation of JVM byte code of already compiled class-files. @@ -15,5 +19,6 @@ public interface OfflineInstrumenter { * @return instrumented byte code * @throws IOException in case of any instrumentation error */ - byte[] instrument(InputStream originalClass, String debugName) throws IOException; + @Throws(IOException::class) + public fun instrument(originalClass: InputStream, debugName: String): ByteArray } \ 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 deleted file mode 100644 index b48583fc..00000000 --- a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/OfflineInstrumenterImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -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/java/kotlinx/kover/features/jvm/Verification.kt b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/Verification.kt new file mode 100644 index 00000000..23154c37 --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/Verification.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.features.jvm + +import java.math.BigDecimal + + +/** + * Entity type for grouping code to coverage evaluation. + */ +public enum class GroupingBy { + /** + * Counts the coverage values for all code. + */ + APPLICATION, + + /** + * Counts the coverage values for each class separately. + */ + CLASS, + + /** + * Counts the coverage values for each package that has classes separately. + */ + PACKAGE +} + +/** + * Type of the metric to evaluate code coverage. + */ +public enum class CoverageUnit { + /** + * Number of lines. + */ + LINE, + + /** + * Number of JVM bytecode instructions. + */ + INSTRUCTION, + + /** + * Number of branches covered. + */ + BRANCH +} + +/** + * Type of counter value to compare with minimal and maximal values if them defined. + */ +public enum class AggregationType { + COVERED_COUNT, + MISSED_COUNT, + COVERED_PERCENTAGE, + MISSED_PERCENTAGE +} + +/** + * Evaluated coverage [value] for given entity with name [entityName]. + * + * Entity could be class, package, etc + */ +public data class CoverageValue(val entityName: String?, val value: BigDecimal) + +/** + * Describes a single bound for the verification rule to enforce + */ +public data class Bound( + val minValue: BigDecimal?, + val maxValue: BigDecimal?, + val coverageUnits: CoverageUnit, + val aggregationForGroup: AggregationType +) + +/** + * Verification rule - a named set of bounds of coverage value to check. + */ +public data class Rule(val name: String, val groupBy: GroupingBy, val bounds: List) + +/** + * Violation of verification rule. + */ +public data class RuleViolations(val rule: Rule, val violations: List) + +/** + * Violation of verification bound. + */ +public data class BoundViolation(val bound: Bound, val isMax: Boolean, val value: BigDecimal, val entityName: String?) 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/impl/ConDySettings.kt similarity index 53% rename from kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/ConDySettings.java rename to kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/ConDySettings.kt index 3b913275..79b88540 100644 --- a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/ConDySettings.java +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/ConDySettings.kt @@ -1,36 +1,36 @@ -package kotlinx.kover.features.jvm; +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.features.jvm.impl + /** * 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"; +internal object ConDySettings { + private const val CONDY_SYSTEM_PARAM_NAME = "coverage.condy.enable" /** * Disable JVM ConDy during instrumentation. * * @return previous value of ConDy setting */ - static String disableConDy() { + fun disableConDy(): String? { // disable ConDy for offline instrumentations - return System.setProperty(CONDY_SYSTEM_PARAM_NAME, "false"); + return System.setProperty(CONDY_SYSTEM_PARAM_NAME, "false") } /** * Restore previous value of JVM ConDy setting. * - * @param prevValue new setting value + * Returns prevValue new setting value. */ - static void restoreConDy(String prevValue) { + fun restoreConDy(prevValue: String?) { if (prevValue == null) { - System.clearProperty(CONDY_SYSTEM_PARAM_NAME); + System.clearProperty(CONDY_SYSTEM_PARAM_NAME) } else { - System.setProperty(CONDY_SYSTEM_PARAM_NAME, prevValue); + System.setProperty(CONDY_SYSTEM_PARAM_NAME, prevValue) } } } diff --git a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/LegacyVerification.kt b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/LegacyVerification.kt new file mode 100644 index 00000000..a2db3199 --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/LegacyVerification.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.features.jvm.impl + +import com.intellij.rt.coverage.aggregate.api.AggregatorApi +import com.intellij.rt.coverage.aggregate.api.Request +import com.intellij.rt.coverage.report.api.Filters +import com.intellij.rt.coverage.verify.Verifier +import com.intellij.rt.coverage.verify.api.Counter +import com.intellij.rt.coverage.verify.api.Target +import com.intellij.rt.coverage.verify.api.ValueType +import com.intellij.rt.coverage.verify.api.VerificationApi +import kotlinx.kover.features.jvm.* +import java.io.File +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.* + +private typealias IntellijRule = com.intellij.rt.coverage.verify.api.Rule + +private typealias IntellijBound = com.intellij.rt.coverage.verify.api.Bound + +internal object LegacyVerification { + internal val ONE_HUNDRED: BigDecimal = BigDecimal(100) + + fun verify( + rules: List, + tempDir: File, + filters: ClassFilters, + reports: List, + outputs: List + ): List { + val intellijFilters: Filters = filters.convert() + + val rulesArray = ArrayList() + + val ic = tempDir.resolve("agg-ic.ic") + val smap = tempDir.resolve("agg-smap.smap") + + val requests = Request(intellijFilters, ic, smap) + AggregatorApi.aggregate(listOf(requests), reports, outputs) + + for (ruleIndex in rules.indices) { + val rule = rules[ruleIndex] + + val bounds: MutableList = ArrayList() + for (boundIndex in rule.bounds.indices) { + val b = rule.bounds[boundIndex] + + bounds.add( + IntellijBound( + boundIndex, + counterToIntellij(b), + valueTypeToIntellij(b), + valueToIntellij(b, b.minValue), + valueToIntellij(b, b.maxValue) + ) + ) + } + + rulesArray.add(IntellijRule(ruleIndex, ic, targetToIntellij(rule), bounds)) + } + + + val verifier = Verifier(rulesArray) + verifier.processRules() + val violations = VerificationApi.verify(rulesArray) + + val ruleViolations = ArrayList() + for (ruleViolation in violations) { + // TreeMap is using for getting stable order in result List - in this case, it is easier to write tests and Gradle build cache will not miss + val resultBounds = TreeMap() + + val rule = rules[ruleViolation.id] + for (boundIndex in ruleViolation.violations.indices) { + val boundViolation = ruleViolation.violations[boundIndex] + val bound = rule.bounds[boundViolation.id] + + for (maxViolation in boundViolation.maxViolations) { + val entityName = if (rule.groupBy != GroupingBy.APPLICATION) maxViolation.targetName else null + resultBounds[ViolationId(boundViolation.id, entityName)] = + BoundViolation(bound, true, intellijToValue(maxViolation.targetValue, bound), entityName) + } + for (minViolation in boundViolation.minViolations) { + val entityName = if (rule.groupBy != GroupingBy.APPLICATION) minViolation.targetName else null + resultBounds[ViolationId(boundViolation.id, entityName)] = + BoundViolation(bound, false, intellijToValue(minViolation.targetValue, bound), entityName) + } + } + + ruleViolations.add(RuleViolations(rule, ArrayList(resultBounds.values))) + } + + return ruleViolations + } + + private fun intellijToValue(intellijValue: BigDecimal, bound: Bound): BigDecimal { + return if (isPercentage(bound.aggregationForGroup)) { + intellijValue.multiply(ONE_HUNDRED) + } else { + intellijValue + } + } + + private fun targetToIntellij(rule: Rule): Target { + return when (rule.groupBy) { + GroupingBy.APPLICATION -> Target.ALL + GroupingBy.CLASS -> Target.CLASS + GroupingBy.PACKAGE -> Target.PACKAGE + } + } + + private fun counterToIntellij(bound: Bound): Counter { + return when (bound.coverageUnits) { + CoverageUnit.LINE -> Counter.LINE + CoverageUnit.INSTRUCTION -> Counter.INSTRUCTION + CoverageUnit.BRANCH -> Counter.BRANCH + } + } + + private fun valueTypeToIntellij(bound: Bound): ValueType { + return when (bound.aggregationForGroup) { + AggregationType.COVERED_COUNT -> ValueType.COVERED + AggregationType.MISSED_COUNT -> ValueType.MISSED + AggregationType.COVERED_PERCENTAGE -> ValueType.COVERED_RATE + AggregationType.MISSED_PERCENTAGE -> ValueType.MISSED_RATE + } + } + + private fun valueToIntellij(bound: Bound, value: BigDecimal?): BigDecimal? { + if (value == null) return null + + return if (isPercentage(bound.aggregationForGroup)) { + value.divide(ONE_HUNDRED, 6, RoundingMode.HALF_UP) + } else { + value + } + } + + private fun isPercentage(aggregationType: AggregationType): Boolean { + return aggregationType == AggregationType.COVERED_PERCENTAGE || aggregationType == AggregationType.MISSED_PERCENTAGE + } + + private data class ViolationId(private val index: Int, private val entityName: String?) : Comparable { + override fun compareTo(other: ViolationId): Int { + // first compared by index + index.compareTo(other.index).takeIf { it != 0 }?.let { return it } + + // if indexes are equals then compare by entity name + if (entityName == null) { + // bounds with empty entity names goes first + return if (other.entityName == null) 0 else -1 + } + + if (other.entityName == null) return 1 + + return entityName.compareTo(other.entityName) + } + } +} diff --git a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/OfflineInstrumenterImpl.kt b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/OfflineInstrumenterImpl.kt new file mode 100644 index 00000000..9d5cdbd3 --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/OfflineInstrumenterImpl.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.features.jvm.impl + +import com.intellij.rt.coverage.instrument.api.OfflineInstrumentationApi +import kotlinx.kover.features.jvm.KoverFeatures.version +import kotlinx.kover.features.jvm.OfflineInstrumenter +import java.io.IOException +import java.io.InputStream + + +/** + * Implementation of [OfflineInstrumenter]. + * The class should not be explicitly used from the outside. + */ +internal class OfflineInstrumenterImpl(private val countHits: Boolean): OfflineInstrumenter { + + override fun instrument(originalClass: InputStream, debugName: String): ByteArray { + val previousConDySetting = ConDySettings.disableConDy() + try { + return OfflineInstrumentationApi.instrument(originalClass, countHits) + } catch (e: Throwable) { + throw IOException( + String.format( + "Error while instrumenting '%s' with Kover instrumenter version '%s'", + debugName, + version + ), + e + ) + } finally { + ConDySettings.restoreConDy(previousConDySetting) + } + } +} diff --git a/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/Wildcards.kt b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/Wildcards.kt new file mode 100644 index 00000000..9f326d2e --- /dev/null +++ b/kover-features-jvm/src/main/java/kotlinx/kover/features/jvm/impl/Wildcards.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.features.jvm.impl + +import com.intellij.rt.coverage.report.api.Filters +import kotlinx.kover.features.jvm.ClassFilters +import java.util.regex.Pattern + +internal fun ClassFilters.convert(): Filters { + return Filters( + convert(includeClasses), + convert(excludeClasses), + convert(excludeAnnotation) + ) +} + +private fun convert(templates: Set): List { + val patterns = ArrayList(templates.size) + for (template in templates) { + patterns.add(Pattern.compile(template.wildcardsToRegex())) + } + return patterns +} + +/** + * Replaces characters `*` to `.*`, `#` to `[^.]*` and `?` to `.` regexp characters and also add escape char '\' before regexp metacharacters (see [regexMetacharactersSet]). + */ +internal fun String.wildcardsToRegex(): String { + // in most cases, the characters `*` or `.` will be present therefore, we increase the capacity in advance + val builder = StringBuilder(length * 2) + + forEach { char -> + when (char) { + in regexMetacharactersSet -> builder.append('\\').append(char) + '*' -> builder.append('.').append("*") + '?' -> builder.append('.') + '#' -> builder.append("[^.]*") + else -> builder.append(char) + } + } + + return builder.toString() +} + +private val regexMetacharactersSet = "<([{\\^-=$!|]})+.>".toSet() diff --git a/kover-gradle-plugin/build.gradle.kts b/kover-gradle-plugin/build.gradle.kts index 109a93e5..cccf09ef 100644 --- a/kover-gradle-plugin/build.gradle.kts +++ b/kover-gradle-plugin/build.gradle.kts @@ -44,7 +44,6 @@ dependencies { // exclude transitive dependency on stdlib, the Gradle version should be used compileOnly(kotlin("stdlib")) compileOnly(libs.gradlePlugin.kotlin) - compileOnly(libs.intellij.reporter) functionalTestImplementation(kotlin("test")) functionalTestImplementation(libs.junit.jupiter) diff --git a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/framework/checker/Checker.kt b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/framework/checker/Checker.kt index 1f368ace..54ea8a95 100644 --- a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/framework/checker/Checker.kt +++ b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/framework/checker/Checker.kt @@ -4,11 +4,11 @@ package kotlinx.kover.gradle.plugin.test.functional.framework.checker +import kotlinx.kover.features.jvm.KoverFeatures import kotlinx.kover.gradle.plugin.commons.* import kotlinx.kover.gradle.plugin.test.functional.framework.common.* import kotlinx.kover.gradle.plugin.test.functional.framework.runner.* import kotlinx.kover.gradle.plugin.tools.* -import kotlinx.kover.gradle.plugin.util.* import org.opentest4j.* import org.w3c.dom.* import java.io.* @@ -401,7 +401,7 @@ private class XmlReportCheckerImpl(val context: CheckerContextImpl, file: File) private class VerifyReportCheckerImpl(val context: CheckerContextImpl, val content: String) : VerifyReportChecker { override fun assertKoverResult(expected: String) { if (context.project.toolVariant.vendor != CoverageToolVendor.KOVER) return - val regex = expected.wildcardsToRegex().toRegex() + val regex = KoverFeatures.koverWildcardToRegex(expected).toRegex() if (!content.matches(regex)) { throw AssertionError("Unexpected verification result for Kover Tool.\n\tActual\n[\n$content\n]\nExpected regex\n[\n$expected\n]") } @@ -409,7 +409,7 @@ private class VerifyReportCheckerImpl(val context: CheckerContextImpl, val conte override fun assertJaCoCoResult(expected: String) { if (context.project.toolVariant.vendor != CoverageToolVendor.JACOCO) return - val regex = expected.wildcardsToRegex().toRegex() + val regex = KoverFeatures.koverWildcardToRegex(expected).toRegex() if (!content.matches(regex)) { throw AssertionError("Unexpected verification result for JaCoCo Tool.\n\tActual\n[\n$content\n]\nExpected regex\n[\n$expected\n]") } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/commons/Paths.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/commons/Paths.kt index 33a62a0f..699c408b 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/commons/Paths.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/commons/Paths.kt @@ -11,7 +11,7 @@ import java.io.File internal fun agentFilePath(toolVariant: CoverageToolVariant): String { return if (toolVariant.vendor == CoverageToolVendor.KOVER) { - "kover${separator}kover-jvm-agent-${KoverFeatures.getVersion()}.jar" + "kover${separator}kover-jvm-agent-${KoverFeatures.version}.jar" } else { "kover${separator}jacoco-coverage-agent-${toolVariant.version}.jar" } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/commons/Types.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/commons/Types.kt index d0ceb584..3ae5d13f 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/commons/Types.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/commons/Types.kt @@ -63,7 +63,6 @@ internal class ReportContext( ) internal class GradleReportServices( - val workerExecutor: WorkerExecutor, val antBuilder: AntBuilder, val objects: ObjectFactory ) 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 cb508638..9bce6473 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 @@ -28,5 +28,5 @@ public object KoverVersions { * Current version of Kover Gradle Plugin */ public val version: String - get() = KoverFeatures.getVersion() + get() = KoverFeatures.version } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tasks/reports/AbstractKoverReportTask.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tasks/reports/AbstractKoverReportTask.kt index 6617f158..1574b255 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tasks/reports/AbstractKoverReportTask.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tasks/reports/AbstractKoverReportTask.kt @@ -84,13 +84,10 @@ internal abstract class AbstractKoverReportTask : DefaultTask() { @get:Inject protected abstract val obj: ObjectFactory - @get:Inject - protected abstract val workerExecutor: WorkerExecutor - private val rootDir: File = project.rootDir protected fun context(): ReportContext { - val services = GradleReportServices(workerExecutor, ant, obj) + val services = GradleReportServices(ant, obj) return ReportContext(collectAllFiles(), filters.get(), reportClasspath, temporaryDir, projectPath, services) } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/Verification.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/Verification.kt index e570bd32..0e90f6b6 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/Verification.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/Verification.kt @@ -4,17 +4,19 @@ package kotlinx.kover.gradle.plugin.tools +import kotlinx.kover.features.jvm.* import kotlinx.kover.gradle.plugin.dsl.* +import kotlinx.kover.gradle.plugin.dsl.AggregationType +import kotlinx.kover.gradle.plugin.dsl.CoverageUnit import java.io.File import java.io.Serializable -import java.math.* import java.nio.charset.Charset -internal fun CoverageMeasures.writeToFile(file: File, header: String?, lineFormat: String) { +internal fun Iterable.writeToFile(file: File, header: String?, lineFormat: String) { file.bufferedWriter(Charset.forName("UTF-8")).use { writer -> header?.let { h -> writer.appendLine(h) } - values.forEach { coverage -> + forEach { coverage -> val entityName = coverage.entityName ?: "application" writer.appendLine( lineFormat.replace("", coverage.value.stripTrailingZeros().toPlainString()) @@ -40,42 +42,18 @@ internal data class CoverageRequest( val lineFormat: String, ): Serializable -internal data class CoverageMeasures( - val values: List -) - -internal data class CoverageValue( - val value: BigDecimal, - val entityName: String? = null, -) - -internal data class RuleViolations( - val entityType: GroupingEntityType, - val bounds: List, - val name: String -) - -internal data class BoundViolations( - val isMax: Boolean, - val expectedValue: BigDecimal, - val actualValue: BigDecimal, - val metric: CoverageUnit, - val aggregation: AggregationType, - val entityName: String? = null -) - internal fun generateErrorMessage(violations: List): String { val messageBuilder = StringBuilder() violations.forEach { rule -> - val namedRule = if (rule.name.isNotEmpty()) "Rule '${rule.name}'" else "Rule" + val namedRule = if (rule.rule.name.isNotEmpty()) "Rule '${rule.rule.name}'" else "Rule" - if (rule.bounds.size == 1) { - messageBuilder.appendLine("$namedRule violated: ${rule.bounds[0].format(rule)}") + if (rule.violations.size == 1) { + messageBuilder.appendLine("$namedRule violated: ${rule.violations[0].format(rule)}") } else { messageBuilder.appendLine("$namedRule violated:") - rule.bounds.forEach { bound -> + rule.violations.forEach { bound -> messageBuilder.append(" ") messageBuilder.appendLine(bound.format(rule)) } @@ -85,27 +63,32 @@ internal fun generateErrorMessage(violations: List): String { return messageBuilder.toString() } -private fun BoundViolations.format(rule: RuleViolations): String { +private fun BoundViolation.format(rule: RuleViolations): String { val directionText = if (isMax) "maximum" else "minimum" - val metricText = when (metric) { - CoverageUnit.LINE -> "lines" - CoverageUnit.INSTRUCTION -> "instructions" - CoverageUnit.BRANCH -> "branches" + val metricText = when (bound.coverageUnits) { + FeatureCoverageUnit.LINE -> "lines" + FeatureCoverageUnit.INSTRUCTION -> "instructions" + FeatureCoverageUnit.BRANCH -> "branches" } - val valueTypeText = when (aggregation) { - AggregationType.COVERED_COUNT -> "covered count" - AggregationType.MISSED_COUNT -> "missed count" - AggregationType.COVERED_PERCENTAGE -> "covered percentage" - AggregationType.MISSED_PERCENTAGE -> "missed percentage" + val valueTypeText = when (bound.aggregationForGroup) { + FeatureAggregationType.COVERED_COUNT -> "covered count" + FeatureAggregationType.MISSED_COUNT -> "missed count" + FeatureAggregationType.COVERED_PERCENTAGE -> "covered percentage" + FeatureAggregationType.MISSED_PERCENTAGE -> "missed percentage" } - val entityText = when (rule.entityType) { - GroupingEntityType.APPLICATION -> "" - GroupingEntityType.CLASS -> " for class '$entityName'" - GroupingEntityType.PACKAGE -> " for package '$entityName'" + val entityText = when (rule.rule.groupBy) { + GroupingBy.APPLICATION -> "" + GroupingBy.CLASS -> " for class '$entityName'" + GroupingBy.PACKAGE -> " for package '$entityName'" } - return "$metricText $valueTypeText$entityText is $actualValue, but expected $directionText is $expectedValue" + val expectedValue = if (isMax) bound.maxValue else bound.minValue + + return "$metricText $valueTypeText$entityText is $value, but expected $directionText is $expectedValue" } + +private typealias FeatureCoverageUnit = kotlinx.kover.features.jvm.CoverageUnit +private typealias FeatureAggregationType = kotlinx.kover.features.jvm.AggregationType \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Evaluation.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Evaluation.kt index 0ed32c6d..18ef3369 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Evaluation.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Evaluation.kt @@ -4,14 +4,13 @@ package kotlinx.kover.gradle.plugin.tools.jacoco +import kotlinx.kover.features.jvm.CoverageValue import kotlinx.kover.gradle.plugin.commons.KoverCriticalException import kotlinx.kover.gradle.plugin.commons.ReportContext import kotlinx.kover.gradle.plugin.commons.VerificationBound import kotlinx.kover.gradle.plugin.commons.VerificationRule import kotlinx.kover.gradle.plugin.tools.* -import kotlinx.kover.gradle.plugin.tools.CoverageMeasures import kotlinx.kover.gradle.plugin.tools.CoverageRequest -import kotlinx.kover.gradle.plugin.tools.CoverageValue import kotlinx.kover.gradle.plugin.tools.writeToFile import kotlinx.kover.gradle.plugin.util.ONE_HUNDRED import java.io.File @@ -28,15 +27,15 @@ internal fun ReportContext.printJacocoCoverage(request: CoverageRequest, outputF } val values = violations.flatMap { rule -> - if (rule.bounds.isEmpty()) { + if (rule.violations.isEmpty()) { throw KoverCriticalException("Expected at least one bound violation for JaCoCo") } - rule.bounds.map { - CoverageValue(it.actualValue, it.entityName) + rule.violations.map { + CoverageValue(it.entityName, it.value) } } - CoverageMeasures(values).writeToFile( + values.writeToFile( outputFile, request.header, request.lineFormat diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoAnt.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoAnt.kt index 126cf475..68a1ac88 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoAnt.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoAnt.kt @@ -6,8 +6,8 @@ package kotlinx.kover.gradle.plugin.tools.jacoco import groovy.lang.Closure import groovy.lang.GroovyObject +import kotlinx.kover.features.jvm.KoverFeatures import kotlinx.kover.gradle.plugin.commons.ReportContext -import kotlinx.kover.gradle.plugin.util.wildcardsToRegex import java.io.File @@ -88,6 +88,6 @@ internal inline fun GroovyObject.invokeWithBody( */ private fun String.wildcardsToClassFileRegex(): String { val filenameWithWildcards = this.replace('.', File.separatorChar) + ".class" - return filenameWithWildcards.wildcardsToRegex() + return KoverFeatures.koverWildcardToRegex(filenameWithWildcards) } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Verification.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Verification.kt index 79fdcd05..be9b5eea 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Verification.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Verification.kt @@ -5,13 +5,16 @@ package kotlinx.kover.gradle.plugin.tools.jacoco import groovy.lang.GroovyObject +import kotlinx.kover.features.jvm.Bound +import kotlinx.kover.features.jvm.BoundViolation +import kotlinx.kover.features.jvm.Rule +import kotlinx.kover.features.jvm.RuleViolations import kotlinx.kover.gradle.plugin.commons.* import kotlinx.kover.gradle.plugin.dsl.AggregationType import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType import kotlinx.kover.gradle.plugin.dsl.CoverageUnit -import kotlinx.kover.gradle.plugin.tools.BoundViolations -import kotlinx.kover.gradle.plugin.tools.RuleViolations import kotlinx.kover.gradle.plugin.tools.generateErrorMessage +import kotlinx.kover.gradle.plugin.tools.kover.convert import kotlinx.kover.gradle.plugin.util.ONE_HUNDRED import org.gradle.internal.reflect.JavaMethod import java.io.File @@ -119,7 +122,11 @@ private fun GroovyObject.violations(): List { val isMax = match.groupValues[6].asIsMax(it) val expected = match.groupValues[7].asValue(it, agg) - RuleViolations(entityType, listOf(BoundViolations(isMax, expected, value, coverageUnits, agg, entityName)), "") + val bound = + Bound(if (!isMax) expected else null, if (isMax) expected else null, coverageUnits.convert(), agg.convert()) + val rule = Rule("", entityType.convert(), listOf(bound)) + + RuleViolations(rule, listOf(BoundViolation(bound, isMax, value, entityName))) }.toList() } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Actions.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Actions.kt deleted file mode 100644 index 52e6f03b..00000000 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Actions.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.gradle.plugin.tools.kover - -import com.intellij.rt.coverage.util.ErrorReporter -import kotlinx.kover.gradle.plugin.commons.ArtifactContent -import kotlinx.kover.gradle.plugin.commons.ReportContext -import kotlinx.kover.gradle.plugin.commons.ReportFilters -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.provider.Property -import org.gradle.workers.WorkAction -import org.gradle.workers.WorkParameters -import org.gradle.workers.WorkQueue - -private const val FREE_MARKER_LOGGER_PROPERTY_NAME = "org.freemarker.loggerLibrary" - -internal interface ReportParameters: WorkParameters { - val filters: Property - - val files: Property - val tempDir: DirectoryProperty - val projectPath: Property - val charset: Property -} - -internal abstract class AbstractReportAction : WorkAction { - protected abstract fun generate() - - final override fun execute() { - // print to stdout only critical errors - ErrorReporter.setLogLevel(ErrorReporter.ERROR) - - // disable freemarker logging to stdout for the time of report generation - val oldFreemarkerLogger = System.setProperty(FREE_MARKER_LOGGER_PROPERTY_NAME, "none") - try { - generate() - } finally { - if (oldFreemarkerLogger == null) { - System.clearProperty(FREE_MARKER_LOGGER_PROPERTY_NAME) - } else { - System.setProperty(FREE_MARKER_LOGGER_PROPERTY_NAME, oldFreemarkerLogger) - } - } - } -} - -internal inline fun , P : ReportParameters> ReportContext.submitAction(noinline parametersConfig: P.() -> Unit) { - val workQueue: WorkQueue = services.workerExecutor.classLoaderIsolation { - classpath.from(this@submitAction.classpath) - } - workQueue.submit(A::class.java) { parametersConfig(this) } -} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Evaluation.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Evaluation.kt index 64daee36..cf7406ff 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Evaluation.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Evaluation.kt @@ -4,71 +4,33 @@ package kotlinx.kover.gradle.plugin.tools.kover -import kotlinx.kover.gradle.plugin.commons.KoverCriticalException +import kotlinx.kover.features.jvm.KoverLegacyFeatures import kotlinx.kover.gradle.plugin.commons.ReportContext -import kotlinx.kover.gradle.plugin.commons.VerificationBound -import kotlinx.kover.gradle.plugin.commons.VerificationRule -import kotlinx.kover.gradle.plugin.tools.* -import kotlinx.kover.gradle.plugin.tools.CoverageMeasures import kotlinx.kover.gradle.plugin.tools.CoverageRequest -import kotlinx.kover.gradle.plugin.tools.CoverageValue +import kotlinx.kover.gradle.plugin.tools.writeNoSources import kotlinx.kover.gradle.plugin.tools.writeToFile -import kotlinx.kover.gradle.plugin.util.ONE_HUNDRED -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property import java.io.File -import java.math.BigDecimal - internal fun ReportContext.printCoverage(request: CoverageRequest, outputFile: File) { - submitAction { - this.outputFile.set(outputFile) - this.request.convention(request) - - filters.convention(this@printCoverage.filters) - files.convention(this@printCoverage.files) - tempDir.set(this@printCoverage.tempDir) - projectPath.convention(this@printCoverage.projectPath) + // change API after https://youtrack.jetbrains.com/issue/IDEA-323463 will be implemented + val coverage = KoverLegacyFeatures.evalCoverage( + request.entity.convert(), + request.metric.convert(), + request.aggregation.convert(), + tempDir, + filters.toKoverFeatures(), + files.reports.toList(), + files.outputs.toList() + ) + + if (coverage.isEmpty()) { + outputFile.writeNoSources(request.header) + return } + coverage.writeToFile( + outputFile, + request.header, + request.lineFormat + ) } - -internal abstract class CollectCoverageAction : AbstractReportAction() { - override fun generate() { - val request = parameters.request.get() - val bound = VerificationBound(ONE_HUNDRED, BigDecimal.ZERO, request.metric, request.aggregation) - val failRule = VerificationRule(true, "", request.entity, listOf(bound)) - - // change API after https://youtrack.jetbrains.com/issue/IDEA-323463 will be implemented - val violations = koverVerify( - listOf(failRule), - parameters.filters.get(), - parameters.tempDir.get().asFile, - parameters.files.get() - ) - - if (violations.isEmpty()) { - parameters.outputFile.get().asFile.writeNoSources(parameters.request.get().header) - return - } - - val violation = violations.singleOrNull() ?: throw KoverCriticalException("Expected only one rule violation for Kover") - if (violation.bounds.isEmpty()) { - throw KoverCriticalException("Expected at least one bound violation for Kover") - } - - val values = violation.bounds.map { - CoverageValue(it.actualValue, it.entityName) - } - CoverageMeasures(values).writeToFile( - parameters.outputFile.get().asFile, - parameters.request.get().header, - parameters.request.get().lineFormat - ) - } -} - -internal interface CollectCoverageParameters : ReportParameters { - val outputFile: RegularFileProperty - val request: Property -} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/IcReport.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/IcReport.kt index 9ae1a3b2..8a7de8e9 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/IcReport.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/IcReport.kt @@ -1,37 +1,17 @@ package kotlinx.kover.gradle.plugin.tools.kover -import com.intellij.rt.coverage.aggregate.api.AggregatorApi -import com.intellij.rt.coverage.aggregate.api.Request +import kotlinx.kover.features.jvm.KoverLegacyFeatures import kotlinx.kover.gradle.plugin.commons.ReportContext -import org.gradle.api.provider.Property import java.io.File -internal fun ReportContext.koverBinaryReport(binary: File) { - submitAction { - binaryFile.set(binary) - filters.convention(this@koverBinaryReport.filters) - - files.convention(this@koverBinaryReport.files) - tempDir.set(this@koverBinaryReport.tempDir) - projectPath.convention(this@koverBinaryReport.projectPath) - } -} - -internal interface BinaryReportParameters : ReportParameters { - val binaryFile: Property -} - -internal abstract class BinaryReportAction : AbstractReportAction() { - override fun generate() { - val binary = parameters.binaryFile.get() - val smapFile = parameters.tempDir.file("report.smap").get().asFile - - val files = parameters.files.get() - val filters = parameters.filters.get() - val request = Request(filters.toIntellij(), binary, smapFile) - - AggregatorApi.aggregate(listOf(request), files.reports.toList(), files.outputs.toList()) - } +internal fun ReportContext.koverBinaryReport(binaryFile: File) { + KoverLegacyFeatures.aggregateIc( + binaryFile, + filters.toKoverFeatures(), + tempDir, + files.reports.toList(), + files.outputs.toList() + ) } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/IntellijApi.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/IntellijApi.kt deleted file mode 100644 index 12a3a0be..00000000 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/IntellijApi.kt +++ /dev/null @@ -1,56 +0,0 @@ -package kotlinx.kover.gradle.plugin.tools.kover - -import com.intellij.rt.coverage.report.api.Filters -import com.intellij.rt.coverage.verify.api.Counter -import com.intellij.rt.coverage.verify.api.Target -import com.intellij.rt.coverage.verify.api.ValueType -import kotlinx.kover.gradle.plugin.commons.ReportFilters -import kotlinx.kover.gradle.plugin.commons.VerificationBound -import kotlinx.kover.gradle.plugin.commons.VerificationRule -import kotlinx.kover.gradle.plugin.dsl.AggregationType -import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType -import kotlinx.kover.gradle.plugin.dsl.CoverageUnit -import kotlinx.kover.gradle.plugin.util.ONE_HUNDRED -import kotlinx.kover.gradle.plugin.util.asPatterns -import java.math.BigDecimal -import java.math.RoundingMode - -internal fun ReportFilters.toIntellij() = Filters( - includesClasses.asPatterns(), - excludesClasses.asPatterns(), - excludesAnnotations.asPatterns() -) - -internal fun VerificationRule.targetToIntellij(): Target { - return when (entityType) { - GroupingEntityType.APPLICATION -> Target.ALL - GroupingEntityType.CLASS -> Target.CLASS - GroupingEntityType.PACKAGE -> Target.PACKAGE - } -} - -internal fun VerificationBound.counterToIntellij(): Counter { - return when (metric) { - CoverageUnit.LINE -> Counter.LINE - CoverageUnit.INSTRUCTION -> Counter.INSTRUCTION - CoverageUnit.BRANCH -> Counter.BRANCH - } -} - -internal fun VerificationBound.valueTypeToIntellij(): ValueType { - return when (aggregation) { - AggregationType.COVERED_COUNT -> ValueType.COVERED - AggregationType.MISSED_COUNT -> ValueType.MISSED - AggregationType.COVERED_PERCENTAGE -> ValueType.COVERED_RATE - AggregationType.MISSED_PERCENTAGE -> ValueType.MISSED_RATE - } -} - -internal fun VerificationBound.valueToIntellij(value: BigDecimal?): BigDecimal? { - value ?: return null - return if (aggregation.isPercentage) { - value.divide(ONE_HUNDRED, 6, RoundingMode.HALF_UP) - } else { - value - } -} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverFeaturesIntegration.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverFeaturesIntegration.kt new file mode 100644 index 00000000..3cc6d696 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverFeaturesIntegration.kt @@ -0,0 +1,58 @@ +package kotlinx.kover.gradle.plugin.tools.kover + +import kotlinx.kover.features.jvm.* +import kotlinx.kover.gradle.plugin.commons.ReportFilters +import kotlinx.kover.gradle.plugin.commons.VerificationBound +import kotlinx.kover.gradle.plugin.commons.VerificationRule +import kotlinx.kover.gradle.plugin.dsl.AggregationType +import kotlinx.kover.gradle.plugin.dsl.CoverageUnit +import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType + +private typealias FeatureCoverageUnit = kotlinx.kover.features.jvm.CoverageUnit +private typealias FeatureAggregationType = kotlinx.kover.features.jvm.AggregationType + +internal fun ReportFilters.toKoverFeatures() = ClassFilters( + includesClasses, + excludesClasses, + excludesAnnotations +) + +internal fun VerificationRule.convert(): Rule { + return Rule( + name, + entityType.convert(), + bounds.map { it.convert() } + ) +} + + +internal fun VerificationBound.convert(): Bound { + return Bound(minValue, maxValue, metric.convert(), aggregation.convert()) +} + + +internal fun GroupingEntityType.convert(): GroupingBy { + return when (this) { + GroupingEntityType.APPLICATION -> GroupingBy.APPLICATION + GroupingEntityType.CLASS -> GroupingBy.CLASS + GroupingEntityType.PACKAGE -> GroupingBy.PACKAGE + } +} + + +internal fun CoverageUnit.convert(): FeatureCoverageUnit { + return when (this) { + CoverageUnit.LINE -> FeatureCoverageUnit.LINE + CoverageUnit.BRANCH -> FeatureCoverageUnit.BRANCH + CoverageUnit.INSTRUCTION -> FeatureCoverageUnit.INSTRUCTION + } +} + +internal fun AggregationType.convert(): FeatureAggregationType { + return when (this) { + AggregationType.COVERED_COUNT -> FeatureAggregationType.COVERED_COUNT + AggregationType.COVERED_PERCENTAGE -> FeatureAggregationType.COVERED_PERCENTAGE + AggregationType.MISSED_COUNT -> FeatureAggregationType.MISSED_COUNT + AggregationType.MISSED_PERCENTAGE -> FeatureAggregationType.MISSED_PERCENTAGE + } +} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverHtmlOrXmlReport.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverHtmlOrXmlReport.kt index 0c5c3ed7..d06d3952 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverHtmlOrXmlReport.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverHtmlOrXmlReport.kt @@ -4,86 +4,32 @@ package kotlinx.kover.gradle.plugin.tools.kover -import com.intellij.rt.coverage.report.api.ReportApi +import kotlinx.kover.features.jvm.KoverLegacyFeatures import kotlinx.kover.gradle.plugin.commons.ReportContext -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property import java.io.File internal fun ReportContext.koverHtmlReport(htmlReportDir: File, htmlTitle: String, charsetName: String?) { - submitAction { - htmlDir.set(htmlReportDir) - title.convention(htmlTitle) - charset.convention(charsetName) - filters.convention(this@koverHtmlReport.filters) - - files.convention(this@koverHtmlReport.files) - tempDir.set(this@koverHtmlReport.tempDir) - projectPath.convention(this@koverHtmlReport.projectPath) - } + htmlReportDir.mkdirs() + + KoverLegacyFeatures.generateHtmlReport( + htmlReportDir, + charsetName, + files.reports.toList(), + files.outputs.toList(), + files.sources.toList(), + htmlTitle, + filters.toKoverFeatures() + ) } internal fun ReportContext.koverXmlReport(xmlReportFile: File, xmlTitle: String) { - submitAction { - xmlFile.set(xmlReportFile) - title.convention(xmlTitle) - filters.convention(this@koverXmlReport.filters) - - files.convention(this@koverXmlReport.files) - - tempDir.set(this@koverXmlReport.tempDir) - projectPath.convention(this@koverXmlReport.projectPath) - } -} - -internal interface XmlReportParameters : ReportParameters { - val xmlFile: RegularFileProperty - val title: Property -} - -internal interface HtmlReportParameters : ReportParameters { - val htmlDir: DirectoryProperty - val title: Property -} - -internal abstract class XmlReportAction : AbstractReportAction() { - override fun generate() { - val files = parameters.files.get() - val filters = parameters.filters.get() - - ReportApi.xmlReport( - parameters.xmlFile.get().asFile, - parameters.title.get(), - files.reports.toList(), - files.outputs.toList(), - files.sources.toList(), - filters.toIntellij() - ) - } -} - -internal abstract class HtmlReportAction : AbstractReportAction() { - override fun generate() { - val htmlDir = parameters.htmlDir.get().asFile - htmlDir.mkdirs() - - val files = parameters.files.get() - val filters = parameters.filters.get() - - // repeat reading freemarker temple from resources if error occurred, see https://github.com/Kotlin/kotlinx-kover/issues/510 - // the values are selected empirically so that the maximum report generation time is not much more than a second - ReportApi.setFreemarkerRetry(7, 150) - - ReportApi.htmlReport( - parameters.htmlDir.get().asFile, - parameters.title.get(), - parameters.charset.orNull, - files.reports.toList(), - files.outputs.toList(), - files.sources.toList(), - filters.toIntellij() - ) - } + KoverLegacyFeatures.generateXmlReport( + xmlReportFile, + files.reports.toList(), + files.outputs.toList(), + files.sources.toList(), + xmlTitle, + filters.toKoverFeatures() + ) } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverOnlineInstrumentation.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverOnlineInstrumentation.kt index 9f3f8864..647e2699 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverOnlineInstrumentation.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverOnlineInstrumentation.kt @@ -4,7 +4,6 @@ package kotlinx.kover.gradle.plugin.tools.kover -import kotlinx.kover.gradle.plugin.util.wildcardsToRegex import java.io.File internal fun buildJvmAgentArgs( diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverTool.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverTool.kt index 529e1563..3707ad0f 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverTool.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/KoverTool.kt @@ -17,10 +17,12 @@ import java.io.File internal class KoverTool(override val variant: CoverageToolVariant) : CoverageTool { - override val jvmAgentDependency: String = "org.jetbrains.kotlinx:kover-jvm-agent:${KoverFeatures.getVersion()}" + override val jvmAgentDependency: String = "org.jetbrains.kotlinx:kover-jvm-agent:${KoverFeatures.version}" - override val jvmReporterDependency: String = "org.jetbrains.intellij.deps:intellij-coverage-reporter:${variant.version}" - override val jvmReporterExtraDependency: String = "org.jetbrains.intellij.deps:intellij-coverage-reporter:${variant.version}" + // since Kover Features is in compile dependency and there is no need in additional dependency to reporter + // we can't just specify null dependency, so use agent as a mock + override val jvmReporterDependency: String = jvmAgentDependency + override val jvmReporterExtraDependency: String = jvmAgentDependency override fun findJvmAgentJar(classpath: FileCollection, archiveOperations: ArchiveOperations): File { diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Verification.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Verification.kt index 4c7ca0eb..0b96524c 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Verification.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/kover/Verification.kt @@ -4,217 +4,24 @@ package kotlinx.kover.gradle.plugin.tools.kover -import com.intellij.rt.coverage.aggregate.api.AggregatorApi -import com.intellij.rt.coverage.aggregate.api.Request -import com.intellij.rt.coverage.verify.Verifier -import com.intellij.rt.coverage.verify.api.* +import kotlinx.kover.features.jvm.KoverLegacyFeatures import kotlinx.kover.gradle.plugin.commons.* -import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType -import kotlinx.kover.gradle.plugin.tools.BoundViolations -import kotlinx.kover.gradle.plugin.tools.RuleViolations import kotlinx.kover.gradle.plugin.tools.generateErrorMessage -import kotlinx.kover.gradle.plugin.util.ONE_HUNDRED -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.ListProperty import java.io.File -import java.util.* internal fun ReportContext.koverVerify(specifiedRules: List, outputReportFile: File) { - submitAction { - outputFile.set(outputReportFile) - rules.convention(specifiedRules) - filters.convention(this@koverVerify.filters) + val violations = KoverLegacyFeatures.verify( + specifiedRules.map { it.convert() }, + tempDir, + filters.toKoverFeatures(), + files.reports.toList(), + files.outputs.toList() + ) - files.convention(this@koverVerify.files) - tempDir.set(this@koverVerify.tempDir) - projectPath.convention(this@koverVerify.projectPath) - } -} - -internal interface VerifyReportParameters: ReportParameters { - val outputFile: RegularFileProperty - val rules: ListProperty -} - -internal abstract class VerifyReportAction : AbstractReportAction() { - override fun generate() { - val violations = koverVerify( - parameters.rules.get(), - parameters.filters.get(), - parameters.tempDir.get().asFile, - parameters.files.get() - ) - - val errorMessage = generateErrorMessage(violations) - parameters.outputFile.get().asFile.writeText(errorMessage) - - if (violations.isNotEmpty()) { - throw KoverVerificationException(errorMessage) - } - } -} - -internal fun koverVerify( - rules: List, - commonFilters: ReportFilters, - tempDir: File, - files: ArtifactContent -): List { - val rulesByFilter = groupRules(rules, commonFilters) - val usedFilters = rulesByFilter.map { it.first } - val groupedRules = rulesByFilter.map { it.second } - - val groups = aggregateBinReports(files, usedFilters, tempDir) - - val rulesArray = mutableListOf() - groups.forEachIndexed { index, group -> - val rulesForGroup = groupedRules[index] - rulesForGroup.forEachIndexed { ruleIndex, rule -> - val bounds = rule.bounds.mapIndexed { boundIndex, b -> - Bound( - boundIndex, b.counterToIntellij(), b.valueTypeToIntellij(), b.valueToIntellij(b.minValue), - b.valueToIntellij(b.maxValue) - ) - - } - rulesArray += Rule(ruleIndex, group.ic, rule.targetToIntellij(), bounds) - } - } - - - val verifier = Verifier(rulesArray) - verifier.processRules() - - val violations = VerificationApi.verify(rulesArray) - - return processViolations(rules, violations) -} - -private fun groupRules( - allRules: List, - commonFilters: ReportFilters -): List>> { - val groupedMap = mutableMapOf>() - - allRules.forEach { - val excludesClasses = commonFilters.excludesClasses - val includesClasses = commonFilters.includesClasses - val excludesAnnotations = commonFilters.excludesAnnotations - - val reportFilters = ReportFilters(includesClasses, emptySet(), excludesClasses, excludesAnnotations) - groupedMap.computeIfAbsent(reportFilters) { mutableListOf() } += it - } - - return groupedMap.entries.map { it.key to it.value } -} - -private fun processViolations( - rules: List, - violations: List -): List { + val errorMessage = generateErrorMessage(violations) + outputReportFile.writeText(errorMessage) - val rulesMap = rules.mapIndexed { index, rule -> index to rule }.associate { it } - // the order of the rules is guaranteed for Kover (as in config) - val result = TreeMap() - - try { - violations.forEach { violation -> - val ruleIndex = violation.id - val rule = rulesMap[ruleIndex] - ?: throw KoverCriticalException("Error occurred while parsing verification result: unmapped rule with index $ruleIndex") - - val boundsMap = rule.bounds.mapIndexed { index, bound -> index to bound }.associate { it } - - // the order of the bound is guaranteed for Kover (as in config + suborder by entity name) - val boundsResult = TreeMap() - - violation.violations.forEach { boundViolation -> - val boundIndex = boundViolation.id - - val bound = boundsMap[boundIndex] - ?: throw KoverCriticalException("Error occurred while parsing verification error: unmapped bound with index $boundIndex and rule index $ruleIndex") - - boundViolation.minViolations.forEach { - bound.minValue - ?: throw KoverCriticalException("Error occurred while parsing verification error: no minimal bound with ID $boundIndex and rule index $ruleIndex") - - val entityName = if (rule.entityType == GroupingEntityType.APPLICATION) null else it.targetName - val value = it.targetValue - val actual = if (bound.aggregation.isPercentage) value * ONE_HUNDRED else value - boundsResult += ViolationId(boundIndex, entityName) to BoundViolations( - false, - bound.minValue, - actual, - bound.metric, - bound.aggregation, - entityName - ) - } - - boundViolation.maxViolations.forEach { - bound.maxValue - ?: throw KoverCriticalException("Error occurred while parsing verification error: no maximal bound with index $boundIndex and rule index $ruleIndex") - - val entityName = if (rule.entityType == GroupingEntityType.APPLICATION) null else it.targetName - val value = it.targetValue - val actual = if (bound.aggregation.isPercentage) value * ONE_HUNDRED else value - boundsResult += ViolationId(boundIndex, entityName) to BoundViolations( - true, - bound.maxValue, - actual, - bound.metric, - bound.aggregation, - entityName - ) - } - } - - result += ruleIndex to RuleViolations(rule.entityType, boundsResult.values.toList(), rule.name) - } - } catch (e: Throwable) { - throw KoverCriticalException("Error occurred while parsing verifier result", e) - } - - return result.values.toList() -} - -private data class ViolationId(val index: Int, val entityName: String?) : Comparable { - override fun compareTo(other: ViolationId): Int { - // first compared by index - index.compareTo(other.index).takeIf { it != 0 }?.let { return it } - - // if indexes are equals then compare by entity name - - if (entityName == null) { - // bounds with empty entity names goes first - return if (other.entityName == null) 0 else -1 - } - if (other.entityName == null) return 1 - - entityName.compareTo(other.entityName).takeIf { it != 0 }?.let { return it } - - // indexes and names are equals - return 0 + if (violations.isNotEmpty()) { + throw KoverVerificationException(errorMessage) } } - -private fun aggregateBinReports(files: ArtifactContent, filters: List, tempDir: File): List { - val aggGroups = filters.mapIndexed { index: Int, reportFilters: ReportFilters -> - val filePrefix = if (filters.size > 1) "-$index" else "" - AggregationGroup( - tempDir.resolve("agg-ic$filePrefix.ic"), - tempDir.resolve("agg-smap$filePrefix.smap"), - reportFilters - ) - } - - val requests = aggGroups.map { group -> - Request(group.filters.toIntellij(), group.ic, group.smap) - } - - AggregatorApi.aggregate(requests, files.reports.toList(), files.outputs.toList()) - - return aggGroups -} - -private class AggregationGroup(val ic: File, val smap: File, val filters: ReportFilters) diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/util/Util.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/util/Util.kt index c75ae096..f9db4847 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/util/Util.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/util/Util.kt @@ -5,55 +5,9 @@ package kotlinx.kover.gradle.plugin.util import java.io.File -import java.util.regex.Pattern - -internal fun Iterable.asPatterns(): List = map { Pattern.compile(it.wildcardsToRegex()) } - -/** - * Executes `block` of code only if boolean value is `true`. - */ -internal inline fun Boolean.ifTrue(block: () -> T): T? { - return if (this) { - block() - } else { - null - } -} - -/** - * Executes `block` of code only if boolean value is `false`. - */ -internal inline fun Boolean.ifFalse(block: () -> T): T? { - return if (!this) { - block() - } else { - null - } -} - -/** - * Replaces characters `*` or `.` to `.*`, `#` to `[^.]*` and `?` to `.` regexp characters. - */ -internal fun String.wildcardsToRegex(): String { - // in most cases, the characters `*` or `.` will be present therefore, we increase the capacity in advance - val builder = StringBuilder(length * 2) - - forEach { char -> - when (char) { - in regexMetacharactersSet -> builder.append('\\').append(char) - '*' -> builder.append(".*") - '?' -> builder.append('.') - '#' -> builder.append("[^.]*") - else -> builder.append(char) - } - } - - return builder.toString() -} internal val ONE_HUNDRED = 100.toBigDecimal() -private val regexMetacharactersSet = "<([{\\^-=$!|]})+.>".toSet() internal fun File.subdirs(): List { return listFiles { it ->