Skip to content

Commit

Permalink
Fixed JaCoCo error: Can't add different class with same name
Browse files Browse the repository at this point in the history
Now duplicate classes are added to the JaCoCo report only once - this behavior copies the behavior from Kover report generator

Resolves #634
Resolves #613
  • Loading branch information
shanshin committed Jun 14, 2024
1 parent f8d3b5e commit cf0ce8a
Show file tree
Hide file tree
Showing 15 changed files with 305 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.test.functional.cases

import kotlinx.kover.gradle.plugin.test.functional.framework.checker.CheckerContext
import kotlinx.kover.gradle.plugin.test.functional.framework.starter.TemplateTest

internal class ClassDupTest {
/**
* Checking that if there are duplicate files in different versions, the JaCoCo report still continues to be generated.
* This behavior is identical to the behavior in Kover reporter.
*/
@TemplateTest("android-class-dup", [":app:koverXmlReport"])
fun CheckerContext.test() {
subproject(":app") {
xmlReport {
// class present in report
classCounter("kotlinx.kover.test.android.DupClass").assertFullyMissed()
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

plugins {
id ("org.jetbrains.kotlinx.kover")
id ("com.android.application")
id ("org.jetbrains.kotlin.android")
}

android {
namespace = "kotlinx.kover.test.android"
compileSdk = 32

defaultConfig {
applicationId = "kotlinx.kover.test.android"
minSdk = 21
targetSdk = 31
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = true
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
viewBinding = true
}
}

dependencies {
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.appcompat:appcompat:1.5.0")
implementation("com.google.android.material:material:1.6.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation("junit:junit:4.13.2")
}


/*
* Kover configs
*/

kover {
useJacoco()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.kover.test.android

object DupClass {
fun log(message: String) {
println("DEBUG: $message")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application android:label="@string/app_name">
<uses-library android:name="com.google.android.things" android:required="false" />

<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Make this the first activity that is displayed when the device boots. -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
-->

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/main_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="104dp"
android:layout_marginTop="28dp"
android:text="SERIALIZATION TEST"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<EditText
android:id="@+id/encoded_text"
android:layout_width="373dp"
android:layout_height="411dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:editable="false"
android:ems="10"
android:gravity="start|top"
android:inputType="textMultiLine"
android:textAlignment="viewStart"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_label" />

</androidx.constraintlayout.widget.ConstraintLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
-->

<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!--
~ Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
-->

<resources>
<string name="app_name">Android Test</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!--
~ Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
-->

<resources>

<style name="Theme.App" parent="android:Theme.Material.Light.DarkActionBar" />
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.kover.test.android

object DupClass {
fun log(message: String) {
println("DEBUG: $message")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.kover.test.android

import org.junit.Test

import org.junit.Assert.*


class LocalTests {
@Test
fun testDebugUtils() {
println("test")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

plugins {
id("com.android.application") version "7.4.0" apply false
id("com.android.library") version "7.4.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.20" apply false
id("org.jetbrains.kotlinx.kover") version "0.7.1" apply false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
#

# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

rootProject.name = "android_kts"
include(":app")
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package kotlinx.kover.gradle.plugin.tools.jacoco

import groovy.lang.Closure
import groovy.lang.GroovyObject
import kotlinx.kover.features.jvm.KoverFeatures
import kotlinx.kover.features.jvm.KoverFeatures.koverWildcardToRegex
import kotlinx.kover.gradle.plugin.commons.ReportContext
import java.io.File

Expand All @@ -25,24 +25,27 @@ internal fun ReportContext.callAntReport(
)
)


val filteredOutput = if (filters.excludesClasses.isNotEmpty() || filters.includesClasses.isNotEmpty()) {
val excludeRegexes = filters.excludesClasses.map { Regex(it.wildcardsToClassFileRegex()) }
val includeRegexes = filters.includesClasses.map { Regex(it.wildcardsToClassFileRegex()) }

val outputCollections = files.outputs.map { output ->
services.objects.fileCollection().from(output).asFileTree.filter { file ->
// the `canonicalPath` is used because a `File.separatorChar` was used to construct the class-file regex
val path = file.toRelativeString(output)
// if the inclusion rules are declared, then the file must fit at least one of them
(includeRegexes.isEmpty() || includeRegexes.any { regex -> path.matches(regex) })
// if the exclusion rules are declared, then the file should not fit any of them
&& excludeRegexes.none { regex -> path.matches(regex) }
val filesByClassName = mutableMapOf<String, File>()
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
}
}
services.objects.fileCollection().from(outputCollections)
}

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 {
services.objects.fileCollection().from(files.outputs)
filesByClassName.values
}


Expand All @@ -55,7 +58,7 @@ internal fun ReportContext.callAntReport(
services.objects.fileCollection().from(files.sources).addToAntBuilder(this, "resources")
}
invokeWithBody("classfiles") {
filteredOutput.addToAntBuilder(this, "resources")
services.objects.fileCollection().from(classes).addToAntBuilder(this, "resources")
}
}
block()
Expand Down Expand Up @@ -84,10 +87,13 @@ internal inline fun GroovyObject.invokeWithBody(
}

/**
* Replaces characters `.` to `|` or `\` and added `.class` as postfix and `.* /` or `.*\` as prefix.
* Replaces characters `|` or `\` to `.` and remove postfix `.class`.
*/
private fun String.wildcardsToClassFileRegex(): String {
val filenameWithWildcards = this.replace('.', File.separatorChar) + ".class"
return KoverFeatures.koverWildcardToRegex(filenameWithWildcards)
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"

0 comments on commit cf0ce8a

Please sign in to comment.