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

kotlinx.kover.maven.plugin.mojo.abstracts.AbstractCoverageTaskMojo.kt Maven / Gradle / Ivy

/*
 * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

package kotlinx.kover.maven.plugin.mojo.abstracts

import kotlinx.kover.features.jvm.ClassFilters
import kotlinx.kover.features.jvm.KoverFeatures
import kotlinx.kover.maven.plugin.Constants
import kotlinx.kover.maven.plugin.MavenReportFilters
import org.apache.maven.artifact.versioning.DefaultArtifactVersion
import org.apache.maven.artifact.versioning.VersionRange
import org.apache.maven.plugins.annotations.Parameter
import org.apache.maven.project.MavenProject
import org.codehaus.plexus.util.xml.Xpp3Dom
import java.io.File
import java.nio.file.Files
import kotlin.io.path.Path

/**
 * Common class using for processing project coverage.
 */
abstract class AbstractCoverageTaskMojo : AbstractKoverMojo() {
    /**
     * Filters to limit the code that gets into the report.
     */
    @Parameter
    private var filters: MavenReportFilters? = null

    /**
     * Flag to use code coverage from dependencies.
     *
     * `false` by default.
     */
    @Parameter(property = "kover.aggregate", defaultValue = "false")
    protected var aggregate: Boolean = false

    /**
     * Binary reports that built in advance, before the start of the project build.
     *
     * It's convenient to use when tests are performed on CI on different nodes.
     */
    @Parameter
    private val additionalBinaryReports: MutableList = mutableListOf()

    /**
     * Project modules involved in the build.
     */
    @Parameter(property = "reactorProjects", readonly = true)
    private lateinit var reactorProjects: List

    protected abstract fun processCoverage(
        binaryReports: List,
        outputDirs: List,
        sourceDirs: List,
        filters: ClassFilters
    )


    final override fun doExecute() {
        val merged = collectVariants()
        val allBinaryReports = merged.binaryReports + additionalBinaryReports
        processCoverage(allBinaryReports, merged.outputs, merged.sources, koverFilters())
    }

    /**
     * Create temporary dir on each call.
     */
    protected fun tempDirectory(): File {
        val commonTmpDir = File(project.build.directory).resolve(Constants.TMP_DIR_NAME)
        commonTmpDir.mkdirs()
        return Files.createTempDirectory(Path(commonTmpDir.path), "kover")
            .toFile()
    }

    /**
     * Get filters as Kover filters classes.
     */
    private fun koverFilters(): ClassFilters {
        return filters?.let { nnFilters ->
            ClassFilters(
                nnFilters.includes.classes.toSet(),
                nnFilters.excludes.classes.toSet(),
                nnFilters.includes.annotatedBy.toSet(),
                nnFilters.excludes.annotatedBy.toSet(),
                nnFilters.includes.inheritedFrom.toSet(),
                nnFilters.excludes.inheritedFrom.toSet()
            )
        } ?: ClassFilters(emptySet(), emptySet(), emptySet(), emptySet(), emptySet(), emptySet())
    }

    /**
     * Collect directory and file information from current project and all its dependencies.
     */
    private fun collectVariants(): KoverArtifact {
        val currentArtifact = project.extractArtifact(false)

        val artifacts = if (aggregate) {
            val dependencyArtifacts =
                project.getDependencyProjects().map { pair -> pair.first.extractArtifact(pair.second) }
            dependencyArtifacts + currentArtifact
        } else {
            listOf(currentArtifact)
        }

        val binaryReports = mutableSetOf()
        val sources = mutableSetOf()
        val outputs = mutableSetOf()

        artifacts
            .map { it.filterProjectSources() }
            .forEach { artifact ->
                binaryReports += artifact.binaryReports.filter { it.exists() }
                sources += artifact.sources.filter { it.exists() && it.isDirectory }
                outputs += artifact.outputs.filter { it.exists() && it.isDirectory }
            }

        return KoverArtifact("", binaryReports.toList(), sources.toList(), outputs.toList())
    }

