From 8cd1d87ca60c1ce30160fe6a233039292396016b Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Wed, 7 Aug 2024 18:14:29 +0200 Subject: [PATCH 1/3] Replaced JaCoCo ant-calls with programmatic calls of JaCoCo's classes Resolves #630 Fixes #666 --- gradle/libs.versions.toml | 3 +- .../kover/features/jvm/KoverLegacyFeatures.kt | 13 +- kover-gradle-plugin/build.gradle.kts | 1 + .../test/functional/cases/LoggingTaskTests.kt | 4 +- .../functional/cases/VerificationTests.kt | 41 +-- .../functional/framework/checker/Checker.kt | 13 +- .../framework/checker/CheckerTypes.kt | 3 +- .../kover/gradle/plugin/commons/Types.kt | 3 +- .../kover/gradle/plugin/dsl/KoverVersions.kt | 2 +- .../tasks/reports/AbstractKoverReportTask.kt | 6 +- .../plugin/tasks/reports/KoverDoVerifyTask.kt | 5 +- .../kover/gradle/plugin/tools/CoverageTool.kt | 2 +- .../gradle/plugin/tools/jacoco/Commons.kt | 101 +++++++ .../gradle/plugin/tools/jacoco/Evaluation.kt | 43 --- .../gradle/plugin/tools/jacoco/JacocoAnt.kt | 99 ------- .../tools/jacoco/JacocoHtmlOrXmlReport.kt | 84 +++++- .../gradle/plugin/tools/jacoco/JacocoTool.kt | 10 +- .../plugin/tools/jacoco/PrintCoverage.kt | 70 +++++ .../plugin/tools/jacoco/Verification.kt | 256 +++++++++++------- .../gradle/plugin/tools/kover/KoverTool.kt | 10 +- 20 files changed, 438 insertions(+), 331 deletions(-) create mode 100644 kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Commons.kt delete mode 100644 kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Evaluation.kt delete mode 100644 kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoAnt.kt create mode 100644 kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/PrintCoverage.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3df2461c..08f2f34f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ maven-embedder = "3.9.8" maven-api = "3.0" maven-resolver = "1.9.21" maven-slf4j = "1.7.36" +jacoco = "0.8.12" [libraries] @@ -40,7 +41,7 @@ maven-resolver-file = { module = "org.apache.maven.resolver:maven-resolver-trans maven-resolver-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "maven-resolver" } maven-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "maven-slf4j" } - +jacoco-reporter = {module = "org.jacoco:org.jacoco.report", version.ref = "jacoco" } [plugins] gradle-pluginPublish = { id = "com.gradle.plugin-publish", version.ref = "gradle-plugin-publish" } 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 index f5731ff5..6b1c1573 100644 --- 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 @@ -197,11 +197,12 @@ public object KoverLegacyFeatures { messageBuilder.appendLine("$namedRule violated: ${rule.violations[0].format(rule)}") } else { messageBuilder.appendLine("$namedRule violated:") - - rule.violations.forEach { bound -> - messageBuilder.append(" ") - messageBuilder.appendLine(bound.format(rule)) - } + rule.violations.map { bound -> bound.format(rule) } + .toSortedSet() + .forEach { bound -> + messageBuilder.append(" ") + messageBuilder.appendLine(bound.format(rule)) + } } } @@ -300,5 +301,5 @@ private fun BoundViolation.format(rule: RuleViolations): String { val expectedValue = if (isMax) bound.maxValue else bound.minValue - return "$metricText $valueTypeText$entityText is $value, but expected $directionText is $expectedValue" + return "$metricText $valueTypeText$entityText is $value, but expected $directionText is ${expectedValue?.toPlainString()}" } diff --git a/kover-gradle-plugin/build.gradle.kts b/kover-gradle-plugin/build.gradle.kts index d5bd0b3e..79ba7d1f 100644 --- a/kover-gradle-plugin/build.gradle.kts +++ b/kover-gradle-plugin/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { // exclude transitive dependency on stdlib, the Gradle version should be used compileOnly(kotlin("stdlib")) compileOnly(libs.gradlePlugin.kotlin) + compileOnly(libs.jacoco.reporter) functionalTestImplementation(kotlin("test")) functionalTestImplementation(libs.junit.jupiter) diff --git a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/LoggingTaskTests.kt b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/LoggingTaskTests.kt index e744fe1a..d5bdee9d 100644 --- a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/LoggingTaskTests.kt +++ b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/LoggingTaskTests.kt @@ -148,9 +148,9 @@ internal class LoggingTaskTests { taskOutput("koverPrintCoverage") { assertEquals( "Coverage for classes:\n" + - "Class org.jetbrains.ExampleClass covered instructions=7\n" + + "Class org.jetbrains.Unused covered instructions=0\n" + "Class org.jetbrains.SecondClass covered instructions=7\n" + - "Class org.jetbrains.Unused covered instructions=0\n\n", + "Class org.jetbrains.ExampleClass covered instructions=7\n\n", this ) } diff --git a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/VerificationTests.kt b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/VerificationTests.kt index ccf4c8c0..a2744a2e 100644 --- a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/VerificationTests.kt +++ b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/VerificationTests.kt @@ -97,14 +97,14 @@ internal class VerificationTests { run("koverHtmlReport", "koverVerify", errorExpected = true) { verification { - assertKoverResult("""Rule 'counts rule' violated: - lines covered percentage is 46.590900, but expected minimum is 58 - lines covered count is 41, but expected maximum is 3 + assertResult("""Rule 'counts rule' violated: + lines covered count is *, but expected maximum is 3 + lines covered percentage is *, but expected minimum is 58 Rule 'fully uncovered instructions by classes' violated: - instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.FullyCovered' is 0.000000, but expected minimum is 100 + instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.FullyCovered' is *, but expected minimum is 100 instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.PartiallyCoveredFirst' is *, but expected minimum is 100 instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.PartiallyCoveredSecond' is *, but expected minimum is 100 - instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubFullyCovered' is 0.000000, but expected minimum is 100 + instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubFullyCovered' is *, but expected minimum is 100 instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredFirst' is *, but expected minimum is 100 instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredSecond' is *, but expected minimum is 100 Rule 'fully covered instructions by packages' violated: @@ -122,28 +122,6 @@ Rule 'branches by classes' violated: Rule 'missed packages' violated: lines missed count for package 'org.jetbrains.kover.test.functional.verification' is 23, but expected maximum is 1 lines missed count for package 'org.jetbrains.kover.test.functional.verification.subpackage' is 24, but expected maximum is 1 -""") - - assertJaCoCoResult("""Rule violated: lines covered count is 41, but expected maximum is 3 -Rule violated: lines covered percentage is 46.5900, but expected minimum is 58.0000 -Rule violated: branches covered count for class 'org.jetbrains.kover.test.functional.verification.FullyCovered' is 0, but expected minimum is 1000 -Rule violated: instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.FullyCovered' is 0.0000, but expected minimum is 100.0000 -Rule violated: branches covered count for class 'org.jetbrains.kover.test.functional.verification.PartiallyCoveredFirst' is 2, but expected minimum is 1000 -Rule violated: instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.PartiallyCoveredFirst' is *, but expected minimum is 100.0000 -Rule violated: branches covered count for class 'org.jetbrains.kover.test.functional.verification.PartiallyCoveredSecond' is 1, but expected minimum is 1000 -Rule violated: instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.PartiallyCoveredSecond' is *, but expected minimum is 100.0000 -Rule violated: branches covered count for class 'org.jetbrains.kover.test.functional.verification.Uncovered' is 0, but expected minimum is 1000 -Rule violated: branches covered count for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubFullyCovered' is 0, but expected minimum is 1000 -Rule violated: instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubFullyCovered' is 0.0000, but expected minimum is 100.0000 -Rule violated: branches covered count for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredFirst' is 0, but expected minimum is 1000 -Rule violated: instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredFirst' is *, but expected minimum is 100.0000 -Rule violated: branches covered count for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredSecond' is 1, but expected minimum is 1000 -Rule violated: instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredSecond' is *, but expected minimum is 100.0000 -Rule violated: branches covered count for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubUncovered' is 0, but expected minimum is 1000 -Rule violated: instructions covered percentage for package 'org.jetbrains.kover.test.functional.verification.subpackage' is *, but expected minimum is 100.0000 -Rule violated: lines missed count for package 'org.jetbrains.kover.test.functional.verification.subpackage' is 24, but expected maximum is 1 -Rule violated: instructions covered percentage for package 'org.jetbrains.kover.test.functional.verification' is *, but expected minimum is 100.0000 -Rule violated: lines missed count for package 'org.jetbrains.kover.test.functional.verification' is 23, but expected maximum is 1 """) } } @@ -169,8 +147,7 @@ Rule violated: lines missed count for package 'org.jetbrains.kover.test.function run("koverVerify", errorExpected = true) { verification { - assertKoverResult("Rule 'root rule' violated: lines covered percentage is *, but expected minimum is 99\n") - assertJaCoCoResult("Rule violated: lines covered percentage is *, but expected minimum is 99.0000\n") + assertResult("Rule 'root rule' violated: lines covered percentage is *, but expected minimum is 99\n") } } } @@ -198,8 +175,7 @@ Rule violated: lines missed count for package 'org.jetbrains.kover.test.function taskOutput(":koverVerify") { match { assertContains("Kover Verification Error") - assertKoverContains("Rule 'root rule' violated: lines covered percentage is *, but expected minimum is 99\n") - assertJaCoCoContains("Rule violated: lines covered percentage is *, but expected minimum is 99.0000\n") + assertContains("Rule 'root rule' violated: lines covered percentage is *, but expected minimum is 99\n") } } } @@ -233,8 +209,7 @@ Rule violated: lines missed count for package 'org.jetbrains.kover.test.function taskOutput(":koverVerify") { match { assertContains("Kover Verification Error") - assertKoverContains("Rule 'root rule' violated: lines covered percentage is *, but expected minimum is 99\n") - assertJaCoCoContains("Rule violated: lines covered percentage is *, but expected minimum is 99.0000\n") + assertContains("Rule 'root rule' violated: lines covered percentage is *, but expected minimum is 99\n") } } } 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 5a94582a..d24af708 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 @@ -403,19 +403,10 @@ 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 = 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]") - } - } - - override fun assertJaCoCoResult(expected: String) { - if (context.project.toolVariant.vendor != CoverageToolVendor.JACOCO) return + override fun assertResult(expected: String) { 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]") + throw AssertionError("Unexpected verification result.\n\tActual\n[\n$content\n]\nExpected regex\n[\n$expected\n]") } } } diff --git a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/framework/checker/CheckerTypes.kt b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/framework/checker/CheckerTypes.kt index ebcb9cb9..fdec1ce8 100644 --- a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/framework/checker/CheckerTypes.kt +++ b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/framework/checker/CheckerTypes.kt @@ -87,8 +87,7 @@ internal interface Counter { } internal interface VerifyReportChecker { - fun assertKoverResult(expected: String) - fun assertJaCoCoResult(expected: String) + fun assertResult(expected: String) } internal interface TextMatcher { 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 a16baff9..b9741a0d 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 @@ -64,7 +64,8 @@ internal class ReportContext( internal class GradleReportServices( val antBuilder: AntBuilder, - val objects: ObjectFactory + val objects: ObjectFactory, + val workerExecutor: WorkerExecutor ) internal data class ReportFilters( 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 699a0a06..1fe12457 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 @@ -17,7 +17,7 @@ public object KoverVersions { /** * JaCoCo coverage tool version used by default. */ - public const val JACOCO_TOOL_DEFAULT_VERSION = "0.8.11" + public const val JACOCO_TOOL_DEFAULT_VERSION = "0.8.12" /** * Current version of Kover Gradle Plugin 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 b35c3d13..9df5d437 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 @@ -13,6 +13,7 @@ import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.* import org.gradle.api.tasks.* import org.gradle.kotlin.dsl.* +import org.gradle.workers.WorkerExecutor import java.io.File import javax.inject.Inject @@ -65,10 +66,13 @@ 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(ant, obj) + val services = GradleReportServices(ant, obj, workerExecutor) return ReportContext(collectAllFiles(), filters.get(), reportClasspath, temporaryDir, projectPath, services) } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tasks/reports/KoverDoVerifyTask.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tasks/reports/KoverDoVerifyTask.kt index 627ffc65..4aa083ff 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tasks/reports/KoverDoVerifyTask.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tasks/reports/KoverDoVerifyTask.kt @@ -22,10 +22,7 @@ internal abstract class KoverDoVerifyTask @Inject constructor(@get:Internal over @TaskAction fun verify() { val enabledRules = rules.get().filter { it.isEnabled } - val violations = tool.get().verify(enabledRules, context()) - - val errorMessage = KoverLegacyFeatures.violationMessage(violations) - resultFile.get().asFile.writeText(errorMessage) + tool.get().verify(enabledRules, resultFile.get().asFile, context()) } } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/CoverageTool.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/CoverageTool.kt index b440b38f..605b7364 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/CoverageTool.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/CoverageTool.kt @@ -113,7 +113,7 @@ internal interface CoverageTool { /** * Perform verification. */ - fun verify(rules: List, context: ReportContext): List + fun verify(rules: List, output: File, context: ReportContext) /** * Calculate coverage according to the specified parameters [request], for each grouped entity. diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Commons.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Commons.kt new file mode 100644 index 00000000..8c2c9b13 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Commons.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.plugin.tools.jacoco + +import kotlinx.kover.features.jvm.KoverFeatures.koverWildcardToRegex +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.WorkParameters +import org.jacoco.core.analysis.Analyzer +import org.jacoco.core.analysis.CoverageBuilder +import org.jacoco.core.tools.ExecFileLoader +import org.jacoco.report.DirectorySourceFileLocator +import org.jacoco.report.IReportVisitor +import org.jacoco.report.MultiSourceFileLocator +import java.io.File + +internal interface CommonJacocoParameters: WorkParameters { + val filters: Property + + val files: Property + val tempDir: DirectoryProperty + val projectPath: Property +} + + +internal fun IReportVisitor.loadContent(name: String?, content: ArtifactContent, filters: ReportFilters) { + val loader = ExecFileLoader() + content.reports.forEach { file -> + file.inputStream().use { loader.load(it) } + } + val sessionInfoStore = loader.sessionInfoStore + val executionDataStore = loader.executionDataStore + + val builder = CoverageBuilder() + val analyzer = Analyzer(executionDataStore, builder) + + val classfiles = collectClassFiles(content.outputs, filters) + classfiles.forEach { classfile -> + analyzer.analyzeAll(classfile) + } + val bundle = builder.getBundle(name) + + visitInfo(sessionInfoStore.infos, executionDataStore.contents) + + val sourceLocator = MultiSourceFileLocator(DEFAULT_TAB_WIDTH) + content.sources.forEach { sourceDir -> + sourceLocator.add(DirectorySourceFileLocator(sourceDir, null, DEFAULT_TAB_WIDTH)) + } + visitBundle(bundle, sourceLocator) +} + +private const val DEFAULT_TAB_WIDTH = 4 + +private fun collectClassFiles(outputs: Iterable, filters: ReportFilters): Collection { + val filesByClassName = mutableMapOf() + outputs.forEach { output -> + output.walk().forEach { file -> + if (file.isFile && file.name.endsWith(CLASS_FILE_EXTENSION)) { + val className = file.toRelativeString(output).filenameToClassname() + filesByClassName[className] = file + } + } + } + + return if (filters.excludesClasses.isNotEmpty() || filters.includesClasses.isNotEmpty()) { + val excludeRegexes = filters.excludesClasses.map { koverWildcardToRegex(it).toRegex() } + val includeRegexes = filters.includesClasses.map { koverWildcardToRegex(it).toRegex() } + + filesByClassName.filterKeys { className -> + ((includeRegexes.isEmpty() || includeRegexes.any { regex -> className.matches(regex) }) + // if the exclusion rules are declared, then the file should not fit any of them + && excludeRegexes.none { regex -> className.matches(regex) }) + }.values + } else { + filesByClassName.values + } +} + +internal fun T.fillCommonParameters(context: ReportContext) { + filters.convention(context.filters) + files.convention(context.files) + tempDir.set(context.tempDir) + projectPath.convention(context.projectPath) +} + +/** + * Replaces characters `|` or `\` to `.` and remove postfix `.class`. + */ +internal fun String.filenameToClassname(): String { + return this.replace(File.separatorChar, '.').removeSuffix(CLASS_FILE_EXTENSION) +} + +/** + * Extension of class-files. + */ +internal const val CLASS_FILE_EXTENSION = ".class" 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 deleted file mode 100644 index 18ef3369..00000000 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/Evaluation.kt +++ /dev/null @@ -1,43 +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.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.CoverageRequest -import kotlinx.kover.gradle.plugin.tools.writeToFile -import kotlinx.kover.gradle.plugin.util.ONE_HUNDRED -import java.io.File -import java.math.BigDecimal - -internal fun ReportContext.printJacocoCoverage(request: CoverageRequest, outputFile: File) { - val bound = VerificationBound(ONE_HUNDRED, BigDecimal.ZERO, request.metric, request.aggregation) - val failRule = VerificationRule(true, "", request.entity, listOf(bound)) - - val violations = doJacocoVerify(listOf(failRule)) - if (violations.isEmpty()) { - outputFile.writeNoSources(request.header) - return - } - - val values = violations.flatMap { rule -> - if (rule.violations.isEmpty()) { - throw KoverCriticalException("Expected at least one bound violation for JaCoCo") - } - rule.violations.map { - CoverageValue(it.entityName, it.value) - } - } - - values.writeToFile( - outputFile, - request.header, - request.lineFormat - ) -} \ No newline at end of file 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 deleted file mode 100644 index 5c7bf41a..00000000 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoAnt.kt +++ /dev/null @@ -1,99 +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.jacoco - -import groovy.lang.Closure -import groovy.lang.GroovyObject -import kotlinx.kover.features.jvm.KoverFeatures.koverWildcardToRegex -import kotlinx.kover.gradle.plugin.commons.ReportContext -import java.io.File - - -internal fun ReportContext.callAntReport( - reportName: String, - block: GroovyObject.() -> Unit -) { - val builder = services.antBuilder - builder.invokeMethod( - "taskdef", - mapOf( - "name" to "jacocoReport", - "classname" to "org.jacoco.ant.ReportTask", - "classpath" to classpath.asPath - ) - ) - - val filesByClassName = mutableMapOf() - files.outputs.forEach { output -> - output.walk().forEach { file -> - if (file.isFile && file.name.endsWith(CLASS_FILE_EXTENSION)) { - val className = file.toRelativeString(output).filenameToClassname() - filesByClassName[className] = file - } - } - } - - val classes = if (filters.excludesClasses.isNotEmpty() || filters.includesClasses.isNotEmpty()) { - val excludeRegexes = filters.excludesClasses.map { koverWildcardToRegex(it).toRegex() } - val includeRegexes = filters.includesClasses.map { koverWildcardToRegex(it).toRegex() } - - filesByClassName.filterKeys { className -> - ((includeRegexes.isEmpty() || includeRegexes.any { regex -> className.matches(regex) }) - // if the exclusion rules are declared, then the file should not fit any of them - && excludeRegexes.none { regex -> className.matches(regex) }) - }.values - } else { - filesByClassName.values - } - - - builder.invokeWithBody("jacocoReport") { - invokeWithBody("executiondata") { - services.objects.fileCollection().from(files.reports).addToAntBuilder(this, "resources") - } - invokeWithBody("structure", mapOf("name" to reportName)) { - invokeWithBody("sourcefiles") { - services.objects.fileCollection().from(files.sources).addToAntBuilder(this, "resources") - } - invokeWithBody("classfiles") { - services.objects.fileCollection().from(classes).addToAntBuilder(this, "resources") - } - } - block() - } -} - - -@Suppress("UNUSED_PARAMETER") -internal inline fun GroovyObject.invokeWithBody( - name: String, - args: Map = emptyMap(), - crossinline body: GroovyObject.() -> Unit -) { - invokeMethod( - name, - listOf( - args, - object : Closure(this) { - fun doCall(ignore: Any?): Any? { - this@invokeWithBody.body() - return null - } - } - ) - ) -} - -/** - * Replaces characters `|` or `\` to `.` and remove postfix `.class`. - */ -private fun String.filenameToClassname(): String { - return this.replace(File.separatorChar, '.').removeSuffix(CLASS_FILE_EXTENSION) -} - -/** - * Extension of class-files. - */ -private const val CLASS_FILE_EXTENSION = ".class" diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoHtmlOrXmlReport.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoHtmlOrXmlReport.kt index b25f47de..c2c37cfd 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoHtmlOrXmlReport.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoHtmlOrXmlReport.kt @@ -4,26 +4,80 @@ package kotlinx.kover.gradle.plugin.tools.jacoco -import kotlinx.kover.gradle.plugin.commons.* -import java.io.* +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 org.gradle.workers.WorkAction +import org.gradle.workers.WorkQueue +import org.jacoco.report.FileMultiReportOutput +import org.jacoco.report.html.HTMLFormatter +import org.jacoco.report.xml.XMLFormatter +import java.io.File +import java.util.* +internal fun ReportContext.jacocoHtmlReport(htmlReportDir: File, htmlTitle: String, charsetName: String?) { + val workQueue: WorkQueue = services.workerExecutor.classLoaderIsolation { + this.classpath.from(this@jacocoHtmlReport.classpath) + } + + workQueue.submit(JacocoHtmlReportAction::class.java) { + htmlDir.set(htmlReportDir) + title.convention(htmlTitle) + charset.convention(charsetName) + + fillCommonParameters(this@jacocoHtmlReport) + } +} + +internal abstract class JacocoHtmlReportAction : WorkAction { + override fun execute() { + val htmlDir = parameters.htmlDir.get().asFile + val output = FileMultiReportOutput(htmlDir) + val formatter = HTMLFormatter() + formatter.footerText = "" + formatter.outputEncoding = parameters.charset.orNull ?: "UTF-8" + formatter.locale = Locale.getDefault() + val visitor = formatter.createVisitor(output) + visitor.loadContent(parameters.title.get(), parameters.files.get(), parameters.filters.get()) + visitor.visitEnd() + } +} -internal fun ReportContext.jacocoHtmlReport(htmlDir: File, title: String, charset: String?) { - callAntReport(title) { - htmlDir.mkdirs() +internal interface HtmlReportParameters : CommonJacocoParameters { + val htmlDir: DirectoryProperty + val title: Property + val charset: Property +} + +internal fun ReportContext.jacocoXmlReport(xmlReportFile: File, xmlTitle: String) { + val workQueue: WorkQueue = services.workerExecutor.classLoaderIsolation { + classpath.from(this@jacocoXmlReport.classpath) + } + + workQueue.submit(JacocoXmlReportAction::class.java) { + xmlFile.set(xmlReportFile) + title.convention(xmlTitle) - val element = if (charset != null) { - mapOf("destdir" to htmlDir, "encoding" to charset) - } else { - mapOf("destdir" to htmlDir) - } - invokeMethod("html", element) + fillCommonParameters(this@jacocoXmlReport) } } -internal fun ReportContext.jacocoXmlReport(xmlFile: File, title: String) { - callAntReport(title) { - xmlFile.parentFile.mkdirs() - invokeMethod("xml", mapOf("destfile" to xmlFile)) +internal interface XmlReportParameters : CommonJacocoParameters { + val xmlFile: RegularFileProperty + val title: Property +} + +internal abstract class JacocoXmlReportAction : WorkAction { + override fun execute() { + val xmlFile = parameters.xmlFile.get().asFile + val stream = xmlFile.outputStream().buffered() + + val xmlFormatter = XMLFormatter() + xmlFormatter.setOutputEncoding("UTF-8") + val visitor = xmlFormatter.createVisitor(stream) + visitor.loadContent(parameters.title.get(), parameters.files.get(), parameters.filters.get()) + + visitor.visitEnd() } } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoTool.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoTool.kt index 05aa9107..2ef8c5b2 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoTool.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/JacocoTool.kt @@ -4,8 +4,6 @@ package kotlinx.kover.gradle.plugin.tools.jacoco -import kotlinx.kover.features.jvm.KoverLegacyFeatures -import kotlinx.kover.features.jvm.RuleViolations import kotlinx.kover.gradle.plugin.commons.ReportContext import kotlinx.kover.gradle.plugin.commons.VerificationRule import kotlinx.kover.gradle.plugin.tools.CoverageRequest @@ -20,8 +18,8 @@ import java.io.File internal class JacocoTool(override val variant: CoverageToolVariant) : CoverageTool { override val jvmAgentDependency: String = "org.jacoco:org.jacoco.agent:${variant.version}" - override val jvmReporterDependency: String = "org.jacoco:org.jacoco.agent:${variant.version}" - override val jvmReporterExtraDependency: String = "org.jacoco:org.jacoco.ant:${variant.version}" + override val jvmReporterDependency: String = "org.jacoco:org.jacoco.report:${variant.version}" + override val jvmReporterExtraDependency: String = "org.jacoco:org.jacoco.report:${variant.version}" override fun findJvmAgentJar(classpath: FileCollection, archiveOperations: ArchiveOperations): File { val fatJar = classpath.filter { it.name.startsWith("org.jacoco.agent") }.singleFile @@ -49,8 +47,8 @@ internal class JacocoTool(override val variant: CoverageToolVariant) : CoverageT throw GradleException("It is not possible to generate an Kover binary report for JaCoCo. Please use Kover toolset") } - override fun verify(rules: List, context: ReportContext): List { - return context.doJacocoVerify(rules) + override fun verify(rules: List, output: File, context: ReportContext) { + return context.doJacocoVerify(rules, output) } override fun collectCoverage( diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/PrintCoverage.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/PrintCoverage.kt new file mode 100644 index 00000000..2beccd31 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/PrintCoverage.kt @@ -0,0 +1,70 @@ +/* + * 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.jacoco + +import kotlinx.kover.features.jvm.CoverageValue +import kotlinx.kover.features.jvm.RuleViolations +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.CoverageRequest +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 org.gradle.workers.WorkQueue +import java.io.File +import java.math.BigDecimal + +internal fun ReportContext.printJacocoCoverage(request: CoverageRequest, outputFile: File) { + val bound = VerificationBound(ONE_HUNDRED, BigDecimal.ZERO, request.metric, request.aggregation) + val failRule = VerificationRule(true, "", request.entity, listOf(bound)) + + val workQueue: WorkQueue = services.workerExecutor.classLoaderIsolation { + classpath.from(this@printJacocoCoverage.classpath) + } + + workQueue.submit(JacocoPrintCoverageAction::class.java) { + this.outputFile.set(outputFile) + header.convention(request.header) + lineFormat.convention(request.lineFormat) + rulesProperty.convention(listOf(failRule)) + + fillCommonParameters(this@printJacocoCoverage) + } +} + +internal interface PrintCoverageParameters: AbstractVerifyParameters { + val outputFile: RegularFileProperty + val header: Property + val lineFormat: Property +} + +internal abstract class JacocoPrintCoverageAction : AbstractJacocoVerifyAction() { + override fun processResult(violations: List) { + val outputFile = parameters.outputFile.get().asFile + if (violations.isEmpty()) { + outputFile.writeNoSources(parameters.header.orNull) + return + } + + val values = violations.flatMap { rule -> + if (rule.violations.isEmpty()) { + throw KoverCriticalException("Expected at least one bound violation for JaCoCo") + } + rule.violations.map { + CoverageValue(it.entityName, it.value) + } + } + + values.writeToFile( + outputFile, + parameters.header.orNull, + parameters.lineFormat.get() + ) + } +} \ No newline at end of file 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 5c598d6a..dfee53ea 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 @@ -4,122 +4,181 @@ 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.features.jvm.* import kotlinx.kover.gradle.plugin.commons.* import kotlinx.kover.gradle.plugin.dsl.AggregationType import kotlinx.kover.gradle.plugin.dsl.CoverageUnit import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType import kotlinx.kover.gradle.plugin.tools.kover.convert import kotlinx.kover.gradle.plugin.util.ONE_HUNDRED -import org.gradle.internal.reflect.JavaMethod +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkQueue +import org.jacoco.core.analysis.ICounter.CounterValue +import org.jacoco.core.analysis.ICoverageNode +import org.jacoco.core.analysis.ICoverageNode.CounterEntity +import org.jacoco.report.check.IViolationsOutput +import org.jacoco.report.check.Limit +import org.jacoco.report.check.RulesChecker +import java.io.File import java.math.BigDecimal -import java.util.* - - -internal fun ReportContext.doJacocoVerify(rules: List): List { - - callAntReport(projectPath) { - invokeWithBody("check", mapOf("failonviolation" to "false", "violationsproperty" to "jacocoErrors")) { - rules.forEach { - val entityType = when (it.entityType) { - GroupingEntityType.APPLICATION -> "BUNDLE" - GroupingEntityType.CLASS -> "CLASS" - GroupingEntityType.PACKAGE -> "PACKAGE" - } - invokeWithBody("rule", mapOf("element" to entityType)) { - it.bounds.forEach { b -> - val limitArgs = mutableMapOf() - limitArgs["counter"] = when (b.metric) { - CoverageUnit.LINE -> "LINE" - CoverageUnit.INSTRUCTION -> "INSTRUCTION" - CoverageUnit.BRANCH -> "BRANCH" - } - - var min: BigDecimal? = b.minValue - var max: BigDecimal? = b.maxValue - when (b.aggregation) { - AggregationType.COVERED_COUNT -> { - limitArgs["value"] = "COVEREDCOUNT" - } - - AggregationType.MISSED_COUNT -> { - limitArgs["value"] = "MISSEDCOUNT" - } - - AggregationType.COVERED_PERCENTAGE -> { - limitArgs["value"] = "COVEREDRATIO" - min = min?.divide(ONE_HUNDRED)?.setScale(4) - max = max?.divide(ONE_HUNDRED)?.setScale(4) - } - - AggregationType.MISSED_PERCENTAGE -> { - limitArgs["value"] = "MISSEDRATIO" - min = min?.divide(ONE_HUNDRED)?.setScale(4) - max = max?.divide(ONE_HUNDRED)?.setScale(4) - } - } - - if (min != null) { - limitArgs["minimum"] = min.toPlainString() - } - - if (max != null) { - limitArgs["maximum"] = max.toPlainString() - } - invokeMethod("limit", limitArgs) - } - } - } - } + +typealias JacocoRule = org.jacoco.report.check.Rule + +internal fun ReportContext.doJacocoVerify(rules: List, output: File) { + val workQueue: WorkQueue = services.workerExecutor.classLoaderIsolation { + classpath.from(this@doJacocoVerify.classpath) + } + + workQueue.submit(JacocoVerifyAction::class.java) { + outputFile.set(output) + rulesProperty.convention(rules) + + fillCommonParameters(this@doJacocoVerify) } - return services.antBuilder.violations() } +internal interface VerifyParameters: AbstractVerifyParameters { + val outputFile: RegularFileProperty +} -private val errorMessageRegex = - "Rule violated for (\\w+) (.+): (\\w+) (.+) is ([\\d\\.]+), but expected (\\w+) is ([\\d\\.]+)".toRegex() +internal abstract class JacocoVerifyAction : AbstractJacocoVerifyAction() { + override fun processResult(violations: List) { + val errorMessage = KoverLegacyFeatures.violationMessage(violations) + val outputFile = parameters.outputFile.get().asFile + outputFile.writeText(errorMessage) + } +} + + + + +internal abstract class AbstractJacocoVerifyAction: WorkAction { -private fun GroovyObject.violations(): List { - val project = getProperty("project") - val properties = JavaMethod.of( - project, - Hashtable::class.java, "getProperties" - ).invoke(project, *arrayOfNulls(0)) - val allErrorsString = properties["jacocoErrors"] as String? ?: return emptyList() + override fun execute() { + val rulesPairs = parameters.rulesProperty.get().toJacoco() - // sorting lines to get a stable order of errors - return allErrorsString.lines().sorted().map { - val match = errorMessageRegex.find(it) - ?: throw KoverCriticalException("Can't parse JaCoCo verification error string:\n$it") + val violationListener = ViolationListener(rulesPairs) + + val formatter = RulesChecker() + formatter.setRules(rulesPairs.map { it.origin }) + val visitor = formatter.createVisitor(violationListener) + visitor.loadContent("application", parameters.files.get(), parameters.filters.get()) + visitor.visitEnd() + + processResult(violationListener.violations()) + } + + abstract fun processResult(violations: List) +} + +internal interface AbstractVerifyParameters : CommonJacocoParameters { + val rulesProperty: ListProperty +} + +private class ViolationListener(rulesPairs: List): IViolationsOutput { + private val violations: Map> = + rulesPairs.associateWith { mutableListOf() } + + override fun onViolation(node: ICoverageNode, rule: JacocoRule, limit: Limit, message: String) { + val bounds = violations.filterKeys { key -> key.isRule(rule) }.values.singleOrNull() + ?: throw KoverCriticalException("Rules not mapped for JaCoCo") + + val match = errorMessageRegex.find(message) + ?: throw KoverCriticalException("Can't parse JaCoCo verification error string:\n$message") - val entityType = match.groupValues[1].asEntityType(it) val entityName = match.groupValues[2].run { if (this == ":") null else this } - val coverageUnits = match.groupValues[3].asCoverageUnit(it) - val agg = match.groupValues[4].asAggType(it) - val value = match.groupValues[5].asValue(it, agg) - val isMax = match.groupValues[6].asIsMax(it) - val expected = match.groupValues[7].asValue(it, agg) - - 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() + val coverageUnits = match.groupValues[3].asCoverageUnit(message) + val agg = match.groupValues[4].asAggType(message) + val value = match.groupValues[5].asValue(message, agg) + val isMax = match.groupValues[6].asIsMax(message) + val expected = match.groupValues[7].asValue(message, agg) + + val bound = Bound(if (!isMax) expected else null, if (isMax) expected else null, coverageUnits.convert(), agg.convert()) + bounds += BoundViolation(bound, isMax, value, entityName) + } + + fun violations() : List { + return violations.mapNotNull { v -> + if (v.value.isEmpty()) return@mapNotNull null + RuleViolations(v.key.koverRule, v.value) + } + } + } -private fun String.asEntityType(line: String): GroupingEntityType = when (this) { - "bundle" -> GroupingEntityType.APPLICATION - "class" -> GroupingEntityType.CLASS - "package" -> GroupingEntityType.PACKAGE - else -> throw KoverCriticalException("Unknown JaCoCo entity type '$this' in verification error:\n$line") +private class JacocoRuleWrapper(val origin: JacocoRule, val koverRule: Rule) { + fun isRule(rule: JacocoRule): Boolean = origin === rule } +private fun List.toJacoco(): List { + return map { rule -> JacocoRuleWrapper(rule.toJacoco(), rule.convert()) } +} + +private fun VerificationRule.toJacoco(): JacocoRule { + val rule = JacocoRule() + + rule.element = when(this.entityType) { + GroupingEntityType.APPLICATION -> ICoverageNode.ElementType.BUNDLE + GroupingEntityType.CLASS -> ICoverageNode.ElementType.CLASS + GroupingEntityType.PACKAGE -> ICoverageNode.ElementType.PACKAGE + } + + rule.limits = bounds.map { bound -> bound.toJacoco() } + + return rule +} + +private fun VerificationBound.toJacoco(): Limit { + val limit = Limit() + + val entity = when (metric) { + CoverageUnit.LINE -> CounterEntity.LINE + CoverageUnit.INSTRUCTION -> CounterEntity.INSTRUCTION + CoverageUnit.BRANCH -> CounterEntity.BRANCH + } + limit.setCounter(entity.name) + var min: BigDecimal? = minValue + var max: BigDecimal? = maxValue + val value: CounterValue + when (aggregation) { + AggregationType.COVERED_COUNT -> { + value = CounterValue.COVEREDCOUNT + } + + AggregationType.MISSED_COUNT -> { + value = CounterValue.MISSEDCOUNT + } + + AggregationType.COVERED_PERCENTAGE -> { + value = CounterValue.COVEREDRATIO + min = min?.divide(ONE_HUNDRED)?.setScale(4) + max = max?.divide(ONE_HUNDRED)?.setScale(4) + } + + AggregationType.MISSED_PERCENTAGE -> { + value = CounterValue.MISSEDRATIO + min = min?.divide(ONE_HUNDRED)?.setScale(4) + max = max?.divide(ONE_HUNDRED)?.setScale(4) + } + } + limit.setValue(value.name) + if (min != null) { + limit.minimum = min.toPlainString() + } + if (max != null) { + limit.maximum = max.toPlainString() + } + return limit +} + + +private val errorMessageRegex = + "Rule violated for (\\w+) (.+): (\\w+) (.+) is ([\\d\\.]+), but expected (\\w+) is ([\\d\\.]+)".toRegex() + + private fun String.asCoverageUnit(line: String): CoverageUnit = when (this) { "lines" -> CoverageUnit.LINE "instructions" -> CoverageUnit.INSTRUCTION @@ -146,11 +205,8 @@ private fun String.asValue(line: String, aggregationType: AggregationType): BigD ?: throw KoverCriticalException("Illegal JaCoCo metric value '$this' in verification error:\n$line") return if (aggregationType.isPercentage) { - value * ONE_HUNDRED + (value * ONE_HUNDRED).stripTrailingZeros() } else { value } } - - - 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 9affbb2a..a8c80e40 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 @@ -53,17 +53,17 @@ internal class KoverTool(override val variant: CoverageToolVariant) : CoverageTo context.koverBinaryReport(binary) } - override fun verify( - rules: List, - context: ReportContext - ): List{ - return KoverLegacyFeatures.verify( + override fun verify(rules: List, output: File, context: ReportContext) { + val violations = KoverLegacyFeatures.verify( rules.map { it.convert() }, context.tempDir, context.filters.toKoverFeatures(), context.files.reports.toList(), context.files.outputs.toList() ) + + val errorMessage = KoverLegacyFeatures.violationMessage(violations) + output.writeText(errorMessage) } override fun collectCoverage(request: CoverageRequest, outputFile: File, context: ReportContext) { From 007de27c8aea1fda9c23e40ed40be8d6c57b6461 Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Fri, 23 Aug 2024 16:54:15 +0200 Subject: [PATCH 2/3] ~review fixes --- .../java/kotlinx/kover/features/jvm/KoverLegacyFeatures.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 6b1c1573..7c807f39 100644 --- 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 @@ -199,9 +199,9 @@ public object KoverLegacyFeatures { messageBuilder.appendLine("$namedRule violated:") rule.violations.map { bound -> bound.format(rule) } .toSortedSet() - .forEach { bound -> + .forEach { boundString -> messageBuilder.append(" ") - messageBuilder.appendLine(bound.format(rule)) + messageBuilder.appendLine(boundString) } } } From 330e0dd6bd802eceb51b52f307308e56ec11651d Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Wed, 28 Aug 2024 17:54:08 +0200 Subject: [PATCH 3/3] ~review fix --- .../kotlinx/kover/gradle/plugin/tools/jacoco/PrintCoverage.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/PrintCoverage.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/PrintCoverage.kt index 2beccd31..4c6c3f1a 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/PrintCoverage.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/tools/jacoco/PrintCoverage.kt @@ -22,6 +22,7 @@ import java.math.BigDecimal internal fun ReportContext.printJacocoCoverage(request: CoverageRequest, outputFile: File) { val bound = VerificationBound(ONE_HUNDRED, BigDecimal.ZERO, request.metric, request.aggregation) + // Since JaCoCo doesn't have an API for explicitly obtaining coverage values, we get them indirectly through verification val failRule = VerificationRule(true, "", request.entity, listOf(bound)) val workQueue: WorkQueue = services.workerExecutor.classLoaderIsolation {