All Downloads are FREE. Search and download functionalities are using the official Maven repository.

kotlinx.kover.gradle.plugin.tools.jacoco.Verification.kt Maven / Gradle / Ivy

There is a newer version: 0.9.0-RC
Show newest version
/*
 * 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.*
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.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

typealias JacocoRule = org.jacoco.report.check.Rule

internal fun ReportContext.doJacocoVerify(rules: List, output: File) {
    val workQueue: WorkQueue = services.workerExecutor.classLoaderIsolation {
        classpath.from([email protected])
    }

    workQueue.submit(JacocoVerifyAction::class.java) {
        outputFile.set(output)
        rulesProperty.convention(rules)

        fillCommonParameters(this@doJacocoVerify)
    }

}

internal interface VerifyParameters: AbstractVerifyParameters {
    val outputFile: RegularFileProperty
}

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 {

    override fun execute() {
        val rulesPairs = parameters.rulesProperty.get().toJacoco()

        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 entityName = match.groupValues[2].run { if (this == ":") null else this }
        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 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
    "branches" -> CoverageUnit.BRANCH
    else -> throw KoverCriticalException("Unknown JaCoCo metric type '$this' in verification error:\n$line")
}

private fun String.asAggType(line: String): AggregationType = when (this) {
    "covered ratio" -> AggregationType.COVERED_PERCENTAGE
    "missed ratio" -> AggregationType.MISSED_PERCENTAGE
    "covered count" -> AggregationType.COVERED_COUNT
    "missed count" -> AggregationType.MISSED_COUNT
    else -> throw KoverCriticalException("Unknown JaCoCo aggregation type '$this' in verification error:\n$line")
}

private fun String.asIsMax(line: String): Boolean = when (this) {
    "minimum" -> false
    "maximum" -> true
    else -> throw KoverCriticalException("Unknown JaCoCo direction '$this' in verification error:\n$line")
}

private fun String.asValue(line: String, aggregationType: AggregationType): BigDecimal {
    val value = toBigDecimalOrNull()
        ?: throw KoverCriticalException("Illegal JaCoCo metric value '$this' in verification error:\n$line")

    return if (aggregationType.isPercentage) {
        (value * ONE_HUNDRED).stripTrailingZeros()
    } else {
        value
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy