From f7f0d1c6ef8f8bc4b0096fd8a4275749d4e76f03 Mon Sep 17 00:00:00 2001 From: Sergey Shanshin Date: Tue, 23 Aug 2022 13:21:04 +0300 Subject: [PATCH] Fixed instrumentation counter in IntelliJ verifier - IntelliJ Engine version upgraded to 1.0.680 - added tests on verification error Fixes #210, #211, #212 --- .../functional/cases/VerificationTests.kt | 123 ++++++++++++------ .../kover/test/functional/core/Runner.kt | 30 ++++- .../kover/test/functional/core/Types.kt | 11 +- .../verification/main/kotlin/FirstClasses.kt | 96 ++++++++++++++ .../verification/main/kotlin/SecondClasses.kt | 86 ++++++++++++ .../verification/test/kotlin/TestClass.kt | 25 ++++ .../kotlinx/kover/api/KoverConstants.kt | 4 +- .../kotlinx/kover/engines/commons/Reports.kt | 2 +- .../engines/intellij/IntellijVerification.kt | 100 ++++++++------ .../kover/engines/jacoco/JacocoReports.kt | 23 +++- 10 files changed, 404 insertions(+), 96 deletions(-) create mode 100644 src/functionalTest/templates/sources/verification/main/kotlin/FirstClasses.kt create mode 100644 src/functionalTest/templates/sources/verification/main/kotlin/SecondClasses.kt create mode 100644 src/functionalTest/templates/sources/verification/test/kotlin/TestClass.kt diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt index 678599c5..9fa9a666 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt @@ -38,15 +38,15 @@ internal class VerificationTests : BaseGradleScriptTest() { } @Test - fun testNotVerifiedIntelliJ() { - val build = diverseBuild(languages = ALL_LANGUAGES, engines = listOf(CoverageEngineVendor.INTELLIJ)) + fun testVerificationError() { + val build = diverseBuild(languages = ALL_LANGUAGES, engines = ALL_ENGINES) build.addKoverRootProject { - sourcesFrom("simple") + sourcesFrom("verification") kover { verify { rule { - name = "test rule" + name = "counts rule" bound { minValue = 58 maxValue = 60 @@ -57,55 +57,94 @@ internal class VerificationTests : BaseGradleScriptTest() { maxValue = 3 } } - } - } - } - - build.prepare().runWithError("koverVerify") { - output { - assertTrue { - this.contains( - "> Rule 'test rule' violated:\n" + - " covered lines percentage is 57.142900, but expected minimum is 58\n" + - " covered lines count is 4, but expected maximum is 3" - ) - } - } - } - } - - @Test - fun testNotVerifiedJaCoCo() { - val build = diverseBuild(languages = ALL_LANGUAGES, engines = listOf(CoverageEngineVendor.JACOCO)) - build.addKoverRootProject { - sourcesFrom("simple") - - kover { - verify { rule { - name = "test rule" + name = "fully uncovered instructions by classes" + target = VerificationTarget.CLASS bound { - minValue = 58 - maxValue = 60 + counter = CounterType.INSTRUCTION + valueType = VerificationValueType.MISSED_PERCENTAGE + minValue = 100 } + } + rule { + name = "fully covered instructions by packages" + target = VerificationTarget.PACKAGE + bound { + counter = CounterType.INSTRUCTION + valueType = VerificationValueType.COVERED_PERCENTAGE + minValue = 100 + } + } + rule { + name = "branches by classes" + target = VerificationTarget.CLASS bound { + counter = CounterType.BRANCH valueType = VerificationValueType.COVERED_COUNT - minValue = 2 - maxValue = 3 + minValue = 1000 + } + } + rule { + name = "missed packages" + target = VerificationTarget.PACKAGE + bound { + valueType = VerificationValueType.MISSED_COUNT + maxValue = 1 } } } } } - build.prepare().runWithError("koverVerify") { - output { - assertTrue { - this.contains( - "[ant:jacocoReport] Rule violated for bundle :: lines covered ratio is 0.50, but expected minimum is 0.58\n" + - "[ant:jacocoReport] Rule violated for bundle :: lines covered count is 4, but expected maximum is 3" - ) - } + build.prepare().runWithError("koverHtmlReport", "koverVerify") { + verification { + assertIntelliJResult("""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 +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.PartiallyCoveredFirst' is 44.642900, but expected minimum is 100 + instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.PartiallyCoveredSecond' is 51.666700, 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.SubPartiallyCoveredFirst' is 52.631600, but expected minimum is 100 + instructions missed percentage for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredSecond' is 66.216200, but expected minimum is 100 +Rule 'fully covered instructions by packages' violated: + instructions covered percentage for package 'org.jetbrains.kover.test.functional.verification' is 48.275900, but expected minimum is 100 + instructions covered percentage for package 'org.jetbrains.kover.test.functional.verification.subpackage' is 43.085100, but expected minimum is 100 +Rule 'branches by classes' violated: + branches covered count for class 'org.jetbrains.kover.test.functional.verification.FullyCovered' is 0, but expected minimum is 1000 + branches covered count for class 'org.jetbrains.kover.test.functional.verification.PartiallyCoveredFirst' is 2, but expected minimum is 1000 + branches covered count for class 'org.jetbrains.kover.test.functional.verification.PartiallyCoveredSecond' is 1, but expected minimum is 1000 + branches covered count for class 'org.jetbrains.kover.test.functional.verification.Uncovered' is 0, but expected minimum is 1000 + branches covered count for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubFullyCovered' is 0, but expected minimum is 1000 + branches covered count for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredFirst' is 0, but expected minimum is 1000 + branches covered count for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredSecond' is 1, but expected minimum is 1000 + branches covered count for class 'org.jetbrains.kover.test.functional.verification.subpackage.SubUncovered' is 0, but expected minimum is 1000 +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 for bundle :: lines covered count is 41, but expected maximum is 3 +Rule violated for bundle :: lines covered ratio is 0.46, but expected minimum is 0.58 +Rule violated for class org.jetbrains.kover.test.functional.verification.FullyCovered: branches covered count is 0, but expected minimum is 1000 +Rule violated for class org.jetbrains.kover.test.functional.verification.FullyCovered: instructions missed ratio is 0, but expected minimum is 1 +Rule violated for class org.jetbrains.kover.test.functional.verification.PartiallyCoveredFirst: branches covered count is 2, but expected minimum is 1000 +Rule violated for class org.jetbrains.kover.test.functional.verification.PartiallyCoveredFirst: instructions missed ratio is 0, but expected minimum is 1 +Rule violated for class org.jetbrains.kover.test.functional.verification.PartiallyCoveredSecond: branches covered count is 1, but expected minimum is 1000 +Rule violated for class org.jetbrains.kover.test.functional.verification.PartiallyCoveredSecond: instructions missed ratio is 0, but expected minimum is 1 +Rule violated for class org.jetbrains.kover.test.functional.verification.Uncovered: branches covered count is 0, but expected minimum is 1000 +Rule violated for class org.jetbrains.kover.test.functional.verification.subpackage.SubFullyCovered: branches covered count is 0, but expected minimum is 1000 +Rule violated for class org.jetbrains.kover.test.functional.verification.subpackage.SubFullyCovered: instructions missed ratio is 0, but expected minimum is 1 +Rule violated for class org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredFirst: branches covered count is 0, but expected minimum is 1000 +Rule violated for class org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredFirst: instructions missed ratio is 0, but expected minimum is 1 +Rule violated for class org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredSecond: branches covered count is 1, but expected minimum is 1000 +Rule violated for class org.jetbrains.kover.test.functional.verification.subpackage.SubPartiallyCoveredSecond: instructions missed ratio is 0, but expected minimum is 1 +Rule violated for class org.jetbrains.kover.test.functional.verification.subpackage.SubUncovered: branches covered count is 0, but expected minimum is 1000 +Rule violated for package org.jetbrains.kover.test.functional.verification.subpackage: instructions covered ratio is 0, but expected minimum is 1 +Rule violated for package org.jetbrains.kover.test.functional.verification.subpackage: lines missed count is 24, but expected maximum is 1 +Rule violated for package org.jetbrains.kover.test.functional.verification: instructions covered ratio is 0, but expected minimum is 1 +Rule violated for package org.jetbrains.kover.test.functional.verification: lines missed count is 23, but expected maximum is 1""") } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt index 7c1c8d36..fd06e277 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt @@ -99,11 +99,13 @@ private class RunResultImpl( override val defaultBinaryReport: String get() { - // IntelliJ is a default Engine - val extension = if (slice?.engine == CoverageEngineVendor.JACOCO) "exec" else "ic" + val extension = if (engineVendor == CoverageEngineVendor.JACOCO) "exec" else "ic" return binaryReportsDirectory() + "/" + defaultTestTask(slice?.type ?: ProjectType.KOTLIN_JVM) + "." + extension } + // IntelliJ is a default Engine + val engineVendor: CoverageEngineVendor = slice?.engine ?: CoverageEngineVendor.INTELLIJ + private val buildScriptFile: File = buildFile() private val buildScript: String by lazy { buildScriptFile.readText() } @@ -132,10 +134,16 @@ private class RunResultImpl( File(buildDir, name).checker() } - override fun xml(filename: String, checker: XmlReport.() -> Unit) { + override fun xml(filename: String, checker: XmlReportChecker.() -> Unit) { val xmlFile = File(buildDir, filename) if (!xmlFile.exists()) throw IllegalStateException("XML file '$filename' not found") - XmlReportImpl(this, xmlFile).checker() + XmlReportCheckerImpl(this, xmlFile).checker() + } + + override fun verification(checker: VerifyReportChecker.() -> Unit) { + val verificationResultFile = File(buildDir, "reports/kover/verification/errors.txt") + if (!verificationResultFile.exists()) throw IllegalStateException("Verification result file '$verificationResultFile' not found") + VerifyReportCheckerImpl(this, verificationResultFile.readText()).checker() } override fun outcome(taskName: String, checker: TaskOutcome.() -> Unit) { @@ -186,8 +194,20 @@ private class CounterImpl(val context: RunResultImpl, val symbol: String, val ty } } +private class VerifyReportCheckerImpl(val context: RunResultImpl, val content: String): VerifyReportChecker { + override fun assertIntelliJResult(expected: String) { + if (context.engineVendor != CoverageEngineVendor.INTELLIJ) return + assertEquals(expected, content, "Unexpected verification result for IntelliJ Engine") + } + + override fun assertJaCoCoResult(expected: String) { + if (context.engineVendor != CoverageEngineVendor.JACOCO) return + assertEquals(expected, content, "Unexpected verification result for JaCoCo Engine") + } + +} -private class XmlReportImpl(val context: RunResultImpl, file: File) : XmlReport { +private class XmlReportCheckerImpl(val context: RunResultImpl, file: File) : XmlReportChecker { private val document = DocumentBuilderFactory.newInstance() // This option disables checking the dtd file for JaCoCo XML file .also { it.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt index 381c3666..20d5e806 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt @@ -129,7 +129,9 @@ internal interface RunResult { fun file(name: String, checker: File.() -> Unit) - fun xml(filename: String, checker: XmlReport.() -> Unit) + fun xml(filename: String, checker: XmlReportChecker.() -> Unit) + + fun verification(checker: VerifyReportChecker.() -> Unit) fun outcome(taskName: String, checker: TaskOutcome.() -> Unit) @@ -151,7 +153,12 @@ internal interface Counter { fun assertFullyCovered() } -internal interface XmlReport { +internal interface VerifyReportChecker { + fun assertIntelliJResult(expected: String) + fun assertJaCoCoResult(expected: String) +} + +internal interface XmlReportChecker { fun classCounter(className: String, type: String = "INSTRUCTION"): Counter fun methodCounter(className: String, methodName: String, type: String = "INSTRUCTION"): Counter } diff --git a/src/functionalTest/templates/sources/verification/main/kotlin/FirstClasses.kt b/src/functionalTest/templates/sources/verification/main/kotlin/FirstClasses.kt new file mode 100644 index 00000000..6fec7728 --- /dev/null +++ b/src/functionalTest/templates/sources/verification/main/kotlin/FirstClasses.kt @@ -0,0 +1,96 @@ +package org.jetbrains.kover.test.functional.verification + +class FullyCovered { + fun function0(i: Int): String { + val j = i + 2 + println("function0") + return "result=$j" + } + + fun function1(i: Int): String { + val j = i + 2 + println("function0") + return "result=$j" + } + + fun name(): String? { + return this::class.simpleName + } +} + +class PartiallyCoveredFirst { + fun function0(i: Int): String { + val j = i + 2 + if (i > 0) { + println("GTZ") + } else if (i == 0) { + println("EZ") + } else { + println("LEZ") + } + println("function0") + return "result=$j" + } + + fun function1(i: Int): String { + val j = i + 2 + println("function1") + return "result=$j" + } + + fun name(): String? { + return this::class.simpleName + } +} + +class PartiallyCoveredSecond { + fun function0(i: Int): String { + val j = i + 2 + println("function0") + return "result=$j" + } + + fun function1(i: Int): String { + val j = i + 2 + println("function1") + if (i > 0) { + println("GTZ") + } else if (i == 0) { + println("EZ") + } else { + println("LEZ") + } + println("function1") + return "result=$j" + } + + fun name(): String? { + return this::class.simpleName + } +} +class Uncovered { + fun function0(i: Int): String { + val j = i + 2 + println("function0") + return "result=$j" + } + + fun function1(i: Int): String { + val j = i + 2 + println("function1") + if (i > 0) { + println("GTZ") + } else { + println("LEZ") + } + return "result=$j" + } + + fun name(): String? { + return this::class.simpleName + } +} + + + + diff --git a/src/functionalTest/templates/sources/verification/main/kotlin/SecondClasses.kt b/src/functionalTest/templates/sources/verification/main/kotlin/SecondClasses.kt new file mode 100644 index 00000000..3c1cd346 --- /dev/null +++ b/src/functionalTest/templates/sources/verification/main/kotlin/SecondClasses.kt @@ -0,0 +1,86 @@ +package org.jetbrains.kover.test.functional.verification.subpackage + +class SubFullyCovered { + fun function0(i: Int): String { + val j = i + 2 + println("function0") + return "result=$j" + } + + fun function1(i: Int): String { + val j = i + 2 + println("function0") + return "result=$j" + } + + fun name(): String? { + return this::class.simpleName + } +} + +class SubPartiallyCoveredFirst { + fun function0(i: Int): String { + val j = i + 2 + println("function0") + return "result=$j" + } + + fun function1(i: Int): String { + val j = i + 2 + println("function1") + return "result=$j" + } + + fun name(): String? { + return this::class.simpleName + } +} + +class SubPartiallyCoveredSecond { + fun function0(i: Int): String { + val j = i + 2 + if (i > 0) { + println("GTZ") + } else if (i == 0) { + println("EZ") + } else { + println("LEZ") + } + println("function0") + return "result=$j" + } + + fun function1(i: Int): String { + val j = i + 2 + if (i > 0) { + println("GTZ") + } else if (i == 0) { + println("EZ") + } else { + println("LEZ") + } + println("function1") + return "result=$j" + } + + fun name(): String? { + return this::class.simpleName + } +} +class SubUncovered { + fun function0(i: Int): String { + val j = i + 2 + println("function0") + return "result=$j" + } + + fun function1(i: Int): String { + val j = i + 2 + println("function1") + return "result=$j" + } + + fun name(): String? { + return this::class.simpleName + } +} diff --git a/src/functionalTest/templates/sources/verification/test/kotlin/TestClass.kt b/src/functionalTest/templates/sources/verification/test/kotlin/TestClass.kt new file mode 100644 index 00000000..a433b0e1 --- /dev/null +++ b/src/functionalTest/templates/sources/verification/test/kotlin/TestClass.kt @@ -0,0 +1,25 @@ +import org.jetbrains.kover.test.functional.verification.* +import org.jetbrains.kover.test.functional.verification.subpackage.* +import kotlin.test.Test + +class TestClass { + @Test + fun test() { + FullyCovered().function0(0) + FullyCovered().function1(1) + FullyCovered().name() + + PartiallyCoveredFirst().function0(0) + PartiallyCoveredFirst().name() + + PartiallyCoveredSecond().function1(1) + + SubFullyCovered().function0(0) + SubFullyCovered().function1(1) + SubFullyCovered().name() + + SubPartiallyCoveredFirst().function0(0) + + SubPartiallyCoveredSecond().function1(1) + } +} diff --git a/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt b/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt index 7d4cdbe1..3cb24969 100644 --- a/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt +++ b/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt @@ -36,8 +36,8 @@ public object KoverPaths { } public object KoverVersions { - internal const val MINIMAL_INTELLIJ_VERSION = "1.0.675" - internal const val DEFAULT_INTELLIJ_VERSION = "1.0.675" + internal const val MINIMAL_INTELLIJ_VERSION = "1.0.680" + internal const val DEFAULT_INTELLIJ_VERSION = "1.0.680" internal const val DEFAULT_JACOCO_VERSION = "0.8.8" } diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt b/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt index e9c7238a..6da66ca6 100644 --- a/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt +++ b/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt @@ -20,7 +20,7 @@ internal class ReportVerificationBound( val id: Int, val minValue: BigDecimal?, val maxValue: BigDecimal?, - val counter: CounterType, + val metric: CounterType, val valueType: VerificationValueType ) diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt index 63e58aa0..6750ce1f 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt @@ -15,6 +15,7 @@ import org.gradle.process.ExecOperations import java.io.* import java.math.BigDecimal import java.math.RoundingMode +import java.util.TreeMap @Suppress("UNUSED_PARAMETER") internal fun Task.intellijVerification( @@ -185,7 +186,7 @@ private fun ReportVerificationRule.targetToReporter(): String { } private fun ReportVerificationBound.counterToReporter(): String { - return when (counter) { + return when (metric) { CounterType.LINE -> "LINE" CounterType.INSTRUCTION -> "INSTRUCTION" CounterType.BRANCH -> "BRANCH" @@ -211,20 +212,26 @@ private fun ReportVerificationBound.valueToReporter(value: BigDecimal): BigDecim @Suppress("UNCHECKED_CAST") private fun processViolationsModel(violations: Map): ViolationsResult { - val rules = mutableMapOf() + val rules = TreeMap() try { violations.forEach { (ruleId, boundViolations) -> - val bounds = mutableMapOf() + val bounds = TreeMap() (boundViolations as Map).forEach { (id, v) -> val minViolations = (v as Map>)["min"] val maxViolations = v["max"] - val min = - minViolations?.mapValues { it.value.let { v -> if (v is String) v.toBigDecimal() else v as BigDecimal } } - ?: emptyMap() - val max = - maxViolations?.mapValues { it.value.let { v -> if (v is String) v.toBigDecimal() else v as BigDecimal } } - ?: emptyMap() + val min = minViolations + ?.mapValues { it.value.let { v -> if (v is String) v.toBigDecimal() else v as BigDecimal } } + //make stable order for entity names + ?.let { TreeMap(it) } + ?: emptyMap() + + + val max = maxViolations + ?.mapValues { it.value.let { v -> if (v is String) v.toBigDecimal() else v as BigDecimal } } + //make stable order for entity names + ?.let { TreeMap(it) } + ?: emptyMap() bounds[id.toInt()] = BoundViolation(min, max) } @@ -244,53 +251,64 @@ private fun raiseViolations(result: ViolationsResult, rules: Iterable val rule = rulesMap[ruleId] ?: throw Exception("Error occurred while parsing verification error: unmapped rule with ID $ruleId") - val ruleName = if (rule.name != null) " '${rule.name}' " else " " + val ruleName = if (rule.name != null) " '${rule.name}'" else "" - val boundsMap = rule.bounds.associateBy { b -> b.id } + messageBuilder.appendLine("Rule${ruleName} violated:") - val boundMessages = rv.boundViolations.mapNotNull { (boundId, v) -> + val boundsMap = rule.bounds.associateBy { b -> b.id } + rv.boundViolations.forEach { (boundId, v) -> val bound = boundsMap[boundId] ?: throw Exception("Error occurred while parsing verification error: unmapped bound with ID $boundId") - val minViolation = v.min["all"] - val maxViolation = v.max["all"] - - if (minViolation != null) { - "${bound.readableValueType} is ${minViolation.fromRateIfNeeded(bound)}, but expected minimum is ${bound.minValue}" - } else if (maxViolation != null) { - "${bound.readableValueType} is ${maxViolation.fromRateIfNeeded(bound)}, but expected maximum is ${bound.maxValue}" - } else { - null - } - } - if (boundMessages.size > 1) { - messageBuilder.append( - "Rule${ruleName}violated:" + boundMessages.joinToString("\n ", "\n ") - ) - } else { - messageBuilder.append("Rule${ruleName}violated: ${boundMessages[0]}") + v.min.forEach { (name, value) -> + messageBuilder.appendLine(bound.formatViolation(value, rule.target, name, false)) + } + v.max.forEach { (name, value) -> + messageBuilder.appendLine(bound.formatViolation(value, rule.target, name, true)) + } } } return messageBuilder.toString() } -private fun BigDecimal.fromRateIfNeeded(bound: ReportVerificationBound): BigDecimal { - return if (bound.valueType == COVERED_PERCENTAGE || bound.valueType == MISSED_PERCENTAGE) { - this.multiply(ONE_HUNDRED) - } else { - this +private fun ReportVerificationBound.formatViolation( + value: BigDecimal, + entityType: VerificationTarget, + entityName: String, + isMax: Boolean +): String { + val directionText = if (isMax) "maximum" else "minimum" + + val metricText = when (metric) { + CounterType.LINE -> "lines" + CounterType.INSTRUCTION -> "instructions" + CounterType.BRANCH -> "branches" + } + + val valueTypeText = when (valueType) { + COVERED_COUNT -> "covered count" + MISSED_COUNT -> "missed count" + COVERED_PERCENTAGE -> "covered percentage" + MISSED_PERCENTAGE -> "missed percentage" + } + + val entityText = when (entityType) { + VerificationTarget.ALL -> "" + VerificationTarget.CLASS -> " for class '$entityName'" + VerificationTarget.PACKAGE -> " for package '$entityName'" } -} -private val ReportVerificationBound.readableValueType: String - get() = when (valueType) { - COVERED_COUNT -> "covered lines count" - MISSED_COUNT -> "missed lines count" - COVERED_PERCENTAGE -> "covered lines percentage" - MISSED_PERCENTAGE -> "missed lines percentage" + val actual = if (valueType == COVERED_PERCENTAGE || valueType == MISSED_PERCENTAGE) { + value.multiply(ONE_HUNDRED) + } else { + value } + val expected = if (isMax) maxValue else minValue + + return " $metricText $valueTypeText$entityText is $actual, but expected $directionText is $expected" +} private data class ViolationsResult(val ruleViolations: Map) diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt index 655f339a..86aed395 100644 --- a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt @@ -43,9 +43,20 @@ internal fun Task.jacocoVerification( callJacocoAntReportTask(projectFiles, classpath) { invokeWithBody("check", mapOf("failonviolation" to "false", "violationsproperty" to "jacocoErrors")) { rules.forEach { - invokeWithBody("rule", mapOf("element" to "BUNDLE")) { + val entityType = when(it.target) { + VerificationTarget.ALL -> "BUNDLE" + VerificationTarget.CLASS -> "CLASS" + VerificationTarget.PACKAGE -> "PACKAGE" + } + invokeWithBody("rule", mapOf("element" to entityType)) { it.bounds.forEach { b -> - val limitArgs = mutableMapOf("counter" to "LINE") + val limitArgs = mutableMapOf() + limitArgs["counter"] = when(b.metric) { + CounterType.LINE -> "LINE" + CounterType.INSTRUCTION -> "INSTRUCTION" + CounterType.BRANCH -> "BRANCH" + } + var min: BigDecimal? = b.minValue var max: BigDecimal? = b.maxValue when (b.valueType) { @@ -81,7 +92,7 @@ internal fun Task.jacocoVerification( } } - return ant.violations + return ant.violations?.orderViolations() } private fun Task.callJacocoAntReportTask( @@ -134,6 +145,12 @@ private val GroovyObject.violations: String? return properties["jacocoErrors"] as String? } +private fun String.orderViolations(): String { + val treeSet = TreeSet() + this.lineSequence().forEach { treeSet += it } + return treeSet.joinToString("\n") +} + @Suppress("UNUSED_PARAMETER") private inline fun GroovyObject.invokeWithBody( name: String,