    /**
     * Filters projects sources according project filters.
     */
    private fun KoverArtifact.filterProjectSources(): KoverArtifact {
        val nnFilters = filters ?: return this

        if (nnFilters.includes.projects.isNotEmpty()) {
            val notIncluded = nnFilters.includes.projects.none { filter ->
                KoverFeatures.koverWildcardToRegex(filter).toRegex().matches(projectName)
            }
            if (notIncluded) {
                return KoverArtifact(projectName, binaryReports, emptyList(), emptyList())
            }
        }
        if (nnFilters.excludes.projects.isNotEmpty()) {
            val excluded = nnFilters.excludes.projects.any { filter ->
                KoverFeatures.koverWildcardToRegex(filter).toRegex().matches(projectName)
            }
            if (excluded) {
                return KoverArtifact(projectName, binaryReports, emptyList(), emptyList())
            }
        }
        return this
    }


    /**
     * Find all dependencies of current project, which are modules.
     *
     * If there are several modules in the project that pass the dependency version condition, then only the very first one will be taken.
     *
     * @return list of pairs: dependency project and sign that it was a test dependency.
     */
    private fun MavenProject.getDependencyProjects(): List> {
        return dependencies
            .filter { it.scope in Constants.DEPENDENCY_SCOPES }
            .mapNotNull { dependency ->
                val versionRange = VersionRange.createFromVersionSpec(dependency.version)

                // take first project, satisfying the condition in dependency
                val projectDependency = reactorProjects.firstOrNull { fromReactor ->
                    fromReactor.groupId == dependency.groupId
                            && fromReactor.artifactId == dependency.artifactId
                            && versionRange.containsVersion(DefaultArtifactVersion(fromReactor.version))
                }

                if (projectDependency != null) {
                    projectDependency to (dependency.scope == Constants.TEST_SCOPE)
                } else {
                    null
                }
            }
    }

    /**
     * Find all binary reports, source directory roots and output directory roots in given project.
     */
    private fun MavenProject.extractArtifact(testProject: Boolean): KoverArtifact {
        // binary report name is fixed
        val binaryReports = listOf(File(build.directory).resolve(Constants.BIN_REPORT_PATH))

        val sources: List
        val outputs: List

        if (testProject) {
            // skip classes of test dependency
            sources = emptyList()
            outputs = emptyList()
        } else {
            sources = findSourceDirs()
            outputs = listOf(File(build.outputDirectory))
        }

        return KoverArtifact(name, binaryReports, sources, outputs)
    }

    /**
     * Collect all Kotlin source roots.
     */
    private fun MavenProject.findSourceDirs(): List {
        // common source roots
        val standardDirs = compileSourceRoots.map { sourcePath -> toAbsoluteFile(sourcePath) }

        val kotlinPlugin =
            project.buildPlugins.firstOrNull { it.groupId == "org.jetbrains.kotlin" && it.artifactId == "kotlin-maven-plugin" }
                ?: return standardDirs

        /*
        Processing Kotlin plugin configuration:
                     
                        compile-kotlin
                        compile
                        
                            compile
                        
                        
                            
                                some/dir
                            
                        
                    
         */
        var kotlinDirs: List? = null
        try {
            kotlinDirs = kotlinPlugin.executions
                .filter { "compile" in it.goals }
                .filter { execution -> execution.configuration != null && execution.configuration is Xpp3Dom }
                .flatMap { execution ->
                    val config = execution.configuration as Xpp3Dom
                    val sourceDirs = config.getChild("sourceDirs") ?: return@flatMap emptyList()
                    sourceDirs.children.map { toAbsoluteFile(it.value) }
                }
        } catch (e: Exception) {
            // in future versions configuration may be changed
            log.warn("Error when trying to read the Kotlin configuration", e)
        }

        return if (kotlinDirs == null) standardDirs else (standardDirs + kotlinDirs)
    }

    private fun MavenProject.toAbsoluteFile(path: String): File {
        val file = File(path)
        return if (file.isAbsolute) {
            file
        } else {
            basedir.resolve(path)
        }
    }

    /**
     * Information about project's files and directories, used to process coverage.
     */
    class KoverArtifact(
        val projectName: String,
        val binaryReports: List,
        val sources: List,
        val outputs: List
    )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy