├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── diff-coverage ├── src │ ├── funcTest │ │ ├── resources │ │ │ ├── multi-module-test-project │ │ │ │ ├── module1 │ │ │ │ │ ├── build.gradle │ │ │ │ │ └── src │ │ │ │ │ │ ├── test │ │ │ │ │ │ └── java │ │ │ │ │ │ │ └── com │ │ │ │ │ │ │ └── module1 │ │ │ │ │ │ │ └── Class1Test.java │ │ │ │ │ │ └── main │ │ │ │ │ │ └── java │ │ │ │ │ │ └── com │ │ │ │ │ │ └── module1 │ │ │ │ │ │ └── Class1.java │ │ │ │ ├── settings.gradle │ │ │ │ ├── module2 │ │ │ │ │ └── src │ │ │ │ │ │ ├── test │ │ │ │ │ │ └── java │ │ │ │ │ │ │ └── com │ │ │ │ │ │ │ └── module2 │ │ │ │ │ │ │ └── Class2Test.java │ │ │ │ │ │ └── main │ │ │ │ │ │ └── java │ │ │ │ │ │ └── com │ │ │ │ │ │ └── module2 │ │ │ │ │ │ └── Class2.java │ │ │ │ ├── build.gradle │ │ │ │ └── test.diff │ │ │ ├── single-module-test-project │ │ │ │ ├── src │ │ │ │ │ ├── main │ │ │ │ │ │ └── java │ │ │ │ │ │ │ └── com │ │ │ │ │ │ │ └── java │ │ │ │ │ │ │ └── test │ │ │ │ │ │ │ ├── UnchagedClass.java │ │ │ │ │ │ │ └── Class1.java │ │ │ │ │ └── test │ │ │ │ │ │ └── java │ │ │ │ │ │ └── com │ │ │ │ │ │ └── java │ │ │ │ │ │ └── test │ │ │ │ │ │ └── Class1Test.java │ │ │ │ ├── build.gradle │ │ │ │ └── test.diff.file │ │ │ └── git-diff-source-test-files │ │ │ │ └── Class1GitTest.java │ │ └── kotlin │ │ │ └── com │ │ │ └── form │ │ │ └── coverage │ │ │ └── gradle │ │ │ ├── MockHttpServer.kt │ │ │ ├── TestingFiles.kt │ │ │ ├── BaseDiffCoverageTest.kt │ │ │ ├── DiffCoverageTestUtils.kt │ │ │ ├── DiffCoverageMultiModuleTest.kt │ │ │ └── DiffCoverageSingleModuleTest.kt │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── form │ │ │ └── coverage │ │ │ └── gradle │ │ │ ├── DiffCoveragePlugin.kt │ │ │ ├── ChangesetCoverageConfiguration.kt │ │ │ ├── DiffTaskAutoConfiguration.kt │ │ │ └── DiffCoverageTask.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── form │ │ └── coverage │ │ └── gradle │ │ ├── ViolationRulesTest.kt │ │ └── DiffCoverageTaskTest.kt └── build.gradle ├── jacoco-filtering-extension ├── src │ ├── main │ │ ├── kotlin │ │ │ ├── com │ │ │ │ └── form │ │ │ │ │ └── coverage │ │ │ │ │ ├── diff │ │ │ │ │ ├── git │ │ │ │ │ │ ├── CrlfStrategy.kt │ │ │ │ │ │ └── JgitDiff.kt │ │ │ │ │ ├── parse │ │ │ │ │ │ ├── ClassFile.kt │ │ │ │ │ │ └── ModifiedLinesDiffParser.kt │ │ │ │ │ ├── CodeUpdateInfo.kt │ │ │ │ │ └── DiffSource.kt │ │ │ │ │ ├── http │ │ │ │ │ └── HttpClient.kt │ │ │ │ │ ├── report │ │ │ │ │ ├── Reports.kt │ │ │ │ │ ├── analyzable │ │ │ │ │ │ ├── AnalyzableReport.kt │ │ │ │ │ │ ├── FullCoverageAnalyzableReport.kt │ │ │ │ │ │ └── DiffCoverageAnalyzableReport.kt │ │ │ │ │ ├── ReportsFactory.kt │ │ │ │ │ └── ReportGenerator.kt │ │ │ │ │ ├── config │ │ │ │ │ └── DiffCoverageConfig.kt │ │ │ │ │ └── filters │ │ │ │ │ └── ModifiedLinesFilter.kt │ │ │ └── org │ │ │ │ └── jacoco │ │ │ │ └── core │ │ │ │ └── internal │ │ │ │ └── analysis │ │ │ │ ├── FilteringClassAnalyzer.kt │ │ │ │ └── FilteringAnalyzer.kt │ │ └── resources │ │ │ └── log4j2.xml │ └── test │ │ ├── kotlin │ │ └── com │ │ │ └── form │ │ │ └── coverage │ │ │ ├── diff │ │ │ ├── git │ │ │ │ ├── CrlfStrategyTest.kt │ │ │ │ └── JgitDiffTest.kt │ │ │ ├── parse │ │ │ │ └── ClassFileTest.kt │ │ │ ├── UrlDiffSourceTest.kt │ │ │ ├── FileDiffSourceTest.kt │ │ │ ├── DiffSourceFactoryTest.kt │ │ │ ├── CodeUpdateInfoTest.kt │ │ │ └── ModifiedLinesDiffParserTest.kt │ │ │ └── filters │ │ │ └── ModifiedLinesFilterTest.kt │ │ └── resources │ │ └── testintPatch1.patch └── build.gradle ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── ci.yml ├── LICENSE ├── gradlew.bat ├── CHANGELOG.md ├── README.md ├── gradlew └── config └── detekt └── detekt.yml /gradle.properties: -------------------------------------------------------------------------------- 1 | version=0.9.5 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/form-com/diff-coverage-gradle/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'diff-coverage-gradle-plugin' 2 | include 'jacoco-filtering-extension' 3 | include 'diff-coverage' 4 | 5 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/multi-module-test-project/module1/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(":module2") 3 | } 4 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/multi-module-test-project/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'multi-module-test-project' 2 | include 'module1' 3 | include 'module2' 4 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/single-module-test-project/src/main/java/com/java/test/UnchagedClass.java: -------------------------------------------------------------------------------- 1 | package com.java.test; 2 | public class UnchagedClass { 3 | public void method() { 4 | System.out.println(1); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/single-module-test-project/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.form.diff-coverage' 3 | id 'java' 4 | id 'jacoco' 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | testImplementation 'junit:junit:4.12' 13 | } 14 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/multi-module-test-project/module2/src/test/java/com/module2/Class2Test.java: -------------------------------------------------------------------------------- 1 | package com.module2; 2 | 3 | import org.junit.Test; 4 | 5 | public class Class2Test { 6 | @Test 7 | public void printMin() { 8 | new Class2().min(1, 2); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/multi-module-test-project/module1/src/test/java/com/module1/Class1Test.java: -------------------------------------------------------------------------------- 1 | package com.module1; 2 | 3 | import org.junit.Test; 4 | 5 | public class Class1Test { 6 | @Test 7 | public void printMax() { 8 | new Class1().printMax(1, 2); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/multi-module-test-project/module1/src/main/java/com/module1/Class1.java: -------------------------------------------------------------------------------- 1 | package com.module1; 2 | 3 | import com.module2.Class2; 4 | 5 | public class Class1 { 6 | public void printMax(int a, int b) { 7 | System.out.println(new Class2().max(a, b)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/diff/git/CrlfStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff.git 2 | 3 | import org.eclipse.jgit.lib.CoreConfig 4 | 5 | fun getCrlf(lineSeparator: String = System.lineSeparator()): CoreConfig.AutoCRLF { 6 | return if (lineSeparator == "\r\n") { 7 | CoreConfig.AutoCRLF.TRUE 8 | } else { 9 | CoreConfig.AutoCRLF.INPUT 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/git-diff-source-test-files/Class1GitTest.java: -------------------------------------------------------------------------------- 1 | package com.java.test; 2 | 3 | public class Class1 { 4 | 5 | public int covered(boolean arg) { 6 | return 0; 7 | } 8 | 9 | public int partialCovered(boolean arg) { 10 | int result = 0; 11 | return result; 12 | } 13 | 14 | public int notCovered(boolean arg) { 15 | return 0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/multi-module-test-project/module2/src/main/java/com/module2/Class2.java: -------------------------------------------------------------------------------- 1 | package com.module2; 2 | 3 | public class Class2 { 4 | 5 | public int max( int a, int b ) { 6 | return a > b ? a : b; 7 | } 8 | 9 | public int min( int a, int b ) { 10 | if (a < b) { 11 | return a; 12 | } else { 13 | return b; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/http/HttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.http 2 | 3 | import org.apache.hc.client5.http.classic.methods.HttpGet 4 | import org.apache.hc.client5.http.impl.classic.HttpClientBuilder 5 | 6 | fun executeGetRequest(url: String): String { 7 | return HttpClientBuilder.create().build().use { httpClient -> 8 | val httpResponse = httpClient.execute(HttpGet(url)) 9 | 10 | httpResponse.entity.content.reader().readText() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/multi-module-test-project/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'jacoco' 4 | id 'com.form.diff-coverage' 5 | } 6 | 7 | group 'org.example' 8 | version '1.0-SNAPSHOT' 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | subprojects { 15 | apply plugin: 'java' 16 | apply plugin: 'jacoco' 17 | 18 | sourceCompatibility = 1.8 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | testImplementation 'junit:junit:4.13.2' 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | *.diff 5 | *.patch 6 | 7 | # Log file 8 | *.log 9 | 10 | # BlueJ files 11 | *.ctxt 12 | 13 | # Mobile Tools for Java (J2ME) 14 | .mtj.tmp/ 15 | 16 | # Package Files # 17 | *.jar 18 | *.war 19 | *.ear 20 | *.zip 21 | *.tar.gz 22 | *.rar 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | 27 | .gradle/ 28 | build/ 29 | out/ 30 | .idea/ 31 | .kotlintest 32 | *.iml 33 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 34 | !gradle-wrapper.jar 35 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.form.coverage' 2 | 3 | ext { 4 | jacoco_version = '0.8.8' 5 | } 6 | 7 | publishing { 8 | publications { 9 | jacocoFilteringExtension(MavenPublication){ 10 | from components.java 11 | } 12 | } 13 | } 14 | 15 | dependencies { 16 | implementation 'org.eclipse.jgit:org.eclipse.jgit:5.12.0.202106070339-r' 17 | implementation "org.jacoco:org.jacoco.core:$jacoco_version" 18 | implementation "org.jacoco:org.jacoco.report:$jacoco_version" 19 | implementation 'org.apache.httpcomponents.client5:httpclient5:5.1.3' 20 | } 21 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/single-module-test-project/src/main/java/com/java/test/Class1.java: -------------------------------------------------------------------------------- 1 | package com.java.test; 2 | 3 | public class Class1 { 4 | 5 | public int covered(boolean arg) { 6 | if(arg) { 7 | return 1; 8 | } 9 | return 0; 10 | } 11 | 12 | public int partialCovered(boolean arg) { 13 | int result; 14 | if (arg) { 15 | result = 1; 16 | } else { 17 | result = 0; 18 | } 19 | return result; 20 | } 21 | 22 | public int notCovered(boolean arg) { 23 | return arg ? 1 : 0; 24 | } 25 | } -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/kotlin/com/form/coverage/diff/git/CrlfStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff.git 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | import org.eclipse.jgit.lib.CoreConfig 6 | 7 | class CrlfStrategyTest : StringSpec({ 8 | 9 | "crlf should be `Auto` when line separator is \\r\\n" { 10 | val crlf = getCrlf("\r\n") 11 | crlf shouldBe CoreConfig.AutoCRLF.TRUE 12 | } 13 | 14 | "crlf should be `Input` when line separator is \\n" { 15 | val crlf = getCrlf("\n") 16 | crlf shouldBe CoreConfig.AutoCRLF.INPUT 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /diff-coverage/src/main/kotlin/com/form/coverage/gradle/DiffCoveragePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | 6 | class DiffCoveragePlugin : Plugin { 7 | 8 | override fun apply(project: Project) { 9 | val extension = project.extensions.create(DIFF_COV_EXTENSION, ChangesetCoverageConfiguration::class.java) 10 | 11 | project.tasks.create(DIFF_COV_TASK, DiffCoverageTask::class.java) { 12 | it.diffCoverageReport = extension 13 | } 14 | } 15 | 16 | companion object { 17 | const val DIFF_COV_EXTENSION = "diffCoverageReport" 18 | const val DIFF_COV_TASK = "diffCoverage" 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | diff-coverage 5 | %d{ISO8601} %-5p [%c] %m\n 6 | 7 | 8 | 9 | 10 | ${log-pattern} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/diff/parse/ClassFile.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff.parse 2 | 3 | import java.nio.file.Path 4 | import java.nio.file.Paths 5 | 6 | class ClassFile( 7 | private val sourceFileName: String, 8 | private val className: String 9 | ) { 10 | val path: String 11 | get() = Paths.get(className).let { 12 | if (it.parent == null) { 13 | sourceFileName 14 | } else { 15 | it.parent.resolveWithNormalize(sourceFileName) 16 | } 17 | } 18 | 19 | private fun Path.resolveWithNormalize(fileName: String): String { 20 | return resolve(fileName) 21 | .toString() 22 | .replace("\\", "/") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/single-module-test-project/src/test/java/com/java/test/Class1Test.java: -------------------------------------------------------------------------------- 1 | package com.java.test; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class Class1Test { 8 | 9 | private Class1 class1 = new Class1(); 10 | 11 | @Test 12 | public void coveredShouldReturn1() { 13 | int covered = class1.covered(true); 14 | assertEquals(1, covered); 15 | } 16 | 17 | @Test 18 | public void coveredShouldReturn0() { 19 | int covered = class1.covered(false); 20 | assertEquals(0, covered); 21 | } 22 | 23 | @Test 24 | public void partialCoveredShouldReturn1() { 25 | int covered = class1.partialCovered(true); 26 | assertEquals(1, covered); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /diff-coverage/src/test/kotlin/com/form/coverage/gradle/ViolationRulesTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.equality.shouldBeEqualToComparingFields 5 | 6 | class ViolationRulesTest : StringSpec({ 7 | 8 | "failOnCoverageLessThan should set all coverage values to a single value and set failOnViolation=true" { 9 | val expectedCoverage = 0.9 10 | val violationRules = ViolationRules().apply { 11 | failIfCoverageLessThan(expectedCoverage) 12 | } 13 | 14 | violationRules shouldBeEqualToComparingFields ViolationRules( 15 | expectedCoverage, 16 | expectedCoverage, 17 | expectedCoverage, 18 | true 19 | ) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/kotlin/com/form/coverage/diff/parse/ClassFileTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff.parse 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class ClassFileTest : StringSpec({ 7 | 8 | "ClassFile.path should return relative path when class has no package" { 9 | // setup 10 | val classFile = ClassFile("Class1.java", "Class1") 11 | 12 | // run // assert 13 | classFile.path shouldBe "Class1.java" 14 | } 15 | 16 | "ClassFile.path should return relative path when class has package" { 17 | // setup 18 | val classFile = ClassFile("Class1.java", "com/java/test/Class1") 19 | 20 | // run // assert 21 | classFile.path shouldBe "com/java/test/Class1.java" 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/kotlin/com/form/coverage/gradle/MockHttpServer.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import com.sun.net.httpserver.HttpServer 4 | import java.net.InetSocketAddress 5 | 6 | class MockHttpServer( 7 | port: Int, 8 | responseContent: String 9 | ): AutoCloseable { 10 | 11 | private val httpServer = HttpServer.create(InetSocketAddress(port), 0) 12 | private val response: ByteArray = responseContent.toByteArray() 13 | 14 | init { 15 | httpServer.createContext("/") { httpExchange -> 16 | httpExchange.sendResponseHeaders(200, response.size.toLong()) 17 | httpExchange.responseBody.use { 18 | it.write(response) 19 | } 20 | } 21 | httpServer.executor = null 22 | httpServer.start() 23 | } 24 | 25 | override fun close() { 26 | httpServer.stop(3) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: SurpSG 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Desktop (please complete the following information):** 14 | - OS: [e.g. Windows 10] 15 | - Gradle version: [e.g. 7.4.2] 16 | - Diff Coverage plugin version [e.g. 0.9.3] 17 | 18 | **To Reproduce** 19 | If possible, provide your configuration of the plugin, for example: 20 | ```groovy 21 | diffCoverageReport { 22 | diffSource.file = 'diff.patch' 23 | 24 | violationRules.failIfCoverageLessThan 0.9 25 | } 26 | ``` 27 | 28 | **Expected behavior** 29 | A clear and concise description of what you expected to happen. 30 | 31 | **Logs** 32 | If applicable, add stacktrace, Gradle output. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/kotlin/com/form/coverage/gradle/TestingFiles.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import java.io.File 4 | 5 | inline fun getResourceFile(filePath: String): File { 6 | return T::class.java.classLoader 7 | .getResource(filePath)!!.file 8 | .let(::File) 9 | } 10 | 11 | inline fun File.copyFileFromResources(fileFrom: String, fileTo: String): File { 12 | val target: File = resolve(fileTo).apply { 13 | parentFile.mkdirs() 14 | } 15 | return getResourceFile(fileFrom).copyTo(target, true) 16 | } 17 | 18 | inline fun File.copyDirFromResources( 19 | dirToCopy: String, 20 | destDir: String = dirToCopy 21 | ): File { 22 | val target = resolve(destDir) 23 | getResourceFile(dirToCopy).copyRecursively(target, true) 24 | return target 25 | } 26 | 27 | fun File.toUnixAbsolutePath(): String = absolutePath.replace("\\", "/") 28 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/report/Reports.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.report 2 | 3 | import com.form.coverage.diff.DiffSource 4 | import org.jacoco.report.check.Rule 5 | import java.nio.file.Path 6 | 7 | open class FullReport( 8 | private val baseReportDir: Path, 9 | val reports: Set 10 | ) { 11 | fun resolveReportAbsolutePath(report: Report): Path { 12 | return baseReportDir.resolve(report.reportDirName) 13 | } 14 | } 15 | 16 | class DiffReport( 17 | baseReportDir: Path, 18 | reports: Set, 19 | val diffSource: DiffSource, 20 | val violation: Violation 21 | ) : FullReport(baseReportDir, reports) 22 | 23 | data class Report( 24 | val reportType: ReportType, 25 | val reportDirName: String 26 | ) 27 | 28 | enum class ReportType { 29 | HTML, XML, CSV 30 | } 31 | 32 | data class Violation( 33 | val failOnViolation: Boolean, 34 | val violationRules: List 35 | ) 36 | 37 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/single-module-test-project/test.diff.file: -------------------------------------------------------------------------------- 1 | Index: src/main/java/com/java/test/Class1.java 2 | IDEA additional info: 3 | Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP 4 | <+>UTF-8 5 | =================================================================== 6 | --- src/main/java/com/java/test/Class1.java (date 1563021950000) 7 | +++ src/main/java/com/java/test/Class1.java (date 1563021990000) 8 | @@ -3,15 +3,23 @@ 9 | public class Class1 { 10 | 11 | public int covered(boolean arg) { 12 | + if(arg) { 13 | + return 1; 14 | + } 15 | return 0; 16 | } 17 | 18 | public int partialCovered(boolean arg) { 19 | - int result = 0; 20 | + int result; 21 | + if (arg) { 22 | + result = 1; 23 | + } else { 24 | + result = 0; 25 | + } 26 | return result; 27 | } 28 | 29 | public int notCovered(boolean arg) { 30 | - return 0; 31 | + return arg ? 1 : 0; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/kotlin/com/form/coverage/diff/UrlDiffSourceTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.StringSpec 5 | import java.net.UnknownHostException 6 | 7 | class UrlDiffSourceTest : StringSpec() { 8 | 9 | init { 10 | "pullDiff should throw when url is invalid" { 11 | // setup 12 | val urlDiffSource = UrlDiffSource("invalid url format") 13 | 14 | // run 15 | // assert 16 | shouldThrow { 17 | urlDiffSource.pullDiff() 18 | } 19 | } 20 | 21 | "pullDiff should throw when url doesn't exist" { 22 | // setup 23 | val urlDiffSource = UrlDiffSource("http://1.html") 24 | 25 | // run 26 | // assert 27 | shouldThrow { 28 | urlDiffSource.pullDiff() 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/diff/CodeUpdateInfo.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("CodeUpdateInfo") 2 | package com.form.coverage.diff 3 | 4 | import com.form.coverage.diff.parse.ClassFile 5 | 6 | class CodeUpdateInfo( 7 | private val fileNameToModifiedLineNumbers: Map> 8 | ) { 9 | 10 | fun getClassModifications(classFile: ClassFile): ClassModifications { 11 | return ClassModifications( 12 | getModInfoByClass(classFile) 13 | ) 14 | } 15 | 16 | fun isInfoExists(classFile: ClassFile): Boolean { 17 | return getModInfoByClass(classFile).isNotEmpty() 18 | } 19 | 20 | private fun getModInfoByClass(classFile: ClassFile): Set { 21 | return fileNameToModifiedLineNumbers.asSequence() 22 | .filter { it.key.endsWith(classFile.path) } 23 | .map { it.value } 24 | .firstOrNull() ?: emptySet() 25 | } 26 | } 27 | 28 | class ClassModifications(private val modifiedLines: Set) { 29 | fun isLineModified(lineNumber: Int): Boolean = modifiedLines.contains(lineNumber) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Form.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/config/DiffCoverageConfig.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.config 2 | 3 | import java.io.File 4 | 5 | data class DiffSourceConfig( 6 | val file: String = "", 7 | val url: String = "", 8 | val diffBase: String = "" 9 | ) 10 | 11 | data class ViolationRuleConfig( 12 | val minLines: Double = 0.0, 13 | val minBranches: Double = 0.0, 14 | val minInstructions: Double = 0.0, 15 | val failOnViolation: Boolean = false 16 | ) 17 | 18 | data class ReportsConfig( 19 | val html: ReportConfig, 20 | val xml: ReportConfig, 21 | val csv: ReportConfig, 22 | val baseReportDir: String = "", 23 | val fullCoverageReport: Boolean = false 24 | ) 25 | 26 | data class ReportConfig( 27 | val enabled: Boolean, 28 | val outputFileName: String 29 | ) 30 | 31 | data class DiffCoverageConfig( 32 | val reportName: String, 33 | val diffSourceConfig: DiffSourceConfig, 34 | val reportsConfig: ReportsConfig, 35 | val violationRuleConfig: ViolationRuleConfig, 36 | val execFiles: Set, 37 | val classFiles: Set, 38 | val sourceFiles: Set 39 | ) 40 | 41 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/report/analyzable/AnalyzableReport.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.report.analyzable 2 | 3 | import com.form.coverage.config.DiffCoverageConfig 4 | import com.form.coverage.diff.DiffSource 5 | import com.form.coverage.report.DiffReport 6 | import com.form.coverage.report.reportFactory 7 | import org.jacoco.core.analysis.Analyzer 8 | import org.jacoco.core.analysis.ICoverageVisitor 9 | import org.jacoco.core.data.ExecutionDataStore 10 | import org.jacoco.report.IReportVisitor 11 | 12 | internal interface AnalyzableReport { 13 | 14 | fun buildVisitor(): IReportVisitor 15 | fun buildAnalyzer(executionDataStore: ExecutionDataStore, coverageVisitor: ICoverageVisitor): Analyzer 16 | } 17 | 18 | internal fun analyzableReportFactory( 19 | diffCoverageConfig: DiffCoverageConfig, 20 | diffSource: DiffSource 21 | ): Set { 22 | return reportFactory(diffCoverageConfig, diffSource) 23 | .map { reportMode -> 24 | when (reportMode) { 25 | is DiffReport -> DiffCoverageAnalyzableReport(reportMode) 26 | else -> FullCoverageAnalyzableReport(reportMode) 27 | } 28 | }.toSet() 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/kotlin/com/form/coverage/gradle/BaseDiffCoverageTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import org.gradle.testkit.runner.GradleRunner 4 | import org.junit.jupiter.api.io.TempDir 5 | import java.io.File 6 | 7 | abstract class BaseDiffCoverageTest { 8 | 9 | @TempDir 10 | lateinit var tempTestDir: File 11 | 12 | lateinit var rootProjectDir: File 13 | lateinit var buildFile: File 14 | lateinit var diffFilePath: String 15 | lateinit var gradleRunner: GradleRunner 16 | 17 | /** 18 | * should be invoked in @Before test class method 19 | */ 20 | fun initializeGradleTest() { 21 | val configuration: TestConfiguration = buildTestConfiguration() 22 | 23 | rootProjectDir = tempTestDir.copyDirFromResources(configuration.resourceTestProject) 24 | diffFilePath = rootProjectDir.resolve(configuration.diffFilePath).toUnixAbsolutePath() 25 | buildFile = rootProjectDir.resolve(configuration.rootBuildFilePath) 26 | 27 | gradleRunner = buildGradleRunner(rootProjectDir).apply { 28 | runTask("test") 29 | } 30 | } 31 | 32 | abstract fun buildTestConfiguration(): TestConfiguration 33 | } 34 | 35 | class TestConfiguration( 36 | val resourceTestProject: String, 37 | val rootBuildFilePath: String, 38 | val diffFilePath: String, 39 | ) 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: 10 | - 'master' 11 | pull_request: 12 | branches: 13 | - '*' 14 | 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | # This workflow contains a single job called "build" 18 | build: 19 | # The type of runner that the job will run on 20 | runs-on: ubuntu-latest 21 | 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 25 | - uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Set up JDK 1.8 30 | uses: actions/setup-java@v1 31 | with: 32 | java-version: 1.8 33 | 34 | - name: Grant execute permission for gradlew 35 | run: chmod +x gradlew 36 | 37 | - name: build & test 38 | run: ./gradlew check 39 | 40 | - name: run diff coverage check 41 | run: ./gradlew diffCoverage -PdiffBase=refs/remotes/origin/master 42 | 43 | - name: create coverage report 44 | run: ./gradlew jacocoRootReport 45 | 46 | - name: Upload coverage to Codecov 47 | uses: codecov/codecov-action@v1 48 | with: 49 | token: ${{ secrets.CODECOV_TOKEN }} 50 | file: build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml 51 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/resources/multi-module-test-project/test.diff: -------------------------------------------------------------------------------- 1 | Index: module2/src/main/java/com/module2/Class2.java 2 | IDEA additional info: 3 | Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP 4 | <+>UTF-8 5 | =================================================================== 6 | diff --git a/module2/src/main/java/com/module2/Class2.java b/module2/src/main/java/com/module2/Class2.java 7 | --- a/module2/src/main/java/com/module2/Class2.java (revision 1f3bd4ec54e7b75ecfa600559afecc6c9dff680b) 8 | +++ b/module2/src/main/java/com/module2/Class2.java (date 1629490858723) 9 | @@ -3,12 +3,12 @@ 10 | public class Class2 { 11 | 12 | public int max( int a, int b ) { 13 | - return a > b ? a : b; 14 | + return a > b ? a : b; // 15 | } 16 | 17 | public int min( int a, int b ) { 18 | - if (a < b) { 19 | - return a; 20 | + if (a < b) { // 21 | + return a; // 22 | } else { 23 | return b; 24 | } 25 | Index: module1/src/main/java/com/module1/Class1.java 26 | IDEA additional info: 27 | Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP 28 | <+>UTF-8 29 | =================================================================== 30 | diff --git a/module1/src/main/java/com/module1/Class1.java b/module1/src/main/java/com/module1/Class1.java 31 | --- a/module1/src/main/java/com/module1/Class1.java (revision 1f3bd4ec54e7b75ecfa600559afecc6c9dff680b) 32 | +++ b/module1/src/main/java/com/module1/Class1.java (date 1629490858735) 33 | @@ -4,6 +4,6 @@ 34 | 35 | public class Class1 { 36 | public void printMax(int a, int b) { 37 | - System.out.println(new Class2().max(a, b)); 38 | + System.out.println(new Class2().max(a, b)); // 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/report/analyzable/FullCoverageAnalyzableReport.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.report.analyzable 2 | 3 | import com.form.coverage.report.FullReport 4 | import com.form.coverage.report.ReportType 5 | import org.jacoco.core.analysis.Analyzer 6 | import org.jacoco.core.analysis.ICoverageVisitor 7 | import org.jacoco.core.data.ExecutionDataStore 8 | import org.jacoco.report.FileMultiReportOutput 9 | import org.jacoco.report.IReportVisitor 10 | import org.jacoco.report.MultiReportVisitor 11 | import org.jacoco.report.csv.CSVFormatter 12 | import org.jacoco.report.html.HTMLFormatter 13 | import org.jacoco.report.xml.XMLFormatter 14 | import java.io.File 15 | import java.io.FileOutputStream 16 | 17 | internal open class FullCoverageAnalyzableReport( 18 | private val report: FullReport 19 | ) : AnalyzableReport { 20 | 21 | override fun buildVisitor(): IReportVisitor { 22 | return report.reports.map { 23 | val reportFile: File = report.resolveReportAbsolutePath(it).toFile() 24 | when (it.reportType) { 25 | ReportType.HTML -> FileMultiReportOutput(reportFile).let(HTMLFormatter()::createVisitor) 26 | ReportType.XML -> reportFile.createFileOutputStream().let(XMLFormatter()::createVisitor) 27 | ReportType.CSV -> reportFile.createFileOutputStream().let(CSVFormatter()::createVisitor) 28 | } 29 | }.let(::MultiReportVisitor) 30 | } 31 | 32 | private fun File.createFileOutputStream(): FileOutputStream { 33 | parentFile.mkdirs() 34 | return FileOutputStream(this) 35 | } 36 | 37 | override fun buildAnalyzer( 38 | executionDataStore: ExecutionDataStore, 39 | coverageVisitor: ICoverageVisitor 40 | ): Analyzer { 41 | return Analyzer(executionDataStore, coverageVisitor) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/kotlin/com/form/coverage/gradle/DiffCoverageTestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import com.form.coverage.gradle.DiffCoveragePlugin.Companion.DIFF_COV_TASK 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.gradle.testkit.runner.BuildResult 6 | import org.gradle.testkit.runner.GradleRunner 7 | import org.gradle.testkit.runner.TaskOutcome 8 | import java.io.File 9 | 10 | fun buildGradleRunner( 11 | projectRoot: File 12 | ): GradleRunner { 13 | return GradleRunner.create() 14 | .withPluginClasspath() 15 | .withProjectDir(projectRoot) 16 | .withTestKitDir(projectRoot.resolve("TestKitDir").apply { 17 | mkdir() 18 | }) 19 | .apply { 20 | // gradle testkit jacoco support 21 | javaClass.classLoader.getResourceAsStream("testkit-gradle.properties")?.use { inputStream -> 22 | File(projectDir, "gradle.properties").outputStream().use { outputStream -> 23 | inputStream.copyTo(outputStream) 24 | } 25 | } 26 | } 27 | } 28 | 29 | fun GradleRunner.runTask(task: String): BuildResult = withArguments(task).build() 30 | 31 | fun GradleRunner.runTaskAndFail(task: String): BuildResult = withArguments(task).buildAndFail() 32 | 33 | fun expectedHtmlReportFiles(vararg packages: String): Array = arrayOf( 34 | "index.html", 35 | "jacoco-resources", 36 | "jacoco-sessions.html" 37 | ) + packages 38 | 39 | fun BuildResult.assertOutputContainsStrings(vararg expectedString: String): BuildResult { 40 | assertThat(output).contains(*expectedString) 41 | return this 42 | } 43 | 44 | fun BuildResult.assertDiffCoverageStatusEqualsTo(status: TaskOutcome): BuildResult { 45 | assertThat(task(":$DIFF_COV_TASK")) 46 | .isNotNull 47 | .extracting { it?.outcome } 48 | .isEqualTo(status) 49 | return this 50 | } 51 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/kotlin/com/form/coverage/gradle/DiffCoverageMultiModuleTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import com.form.coverage.gradle.DiffCoveragePlugin.Companion.DIFF_COV_TASK 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.gradle.testkit.runner.TaskOutcome.FAILED 6 | import org.junit.jupiter.api.BeforeEach 7 | import org.junit.jupiter.api.Test 8 | import java.io.File 9 | 10 | class DiffCoverageMultiModuleTest : BaseDiffCoverageTest() { 11 | 12 | companion object { 13 | const val TEST_PROJECT_RESOURCE_NAME = "multi-module-test-project" 14 | } 15 | 16 | override fun buildTestConfiguration() = TestConfiguration( 17 | TEST_PROJECT_RESOURCE_NAME, 18 | "build.gradle", 19 | "test.diff" 20 | ) 21 | 22 | @BeforeEach 23 | fun setup() { 24 | initializeGradleTest() 25 | } 26 | 27 | @Test 28 | fun `diff-coverage should automatically collect jacoco configuration from submodules in multimodule project`() { 29 | // setup 30 | val baseReportDir = "build/custom/" 31 | val htmlReportDir = rootProjectDir.resolve(baseReportDir).resolve(File("diffCoverage", "html")) 32 | buildFile.appendText( 33 | """ 34 | 35 | diffCoverageReport { 36 | diffSource.file = '$diffFilePath' 37 | reports { 38 | html = true 39 | baseReportDir = '$baseReportDir' 40 | } 41 | violationRules.failIfCoverageLessThan 0.9 42 | } 43 | """.trimIndent() 44 | ) 45 | 46 | // run 47 | val result = gradleRunner.runTaskAndFail(DIFF_COV_TASK) 48 | 49 | // assert 50 | result.assertDiffCoverageStatusEqualsTo(FAILED) 51 | .assertOutputContainsStrings( 52 | "Fail on violations: true. Found violations: 1.", 53 | "Rule violated for bundle $TEST_PROJECT_RESOURCE_NAME: " + 54 | "branches covered ratio is 0.5, but expected minimum is 0.9" 55 | ) 56 | assertThat(htmlReportDir.list()).containsExactlyInAnyOrder( 57 | *expectedHtmlReportFiles("com.module1", "com.module2") 58 | ) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/report/ReportsFactory.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.report 2 | 3 | import com.form.coverage.config.DiffCoverageConfig 4 | import com.form.coverage.config.ReportsConfig 5 | import com.form.coverage.config.ViolationRuleConfig 6 | import com.form.coverage.diff.DiffSource 7 | import org.jacoco.core.analysis.ICoverageNode 8 | import org.jacoco.report.check.Limit 9 | import org.jacoco.report.check.Rule 10 | import java.nio.file.Paths 11 | 12 | internal fun reportFactory( 13 | diffSourceConfig: DiffCoverageConfig, 14 | diffSource: DiffSource 15 | ): Set { 16 | val reports: Set = diffSourceConfig.reportsConfig.toReportTypes() 17 | 18 | val violationRule: Rule = buildRule(diffSourceConfig.violationRuleConfig) 19 | val baseReportDir = Paths.get(diffSourceConfig.reportsConfig.baseReportDir) 20 | val report: MutableSet = mutableSetOf( 21 | DiffReport( 22 | baseReportDir.resolve("diffCoverage"), 23 | reports, 24 | diffSource, 25 | Violation( 26 | diffSourceConfig.violationRuleConfig.failOnViolation, 27 | listOf(violationRule) 28 | ) 29 | ) 30 | ) 31 | 32 | if (diffSourceConfig.reportsConfig.fullCoverageReport) { 33 | report += FullReport( 34 | baseReportDir.resolve("fullReport"), 35 | reports 36 | ) 37 | } 38 | 39 | return report 40 | } 41 | 42 | private fun ReportsConfig.toReportTypes(): Set = sequenceOf( 43 | ReportType.HTML to html, 44 | ReportType.CSV to csv, 45 | ReportType.XML to xml 46 | ).filter { it.second.enabled }.map { 47 | Report(it.first, it.second.outputFileName) 48 | }.toSet() 49 | 50 | private fun buildRule( 51 | violationRulesOptions: ViolationRuleConfig 52 | ): Rule { 53 | return sequenceOf( 54 | ICoverageNode.CounterEntity.INSTRUCTION to violationRulesOptions.minInstructions, 55 | ICoverageNode.CounterEntity.BRANCH to violationRulesOptions.minBranches, 56 | ICoverageNode.CounterEntity.LINE to violationRulesOptions.minLines 57 | ).filter { 58 | it.second > 0.0 59 | }.map { 60 | Limit().apply { 61 | setCounter(it.first.name) 62 | minimum = it.second.toString() 63 | } 64 | }.toList().let { 65 | Rule().apply { 66 | limits = it 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /diff-coverage/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-gradle-plugin' 3 | id 'com.gradle.plugin-publish' version '0.21.0' 4 | id 'com.github.johnrengelman.shadow' version '7.1.2' 5 | 6 | id "pl.droidsonroids.jacoco.testkit" version "1.0.8" 7 | id 'jacoco' 8 | } 9 | 10 | group 'com.form.coverage' 11 | 12 | gradlePlugin { 13 | plugins { 14 | diffCoveragePlugin { 15 | id = "com.form.diff-coverage" 16 | displayName = "Diff Coverage" 17 | description = "Plugin that computes code coverage on modified code" 18 | implementationClass = "com.form.coverage.gradle.DiffCoveragePlugin" 19 | } 20 | } 21 | } 22 | pluginBundle { 23 | website = 'https://github.com/form-com/diff-coverage-gradle' 24 | vcsUrl = 'https://github.com/form-com/diff-coverage-gradle.git' 25 | tags = ["coverage", "jacoco", "differential-coverage", "diff-coverage", "code-coverage"] 26 | } 27 | 28 | ext { 29 | junit_version = '5.7.2' 30 | } 31 | 32 | sourceSets { 33 | functionalTest { 34 | kotlin.srcDir file('src/funcTest/kotlin') 35 | resources.srcDir file('src/funcTest/resources') 36 | compileClasspath += sourceSets.main.output 37 | runtimeClasspath += sourceSets.main.output + compileClasspath 38 | } 39 | } 40 | 41 | configurations { 42 | functionalTestImplementation.extendsFrom testImplementation 43 | functionalTestRuntimeOnly.extendsFrom runtimeOnly 44 | } 45 | 46 | task functionalTest(type: Test) { 47 | description = 'Runs the functional tests.' 48 | group = 'verification' 49 | testClassesDirs = sourceSets.functionalTest.output.classesDirs 50 | classpath = sourceSets.functionalTest.runtimeClasspath 51 | useJUnitPlatform() 52 | } 53 | check.dependsOn functionalTest 54 | 55 | gradlePlugin { 56 | testSourceSets sourceSets.functionalTest 57 | } 58 | 59 | jacocoTestKit { 60 | applyTo('functionalTestRuntimeOnly', tasks.named('functionalTest')) 61 | } 62 | functionalTest.dependsOn += generateJacocoFunctionalTestKitProperties 63 | 64 | dependencies { 65 | implementation project(':jacoco-filtering-extension') 66 | 67 | testImplementation 'org.assertj:assertj-core:3.20.2' 68 | 69 | functionalTestImplementation 'org.eclipse.jgit:org.eclipse.jgit:5.12.0.202106070339-r' 70 | functionalTestImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version" 71 | functionalTestImplementation "org.junit.jupiter:junit-jupiter-params:$junit_version" 72 | functionalTestRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit_version" 73 | 74 | } 75 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/kotlin/com/form/coverage/diff/FileDiffSourceTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.matchers.collections.shouldContainExactly 6 | import io.kotest.matchers.should 7 | import io.kotest.matchers.string.endWith 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | import java.io.File 11 | import java.nio.file.Files 12 | import java.nio.file.StandardOpenOption 13 | 14 | class FileDiffSourceTest : StringSpec() { 15 | 16 | private val testProjectDir: File by lazy { 17 | val file = Files.createTempDirectory("JgitDiffTest").toFile() 18 | afterSpec { 19 | file.delete() 20 | } 21 | file 22 | } 23 | 24 | init { 25 | "pullDiff should throw when file doesn't exist" { 26 | // setup 27 | val fileDiffSource = FileDiffSource("file-doesn't-exist") 28 | 29 | // run 30 | val exception = shouldThrow { 31 | fileDiffSource.pullDiff() 32 | } 33 | 34 | // assert 35 | exception.message should endWith("not a file or doesn't exist") 36 | } 37 | 38 | "pullDiff should throw when specified path is dir" { 39 | // setup 40 | val fileDiffSource = FileDiffSource(testProjectDir.newFolder().absolutePath) 41 | 42 | // run 43 | val exception = shouldThrow { 44 | fileDiffSource.pullDiff() 45 | } 46 | 47 | // assert 48 | exception.message should endWith("not a file or doesn't exist") 49 | } 50 | 51 | "pullDiff should return file lines" { 52 | // setup 53 | val expectedLines = listOf("1", "2", "3") 54 | val newFile = testProjectDir.newFile().apply { 55 | withContext(Dispatchers.IO) { 56 | Files.write(toPath(), expectedLines, StandardOpenOption.APPEND) 57 | } 58 | } 59 | 60 | val fileDiffSource = FileDiffSource(newFile.absolutePath) 61 | 62 | // run 63 | val diffLines = fileDiffSource.pullDiff() 64 | 65 | // assert 66 | diffLines shouldContainExactly expectedLines 67 | } 68 | 69 | } 70 | 71 | private fun File.newFolder(): File { 72 | return resolve("${System.nanoTime()}").apply { 73 | mkdir() 74 | } 75 | } 76 | 77 | private fun File.newFile(): File { 78 | return resolve("${System.nanoTime()}").apply { 79 | createNewFile() 80 | } 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/org/jacoco/core/internal/analysis/FilteringClassAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package org.jacoco.core.internal.analysis 2 | 3 | import org.jacoco.core.internal.analysis.filter.Filters 4 | import org.jacoco.core.internal.analysis.filter.IFilter 5 | import org.jacoco.core.internal.analysis.filter.IFilterContext 6 | import org.jacoco.core.internal.analysis.filter.IFilterOutput 7 | import org.jacoco.core.internal.flow.MethodProbesVisitor 8 | import org.jacoco.core.internal.instr.InstrSupport 9 | import org.objectweb.asm.MethodVisitor 10 | import org.objectweb.asm.tree.MethodNode 11 | 12 | internal open class FilteringClassAnalyzer( 13 | private val coverage: ClassCoverageImpl, 14 | private val probes: BooleanArray?, 15 | private val stringPool: StringPool, 16 | customFilter: IFilter 17 | ) : ClassAnalyzer(coverage, probes, stringPool) { 18 | 19 | private val filter: IFilter 20 | 21 | init { 22 | this.filter = createFilters(customFilter) 23 | } 24 | 25 | private fun createFilters(customFilter: IFilter): IFilter { 26 | return object : IFilter { 27 | private val allFilters = Filters.all() 28 | override fun filter( 29 | methodNode: MethodNode, context: IFilterContext, output: IFilterOutput 30 | ) { 31 | allFilters.filter(methodNode, context, output) 32 | customFilter.filter(methodNode, context, output) 33 | } 34 | } 35 | } 36 | 37 | override fun visitMethod( 38 | access: Int, 39 | name: String, 40 | desc: String, 41 | signature: String?, 42 | exceptions: Array? 43 | ): MethodProbesVisitor { 44 | InstrSupport.assertNotInstrumented(name, coverage.name) 45 | 46 | val builder = InstructionsBuilder(probes) 47 | return object : MethodAnalyzer(builder) { 48 | 49 | override fun accept(methodNode: MethodNode, methodVisitor: MethodVisitor) { 50 | super.accept(methodNode, methodVisitor) 51 | addMethodCoverage( 52 | stringPool[name], 53 | stringPool[desc], 54 | stringPool[signature], 55 | builder, 56 | methodNode 57 | ) 58 | } 59 | } 60 | } 61 | 62 | private fun addMethodCoverage( 63 | name: String, 64 | desc: String, 65 | signature: String?, 66 | icc: InstructionsBuilder, 67 | methodNode: MethodNode 68 | ) { 69 | val methodCoverageCalculator = MethodCoverageCalculator(icc.instructions) 70 | filter.filter(methodNode, this, methodCoverageCalculator) 71 | 72 | MethodCoverageImpl(name, desc, signature).run { 73 | methodCoverageCalculator.calculate(this) 74 | if (containsCode()) { 75 | coverage.addMethod(this) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/diff/DiffSource.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff 2 | 3 | import com.form.coverage.config.DiffSourceConfig 4 | import com.form.coverage.diff.git.JgitDiff 5 | import com.form.coverage.http.executeGetRequest 6 | import java.io.File 7 | 8 | const val DEFAULT_PATCH_FILE_NAME: String = "diff.patch" 9 | 10 | interface DiffSource { 11 | 12 | val sourceDescription: String 13 | fun pullDiff(): List 14 | fun saveDiffTo(dir: File): File 15 | } 16 | 17 | internal class FileDiffSource( 18 | private val filePath: String 19 | ) : DiffSource { 20 | 21 | override val sourceDescription = "File: $filePath" 22 | 23 | override fun pullDiff(): List { 24 | val file = File(filePath) 25 | return if (file.exists() && file.isFile) { 26 | file.readLines() 27 | } else { 28 | throw RuntimeException("'$filePath' not a file or doesn't exist") 29 | } 30 | } 31 | 32 | override fun saveDiffTo(dir: File): File { 33 | return File(filePath).copyTo(dir.resolve(DEFAULT_PATCH_FILE_NAME), true) 34 | } 35 | } 36 | 37 | internal class UrlDiffSource( 38 | private val url: String 39 | ) : DiffSource { 40 | override val sourceDescription = "URL: $url" 41 | 42 | private val diffContent: String by lazy { executeGetRequest(url) } 43 | 44 | override fun pullDiff(): List = diffContent.lines() 45 | 46 | override fun saveDiffTo(dir: File): File { 47 | return dir.resolve(DEFAULT_PATCH_FILE_NAME).apply { 48 | writeText(diffContent) 49 | } 50 | } 51 | } 52 | 53 | internal class GitDiffSource( 54 | private val projectRoot: File, 55 | private val compareWith: String 56 | ) : DiffSource { 57 | 58 | private val diffContent: String by lazy { 59 | JgitDiff(projectRoot.resolve(".git")).obtain(compareWith) 60 | } 61 | 62 | override val sourceDescription = "Git: diff $compareWith" 63 | 64 | override fun pullDiff(): List = diffContent.lines() 65 | 66 | override fun saveDiffTo(dir: File): File { 67 | return dir.resolve(DEFAULT_PATCH_FILE_NAME).apply { 68 | writeText(diffContent) 69 | } 70 | } 71 | } 72 | 73 | internal fun diffSourceFactory( 74 | projectRoot: File, 75 | diffSourceConfig: DiffSourceConfig 76 | ): DiffSource = when { 77 | 78 | diffSourceConfig.file.isNotBlank() && diffSourceConfig.url.isNotBlank() -> throw IllegalStateException( 79 | "Expected only Git configuration or file or URL diff source more than one: " + 80 | "git.diffBase=${diffSourceConfig.diffBase} file=${diffSourceConfig.file}, url=${diffSourceConfig.url}" 81 | ) 82 | 83 | diffSourceConfig.file.isNotBlank() -> FileDiffSource(diffSourceConfig.file) 84 | diffSourceConfig.url.isNotBlank() -> UrlDiffSource(diffSourceConfig.url) 85 | diffSourceConfig.diffBase.isNotBlank() -> GitDiffSource(projectRoot, diffSourceConfig.diffBase) 86 | 87 | else -> throw IllegalStateException("Expected Git configuration or file or URL diff source but all are blank") 88 | } 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Diff-Coverage Gradle plugin Changelog 2 | 3 | ## [0.9.5] 4 | ### Dependency Upgrades 5 | - Updated log4j dependency to 2.18.0 #58 6 | 7 | 8 | ## [0.9.4] 9 | ### Dependency Upgrades 10 | - Updated [JaCoCo] to [0.8.8](https://github.com/jacoco/jacoco/releases/tag/v0.8.8) #54 11 | - Diff-Coverage now supports Java 18 12 | 13 | 14 | ## [0.9.3] 15 | ### Fixed 16 | - Fixed incorrect diff parsing when it contains `\ No newline at end of file` 17 | 18 | 19 | ## [0.9.2] 20 | ### Fixed 21 | - redirects handling for `diffSource.url` 22 | 23 | 24 | ## [0.9.1] 25 | ### Fixed 26 | - Fixed incorrect diff generation by JGit [#34](https://github.com/form-com/diff-coverage-gradle/issues/34) 27 | 28 | ## [0.9.0] 29 | ### Added 30 | - autoconfiguration of `jacocoExecFiles`, `classesDirs`, `srcDirs` if JaCoCo plugin is applied and custom values are not set [#24](https://github.com/form-com/diff-coverage-gradle/issues/24) 31 | - source file collection `diffCoverageReport.srcDirs` as `diffCoverage` task input [#28](https://github.com/form-com/diff-coverage-gradle/issues/28) 32 | ### Changed 33 | - fail build if any of `jacocoExecFiles`, `classesDirs` or `srcDirs` is not configured and cannot be autoconfigured from JaCoCo plugin [#29](https://github.com/form-com/diff-coverage-gradle/issues/29) 34 | ### Fixed 35 | - error message if provided Git revision doesn't exist [#19](https://github.com/form-com/diff-coverage-gradle/issues/19) 36 | - Diff Coverage task fail when only csv or xml report is enabled [#26](https://github.com/form-com/diff-coverage-gradle/issues/26) 37 | 38 | ## [0.8.1] 39 | ### Fixed 40 | - parsing of diff files that contains paths with special characters [#22](https://github.com/form-com/diff-coverage-gradle/issues/22) 41 | 42 | ## [0.8.0] 43 | ### Fixed 44 | - compatibility with gradle 6.7.1 [#14](https://github.com/form-com/diff-coverage-gradle/issues/14) 45 | - inability to create Diff Coverage outputs when report dir isn't created [#16](https://github.com/form-com/diff-coverage-gradle/issues/16) 46 | ### Added 47 | - configuration function `failIfCoverageLessThan` that reduces Diff Coverage configuration verbosity [#17](https://github.com/form-com/diff-coverage-gradle/issues/17) 48 | ```groovy 49 | diffCoverageReport { 50 | violationRules.failIfCoverageLessThan 0.9 51 | 52 | // configuration above do the same as configuration below 53 | violationRules { 54 | minBranches = 0.9 55 | minLines = 0.9 56 | minInstructions = 0.9 57 | failOnViolation = true 58 | } 59 | } 60 | ``` 61 | 62 | ## [0.7.2] 63 | ### Fixed 64 | - compatibility with Gradle v7 [#15](https://github.com/form-com/diff-coverage-gradle/issues/15) 65 | 66 | ## [0.7.1] 67 | ### Fixed 68 | - NPE for projects containing classes out of package [#10](https://github.com/form-com/diff-coverage-gradle/issues/10) 69 | 70 | ## [0.7.0] 71 | ### Added 72 | - JGit as diff source [#7](https://github.com/form-com/diff-coverage-gradle/issues/7) 73 | - support of csv diff report [#8](https://github.com/form-com/diff-coverage-gradle/issues/8) 74 | - support of xml diff report [#8](https://github.com/form-com/diff-coverage-gradle/issues/8) 75 | 76 | ## [0.6.0] 77 | first public release :birthday: 78 | 79 | 80 | [JaCoCo]: https://github.com/jacoco/jacoco/releases/tag/v0.8.8 81 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/kotlin/com/form/coverage/diff/DiffSourceFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff 2 | 3 | import com.form.coverage.config.DiffSourceConfig 4 | import io.kotest.assertions.throwables.shouldThrow 5 | import io.kotest.core.spec.style.StringSpec 6 | import io.kotest.matchers.should 7 | import io.kotest.matchers.shouldBe 8 | import io.kotest.matchers.string.startWith 9 | import io.kotest.matchers.types.shouldBeTypeOf 10 | import java.io.File 11 | 12 | class DiffSourceFactoryTest : StringSpec() { 13 | init { 14 | 15 | "diffSourceFactory should return file diff source" { 16 | // setup 17 | val filePath = "someFile" 18 | val diffConfig = buildDiffSourceOption(file = filePath) 19 | 20 | // run 21 | val diffSource = diffSourceFactory(File("."), diffConfig) 22 | 23 | // assert 24 | diffSource.shouldBeTypeOf() 25 | diffSource.sourceDescription shouldBe "File: $filePath" 26 | } 27 | 28 | "diffSourceFactory should return url diff source" { 29 | // setup 30 | val url = "someUrl" 31 | val diffConfig = buildDiffSourceOption(url = url) 32 | 33 | // run 34 | val diffSource = diffSourceFactory(File("."), diffConfig) 35 | 36 | // assert 37 | diffSource.shouldBeTypeOf() 38 | diffSource.sourceDescription shouldBe "URL: $url" 39 | } 40 | 41 | "diffSourceFactory should return git diff source" { 42 | // setup 43 | val compareWith = "develop" 44 | val diffConfig = buildDiffSourceOption(git = compareWith) 45 | 46 | // run 47 | val diffSource = diffSourceFactory(File("."), diffConfig) 48 | 49 | // assert 50 | diffSource.shouldBeTypeOf() 51 | diffSource.sourceDescription shouldBe "Git: diff $compareWith" 52 | } 53 | 54 | "diffSourceFactory should throw when no source specified" { 55 | // setup 56 | val diffConfig = buildDiffSourceOption() 57 | 58 | // run 59 | val exception = shouldThrow { 60 | diffSourceFactory(File("."), diffConfig) 61 | } 62 | 63 | // assert 64 | exception.message should startWith( 65 | "Expected Git configuration or file or URL diff source but all are blank" 66 | ) 67 | } 68 | 69 | "diffSourceFactory should throw when both sources specified" { 70 | // setup 71 | val diffConfig = buildDiffSourceOption(file = "file", url = "url", git = "master") 72 | 73 | // run 74 | val exception = shouldThrow { 75 | diffSourceFactory(File("."), diffConfig) 76 | } 77 | 78 | // assert 79 | exception.message should startWith( 80 | "Expected only Git configuration or file or URL diff source more than one:" 81 | ) 82 | } 83 | } 84 | 85 | private fun buildDiffSourceOption(file: String = "", url: String = "", git: String = ""): DiffSourceConfig { 86 | return DiffSourceConfig(file, url, git) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/report/analyzable/DiffCoverageAnalyzableReport.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.report.analyzable 2 | 3 | import com.form.coverage.diff.CodeUpdateInfo 4 | import com.form.coverage.diff.parse.ModifiedLinesDiffParser 5 | import com.form.coverage.filters.ModifiedLinesFilter 6 | import com.form.coverage.report.DiffReport 7 | import org.jacoco.core.analysis.Analyzer 8 | import org.jacoco.core.analysis.ICoverageVisitor 9 | import org.jacoco.core.data.ExecutionDataStore 10 | import org.jacoco.core.internal.analysis.FilteringAnalyzer 11 | import org.jacoco.report.IReportVisitor 12 | import org.jacoco.report.MultiReportVisitor 13 | import org.jacoco.report.check.Rule 14 | import org.jacoco.report.check.RulesChecker 15 | import org.slf4j.LoggerFactory 16 | 17 | internal class DiffCoverageAnalyzableReport( 18 | private val diffReport: DiffReport 19 | ) : FullCoverageAnalyzableReport(diffReport) { 20 | 21 | override fun buildVisitor(): IReportVisitor { 22 | val visitors: MutableList = mutableListOf(super.buildVisitor()) 23 | 24 | visitors += createViolationCheckVisitor( 25 | diffReport.violation.failOnViolation, 26 | diffReport.violation.violationRules 27 | ) 28 | 29 | return MultiReportVisitor(visitors) 30 | } 31 | 32 | override fun buildAnalyzer( 33 | executionDataStore: ExecutionDataStore, 34 | coverageVisitor: ICoverageVisitor 35 | ): Analyzer { 36 | val codeUpdateInfo = obtainCodeUpdateInfo() 37 | return FilteringAnalyzer( 38 | executionDataStore, 39 | coverageVisitor, 40 | codeUpdateInfo::isInfoExists 41 | ) { 42 | ModifiedLinesFilter(codeUpdateInfo) 43 | } 44 | } 45 | 46 | private fun obtainCodeUpdateInfo(): CodeUpdateInfo { 47 | val changesMap = ModifiedLinesDiffParser().collectModifiedLines( 48 | diffReport.diffSource.pullDiff() 49 | ) 50 | changesMap.forEach { (file, rows) -> 51 | log.debug("File $file has ${rows.size} modified lines") 52 | } 53 | return CodeUpdateInfo(changesMap) 54 | } 55 | 56 | private fun createViolationCheckVisitor( 57 | failOnViolation: Boolean, 58 | rules: List 59 | ): IReportVisitor { 60 | val log = LoggerFactory.getLogger("ViolationRules") 61 | val violations = mutableListOf() 62 | 63 | class CoverageRulesVisitor( 64 | rulesCheckerVisitor: IReportVisitor 65 | ) : IReportVisitor by rulesCheckerVisitor { 66 | override fun visitEnd() { 67 | log.warn("Fail on violations: $failOnViolation. Found violations: ${violations.size}.") 68 | if (violations.isNotEmpty() && failOnViolation) { 69 | throw Exception(violations.joinToString("\n")) 70 | } 71 | } 72 | } 73 | 74 | return RulesChecker().apply { 75 | setRules(rules) 76 | }.createVisitor { _, _, _, message -> 77 | log.info("New violation: $message") 78 | violations += message 79 | }.let { CoverageRulesVisitor(it) } 80 | } 81 | 82 | private companion object { 83 | val log = LoggerFactory.getLogger(DiffCoverageAnalyzableReport::class.java) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/filters/ModifiedLinesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.filters 2 | 3 | import com.form.coverage.diff.CodeUpdateInfo 4 | import com.form.coverage.diff.parse.ClassFile 5 | import org.jacoco.core.internal.analysis.filter.IFilter 6 | import org.jacoco.core.internal.analysis.filter.IFilterContext 7 | import org.jacoco.core.internal.analysis.filter.IFilterOutput 8 | import org.objectweb.asm.tree.AbstractInsnNode 9 | import org.objectweb.asm.tree.InsnList 10 | import org.objectweb.asm.tree.LabelNode 11 | import org.objectweb.asm.tree.LineNumberNode 12 | import org.objectweb.asm.tree.MethodNode 13 | import org.slf4j.LoggerFactory 14 | 15 | class ModifiedLinesFilter(private val codeUpdateInfo: CodeUpdateInfo) : IFilter { 16 | 17 | override fun filter( 18 | methodNode: MethodNode, 19 | context: IFilterContext, 20 | output: IFilterOutput 21 | ) { 22 | val classModifications = codeUpdateInfo.getClassModifications( 23 | ClassFile( 24 | context.sourceFileName, 25 | context.className 26 | ) 27 | ) 28 | val groupedModifiedLines = collectLineNodes(methodNode.instructions).groupBy { 29 | classModifications.isLineModified(it.lineNode.line) 30 | } 31 | 32 | groupedModifiedLines[false]?.forEach { 33 | output.ignore(it.lineNode.previous, it.lineNodeLastInstruction) 34 | } 35 | 36 | if (log.isDebugEnabled) { 37 | groupedModifiedLines[true] 38 | ?.map { it.lineNode.line } 39 | ?.takeIf { it.isNotEmpty() } 40 | ?.let { 41 | log.debug("Modified lines in ${context.className}#${methodNode.name}") 42 | log.debug("\tlines: $it") 43 | } 44 | } 45 | } 46 | 47 | private fun collectLineNodes(instructionNodes: InsnList): Sequence { 48 | val lineNodes = ArrayList() 49 | 50 | val iterator = instructionNodes.iterator() 51 | val nextLineNode = getNextLineNode(iterator) ?: return emptySequence() 52 | 53 | var currentNode = LineNode(nextLineNode) 54 | while (iterator.hasNext()) { 55 | val instructionNode = iterator.next() 56 | if (instructionNode is LabelNode && instructionNode.next is LineNumberNode) { 57 | lineNodes.add(currentNode) 58 | currentNode = LineNode(instructionNode.next as LineNumberNode) 59 | } else { 60 | currentNode.lineNodeLastInstruction = instructionNode 61 | } 62 | } 63 | lineNodes.add(currentNode) 64 | return lineNodes.asSequence() 65 | } 66 | 67 | private fun getNextLineNode(instructionNodes: ListIterator): LineNumberNode? { 68 | while (instructionNodes.hasNext()) { 69 | val node = instructionNodes.next() 70 | if (node is LineNumberNode) { 71 | return node 72 | } 73 | } 74 | return null 75 | } 76 | 77 | private class LineNode( 78 | val lineNode: LineNumberNode, 79 | var lineNodeLastInstruction: AbstractInsnNode = lineNode 80 | ) 81 | 82 | private companion object { 83 | val log = LoggerFactory.getLogger(ModifiedLinesFilter::class.java) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/resources/testintPatch1.patch: -------------------------------------------------------------------------------- 1 | Index: jacoco-filtering-extension/src/main/kotlin/com/form/coverage/filters/ModifiedLinesFilter.kt 2 | IDEA additional info: 3 | Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP 4 | <+>UTF-8 5 | =================================================================== 6 | --- jacoco-filtering-extension/src/main/kotlin/com/form/coverage/filters/ModifiedLinesFilter.kt (revision 7c5d3c0103a70a47a9fa119cb620045957f9fc77) 7 | +++ jacoco-filtering-extension/src/main/kotlin/com/form/coverage/filters/ModifiedLinesFilter.kt (revision 4623b37b5e759386f58cb3c38d87a8ddb247a17d) 8 | @@ -4,10 +4,8 @@ 9 | import org.jacoco.core.internal.analysis.filter.IFilter 10 | import org.jacoco.core.internal.analysis.filter.IFilterContext 11 | import org.jacoco.core.internal.analysis.filter.IFilterOutput 12 | -import org.objectweb.asm.tree.AbstractInsnNode 13 | -import org.objectweb.asm.tree.InsnList 14 | -import org.objectweb.asm.tree.LineNumberNode 15 | -import org.objectweb.asm.tree.MethodNode 16 | +import org.objectweb.asm.tree.* 17 | +import org.slf4j.LoggerFactory 18 | import java.util.* 19 | 20 | class ModifiedLinesFilter(private val classModifications: ClassModifications) : IFilter { 21 | @@ -17,13 +15,21 @@ 22 | context: IFilterContext, 23 | output: IFilterOutput 24 | ) { 25 | - collectLineNodes(methodNode.instructions) 26 | - .filter { 27 | - !classModifications.isLineModified(it.lineNode.line) 28 | - } 29 | - .forEach { 30 | - output.ignore(it.lineNode, it.lineNodeLastInstruction) 31 | - } 32 | + val groupedModifiedLines = collectLineNodes(methodNode.instructions).groupBy { 33 | + classModifications.isLineModified(it.lineNode.line) 34 | + } 35 | + 36 | + groupedModifiedLines[false]?.forEach { 37 | + output.ignore(it.lineNode.previous, it.lineNodeLastInstruction) 38 | + } 39 | + 40 | + if(log.isDebugEnabled) { 41 | + log.debug("Modified lines in ${context.className}#${methodNode.name}") 42 | + val lines = groupedModifiedLines[true] 43 | + ?.map { it.lineNode.line } 44 | + ?: emptyList() 45 | + log.debug("\tlines: $lines") 46 | + } 47 | } 48 | 49 | private fun collectLineNodes(instructionNodes: InsnList): Sequence { 50 | @@ -31,14 +37,15 @@ 51 | 52 | val iterator = instructionNodes.iterator() 53 | val nextLineNode = getNextLineNode(iterator) ?: return emptySequence() 54 | + 55 | var currentNode = LineNode(nextLineNode) 56 | while (iterator.hasNext()) { 57 | - val next = iterator.next() 58 | - if (next is LineNumberNode) { 59 | + val instructionNode = iterator.next() 60 | + if (instructionNode is LabelNode && instructionNode.next is LineNumberNode) { 61 | lineNodes.add(currentNode) 62 | - currentNode = LineNode(next) 63 | + currentNode = LineNode(instructionNode.next as LineNumberNode) 64 | } else { 65 | - currentNode.lineNodeLastInstruction = next 66 | + currentNode.lineNodeLastInstruction = instructionNode 67 | } 68 | } 69 | lineNodes.add(currentNode) 70 | @@ -60,4 +67,8 @@ 71 | val lineNode: LineNumberNode, 72 | var lineNodeLastInstruction: AbstractInsnNode = lineNode 73 | ) 74 | + 75 | + private companion object { 76 | + val log = LoggerFactory.getLogger( ModifiedLinesFilter::class.java ) 77 | + } 78 | } 79 | -------------------------------------------------------------------------------- /diff-coverage/src/main/kotlin/com/form/coverage/gradle/ChangesetCoverageConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import org.gradle.api.Action 4 | import org.gradle.api.file.FileCollection 5 | import org.gradle.api.tasks.Input 6 | import org.gradle.api.tasks.InputFiles 7 | import org.gradle.api.tasks.Nested 8 | import org.gradle.api.tasks.Optional 9 | import java.nio.file.Paths 10 | 11 | open class ChangesetCoverageConfiguration( 12 | @Optional @InputFiles var jacocoExecFiles: FileCollection? = null, 13 | @Optional @InputFiles var classesDirs: FileCollection? = null, 14 | @Optional @InputFiles var srcDirs: FileCollection? = null, 15 | @Nested val diffSource: DiffSourceConfiguration = DiffSourceConfiguration(), 16 | @Nested val reportConfiguration: ReportsConfiguration = ReportsConfiguration(), 17 | @Nested val violationRules: ViolationRules = ViolationRules() 18 | ) { 19 | 20 | fun reports(action: Action) { 21 | action.execute(reportConfiguration) 22 | } 23 | 24 | fun violationRules(action: Action) { 25 | action.execute(violationRules) 26 | } 27 | 28 | fun diffSource(action: Action) { 29 | action.execute(diffSource) 30 | } 31 | 32 | override fun toString(): String { 33 | return "ChangesetCoverageConfiguration(" + 34 | "jacocoExecFiles=$jacocoExecFiles, " + 35 | "classesDirs=$classesDirs, " + 36 | "srcDirs=$srcDirs, " + 37 | "diffSource=$diffSource, " + 38 | "reportConfiguration=$reportConfiguration, " + 39 | "violationRules=$violationRules)" 40 | } 41 | } 42 | 43 | open class DiffSourceConfiguration( 44 | @Input var file: String = "", 45 | @Input var url: String = "", 46 | @Nested val git: GitConfiguration = GitConfiguration() 47 | ) { 48 | override fun toString(): String { 49 | return "DiffSourceConfiguration(file='$file', url='$url', git=$git)" 50 | } 51 | } 52 | 53 | open class GitConfiguration(@Input var diffBase: String = "") { 54 | fun compareWith(diffBase: String) { 55 | this.diffBase = diffBase 56 | } 57 | 58 | override fun toString(): String { 59 | return "GitConfiguration(diffBase='$diffBase')" 60 | } 61 | } 62 | 63 | open class ReportsConfiguration( 64 | @Input var html: Boolean = false, 65 | @Input var xml: Boolean = false, 66 | @Input var csv: Boolean = false, 67 | @Input var baseReportDir: String = Paths.get("build", "reports", "jacoco").toString(), 68 | @Input var fullCoverageReport: Boolean = false 69 | ) { 70 | 71 | override fun toString() = "ReportsConfiguration(" + 72 | "html=$html, " + 73 | "xml=$xml, " + 74 | "csv=$csv, " + 75 | "baseReportDir='$baseReportDir'" 76 | } 77 | 78 | open class ViolationRules( 79 | @Input var minLines: Double = 0.0, 80 | @Input var minBranches: Double = 0.0, 81 | @Input var minInstructions: Double = 0.0, 82 | @Input var failOnViolation: Boolean = false 83 | ) { 84 | fun failIfCoverageLessThan(minCoverage: Double) { 85 | minLines = minCoverage 86 | minBranches = minCoverage 87 | minInstructions = minCoverage 88 | failOnViolation = true 89 | } 90 | 91 | override fun toString(): String { 92 | return "ViolationRules(" + 93 | "minLines=$minLines, " + 94 | "minBranches=$minBranches, " + 95 | "minInstructions=$minInstructions, " + 96 | "failOnViolation=$failOnViolation" + 97 | ")" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/report/ReportGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.report 2 | 3 | import com.form.coverage.config.DiffCoverageConfig 4 | import com.form.coverage.diff.DiffSource 5 | import com.form.coverage.diff.diffSourceFactory 6 | import com.form.coverage.report.analyzable.AnalyzableReport 7 | import com.form.coverage.report.analyzable.analyzableReportFactory 8 | import org.jacoco.core.analysis.Analyzer 9 | import org.jacoco.core.analysis.CoverageBuilder 10 | import org.jacoco.core.analysis.IBundleCoverage 11 | import org.jacoco.core.analysis.ICoverageVisitor 12 | import org.jacoco.core.tools.ExecFileLoader 13 | import org.jacoco.report.DirectorySourceFileLocator 14 | import org.jacoco.report.ISourceFileLocator 15 | import org.jacoco.report.MultiSourceFileLocator 16 | import org.slf4j.LoggerFactory 17 | import java.io.File 18 | import java.io.IOException 19 | 20 | class ReportGenerator( 21 | projectRoot: File, 22 | private val diffCoverageConfig: DiffCoverageConfig 23 | ) { 24 | private val jacocoExec: Set = diffCoverageConfig.execFiles.filter(File::exists).toSet() 25 | private val classesSources: Set = diffCoverageConfig.classFiles.filter(File::exists).toSet() 26 | private val src: Set = diffCoverageConfig.sourceFiles.filter(File::exists).toSet() 27 | 28 | private val diffSource: DiffSource = diffSourceFactory(projectRoot, diffCoverageConfig.diffSourceConfig) 29 | private val analyzableReports: Set = analyzableReportFactory(diffCoverageConfig, diffSource) 30 | 31 | fun saveDiffToDir(dir: File) = diffSource.saveDiffTo(dir) 32 | 33 | fun create() { 34 | val execFileLoader = loadExecFiles() 35 | 36 | analyzableReports.forEach { 37 | create(execFileLoader, it) 38 | } 39 | } 40 | 41 | private fun loadExecFiles(): ExecFileLoader { 42 | val execFileLoader = ExecFileLoader() 43 | jacocoExec.forEach { 44 | log.debug("Loading exec data $it") 45 | try { 46 | execFileLoader.load(it) 47 | } catch (e: IOException) { 48 | throw RuntimeException("Cannot load coverage data from file: $it", e) 49 | } 50 | } 51 | return execFileLoader 52 | } 53 | 54 | private fun create(execFileLoader: ExecFileLoader, analyzableReport: AnalyzableReport) { 55 | val bundleCoverage = analyzeStructure { coverageVisitor -> 56 | analyzableReport.buildAnalyzer(execFileLoader.executionDataStore, coverageVisitor) 57 | } 58 | 59 | analyzableReport.buildVisitor().run { 60 | visitInfo( 61 | execFileLoader.sessionInfoStore.infos, 62 | execFileLoader.executionDataStore.contents 63 | ) 64 | 65 | visitBundle( 66 | bundleCoverage, 67 | createSourcesLocator() 68 | ) 69 | 70 | visitEnd() 71 | } 72 | } 73 | 74 | private fun analyzeStructure( 75 | createAnalyzer: (ICoverageVisitor) -> Analyzer 76 | ): IBundleCoverage { 77 | CoverageBuilder().let { builder -> 78 | 79 | val analyzer = createAnalyzer(builder) 80 | 81 | classesSources.forEach { analyzer.analyzeAll(it) } 82 | 83 | return builder.getBundle(diffCoverageConfig.reportName) 84 | } 85 | } 86 | 87 | private fun createSourcesLocator(): ISourceFileLocator { 88 | return src.asSequence() 89 | .map { 90 | DirectorySourceFileLocator(it, "utf-8", DEFAULT_TAB_WIDTH) 91 | } 92 | .fold(MultiSourceFileLocator(DEFAULT_TAB_WIDTH)) { accumulator, sourceLocator -> 93 | accumulator.apply { 94 | add(sourceLocator) 95 | } 96 | } 97 | } 98 | 99 | companion object { 100 | val log = LoggerFactory.getLogger(ReportGenerator::class.java) 101 | 102 | const val DEFAULT_TAB_WIDTH = 4 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /diff-coverage/src/main/kotlin/com/form/coverage/gradle/DiffTaskAutoConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import org.gradle.api.file.FileCollection 4 | import org.gradle.testing.jacoco.tasks.JacocoReport 5 | 6 | internal fun DiffCoverageTask.collectFileCollectionOrThrow( 7 | sourceType: ConfigurationSourceType 8 | ): FileCollection { 9 | val (collectionSource, fileCollection) = collectFileCollectionOrAutoconfigure(sourceType) 10 | return if (fileCollection.isEmpty) { 11 | throwMissedConfigurationException(collectionSource, sourceType) 12 | } else { 13 | logger.debug( 14 | "{}({}) was configured from {}", 15 | sourceType.sourceConfigurationPath, 16 | sourceType.resourceName, 17 | collectionSource.pluginName 18 | ) 19 | fileCollection 20 | } 21 | } 22 | 23 | private fun throwMissedConfigurationException( 24 | collectionSource: FileCollectionSource, 25 | sourceType: ConfigurationSourceType 26 | ): Nothing { 27 | val errorMessage = if (collectionSource == FileCollectionSource.DIFF_COVERAGE) { 28 | "'${sourceType.sourceConfigurationPath}' file collection is empty." 29 | } else { 30 | "'${sourceType.sourceConfigurationPath}' is not configured." 31 | } 32 | throw IllegalArgumentException(errorMessage) 33 | } 34 | 35 | private fun DiffCoverageTask.collectFileCollectionOrAutoconfigure( 36 | configurationType: ConfigurationSourceType 37 | ): Pair { 38 | val configurationSource: ConfigurationSource = obtainConfigurationSource(configurationType) 39 | val customConfigurationSource: FileCollection? = configurationSource.customConfigurationSource(diffCoverageReport) 40 | return if (customConfigurationSource != null) { 41 | FileCollectionSource.DIFF_COVERAGE to customConfigurationSource 42 | } else { 43 | logger.debug( 44 | "{} is not configured. Attempting to autoconfigure from JaCoCo...", 45 | configurationType.sourceConfigurationPath 46 | ) 47 | FileCollectionSource.JACOCO to jacocoTestReportsSettings(configurationSource.autoconfigurationMapper) 48 | } 49 | } 50 | 51 | private fun DiffCoverageTask.jacocoTestReportsSettings( 52 | jacocoSettings: (JacocoReport) -> FileCollection 53 | ): FileCollection { 54 | return listOf(project).union(project.subprojects).asSequence() 55 | .map { it.tasks.findByName(DiffCoverageTask.JACOCO_REPORT_TASK) } 56 | .filterNotNull() 57 | .onEach { logger.debug("Found JaCoCo configuration in gradle project '{}'", it.project.name) } 58 | .map { jacocoSettings(it as JacocoReport) } 59 | .fold(project.files() as FileCollection) { aggregated, nextCollection -> 60 | aggregated.plus(nextCollection) 61 | } 62 | } 63 | 64 | private fun obtainConfigurationSource( 65 | configurationSourceType: ConfigurationSourceType 66 | ): ConfigurationSource { 67 | return when (configurationSourceType) { 68 | ConfigurationSourceType.CLASSES -> ConfigurationSource(JacocoReport::getAllClassDirs) { 69 | it.classesDirs 70 | } 71 | ConfigurationSourceType.SOURCES -> ConfigurationSource(JacocoReport::getAllSourceDirs) { 72 | it.srcDirs 73 | } 74 | ConfigurationSourceType.EXEC -> ConfigurationSource(JacocoReport::getExecutionData) { 75 | it.jacocoExecFiles 76 | } 77 | } 78 | } 79 | 80 | private class ConfigurationSource( 81 | val autoconfigurationMapper: (JacocoReport) -> FileCollection, 82 | val customConfigurationSource: (ChangesetCoverageConfiguration) -> FileCollection? 83 | ) 84 | 85 | internal enum class ConfigurationSourceType( 86 | val sourceConfigurationPath: String, 87 | val resourceName: String 88 | ) { 89 | CLASSES("diffCoverageReport.classesDirs", ".class files"), 90 | SOURCES("diffCoverageReport.srcDirs", "sources"), 91 | EXEC("diffCoverageReport.jacocoExecFiles", ".exec files") 92 | } 93 | 94 | private enum class FileCollectionSource(val pluginName: String) { 95 | JACOCO("JaCoCo"), DIFF_COVERAGE("Diff-Coverage") 96 | } 97 | -------------------------------------------------------------------------------- /diff-coverage/src/test/kotlin/com/form/coverage/gradle/DiffCoverageTaskTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import com.form.coverage.gradle.DiffCoveragePlugin.Companion.DIFF_COV_TASK 4 | import com.form.coverage.gradle.DiffCoverageTask.Companion.JACOCO_REPORT_TASK 5 | import io.kotest.assertions.throwables.shouldThrow 6 | import io.kotest.core.spec.style.StringSpec 7 | import io.kotest.data.blocking.forAll 8 | import io.kotest.data.row 9 | import io.kotest.matchers.string.shouldBeEqualIgnoringCase 10 | import io.mockk.every 11 | import io.mockk.mockk 12 | import io.mockk.spyk 13 | import org.gradle.api.Project 14 | import org.gradle.api.file.FileCollection 15 | import org.gradle.api.internal.tasks.DefaultTaskContainer 16 | import org.gradle.testfixtures.ProjectBuilder 17 | 18 | class DiffCoverageTaskTest : StringSpec() { 19 | 20 | init { 21 | "get input file collection should throw when file collection is not specified" { 22 | forAll( 23 | row("'diffCoverageReport.jacocoExecFiles' is not configured.", DiffCoverageTask::getExecFiles), 24 | row("'diffCoverageReport.classesDirs' is not configured.", DiffCoverageTask::getClassesFiles), 25 | row("'diffCoverageReport.srcDirs' is not configured.", DiffCoverageTask::getSourcesFiles) 26 | ) { expectedError, sourceAccessor -> 27 | // setup 28 | val coverageConfiguration = ChangesetCoverageConfiguration() 29 | val diffCoverageTask: DiffCoverageTask = spyDiffCoverageTask(coverageConfiguration) 30 | 31 | // run 32 | val exception = shouldThrow { 33 | sourceAccessor(diffCoverageTask) 34 | } 35 | 36 | // assert 37 | exception.message shouldBeEqualIgnoringCase expectedError 38 | } 39 | } 40 | 41 | "get input file collection should throw when file collection is empty" { 42 | forAll( 43 | row("'diffCoverageReport.jacocoExecFiles' file collection is empty.", DiffCoverageTask::getExecFiles), 44 | row("'diffCoverageReport.classesDirs' file collection is empty.", DiffCoverageTask::getClassesFiles), 45 | row("'diffCoverageReport.srcDirs' file collection is empty.", DiffCoverageTask::getSourcesFiles) 46 | ) { expectedError, sourceAccessor -> 47 | // setup 48 | val emptyFileCollection: FileCollection = mockk { 49 | every { isEmpty } returns true 50 | } 51 | val diffCoverageTask: DiffCoverageTask = spyDiffCoverageTask( 52 | ChangesetCoverageConfiguration().apply { 53 | jacocoExecFiles = emptyFileCollection 54 | classesDirs = emptyFileCollection 55 | srcDirs = emptyFileCollection 56 | } 57 | ) 58 | 59 | // run 60 | val exception = shouldThrow { 61 | sourceAccessor(diffCoverageTask) 62 | } 63 | 64 | // assert 65 | exception.message shouldBeEqualIgnoringCase expectedError 66 | } 67 | } 68 | 69 | } 70 | 71 | private fun spyDiffCoverageTask(configuration: ChangesetCoverageConfiguration): DiffCoverageTask { 72 | val coverageTask = ProjectBuilder.builder().build().tasks 73 | .create(DIFF_COV_TASK, DiffCoverageTask::class.java) { 74 | it.diffCoverageReport = configuration 75 | } 76 | return spyk(coverageTask) { 77 | every { project } returns mockProject() 78 | } 79 | } 80 | 81 | private fun mockProject() = mockk { 82 | every { subprojects } returns emptySet() 83 | every { tasks } returns mockk { 84 | every { findByName(JACOCO_REPORT_TASK) } returns null 85 | } 86 | every { files() } returns mockk { 87 | every { isEmpty } returns true 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/diff/git/JgitDiff.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff.git 2 | 3 | import org.eclipse.jgit.api.Git 4 | import org.eclipse.jgit.diff.DiffEntry 5 | import org.eclipse.jgit.diff.DiffFormatter 6 | import org.eclipse.jgit.lib.ConfigConstants 7 | import org.eclipse.jgit.lib.Constants 8 | import org.eclipse.jgit.lib.ObjectId 9 | import org.eclipse.jgit.lib.Repository 10 | import org.eclipse.jgit.revwalk.RevCommit 11 | import org.eclipse.jgit.revwalk.RevWalk 12 | import org.eclipse.jgit.revwalk.filter.RevFilter 13 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder 14 | import org.eclipse.jgit.treewalk.AbstractTreeIterator 15 | import org.eclipse.jgit.treewalk.CanonicalTreeParser 16 | import org.eclipse.jgit.treewalk.filter.TreeFilter 17 | import java.io.ByteArrayOutputStream 18 | import java.io.File 19 | 20 | class JgitDiff(workingDir: File) { 21 | 22 | private val repository: Repository = initRepository(workingDir) 23 | 24 | private fun initRepository(workingDir: File): Repository = try { 25 | FileRepositoryBuilder().apply { 26 | findGitDir(workingDir) 27 | readEnvironment() 28 | isMustExist = true 29 | }.build() 30 | } catch (e: IllegalArgumentException) { 31 | throw IllegalArgumentException( 32 | "Git directory not found in the project root ${workingDir.absolutePath}", 33 | e 34 | ) 35 | } 36 | 37 | fun obtain(revision: String): String { 38 | val diffContent = ByteArrayOutputStream() 39 | Git(repository).use { git -> 40 | DiffFormatter(diffContent).apply { 41 | initialize() 42 | 43 | obtainDiffEntries(git, revision).forEach { 44 | format(it) 45 | } 46 | 47 | close() 48 | } 49 | } 50 | 51 | return String(diffContent.toByteArray()) 52 | } 53 | 54 | private fun DiffFormatter.initialize() { 55 | setRepository(repository) 56 | repository.config.setEnum( 57 | ConfigConstants.CONFIG_CORE_SECTION, 58 | null, 59 | ConfigConstants.CONFIG_KEY_AUTOCRLF, 60 | getCrlf() 61 | ) 62 | setQuotePaths(false) 63 | pathFilter = TreeFilter.ALL 64 | } 65 | 66 | private fun obtainDiffEntries(git: Git, target: String): List { 67 | repository.newObjectReader().use { reader -> 68 | RevWalk(repository).use { revWalk -> 69 | revWalk.revFilter = RevFilter.MERGE_BASE 70 | 71 | val targetId: ObjectId = repository.resolve(target) ?: throw buildUnknownRevisionException(target) 72 | revWalk.markStart(revWalk.parseCommit(targetId)) 73 | 74 | val currentHeadCommit: RevCommit = revWalk.parseCommit(repository.resolve(Constants.HEAD)) 75 | revWalk.markStart(currentHeadCommit) 76 | 77 | val targetRevisionTreeParser: AbstractTreeIterator = CanonicalTreeParser().apply { 78 | // Be careful, commits may have multiple merge bases where diff A...B is complicated 79 | val base: RevCommit = revWalk.parseCommit(revWalk.next()) 80 | reset(reader, base.tree) 81 | } 82 | 83 | val currentHeadTreeParser = CanonicalTreeParser().apply { 84 | reset(reader, currentHeadCommit.tree) 85 | } 86 | 87 | return git.diff() 88 | .setOldTree(targetRevisionTreeParser) 89 | .setNewTree(currentHeadTreeParser) 90 | .setCached(true) 91 | .call() 92 | } 93 | } 94 | } 95 | 96 | private fun buildUnknownRevisionException(name: String): UnknownRevisionException { 97 | return UnknownRevisionException( 98 | """ 99 | Unknown revision '$name'. Available branches: ${branches()} 100 | """.trimIndent() 101 | ) 102 | } 103 | 104 | private fun branches(): String { 105 | return Git(repository).branchList().call() 106 | .asSequence() 107 | .map { it.name } 108 | .sorted() 109 | .joinToString(", ") 110 | } 111 | 112 | } 113 | 114 | class UnknownRevisionException(message: String) : RuntimeException(message) 115 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/kotlin/com/form/coverage/diff/CodeUpdateInfoTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff 2 | 3 | import com.form.coverage.diff.parse.ClassFile 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.data.blocking.forAll 6 | import io.kotest.data.row 7 | import io.kotest.matchers.shouldBe 8 | import io.kotest.property.checkAll 9 | 10 | class ClassModificationsTest : StringSpec({ 11 | 12 | "isLineModified should return true or false depends on line is modified or not" { 13 | forAll( 14 | row(true, 1, setOf(1, 2, 3)), 15 | row(false, 1, setOf()), 16 | row(false, 0, setOf(1, 2)), 17 | row(false, -1, setOf(1, 2)), 18 | row(false, 3, setOf(1, 2)) 19 | ) { isModified, line, lines -> 20 | // setup 21 | val classModifications = ClassModifications(lines) 22 | 23 | // assert 24 | classModifications.isLineModified(line) shouldBe isModified 25 | } 26 | } 27 | }) 28 | 29 | class CodeUpdateInfoTest : StringSpec({ 30 | 31 | "getClassModifications should return empty ClassModifications when no such info" { 32 | checkAll(10) { lineNumber -> 33 | // setup 34 | val codeUpdateInfo = CodeUpdateInfo( 35 | mapOf("com/package/Class.java" to setOf(12)) 36 | ) 37 | 38 | // run 39 | val classModifications = codeUpdateInfo.getClassModifications( 40 | ClassFile("UnknownClass.java", "com/package/UnknownClass") 41 | ) 42 | 43 | // assert 44 | classModifications.isLineModified(lineNumber) shouldBe false 45 | } 46 | } 47 | 48 | 49 | "isInfoExists should return true when modifications info exists for class" { 50 | forAll( 51 | row(setOf(1, 2, 3)), 52 | row(setOf(1, 2)) 53 | ) { set -> 54 | // setup 55 | val codeUpdateInfo = CodeUpdateInfo( 56 | mapOf("module/src/main/java/com/package/Class.java" to set) 57 | ) 58 | 59 | // run 60 | val infoExists = codeUpdateInfo.isInfoExists( 61 | ClassFile("Class.java", "com/package/Class") 62 | ) 63 | 64 | // assert 65 | infoExists shouldBe true 66 | } 67 | } 68 | 69 | "getClassModifications should return modifications for class when there is similar class name exists" { 70 | // setup 71 | val expectedLineNumber = 1 72 | val requestedLineNumber1 = 2 73 | val requestedLineNumber2 = 3 74 | val codeUpdateInfo = CodeUpdateInfo( 75 | mapOf( 76 | "src/com/package/ClassSuffix.java" to setOf(requestedLineNumber1), 77 | "src/com/package/Class.java" to setOf(expectedLineNumber), 78 | "src/com/package/PrefixClass.java" to setOf(requestedLineNumber2) 79 | ) 80 | ) 81 | 82 | // run 83 | val modifications = codeUpdateInfo.getClassModifications( 84 | ClassFile("Class.java", "com/package/Class") 85 | ) 86 | 87 | // assert 88 | modifications.isLineModified(2) shouldBe false 89 | modifications.isLineModified(3) shouldBe false 90 | modifications.isLineModified(1) shouldBe true 91 | } 92 | 93 | "isInfoExists should return false when modifications info doesn't exist for class" { 94 | forAll( 95 | row( 96 | "OtherClass.java", 97 | "com/package/OtherClass", 98 | mapOf("src/java/com/package/Class.java" to setOf(1, 2, 3)) 99 | ), 100 | row( 101 | "Class.java", 102 | "com/package/Class", 103 | mapOf("src/java/com/package/Class.java" to setOf()) 104 | ), 105 | row( 106 | "Class.java", 107 | "com/package/Class", 108 | mapOf() 109 | ) 110 | ) { classSourceFile, classNameToCheck, mapOfModifiedLines -> 111 | // setup 112 | val codeUpdateInfo = CodeUpdateInfo(mapOfModifiedLines) 113 | 114 | // run 115 | val infoExists = codeUpdateInfo.isInfoExists( 116 | ClassFile(classSourceFile, classNameToCheck) 117 | ) 118 | 119 | // assert 120 | infoExists shouldBe false 121 | } 122 | } 123 | 124 | }) 125 | -------------------------------------------------------------------------------- /diff-coverage/src/main/kotlin/com/form/coverage/gradle/DiffCoverageTask.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import com.form.coverage.config.DiffCoverageConfig 4 | import com.form.coverage.config.DiffSourceConfig 5 | import com.form.coverage.config.ReportConfig 6 | import com.form.coverage.config.ReportsConfig 7 | import com.form.coverage.config.ViolationRuleConfig 8 | import com.form.coverage.report.ReportGenerator 9 | import org.gradle.api.DefaultTask 10 | import org.gradle.api.Project 11 | import org.gradle.api.file.FileCollection 12 | import org.gradle.api.tasks.Input 13 | import org.gradle.api.tasks.InputFiles 14 | import org.gradle.api.tasks.Nested 15 | import org.gradle.api.tasks.OutputDirectory 16 | import org.gradle.api.tasks.TaskAction 17 | import java.io.File 18 | import java.nio.file.Path 19 | import java.nio.file.Paths 20 | 21 | open class DiffCoverageTask : DefaultTask() { 22 | 23 | init { 24 | group = "verification" 25 | description = "Builds coverage report only for modified code" 26 | } 27 | 28 | @Nested 29 | var diffCoverageReport: ChangesetCoverageConfiguration = ChangesetCoverageConfiguration() 30 | 31 | @InputFiles 32 | fun getExecFiles(): FileCollection = collectFileCollectionOrThrow(ConfigurationSourceType.EXEC) 33 | 34 | @InputFiles 35 | fun getClassesFiles(): FileCollection = collectFileCollectionOrThrow(ConfigurationSourceType.CLASSES) 36 | 37 | @InputFiles 38 | fun getSourcesFiles(): FileCollection = collectFileCollectionOrThrow(ConfigurationSourceType.SOURCES) 39 | 40 | @Input 41 | fun getDiffSource(): String = diffCoverageReport.diffSource.let { it.url + it.file } 42 | 43 | @OutputDirectory 44 | fun getOutputDir(): File { 45 | return project.getReportOutputDir().toFile().apply { 46 | logger.debug( 47 | "Diff Coverage output dir: $absolutePath, " + 48 | "exists=${exists()}, isDir=$isDirectory, canRead=${canRead()}, canWrite=${canWrite()}" 49 | ) 50 | } 51 | } 52 | 53 | @TaskAction 54 | fun executeAction() { 55 | logger.info("DiffCoverage configuration: $diffCoverageReport") 56 | val reportDir: File = getOutputDir().apply { 57 | val isCreated = mkdirs() 58 | logger.debug("Creating of report dir '$absolutePath' is successful: $isCreated") 59 | } 60 | 61 | val reportGenerator = ReportGenerator(project.rootProject.projectDir, buildDiffCoverageConfig()) 62 | reportGenerator.saveDiffToDir(reportDir).apply { 63 | logger.info("diff content saved to '$absolutePath'") 64 | } 65 | reportGenerator.create() 66 | } 67 | 68 | private fun Project.getReportOutputDir(): Path { 69 | return Paths.get(diffCoverageReport.reportConfiguration.baseReportDir).let { 70 | if (it.isAbsolute) { 71 | it 72 | } else { 73 | project.projectDir.toPath().resolve(it) 74 | } 75 | } 76 | } 77 | 78 | private fun buildDiffCoverageConfig(): DiffCoverageConfig { 79 | return DiffCoverageConfig( 80 | reportName = project.projectDir.name, 81 | diffSourceConfig = DiffSourceConfig( 82 | file = diffCoverageReport.diffSource.file, 83 | url = diffCoverageReport.diffSource.url, 84 | diffBase = diffCoverageReport.diffSource.git.diffBase 85 | ), 86 | reportsConfig = ReportsConfig( 87 | baseReportDir = project.getReportOutputDir().toAbsolutePath().toString(), 88 | html = ReportConfig(enabled = diffCoverageReport.reportConfiguration.html, "html"), 89 | csv = ReportConfig(enabled = diffCoverageReport.reportConfiguration.csv, "report.csv"), 90 | xml = ReportConfig(enabled = diffCoverageReport.reportConfiguration.xml, "report.xml"), 91 | fullCoverageReport = diffCoverageReport.reportConfiguration.fullCoverageReport 92 | ), 93 | violationRuleConfig = ViolationRuleConfig( 94 | minBranches = diffCoverageReport.violationRules.minBranches, 95 | minInstructions = diffCoverageReport.violationRules.minInstructions, 96 | minLines = diffCoverageReport.violationRules.minLines, 97 | failOnViolation = diffCoverageReport.violationRules.failOnViolation 98 | ), 99 | execFiles = getExecFiles().files, 100 | classFiles = getClassesFiles().files, 101 | sourceFiles = getSourcesFiles().files 102 | ) 103 | } 104 | 105 | companion object { 106 | const val JACOCO_REPORT_TASK = "jacocoTestReport" 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/org/jacoco/core/internal/analysis/FilteringAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package org.jacoco.core.internal.analysis 2 | 3 | import com.form.coverage.diff.parse.ClassFile 4 | import org.jacoco.core.analysis.Analyzer 5 | import org.jacoco.core.analysis.IClassCoverage 6 | import org.jacoco.core.analysis.ICoverageVisitor 7 | import org.jacoco.core.data.ExecutionDataStore 8 | import org.jacoco.core.internal.analysis.filter.IFilter 9 | import org.jacoco.core.internal.data.CRC64 10 | import org.jacoco.core.internal.flow.ClassProbesAdapter 11 | import org.jacoco.core.internal.instr.InstrSupport 12 | import org.objectweb.asm.ClassReader 13 | import org.objectweb.asm.ClassVisitor 14 | import org.objectweb.asm.Opcodes 15 | import java.io.IOException 16 | 17 | class FilteringAnalyzer( 18 | private val executionData: ExecutionDataStore, 19 | private val coverageVisitor: ICoverageVisitor, 20 | private val classFilter: (ClassFile) -> Boolean, 21 | private val customFilterProvider: (IClassCoverage) -> IFilter 22 | ) : Analyzer(executionData, coverageVisitor) { 23 | 24 | override fun analyzeClass(buffer: ByteArray, location: String) { 25 | try { 26 | analyzeClass(buffer) 27 | } catch (cause: RuntimeException) { 28 | throw analyzerError(location, cause) 29 | } 30 | } 31 | 32 | private fun analyzeClass(source: ByteArray) { 33 | val classId = CRC64.classId(source) 34 | val reader = InstrSupport.classReaderFor(source) 35 | if (reader.access and Opcodes.ACC_MODULE != 0) { 36 | return 37 | } 38 | if (reader.access and Opcodes.ACC_SYNTHETIC != 0) { 39 | return 40 | } 41 | val shouldComputeClassCoverage = SourceFileNameReader(source).readFileName() 42 | ?.let { ClassFile(it, reader.className) } 43 | ?.let { classFilter(it) } 44 | ?: false 45 | if (shouldComputeClassCoverage) { 46 | reader.accept( 47 | createAnalyzingVisitor(classId, reader.className), 48 | 0 49 | ) 50 | } 51 | } 52 | 53 | private fun createAnalyzingVisitor(classid: Long, className: String): ClassVisitor { 54 | val data = executionData.get(classid) 55 | 56 | val (probes, noMatch) = if (data == null) { 57 | ExecutionInfo(null, executionData.contains(className)) 58 | } else { 59 | ExecutionInfo(data.probes, false) 60 | } 61 | 62 | return ClassProbesAdapter( 63 | buildClassAnalyzer( 64 | ClassCoverageImpl(className, classid, noMatch), 65 | probes 66 | ), 67 | false 68 | ) 69 | } 70 | 71 | private fun buildClassAnalyzer( 72 | coverage: ClassCoverageImpl, 73 | probes: BooleanArray? 74 | ): FilteringClassAnalyzer { 75 | return object : FilteringClassAnalyzer( 76 | coverage, 77 | probes, 78 | StringPool(), 79 | customFilterProvider(coverage) 80 | ) { 81 | override fun visitEnd() { 82 | super.visitEnd() 83 | coverageVisitor.visitCoverage(coverage) 84 | } 85 | } 86 | } 87 | 88 | private fun analyzerError(location: String, cause: Exception): IOException { 89 | return IOException("Error while analyzing $location.").apply { 90 | initCause(cause) 91 | } 92 | } 93 | 94 | private data class ExecutionInfo( 95 | val probes: BooleanArray?, 96 | val noMatch: Boolean 97 | ) 98 | 99 | private class SourceFileNameReader(source: ByteArray): ClassReader(source) { 100 | 101 | fun readFileName(): String? { 102 | val charBuffer = CharArray(maxStringLength) 103 | var shift = computeAttributesShift() 104 | for (i in readUnsignedShort(shift) downTo 1) { 105 | val attrName = readUTF8(shift + 2, charBuffer) 106 | if ("SourceFile" == attrName) { 107 | return readUTF8(shift + 8, charBuffer) 108 | } 109 | shift += 6 + readInt(shift + 4) 110 | } 111 | return null 112 | } 113 | 114 | private fun computeAttributesShift(): Int { 115 | // skips the header 116 | var shift = header + 8 + readUnsignedShort(header + 6) * 2 117 | // skips fields and methods 118 | for (i in readUnsignedShort(shift) downTo 1) { 119 | for (j in readUnsignedShort(shift + 8) downTo 1) { 120 | shift += 6 + readInt(shift + 12) 121 | } 122 | shift += 8 123 | } 124 | shift += 2 125 | for (i in readUnsignedShort(shift) downTo 1) { 126 | for (j in readUnsignedShort(shift + 8) downTo 1) { 127 | shift += 6 + readInt(shift + 12) 128 | } 129 | shift += 8 130 | } 131 | // the attribute_info structure starts just after the methods 132 | return shift + 2 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/main/kotlin/com/form/coverage/diff/parse/ModifiedLinesDiffParser.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff.parse 2 | 3 | import org.eclipse.jgit.util.QuotedString 4 | import org.slf4j.LoggerFactory 5 | import java.util.regex.Pattern 6 | 7 | internal class ModifiedLinesDiffParser { 8 | 9 | fun collectModifiedLines(lines: List): Map> { 10 | val iterator = lines.listIterator() 11 | 12 | val fileNameToChangedLines = HashMap>() 13 | while (iterator.hasNext()) { 14 | val patchedFileRow = moveToNextFile(iterator) ?: return fileNameToChangedLines 15 | val patchedFileRelativePath = parseFileRelativePath(patchedFileRow) 16 | log.debug("Found modified file: $patchedFileRelativePath") 17 | 18 | val fileChangedLines = collectFilesChangedLines(iterator) 19 | 20 | if (fileChangedLines.isNotEmpty()) { 21 | fileNameToChangedLines[patchedFileRelativePath] = fileChangedLines 22 | } 23 | } 24 | return fileNameToChangedLines 25 | } 26 | 27 | private fun parseFileRelativePath(diffFilePath: String): String { 28 | val parsedPath = parseFilePath(FILE_RELATIVE_PATH_PATTERN, diffFilePath) 29 | ?: parseFilePath(FILE_RELATIVE_PATH_QUOTED_PATTERN, diffFilePath, QuotedString.GIT_PATH::dequote) 30 | 31 | return parsedPath ?: throw IllegalArgumentException("Couldn't parse file relative path: $diffFilePath") 32 | } 33 | 34 | private fun parseFilePath( 35 | pattern: Pattern, 36 | diffFilePath: String, 37 | pathMapper: (String) -> String = { it } 38 | ): String? { 39 | val matcher = pattern.matcher(diffFilePath) 40 | return if (matcher.find()) { 41 | pathMapper(matcher.group(1)) 42 | } else { 43 | null 44 | } 45 | } 46 | 47 | private fun moveToNextFile(iterator: ListIterator): String? { 48 | while (iterator.hasNext()) { 49 | val next = iterator.next() 50 | if (next.startsWith(FILE_NAME_TO_SIGNS)) { 51 | return next 52 | } 53 | } 54 | return null 55 | } 56 | 57 | private fun collectFilesChangedLines( 58 | iterator: ListIterator 59 | ): Set { 60 | val fileChangedLines = HashSet() 61 | while (iterator.hasNext()) { 62 | val nextLine = iterator.next() 63 | if (nextLine.startsWith(HUNK_RANGE_INFO_SIGNS)) { 64 | val fileOffset = parseFileDiffBlockOffset(nextLine) 65 | fileChangedLines += obtainFilesAddedOrUpdatedLines(iterator, fileOffset) 66 | } else { 67 | break 68 | } 69 | } 70 | 71 | return fileChangedLines 72 | } 73 | 74 | private fun parseFileDiffBlockOffset(line: String): Int { 75 | val matcher = FILE_OFFSET_PATTERN.matcher(line) 76 | return if (matcher.find()) { 77 | matcher.group(1).toInt() 78 | } else { 79 | throw IllegalArgumentException("Couldn't parse file's range information: $line") 80 | } 81 | } 82 | 83 | private fun obtainFilesAddedOrUpdatedLines( 84 | iterator: ListIterator, 85 | fileOffset: Int 86 | ): Set { 87 | val modifiedOrAddedLinesNumbers = HashSet() 88 | var lineNumber = fileOffset 89 | while (iterator.hasNext()) { 90 | val line = iterator.next() 91 | when { 92 | line.isLineAdded() -> modifiedOrAddedLinesNumbers += lineNumber 93 | line.isLineDeleted() -> lineNumber -= 1 94 | line.isNoNewLineAtEndOfFile() -> Unit // do nothing 95 | line.isNotEmpty() && !line.isNoModLine() -> { 96 | iterator.previous() 97 | return modifiedOrAddedLinesNumbers 98 | } 99 | } 100 | 101 | lineNumber++ 102 | } 103 | return modifiedOrAddedLinesNumbers 104 | } 105 | 106 | private fun String.isLineAdded() = startsWith(ADDED_LINE_SIGN) 107 | private fun String.isLineDeleted() = startsWith(DELETED_LINE_SIGN) && !startsWith(FILE_NAME_FROM_SIGNS) 108 | private fun String.isNoModLine() = startsWith(NO_MOD_LINE_SIGN) 109 | private fun String.isNoNewLineAtEndOfFile() = startsWith(NO_NEW_LINE_AT_END_OF_FILE) 110 | 111 | private companion object { 112 | val FILE_OFFSET_PATTERN = Pattern.compile("^@@.*\\+(\\d+)(,\\d+)? @@")!! 113 | val FILE_RELATIVE_PATH_PATTERN = Pattern.compile("^\\+\\+\\+\\s([^\"][^\\t]*)([\\t]+.*)?\$")!! 114 | val FILE_RELATIVE_PATH_QUOTED_PATTERN = Pattern.compile("^\\+\\+\\+\\s(\".+?\")\\s*\\t*.*\$")!! 115 | 116 | val log = LoggerFactory.getLogger(ModifiedLinesDiffParser::class.java) 117 | 118 | const val ADDED_LINE_SIGN = '+' 119 | const val DELETED_LINE_SIGN = '-' 120 | const val NO_MOD_LINE_SIGN = ' ' 121 | const val NO_NEW_LINE_AT_END_OF_FILE = "\\ No newline at end of file" 122 | 123 | const val FILE_NAME_FROM_SIGNS = "--- " 124 | const val FILE_NAME_TO_SIGNS = "+++ " 125 | const val HUNK_RANGE_INFO_SIGNS = "@@" 126 | } 127 | } 128 | 129 | 130 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/kotlin/com/form/coverage/diff/git/JgitDiffTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff.git 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.matchers.should 6 | import io.kotest.matchers.string.match 7 | import io.kotest.matchers.string.shouldContainOnlyOnce 8 | import io.kotest.matchers.string.shouldEndWith 9 | import io.kotest.matchers.string.startWith 10 | import org.eclipse.jgit.api.Git 11 | import org.eclipse.jgit.api.GitCommand 12 | import org.eclipse.jgit.lib.ConfigConstants 13 | import org.eclipse.jgit.lib.Repository 14 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder 15 | import java.io.File 16 | import java.nio.file.Files 17 | 18 | class JgitDiffTest : StringSpec() { 19 | private lateinit var rootProjectDir: File 20 | 21 | init { 22 | beforeEach { 23 | rootProjectDir = Files.createTempDirectory("JgitDiffTest").toFile() 24 | } 25 | afterEach { 26 | rootProjectDir.delete() 27 | } 28 | 29 | "JgitDiff should throw when git is not initialized"{ 30 | val exception = shouldThrow { 31 | JgitDiff(rootProjectDir) 32 | } 33 | // assert 34 | exception.message should startWith("Git directory not found in the project root") 35 | } 36 | 37 | "jgit diff must encode file path with special symbols" { 38 | Git(initRepository(rootProjectDir)).use { git -> 39 | rootProjectDir.resolve("# 1 } 2.txt").appendText("new-text\n") 40 | git.command(Git::add) { addFilepattern(".") } 41 | } 42 | 43 | val diff: String = JgitDiff(rootProjectDir).obtain("HEAD") 44 | 45 | diff shouldContainOnlyOnce "+++ \"b/\\043 1 \\175 2.txt\"" 46 | } 47 | 48 | "jgit diff must throw if branch name is unknown" { 49 | // setup 50 | val branchName = "unknown-branch" 51 | initRepository(rootProjectDir) 52 | 53 | // run 54 | val exception = shouldThrow { 55 | JgitDiff(rootProjectDir).obtain(branchName) 56 | } 57 | 58 | // assert 59 | exception.message should match( 60 | "Unknown revision '$branchName'. Available branches: refs/heads/master" 61 | ) 62 | } 63 | 64 | "jgit diff must merge changes from target branch" { 65 | // setup 66 | val master = "master" 67 | val testBranch = "current_branch" 68 | 69 | val testFile: File = rootProjectDir.resolve("test-file.txt").apply { 70 | writeText(""" 71 | 1 72 | 2 73 | 3 74 | 75 | """.trimIndent()) 76 | } 77 | Git(initRepository(rootProjectDir)).use { git -> 78 | // create new branch, checkout and apply changes with further commit 79 | git.apply { 80 | checkout(testBranch, true) 81 | writeToFileAndCommit( 82 | testFile, 83 | """ 84 | 1 85 | 2 $testBranch 86 | 3 87 | 88 | """.trimIndent() 89 | ) 90 | } 91 | // checkout master and apply changes with further commit 92 | git.apply { 93 | checkout(master) 94 | writeToFileAndCommit( 95 | testFile, 96 | """ 97 | 1 $master 98 | 2 99 | 3 100 | 101 | """.trimIndent() 102 | ) 103 | } 104 | // checkout back to custom branch and apply changes without commit 105 | git.apply { 106 | checkout(testBranch) 107 | testFile.writeText( 108 | """ 109 | 1 110 | 2 $testBranch 111 | 3 $testBranch 112 | 113 | """.trimIndent() 114 | ) 115 | git.command(Git::add) { addFilepattern(".") } 116 | } 117 | } 118 | 119 | // run 120 | val actualDiff: String = JgitDiff(rootProjectDir).obtain(master) 121 | 122 | // assert 123 | val expectedDiff = """ 124 | 1 125 | -2 126 | -3 127 | +2 $testBranch 128 | +3 $testBranch 129 | 130 | """.trimIndent() 131 | actualDiff shouldEndWith expectedDiff 132 | } 133 | } 134 | 135 | private fun initRepository(rootProjectDir: File): Repository { 136 | val repository: Repository = FileRepositoryBuilder.create(File(rootProjectDir, ".git")).apply { 137 | config.setEnum( 138 | ConfigConstants.CONFIG_CORE_SECTION, 139 | null, 140 | ConfigConstants.CONFIG_KEY_AUTOCRLF, 141 | getCrlf() 142 | ) 143 | create() 144 | } 145 | Git(repository).use { git -> 146 | git.command(Git::add) { addFilepattern(".") } 147 | git.command(Git::commit) { message = "Add all" } 148 | } 149 | return repository 150 | } 151 | 152 | private fun Git.writeToFileAndCommit(file: File, content: String) { 153 | file.writeText(content) 154 | command(Git::add) { addFilepattern(".") } 155 | command(Git::commit) { message = "commit msg" } 156 | } 157 | 158 | private fun Git.checkout(branchName: String, createNew: Boolean = false) { 159 | command(Git::checkout) { 160 | setName(branchName) 161 | setCreateBranch(createNew) 162 | } 163 | } 164 | 165 | private fun > Git.command( 166 | command: Git.() -> T, 167 | configure: T.() -> Unit = {} 168 | ): R { 169 | return command().apply(configure).call() 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diff coverage gradle plugin 2 | [![](https://jitpack.io/v/form-com/diff-coverage-gradle.svg)](https://jitpack.io/#form-com/diff-coverage-gradle) 3 | ![CI](https://github.com/form-com/diff-coverage-gradle/workflows/CI/badge.svg) 4 | [![codecov](https://codecov.io/gh/form-com/diff-coverage-gradle/branch/develop/graph/badge.svg)](https://codecov.io/gh/form-com/diff-coverage-gradle) 5 | [![GitHub issues](https://img.shields.io/github/issues/form-com/diff-coverage-gradle)](https://github.com/form-com/diff-coverage-gradle/issues) 6 | [![GitHub stars](https://img.shields.io/github/stars/form-com/diff-coverage-gradle?style=flat-square)](https://github.com/form-com/diff-coverage-gradle/stargazers) 7 | [![](https://jitpack.io/v/form-com/diff-coverage-gradle/month.svg)](https://jitpack.io/#form-com/diff-coverage-gradle) 8 | 9 | `Diff coverage` is JaCoCo extension that computes code coverage of new/modified code based on a provided [diff](https://en.wikipedia.org/wiki/Diff#Unified_format). 10 | The diff content can be provided via path to patch file, URL or using embedded git(see [parameters description](#Parameters-description)). 11 | 12 | Why should I use it? 13 | * forces each developer to be responsible for its own code quality(see [diffCoverage task](#gradle-task-description)) 14 | * helps to increase total code coverage(especially useful for old legacy projects) 15 | * reduces time of code review(you don't need to waste your time to track what code is covered) 16 | 17 | ## Installation 18 | ### Add plugin dependency 19 | 20 |
21 | 22 | Groovy 23 | 24 | ```groovy 25 | buildscript { 26 | repositories { 27 | maven { url 'https://jitpack.io' } 28 | } 29 | dependencies { 30 | classpath 'com.github.form-com.diff-coverage-gradle:diff-coverage:0.9.4' 31 | } 32 | } 33 | ``` 34 | 35 |
36 |
37 | Kotlin 38 | 39 | ```kotlin 40 | buildscript { 41 | repositories { 42 | maven("https://jitpack.io") 43 | } 44 | dependencies { 45 | classpath("com.github.form-com.diff-coverage-gradle:diff-coverage:0.9.4") 46 | } 47 | } 48 | ``` 49 | 50 |
51 | 52 | ### Apply `JaCoCo` and `Diff Coverage` plugins 53 | * `JaCoCo` is used to collect coverage data 54 | * `Diff Coverage` is used to generate diff report 55 | 56 |
57 | Groovy 58 | 59 | ```groovy 60 | apply plugin: 'jacoco' 61 | apply plugin: 'com.form.diff-coverage' 62 | ``` 63 | 64 |
65 |
66 | Kotlin 67 | 68 | ```kotlin 69 | plugins { 70 | jacoco 71 | } 72 | apply(plugin = "com.form.diff-coverage") 73 | ``` 74 | 75 |
76 | 77 | ## Configuration 78 | 79 |
80 | Groovy 81 | 82 | ```groovy 83 | diffCoverageReport { 84 | diffSource.file = ${PATH_TO_DIFF_FILE} 85 | 86 | violationRules.failIfCoverageLessThan 0.9 87 | 88 | reports { 89 | html = true 90 | } 91 | } 92 | ``` 93 | 94 |
95 |
96 | Kotlin 97 | 98 | ```kotlin 99 | configure { 100 | diffSource.file = ${PATH_TO_DIFF_FILE} 101 | 102 | violationRules.failIfCoverageLessThan(0.9) 103 | reports { 104 | html = true 105 | } 106 | } 107 | ``` 108 | 109 |
110 | 111 |
112 | Full example 113 | 114 | ```groovy 115 | buildscript { 116 | repositories { 117 | maven { url 'https://jitpack.io' } 118 | } 119 | dependencies { 120 | classpath 'com.github.form-com.diff-coverage-gradle:diff-coverage:0.9.4' 121 | } 122 | } 123 | 124 | apply plugin: 'java' 125 | apply plugin: 'jacoco' 126 | apply plugin: 'com.form.diff-coverage' 127 | 128 | diffCoverageReport { 129 | diffSource { 130 | git.compareWith 'refs/remotes/origin/develop' 131 | } 132 | 133 | violationRules.failIfCoverageLessThan 0.9 134 | 135 | reports { 136 | html = true 137 | xml = true 138 | csv = true 139 | } 140 | } 141 | diffCoverage.dependsOn += check 142 | ``` 143 | 144 |
145 | 146 | ## Execute 147 | 148 | ```shell 149 | ./gradlew check diffCoverage 150 | ``` 151 | 152 | ## Parameters description 153 | ```groovy 154 | diffCoverageReport { 155 | diffSource { // Required. Only one of `file`, `url` or git must be spesified 156 | file = 'path/to/file.diff' // Path to diff file 157 | url = 'http://domain.com/file.diff' // URL to retrieve diff by 158 | git.compareWith 'refs/remotes/origin/develop' // Compares current HEAD and all uncommited with provided branch, revision or tag 159 | } 160 | jacocoExecFiles = files('/path/to/jacoco/exec/file.exec') // Required. By default exec files are taken from jacocoTestReport configuration if any 161 | srcDirs = files('/path/to/sources') // Required. By default sources are taken from jacocoTestReport configuration if any 162 | classesDirs = files('/path/to/compiled/classes') // Required. By default classes are taken from jacocoTestReport configuration if any 163 | 164 | reports { 165 | html = true // Optional. default `false` 166 | xml = true // Optional. default `false` 167 | csv = true // Optional. default `false` 168 | baseReportDir = 'base/dir/to/store/reports' // Optional. Default 'build/reports/jacoco/' 169 | } 170 | 171 | violationRules.failIfCoverageLessThan 0.9 // Optional. The function sets all coverage metrics to a single value, sets failOnViolation to true 172 | 173 | // configuration below is equivalent to the configuration above 174 | violationRules { 175 | minBranches = 0.9 // Optional. Default `0.0` 176 | minLines = 0.9 // Optional. Default `0.0` 177 | minInstructions = 0.9 // Optional. Default `0.0` 178 | failOnViolation = true // Optional. Default `false` 179 | } 180 | } 181 | ``` 182 | 183 | ## Gradle task description 184 | The plugin adds a task `diffCoverage` that has no dependencies 185 | * loads code coverage data specified by `diffCoverageReport.jacocoExecFiles` 186 | * analyzes the coverage data and filters according to `diffSource.url`/`diffSource.file` 187 | * generates html report(if enabled: `reports.html = true`) to directory `reports.baseReportsDir` 188 | * checks coverage ratio if `violationRules` is specified. 189 | 190 | Violations check is enabled if any of `minBranches`, `minLines`, `minInstructions` is greater than `0.0`. 191 | 192 | Fails the execution if the violation check is enabled and `violationRules.failOnViolation = true` 193 | 194 | ## Violations check output example 195 | 196 | Passed: 197 | > \>Task :diffCoverage 198 | > 199 | > Fail on violations: true. Found violations: 0. 200 | 201 | Failed: 202 | >\> Task :diffCoverage FAILED 203 | > 204 | >Fail on violations: true. Found violations: 2. 205 | > 206 | >FAILURE: Build failed with an exception. 207 | > 208 | >... 209 | > 210 | >\> java.lang.Exception: Rule violated for bundle diff-coverage-gradle: instructions covered ratio is 0.5, but expected minimum is 0.9 211 | > 212 | > Rule violated for bundle diff-coverage-gradle: lines covered ratio is 0.0, but expected minimum is 0.9 213 | 214 | 215 | 216 | ## HTML report example 217 | 218 | `Diff Coverage` plugin generates standard JaCoCo HTML report, but highlights only modified code 219 | 220 | DiffCoverage HTML report 221 | 222 |
223 | JaCoCo HTML report 224 | JaCoCo HTML report 225 |
226 | 227 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/kotlin/com/form/coverage/filters/ModifiedLinesFilterTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.filters 2 | 3 | import com.form.coverage.diff.CodeUpdateInfo 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.data.forAll 6 | import io.kotest.data.row 7 | import io.mockk.every 8 | import io.mockk.mockk 9 | import io.mockk.mockkClass 10 | import io.mockk.verify 11 | import org.jacoco.core.internal.analysis.filter.IFilterContext 12 | import org.jacoco.core.internal.analysis.filter.IFilterOutput 13 | import org.objectweb.asm.tree.* 14 | import kotlin.reflect.KClass 15 | 16 | class ModifiedLinesFilterTest : StringSpec({ 17 | 18 | "filter should ignore all non-modified lines" { 19 | // setup 20 | val classPackage = "com/form" 21 | val classFileName = "Class.java" 22 | val contextClassName = "$classPackage/Class" 23 | val classUpdateInfo = CodeUpdateInfo( 24 | mapOf("$classPackage/$classFileName" to setOf(51)) 25 | ) 26 | val context = mockk { 27 | every { className } returns contextClassName 28 | every { sourceFileName } returns classFileName 29 | } 30 | 31 | val modifiedLineInstructions = listOf( 32 | lineNode( 33 | 51, 34 | VarInsnNode::class, MethodInsnNode::class, VarInsnNode::class, InsnNode::class, 35 | VarInsnNode::class, VarInsnNode::class, VarInsnNode::class, MethodInsnNode::class 36 | ), 37 | lineNode(51, VarInsnNode::class) 38 | ) 39 | 40 | val instructionsToIgnorePartOne = listOf( 41 | lineNode(44, TypeInsnNode::class, InsnNode::class, MethodInsnNode::class, VarInsnNode::class), 42 | lineNode(45, VarInsnNode::class), 43 | lineNode(46, VarInsnNode::class, FieldInsnNode::class), 44 | lineNode( 45 | 47, 46 | TypeInsnNode::class, InsnNode::class, MethodInsnNode::class, MethodInsnNode::class, 47 | InsnNode::class, LdcInsnNode::class, MethodInsnNode::class 48 | ), 49 | lineNode(45, MethodInsnNode::class, VarInsnNode::class), 50 | lineNode( 51 | 49, 52 | VarInsnNode::class, MethodInsnNode::class, TypeInsnNode::class, MethodInsnNode::class, 53 | TypeInsnNode::class, VarInsnNode::class 54 | ), 55 | lineNode(50, VarInsnNode::class, MethodInsnNode::class, TypeInsnNode::class, VarInsnNode::class) 56 | ) 57 | val instructionsToIgnorePartTwo = listOf( 58 | lineNode(53, InsnNode::class, VarInsnNode::class), 59 | lineNode( 60 | 54, 61 | InsnNode::class, VarInsnNode::class, InsnNode::class, VarInsnNode::class, InsnNode::class, 62 | VarInsnNode::class, VarInsnNode::class, VarInsnNode::class, LabelNode::class, FrameNode::class, 63 | VarInsnNode::class, VarInsnNode::class, JumpInsnNode::class, VarInsnNode::class, 64 | VarInsnNode::class, LabelNode::class, InsnNode::class, VarInsnNode::class 65 | ), 66 | lineNode( 67 | 55, 68 | VarInsnNode::class, MethodInsnNode::class, VarInsnNode::class, InsnNode::class, 69 | VarInsnNode::class, VarInsnNode::class, InsnNode::class, InsnNode::class, VarInsnNode::class 70 | ), 71 | lineNode(56, VarInsnNode::class, VarInsnNode::class, InsnNode::class, VarInsnNode::class), 72 | lineNode( 73 | 57, 74 | TypeInsnNode::class, 75 | InsnNode::class, 76 | VarInsnNode::class, 77 | MethodInsnNode::class, 78 | VarInsnNode::class 79 | ), 80 | lineNode( 81 | 58, 82 | VarInsnNode::class, TypeInsnNode::class, VarInsnNode::class, MethodInsnNode::class, 83 | VarInsnNode::class, VarInsnNode::class, VarInsnNode::class, MethodInsnNode::class, 84 | MethodInsnNode::class, InsnNode::class 85 | ), 86 | lineNode( 87 | 59, 88 | VarInsnNode::class, TypeInsnNode::class, VarInsnNode::class, TypeInsnNode::class, InsnNode::class, 89 | VarInsnNode::class, TypeInsnNode::class, InsnNode::class, InsnNode::class, InsnNode::class, 90 | MethodInsnNode::class, VarInsnNode::class, InsnNode::class, VarInsnNode::class, VarInsnNode::class, 91 | VarInsnNode::class, MethodInsnNode::class, InsnNode::class 92 | ), 93 | lineNode(60, VarInsnNode::class, VarInsnNode::class), 94 | lineNode(61, InsnNode::class, LabelNode::class, InsnNode::class), 95 | lineNode(54, IincInsnNode::class, JumpInsnNode::class), 96 | lineNode(63, FrameNode::class, VarInsnNode::class, TypeInsnNode::class, InsnNode::class, LabelNode::class) 97 | ) 98 | 99 | val instructionsList = InsnList().apply { 100 | instructionsToIgnorePartOne 101 | .union(modifiedLineInstructions) 102 | .union(instructionsToIgnorePartTwo) 103 | .flatten() 104 | .forEach(::add) 105 | } 106 | 107 | val methodNode = MethodNode().apply { 108 | instructions = instructionsList 109 | } 110 | 111 | val output = mockk(relaxed = true) 112 | 113 | // run 114 | ModifiedLinesFilter(classUpdateInfo).filter( 115 | methodNode, 116 | context, 117 | output 118 | ) 119 | 120 | // assert 121 | instructionsToIgnorePartOne.union(instructionsToIgnorePartTwo).forEach { 122 | verify(exactly = 1) { 123 | output.ignore(it.first(), it.last()) 124 | } 125 | } 126 | 127 | modifiedLineInstructions.forEach { 128 | verify(exactly = 0) { 129 | output.ignore(it.first(), it.last()) 130 | } 131 | } 132 | } 133 | 134 | "filter should correctly fetch class modifications" { 135 | forAll( 136 | row("com/wa/ModClass"), 137 | row("com/wa/ModClass\$InnerClass"), 138 | row("com/wa/ModClass\$InnerClass\$Lambda"), 139 | row("com/wa/ClassNameDoesNotMatchSourceFile") 140 | ) { contextClassName -> 141 | // setup 142 | val modifiedFilePath = "module/src/main/kotlin/com/wa/ModClass.kt" 143 | val classFileName = "ModClass.kt" 144 | val classUpdateInfo = CodeUpdateInfo( 145 | mapOf(modifiedFilePath to setOf(2)) 146 | ) 147 | val context = mockk { 148 | every { className } returns contextClassName 149 | every { sourceFileName } returns classFileName 150 | } 151 | 152 | val modifiedLine: Set = lineNode(2) 153 | val instructionsToIgnore: Set = lineNode(1) 154 | 155 | val instructionsList = InsnList().apply { 156 | instructionsToIgnore.union(modifiedLine).forEach(::add) 157 | } 158 | 159 | val methodNode = MethodNode().apply { 160 | instructions = instructionsList 161 | } 162 | 163 | val output = mockk(relaxed = true) 164 | 165 | // run 166 | ModifiedLinesFilter(classUpdateInfo).filter( 167 | methodNode, 168 | context, 169 | output 170 | ) 171 | 172 | // assert 173 | verify(exactly = 1) { 174 | output.ignore(instructionsToIgnore.first(), instructionsToIgnore.last()) 175 | } 176 | 177 | verify(exactly = 0) { 178 | output.ignore(modifiedLine.first(), modifiedLine.last()) 179 | } 180 | } 181 | 182 | } 183 | }) 184 | 185 | fun lineNode( 186 | line: Int, 187 | vararg lineNodes: KClass 188 | ): Set { 189 | return LineNumberNode(line, null).let { lineNode -> 190 | listOf( 191 | mockk { every { next } returns lineNode }, 192 | lineNode 193 | ).union(lineNodes.map { 194 | mockkClass(it) { 195 | every { next } returns mockk() 196 | } 197 | }) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /jacoco-filtering-extension/src/test/kotlin/com/form/coverage/diff/ModifiedLinesDiffParserTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.diff 2 | 3 | import com.form.coverage.diff.parse.ModifiedLinesDiffParser 4 | import io.kotest.assertions.throwables.shouldThrow 5 | import io.kotest.core.spec.style.StringSpec 6 | import io.kotest.matchers.maps.shouldContainExactly 7 | import io.kotest.matchers.should 8 | import io.kotest.matchers.shouldBe 9 | import io.kotest.matchers.string.startWith 10 | import java.io.File 11 | 12 | class ModifiedLinesDiffParserTest : StringSpec({ 13 | 14 | "collectModifiedLines should return empty map on empty list" { 15 | // setup 16 | val modifiedLinesDiffParser = ModifiedLinesDiffParser() 17 | 18 | // run 19 | val collectModifiedLines = modifiedLinesDiffParser.collectModifiedLines(emptyList()) 20 | 21 | // assert 22 | collectModifiedLines shouldBe emptyMap() 23 | } 24 | 25 | "collectModifiedLines should throw when file path cannot be parsed" { 26 | // setup 27 | val diffContent = """ 28 | +++ 29 | """.trimIndent().lines() 30 | 31 | // run 32 | val exception = shouldThrow { 33 | ModifiedLinesDiffParser().collectModifiedLines(diffContent) 34 | } 35 | 36 | // assert 37 | exception.message should startWith("Couldn't parse file relative path: ") 38 | } 39 | 40 | "collectModifiedLines should return empty map when diff file has few line breaks" { 41 | // setup 42 | val lines = listOf("", "") 43 | 44 | // run 45 | val collectModifiedLines = ModifiedLinesDiffParser().collectModifiedLines(lines) 46 | 47 | // assert 48 | collectModifiedLines shouldBe emptyMap() 49 | } 50 | 51 | "collectModifiedLines should throw when offset cannot be parsed" { 52 | // setup 53 | val diffContent = """ 54 | --- path/file 55 | +++ path/file 56 | @@ invalid,offset @@ 57 | """.trimIndent().lines() 58 | 59 | // run 60 | val exception = shouldThrow { 61 | ModifiedLinesDiffParser().collectModifiedLines(diffContent) 62 | } 63 | 64 | // assert 65 | exception.message should startWith("Couldn't parse file's range information: ") 66 | } 67 | 68 | "collectModifiedLines should return modified and added lines used 'git diff' format" { 69 | // setup 70 | val modifiedFilePath = "some/file/path.ext" 71 | val toFileGitDiffFormat = "b/$modifiedFilePath" 72 | val diffContent = """ 73 | diff --git a/$modifiedFilePath $toFileGitDiffFormat 74 | index 8a1218a..41da4de 100644 75 | --- a/$modifiedFilePath 76 | +++ b/$modifiedFilePath 77 | @@ -1,5 +1,8 @@ 78 | a #1 79 | b #2 80 | +add row #3 expected 81 | c #4 82 | -d 83 | +d modify #5 expected 84 | e #6 85 | +f add #7 expected 86 | +g add #8 expected 87 | """.trimIndent().lines() 88 | val expected = mapOf( 89 | toFileGitDiffFormat to setOf(3, 5, 7, 8) 90 | ) 91 | 92 | // run 93 | val collectModifiedLines = ModifiedLinesDiffParser().collectModifiedLines(diffContent) 94 | 95 | // assert 96 | collectModifiedLines shouldContainExactly expected 97 | } 98 | 99 | "collectModifiedLines should return modified and added lines when multiple modified files" { 100 | // setup 101 | val modifiedFile1 = "some/file/path1.ext" 102 | val modifiedFile3 = "some/file/path3.ext" 103 | val diffContent = """ 104 | --- $modifiedFile1 105 | +++ $modifiedFile1 106 | @@ -1,2 +1,4 @@ 107 | #1 108 | -2 109 | +#2 expected 110 | +#3 expected 111 | +#4 expected 112 | --- some/file/path2.ext 113 | +++ some/file/path22.ext 114 | @@ -1,2 +1 @@ 115 | 1 116 | -2 117 | --- $modifiedFile3 118 | +++ $modifiedFile3 119 | @@ -1 +1,2 @@ 120 | +0 121 | 1 122 | 123 | some service row 124 | """.trimIndent().lines() 125 | val expected = mapOf( 126 | modifiedFile1 to setOf(2, 3, 4), 127 | modifiedFile3 to setOf(1) 128 | ) 129 | 130 | // run 131 | val collectModifiedLines = ModifiedLinesDiffParser().collectModifiedLines(diffContent) 132 | 133 | // assert 134 | collectModifiedLines shouldContainExactly expected 135 | } 136 | 137 | "collectModifiedLines should not skip modified lines when patch file contains empty lines" { 138 | // setup 139 | val diffFileContent = ModifiedLinesDiffParserTest::class.java.classLoader 140 | .getResource("testintPatch1.patch")!!.file 141 | .let(::File) 142 | val modifiedFileName = 143 | "jacoco-filtering-extension/src/main/kotlin/com/form/coverage/filters/ModifiedLinesFilter.kt" 144 | val modifiedLines: Set = (7..8) 145 | .union(18..32) 146 | .union(40..40) 147 | .union(43..44) 148 | .union(46..46) 149 | .union(48..48) 150 | .union(70..73) 151 | 152 | // run 153 | val collectModifiedLines = ModifiedLinesDiffParser().collectModifiedLines(diffFileContent.readLines()) 154 | 155 | // assert 156 | collectModifiedLines shouldContainExactly mapOf(modifiedFileName to modifiedLines) 157 | } 158 | 159 | "collectModifiedLines should successfully parse diff of a patch file" { 160 | // setup 161 | val filePath = "jacoco-filtering-extension/src/test/resources/testintPatch1.patch" 162 | 163 | val diffContent = """ 164 | --- jacoco-filtering-extension/src/test/resources/testintPatch1.patch 165 | +++ jacoco-filtering-extension/src/test/resources/testintPatch1.patch 166 | @@ -1,10 +1,10 @@ 167 | -Index: jacoco-filtering-extension/src/main/kotlin/com/prev/coverage/filters/ModifiedLinesFilter.kt 168 | +Index: jacoco-filtering-extension/src/main/kotlin/com/form/coverage/filters/ModifiedLinesFilter.kt 169 | IDEA additional info: 170 | Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP 171 | <+>UTF-8 172 | =================================================================== 173 | ---- jacoco-filtering-extension/src/main/kotlin/com/prev/coverage/filters/ModifiedLinesFilter.kt 174 | -+++ jacoco-filtering-extension/src/main/kotlin/com/prev/coverage/filters/ModifiedLinesFilter.kt 175 | +--- jacoco-filtering-extension/src/main/kotlin/com/form/coverage/filters/ModifiedLinesFilter.kt 176 | ++++ jacoco-filtering-extension/src/main/kotlin/com/form/coverage/filters/ModifiedLinesFilter.kt 177 | @@ -4,10 +4,8 @@ 178 | import org.jacoco.core.internal.analysis.filter.IFilter 179 | import org.jacoco.core.internal.analysis.filter.IFilterContext 180 | 181 | """.trimIndent().lines() 182 | 183 | // run 184 | val collectModifiedLines = ModifiedLinesDiffParser().collectModifiedLines(diffContent) 185 | 186 | // assert 187 | collectModifiedLines shouldContainExactly mapOf(filePath to setOf(1, 6, 7)) 188 | } 189 | 190 | "collectModifiedLines should parse quoted paths" { 191 | // setup 192 | val filePath = "b/#1{ }.txt" 193 | 194 | val diffContent = """ 195 | diff --git "a/1\173 \175.txt" "b/1\173 \175.txt" 196 | index 150c4fd..1ddd327 100644 197 | --- "a/1\173 \175.txt" 198 | +++ "b/\0431\173 \175.txt" 199 | @@ -1 +1,2 @@ 200 | prev 201 | +new 202 | """.trimIndent().lines() 203 | 204 | // run 205 | val collectModifiedLines = ModifiedLinesDiffParser().collectModifiedLines(diffContent) 206 | 207 | // assert 208 | collectModifiedLines shouldContainExactly mapOf(filePath to setOf(2)) 209 | } 210 | 211 | "collectModifiedLines should properly parse when patch adds new line at end of file" { 212 | // setup 213 | val fileName = "no-new-line-at-the-end.txt" 214 | val diffContent = """ 215 | --- a/$fileName 216 | +++ b/$fileName 217 | @@ -1,2 +1,3 @@ 218 | first line 219 | -second line 220 | \ No newline at end of file 221 | +second line 222 | +third line with line break 223 | 224 | """.trimIndent().lines() 225 | 226 | // run 227 | val collectModifiedLines = ModifiedLinesDiffParser().collectModifiedLines(diffContent) 228 | 229 | // assert 230 | collectModifiedLines shouldContainExactly mapOf("b/$fileName" to setOf(3, 4)) 231 | } 232 | 233 | }) 234 | -------------------------------------------------------------------------------- /diff-coverage/src/funcTest/kotlin/com/form/coverage/gradle/DiffCoverageSingleModuleTest.kt: -------------------------------------------------------------------------------- 1 | package com.form.coverage.gradle 2 | 3 | import com.form.coverage.diff.git.getCrlf 4 | import com.form.coverage.gradle.DiffCoveragePlugin.Companion.DIFF_COV_TASK 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.eclipse.jgit.api.Git 7 | import org.eclipse.jgit.lib.ConfigConstants 8 | import org.eclipse.jgit.lib.Repository 9 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder 10 | import org.gradle.testkit.runner.TaskOutcome.FAILED 11 | import org.gradle.testkit.runner.TaskOutcome.SUCCESS 12 | import org.junit.jupiter.api.BeforeEach 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.params.ParameterizedTest 15 | import org.junit.jupiter.params.provider.CsvSource 16 | import java.io.File 17 | import java.nio.file.Paths 18 | 19 | class DiffCoverageSingleModuleTest : BaseDiffCoverageTest() { 20 | 21 | companion object { 22 | const val TEST_PROJECT_RESOURCE_NAME = "single-module-test-project" 23 | 24 | private const val MOCK_SERVER_PORT = 8888 25 | } 26 | 27 | override fun buildTestConfiguration() = TestConfiguration( 28 | TEST_PROJECT_RESOURCE_NAME, 29 | "build.gradle", 30 | "test.diff.file" 31 | ) 32 | 33 | @BeforeEach 34 | fun setup() { 35 | initializeGradleTest() 36 | } 37 | 38 | @Test 39 | fun `diff-coverage should validate coverage and fail without report creation`() { 40 | // setup 41 | val baseReportDir = "build/custom/reports/dir/jacoco/" 42 | buildFile.appendText( 43 | """ 44 | diffCoverageReport { 45 | diffSource.file = '$diffFilePath' 46 | reportConfiguration.baseReportDir = '$baseReportDir' 47 | violationRules { 48 | failIfCoverageLessThan 1.0 49 | failOnViolation = true 50 | } 51 | } 52 | """.trimIndent() 53 | ) 54 | 55 | // run 56 | val result = gradleRunner.runTaskAndFail(DIFF_COV_TASK) 57 | 58 | // assert 59 | result.assertDiffCoverageStatusEqualsTo(FAILED) 60 | .assertOutputContainsStrings("Fail on violations: true. Found violations: 3") 61 | assertThat( 62 | rootProjectDir.resolve(baseReportDir).resolve("diffCoverage") 63 | ).doesNotExist() 64 | } 65 | 66 | @ParameterizedTest 67 | @CsvSource( 68 | value = [ 69 | "html, html, true", 70 | "csv, report.csv, false", 71 | "xml, report.xml, false" 72 | ] 73 | ) 74 | fun `diff-coverage should create single report type`( 75 | reportToGenerate: String, 76 | expectedReportFile: String, 77 | isDirectory: Boolean 78 | ) { 79 | // setup 80 | val baseReportDir = "build/custom/reports/dir/jacoco/" 81 | buildFile.appendText( 82 | """ 83 | diffCoverageReport { 84 | diffSource.file = '$diffFilePath' 85 | reportConfiguration.baseReportDir = '$baseReportDir' 86 | reportConfiguration.$reportToGenerate = true 87 | } 88 | """.trimIndent() 89 | ) 90 | 91 | // run 92 | val result = gradleRunner.runTask(DIFF_COV_TASK) 93 | 94 | // assert 95 | result.assertDiffCoverageStatusEqualsTo(SUCCESS) 96 | .assertOutputContainsStrings("Fail on violations: false. Found violations: 0") 97 | 98 | val diffReportDir: File = rootProjectDir.resolve(baseReportDir).resolve("diffCoverage") 99 | assertThat(diffReportDir.list()!!.toList()) 100 | .hasSize(1).first() 101 | .extracting( 102 | { it }, 103 | { diffReportDir.resolve(it).isDirectory } 104 | ) 105 | .containsExactly(expectedReportFile, isDirectory) 106 | } 107 | 108 | @Test 109 | fun `diff-coverage should fail if classes file collection is empty`() { 110 | // setup 111 | buildFile.appendText( 112 | """ 113 | diffCoverageReport { 114 | diffSource.file = '$diffFilePath' 115 | classesDirs = files() 116 | } 117 | """.trimIndent() 118 | ) 119 | 120 | // run 121 | val result = gradleRunner.runTaskAndFail(DIFF_COV_TASK) 122 | 123 | // assert 124 | result.assertOutputContainsStrings("'diffCoverageReport.classesDirs' file collection is empty.") 125 | } 126 | 127 | @Test 128 | fun `diff-coverage should create diffCoverage dir and full coverage with html, csv and xml reports`() { 129 | // setup 130 | val baseReportDir = "build/custom/reports/dir/jacoco/" 131 | buildFile.appendText( 132 | """ 133 | 134 | diffCoverageReport { 135 | diffSource { 136 | file = '$diffFilePath' 137 | } 138 | jacocoExecFiles = files(jacocoTestReport.executionData) 139 | classesDirs = files(jacocoTestReport.classDirectories) 140 | srcDirs = files(jacocoTestReport.sourceDirectories) 141 | 142 | reports { 143 | html = true 144 | xml = true 145 | csv = true 146 | fullCoverageReport = true 147 | baseReportDir = '$baseReportDir' 148 | } 149 | } 150 | """.trimIndent() 151 | ) 152 | 153 | // run 154 | val result = gradleRunner.runTask(DIFF_COV_TASK) 155 | 156 | // assert 157 | result.assertDiffCoverageStatusEqualsTo(SUCCESS) 158 | rootProjectDir.resolve(baseReportDir).apply { 159 | assertAllReportsCreated(resolve("diffCoverage")) 160 | assertAllReportsCreated(resolve("fullReport")) 161 | } 162 | } 163 | 164 | @Test 165 | fun `diff-coverage should use git to generate diff`() { 166 | // setup 167 | prepareTestProjectWithGit() 168 | 169 | buildFile.appendText( 170 | """ 171 | 172 | diffCoverageReport { 173 | diffSource { 174 | git.compareWith 'HEAD' 175 | } 176 | violationRules { 177 | minLines = 0.7 178 | failOnViolation = true 179 | } 180 | } 181 | """.trimIndent() 182 | ) 183 | 184 | // run 185 | val result = gradleRunner.runTaskAndFail(DIFF_COV_TASK) 186 | 187 | // assert 188 | result.assertDiffCoverageStatusEqualsTo(FAILED) 189 | .assertOutputContainsStrings("lines covered ratio is 0.6, but expected minimum is 0.7") 190 | } 191 | 192 | @Test 193 | fun `diff-coverage should fail on violation and generate html report`() { 194 | // setup 195 | val absolutePathBaseReportDir = rootProjectDir 196 | .resolve("build/absolute/path/reports/jacoco/") 197 | .toUnixAbsolutePath() 198 | 199 | buildFile.appendText( 200 | """ 201 | 202 | diffCoverageReport { 203 | diffSource.file = '$diffFilePath' 204 | reports { 205 | html = true 206 | baseReportDir = '$absolutePathBaseReportDir' 207 | } 208 | violationRules { 209 | minBranches = 0.6 210 | minLines = 0.7 211 | minInstructions = 0.8 212 | failOnViolation = true 213 | } 214 | } 215 | """.trimIndent() 216 | ) 217 | 218 | // run 219 | val result = gradleRunner.runTaskAndFail(DIFF_COV_TASK) 220 | 221 | // assert 222 | result.assertDiffCoverageStatusEqualsTo(FAILED) 223 | .assertOutputContainsStrings( 224 | "instructions covered ratio is 0.5, but expected minimum is 0.8", 225 | "branches covered ratio is 0.5, but expected minimum is 0.6", 226 | "lines covered ratio is 0.6, but expected minimum is 0.7" 227 | ) 228 | 229 | val diffCoverageReportDir = Paths.get(absolutePathBaseReportDir, "diffCoverage", "html").toFile() 230 | assertThat(diffCoverageReportDir.list()) 231 | .containsExactlyInAnyOrder( 232 | *expectedHtmlReportFiles("com.java.test") 233 | ) 234 | } 235 | 236 | @Test 237 | fun `diff-coverage should not fail on violation when failOnViolation is false`() { 238 | // setup 239 | buildFile.appendText( 240 | """ 241 | 242 | diffCoverageReport { 243 | diffSource.file = '$diffFilePath' 244 | violationRules { 245 | failIfCoverageLessThan 1.0 246 | failOnViolation = false 247 | } 248 | } 249 | """.trimIndent() 250 | ) 251 | 252 | // run 253 | val result = gradleRunner.runTask(DIFF_COV_TASK) 254 | 255 | // assert 256 | result.assertDiffCoverageStatusEqualsTo(SUCCESS) 257 | .assertOutputContainsStrings("Fail on violations: false. Found violations: 3") 258 | } 259 | 260 | @Test 261 | fun `diff-coverage should get diff info by url`() { 262 | // setup 263 | buildFile.appendText( 264 | """ 265 | 266 | diffCoverageReport { 267 | diffSource.url = 'http://localhost:$MOCK_SERVER_PORT/' 268 | println diffSource.url 269 | violationRules { 270 | minInstructions = 1 271 | failOnViolation = true 272 | } 273 | } 274 | """.trimIndent() 275 | ) 276 | 277 | MockHttpServer(MOCK_SERVER_PORT, File(diffFilePath).readText()).use { 278 | // run 279 | val result = gradleRunner.runTaskAndFail(DIFF_COV_TASK) 280 | 281 | // assert 282 | result.assertDiffCoverageStatusEqualsTo(FAILED) 283 | .assertOutputContainsStrings("instructions covered ratio is 0.5, but expected minimum is 1") 284 | } 285 | } 286 | 287 | @Test 288 | fun `diff-coverage should fail and print available branches if provided branch not found`() { 289 | // setup 290 | val unknownBranch = "unknown-branch" 291 | val newBranch = "new-branch" 292 | buildGitRepository().apply { 293 | add().addFilepattern(".").call() 294 | commit().setMessage("Add all").call() 295 | branchCreate().setName(newBranch).call() 296 | } 297 | 298 | buildFile.appendText( 299 | """ 300 | 301 | diffCoverageReport { 302 | diffSource.git.compareWith '$unknownBranch' 303 | } 304 | """.trimIndent() 305 | ) 306 | 307 | // run 308 | val result = gradleRunner.runTaskAndFail(DIFF_COV_TASK) 309 | 310 | // assert 311 | result.assertDiffCoverageStatusEqualsTo(FAILED) 312 | .assertOutputContainsStrings( 313 | "Unknown revision '$unknownBranch'", 314 | "Available branches: refs/heads/master, refs/heads/$newBranch" 315 | ) 316 | } 317 | 318 | private fun prepareTestProjectWithGit() { 319 | rootProjectDir.resolve(".gitignore").apply { 320 | appendText("\n*") 321 | appendText("\n!*.java") 322 | appendText("\n!gitignore") 323 | appendText("\n!*/") 324 | } 325 | buildGitRepository().use { git -> 326 | git.add().addFilepattern(".").call() 327 | git.commit().setMessage("Add all").call() 328 | 329 | val oldVersionFile = "src/main/java/com/java/test/Class1.java" 330 | val targetFile = rootProjectDir.resolve(oldVersionFile) 331 | getResourceFile("git-diff-source-test-files/Class1GitTest.java") 332 | .copyTo(targetFile, true) 333 | 334 | git.add().addFilepattern(oldVersionFile).call() 335 | git.commit().setMessage("Added old file version").call() 336 | 337 | getResourceFile("$TEST_PROJECT_RESOURCE_NAME/src").copyRecursively( 338 | rootProjectDir.resolve("src"), 339 | true 340 | ) 341 | git.add().addFilepattern(".").call() 342 | } 343 | } 344 | 345 | private fun buildGitRepository(): Git { 346 | val repository: Repository = FileRepositoryBuilder.create(File(rootProjectDir, ".git")).apply { 347 | config.setEnum( 348 | ConfigConstants.CONFIG_CORE_SECTION, 349 | null, 350 | ConfigConstants.CONFIG_KEY_AUTOCRLF, 351 | getCrlf() 352 | ) 353 | create() 354 | } 355 | return Git(repository) 356 | } 357 | 358 | private fun assertAllReportsCreated(baseReportDir: File) { 359 | assertThat(baseReportDir.list()).containsExactlyInAnyOrder("report.xml", "report.csv", "html") 360 | assertThat(baseReportDir.resolve("html").list()) 361 | .containsExactlyInAnyOrder( 362 | *expectedHtmlReportFiles("com.java.test") 363 | ) 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /config/detekt/detekt.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | excludeCorrectable: false 4 | weights: 5 | # complexity: 2 6 | # LongParameterList: 1 7 | # style: 1 8 | # comments: 1 9 | 10 | config: 11 | validation: true 12 | warningsAsErrors: false 13 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' 14 | excludes: '' 15 | 16 | processors: 17 | active: true 18 | exclude: 19 | - 'DetektProgressListener' 20 | # - 'KtFileCountProcessor' 21 | # - 'PackageCountProcessor' 22 | # - 'ClassCountProcessor' 23 | # - 'FunctionCountProcessor' 24 | # - 'PropertyCountProcessor' 25 | # - 'ProjectComplexityProcessor' 26 | # - 'ProjectCognitiveComplexityProcessor' 27 | # - 'ProjectLLOCProcessor' 28 | # - 'ProjectCLOCProcessor' 29 | # - 'ProjectLOCProcessor' 30 | # - 'ProjectSLOCProcessor' 31 | # - 'LicenseHeaderLoaderExtension' 32 | 33 | console-reports: 34 | active: true 35 | exclude: 36 | - 'ProjectStatisticsReport' 37 | - 'ComplexityReport' 38 | - 'NotificationReport' 39 | - 'FindingsReport' 40 | - 'FileBasedFindingsReport' 41 | # - 'LiteFindingsReport' 42 | 43 | output-reports: 44 | active: true 45 | 46 | comments: 47 | active: true 48 | AbsentOrWrongFileLicense: 49 | active: false 50 | licenseTemplateFile: 'license.template' 51 | licenseTemplateIsRegex: false 52 | CommentOverPrivateFunction: 53 | active: false 54 | CommentOverPrivateProperty: 55 | active: false 56 | DeprecatedBlockTag: 57 | active: false 58 | EndOfSentenceFormat: 59 | active: false 60 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' 61 | OutdatedDocumentation: 62 | active: false 63 | matchTypeParameters: true 64 | matchDeclarationsOrder: true 65 | allowParamOnConstructorProperties: false 66 | UndocumentedPublicClass: 67 | active: false 68 | excludes: ['**/test/**'] 69 | searchInNestedClass: true 70 | searchInInnerClass: true 71 | searchInInnerObject: true 72 | searchInInnerInterface: true 73 | UndocumentedPublicFunction: 74 | active: false 75 | excludes: ['**/test/**'] 76 | UndocumentedPublicProperty: 77 | active: false 78 | excludes: ['**/test/**'] 79 | 80 | complexity: 81 | active: true 82 | ComplexCondition: 83 | active: true 84 | threshold: 4 85 | ComplexInterface: 86 | active: false 87 | threshold: 10 88 | includeStaticDeclarations: false 89 | includePrivateDeclarations: false 90 | ComplexMethod: 91 | active: true 92 | threshold: 15 93 | ignoreSingleWhenExpression: false 94 | ignoreSimpleWhenEntries: false 95 | ignoreNestingFunctions: false 96 | nestingFunctions: 97 | - 'also' 98 | - 'apply' 99 | - 'forEach' 100 | - 'isNotNull' 101 | - 'ifNull' 102 | - 'let' 103 | - 'run' 104 | - 'use' 105 | - 'with' 106 | LabeledExpression: 107 | active: false 108 | ignoredLabels: [] 109 | LargeClass: 110 | active: true 111 | threshold: 600 112 | LongMethod: 113 | active: true 114 | threshold: 60 115 | LongParameterList: 116 | active: true 117 | functionThreshold: 6 118 | constructorThreshold: 7 119 | ignoreDefaultParameters: false 120 | ignoreDataClasses: true 121 | ignoreAnnotatedParameter: [] 122 | MethodOverloading: 123 | active: false 124 | threshold: 6 125 | NamedArguments: 126 | active: false 127 | threshold: 3 128 | ignoreArgumentsMatchingNames: false 129 | NestedBlockDepth: 130 | active: true 131 | threshold: 4 132 | ReplaceSafeCallChainWithRun: 133 | active: false 134 | StringLiteralDuplication: 135 | active: false 136 | excludes: ['**/test/**'] 137 | threshold: 3 138 | ignoreAnnotation: true 139 | excludeStringsWithLessThan5Characters: true 140 | ignoreStringsRegex: '$^' 141 | TooManyFunctions: 142 | active: true 143 | excludes: ['**/test/**'] 144 | thresholdInFiles: 11 145 | thresholdInClasses: 16 146 | thresholdInInterfaces: 11 147 | thresholdInObjects: 11 148 | thresholdInEnums: 11 149 | ignoreDeprecated: false 150 | ignorePrivate: false 151 | ignoreOverridden: false 152 | 153 | coroutines: 154 | active: true 155 | GlobalCoroutineUsage: 156 | active: false 157 | InjectDispatcher: 158 | active: false 159 | dispatcherNames: 160 | - 'IO' 161 | - 'Default' 162 | - 'Unconfined' 163 | RedundantSuspendModifier: 164 | active: false 165 | SleepInsteadOfDelay: 166 | active: false 167 | SuspendFunWithCoroutineScopeReceiver: 168 | active: false 169 | SuspendFunWithFlowReturnType: 170 | active: false 171 | 172 | empty-blocks: 173 | active: true 174 | EmptyCatchBlock: 175 | active: true 176 | allowedExceptionNameRegex: '_|(ignore|expected).*' 177 | EmptyClassBlock: 178 | active: true 179 | EmptyDefaultConstructor: 180 | active: true 181 | EmptyDoWhileBlock: 182 | active: true 183 | EmptyElseBlock: 184 | active: true 185 | EmptyFinallyBlock: 186 | active: true 187 | EmptyForBlock: 188 | active: true 189 | EmptyFunctionBlock: 190 | active: true 191 | ignoreOverridden: false 192 | EmptyIfBlock: 193 | active: true 194 | EmptyInitBlock: 195 | active: true 196 | EmptyKtFile: 197 | active: true 198 | EmptySecondaryConstructor: 199 | active: true 200 | EmptyTryBlock: 201 | active: true 202 | EmptyWhenBlock: 203 | active: true 204 | EmptyWhileBlock: 205 | active: true 206 | 207 | exceptions: 208 | active: true 209 | ExceptionRaisedInUnexpectedLocation: 210 | active: true 211 | methodNames: 212 | - 'equals' 213 | - 'finalize' 214 | - 'hashCode' 215 | - 'toString' 216 | InstanceOfCheckForException: 217 | active: false 218 | excludes: ['**/test/**'] 219 | NotImplementedDeclaration: 220 | active: false 221 | ObjectExtendsThrowable: 222 | active: false 223 | PrintStackTrace: 224 | active: true 225 | RethrowCaughtException: 226 | active: true 227 | ReturnFromFinally: 228 | active: true 229 | ignoreLabeled: false 230 | SwallowedException: 231 | active: true 232 | ignoredExceptionTypes: 233 | - 'InterruptedException' 234 | - 'MalformedURLException' 235 | - 'NumberFormatException' 236 | - 'ParseException' 237 | allowedExceptionNameRegex: '_|(ignore|expected).*' 238 | ThrowingExceptionFromFinally: 239 | active: true 240 | ThrowingExceptionInMain: 241 | active: false 242 | ThrowingExceptionsWithoutMessageOrCause: 243 | active: true 244 | excludes: ['**/test/**'] 245 | exceptions: 246 | - 'ArrayIndexOutOfBoundsException' 247 | - 'Exception' 248 | - 'IllegalArgumentException' 249 | - 'IllegalMonitorStateException' 250 | - 'IllegalStateException' 251 | - 'IndexOutOfBoundsException' 252 | - 'NullPointerException' 253 | - 'RuntimeException' 254 | - 'Throwable' 255 | ThrowingNewInstanceOfSameException: 256 | active: true 257 | TooGenericExceptionCaught: 258 | active: false 259 | TooGenericExceptionThrown: 260 | active: false 261 | 262 | naming: 263 | active: true 264 | BooleanPropertyNaming: 265 | active: false 266 | allowedPattern: '^(is|has|are)' 267 | ignoreOverridden: true 268 | ClassNaming: 269 | active: true 270 | classPattern: '[A-Z][a-zA-Z0-9]*' 271 | ConstructorParameterNaming: 272 | active: true 273 | parameterPattern: '[a-z][A-Za-z0-9]*' 274 | privateParameterPattern: '[a-z][A-Za-z0-9]*' 275 | excludeClassPattern: '$^' 276 | ignoreOverridden: true 277 | EnumNaming: 278 | active: true 279 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' 280 | ForbiddenClassName: 281 | active: false 282 | forbiddenName: [] 283 | FunctionMaxLength: 284 | active: false 285 | maximumFunctionNameLength: 30 286 | FunctionMinLength: 287 | active: false 288 | minimumFunctionNameLength: 3 289 | FunctionNaming: 290 | active: true 291 | excludes: ['**/test/**'] 292 | functionPattern: '[a-z][a-zA-Z0-9]*' 293 | excludeClassPattern: '$^' 294 | ignoreOverridden: true 295 | FunctionParameterNaming: 296 | active: true 297 | parameterPattern: '[a-z][A-Za-z0-9]*' 298 | excludeClassPattern: '$^' 299 | ignoreOverridden: true 300 | InvalidPackageDeclaration: 301 | active: false 302 | rootPackage: '' 303 | requireRootInDeclaration: false 304 | LambdaParameterNaming: 305 | active: false 306 | parameterPattern: '[a-z][A-Za-z0-9]*|_' 307 | MatchingDeclarationName: 308 | active: true 309 | mustBeFirst: true 310 | MemberNameEqualsClassName: 311 | active: true 312 | ignoreOverridden: true 313 | NoNameShadowing: 314 | active: false 315 | NonBooleanPropertyPrefixedWithIs: 316 | active: false 317 | ObjectPropertyNaming: 318 | active: true 319 | constantPattern: '[A-Za-z][_A-Za-z0-9]*' 320 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 321 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' 322 | PackageNaming: 323 | active: true 324 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' 325 | TopLevelPropertyNaming: 326 | active: true 327 | constantPattern: '[A-Z][_A-Z0-9]*' 328 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 329 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' 330 | VariableMaxLength: 331 | active: false 332 | maximumVariableNameLength: 64 333 | VariableMinLength: 334 | active: false 335 | minimumVariableNameLength: 1 336 | VariableNaming: 337 | active: true 338 | variablePattern: '[a-z][A-Za-z0-9]*' 339 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' 340 | excludeClassPattern: '$^' 341 | ignoreOverridden: true 342 | 343 | performance: 344 | active: true 345 | ArrayPrimitive: 346 | active: true 347 | ForEachOnRange: 348 | active: true 349 | excludes: ['**/test/**'] 350 | SpreadOperator: 351 | active: true 352 | excludes: ['**/test/**'] 353 | UnnecessaryTemporaryInstantiation: 354 | active: true 355 | 356 | potential-bugs: 357 | active: true 358 | AvoidReferentialEquality: 359 | active: false 360 | forbiddenTypePatterns: 361 | - 'kotlin.String' 362 | CastToNullableType: 363 | active: false 364 | Deprecation: 365 | active: false 366 | DontDowncastCollectionTypes: 367 | active: false 368 | DoubleMutabilityForCollection: 369 | active: false 370 | mutableTypes: 371 | - 'kotlin.collections.MutableList' 372 | - 'kotlin.collections.MutableMap' 373 | - 'kotlin.collections.MutableSet' 374 | - 'java.util.ArrayList' 375 | - 'java.util.LinkedHashSet' 376 | - 'java.util.HashSet' 377 | - 'java.util.LinkedHashMap' 378 | - 'java.util.HashMap' 379 | DuplicateCaseInWhenExpression: 380 | active: true 381 | ElseCaseInsteadOfExhaustiveWhen: 382 | active: false 383 | EqualsAlwaysReturnsTrueOrFalse: 384 | active: true 385 | EqualsWithHashCodeExist: 386 | active: true 387 | ExitOutsideMain: 388 | active: false 389 | ExplicitGarbageCollectionCall: 390 | active: true 391 | HasPlatformType: 392 | active: false 393 | IgnoredReturnValue: 394 | active: false 395 | restrictToAnnotatedMethods: true 396 | returnValueAnnotations: 397 | - '*.CheckResult' 398 | - '*.CheckReturnValue' 399 | ignoreReturnValueAnnotations: 400 | - '*.CanIgnoreReturnValue' 401 | ignoreFunctionCall: [] 402 | ImplicitDefaultLocale: 403 | active: true 404 | ImplicitUnitReturnType: 405 | active: false 406 | allowExplicitReturnType: true 407 | InvalidRange: 408 | active: true 409 | IteratorHasNextCallsNextMethod: 410 | active: true 411 | IteratorNotThrowingNoSuchElementException: 412 | active: true 413 | LateinitUsage: 414 | active: false 415 | excludes: ['**/test/**'] 416 | ignoreOnClassesPattern: '' 417 | MapGetWithNotNullAssertionOperator: 418 | active: false 419 | MissingPackageDeclaration: 420 | active: false 421 | excludes: ['**/*.kts'] 422 | MissingWhenCase: 423 | active: true 424 | allowElseExpression: true 425 | NullCheckOnMutableProperty: 426 | active: false 427 | NullableToStringCall: 428 | active: false 429 | RedundantElseInWhen: 430 | active: true 431 | UnconditionalJumpStatementInLoop: 432 | active: false 433 | UnnecessaryNotNullOperator: 434 | active: true 435 | UnnecessarySafeCall: 436 | active: true 437 | UnreachableCatchBlock: 438 | active: false 439 | UnreachableCode: 440 | active: true 441 | UnsafeCallOnNullableType: 442 | active: true 443 | excludes: ['**/test/**'] 444 | UnsafeCast: 445 | active: true 446 | UnusedUnaryOperator: 447 | active: false 448 | UselessPostfixExpression: 449 | active: false 450 | WrongEqualsTypeParameter: 451 | active: true 452 | 453 | style: 454 | active: true 455 | CanBeNonNullable: 456 | active: false 457 | ClassOrdering: 458 | active: false 459 | CollapsibleIfStatements: 460 | active: false 461 | DataClassContainsFunctions: 462 | active: false 463 | conversionFunctionPrefix: 'to' 464 | DataClassShouldBeImmutable: 465 | active: false 466 | DestructuringDeclarationWithTooManyEntries: 467 | active: false 468 | maxDestructuringEntries: 3 469 | EqualsNullCall: 470 | active: true 471 | EqualsOnSignatureLine: 472 | active: false 473 | ExplicitCollectionElementAccessMethod: 474 | active: false 475 | ExplicitItLambdaParameter: 476 | active: false 477 | ExpressionBodySyntax: 478 | active: false 479 | includeLineWrapping: false 480 | ForbiddenComment: 481 | active: true 482 | values: 483 | - 'FIXME:' 484 | - 'STOPSHIP:' 485 | - 'TODO:' 486 | allowedPatterns: '' 487 | customMessage: '' 488 | ForbiddenImport: 489 | active: false 490 | imports: [] 491 | forbiddenPatterns: '' 492 | ForbiddenMethodCall: 493 | active: false 494 | methods: 495 | - 'kotlin.io.print' 496 | - 'kotlin.io.println' 497 | ForbiddenPublicDataClass: 498 | active: true 499 | excludes: ['**'] 500 | ignorePackages: 501 | - '*.internal' 502 | - '*.internal.*' 503 | ForbiddenVoid: 504 | active: false 505 | ignoreOverridden: false 506 | ignoreUsageInGenerics: false 507 | FunctionOnlyReturningConstant: 508 | active: true 509 | ignoreOverridableFunction: true 510 | ignoreActualFunction: true 511 | excludedFunctions: '' 512 | LibraryCodeMustSpecifyReturnType: 513 | active: true 514 | excludes: ['**'] 515 | LibraryEntitiesShouldNotBePublic: 516 | active: true 517 | excludes: ['**'] 518 | LoopWithTooManyJumpStatements: 519 | active: true 520 | maxJumpCount: 1 521 | MagicNumber: 522 | active: true 523 | excludes: ['**/test/**', '**/FilteringAnalyzer.kt'] 524 | ignoreNumbers: 525 | - '-1' 526 | - '0' 527 | - '1' 528 | - '2' 529 | ignoreHashCodeFunction: true 530 | ignorePropertyDeclaration: false 531 | ignoreLocalVariableDeclaration: false 532 | ignoreConstantDeclaration: true 533 | ignoreCompanionObjectPropertyDeclaration: true 534 | ignoreAnnotation: false 535 | ignoreNamedArgument: true 536 | ignoreEnums: false 537 | ignoreRanges: false 538 | ignoreExtensionFunctions: true 539 | MandatoryBracesIfStatements: 540 | active: false 541 | MandatoryBracesLoops: 542 | active: false 543 | MaxLineLength: 544 | active: true 545 | maxLineLength: 120 546 | excludePackageStatements: true 547 | excludeImportStatements: true 548 | excludeCommentStatements: false 549 | MayBeConst: 550 | active: true 551 | ModifierOrder: 552 | active: true 553 | MultilineLambdaItParameter: 554 | active: false 555 | NestedClassesVisibility: 556 | active: true 557 | NewLineAtEndOfFile: 558 | active: true 559 | NoTabs: 560 | active: false 561 | ObjectLiteralToLambda: 562 | active: false 563 | OptionalAbstractKeyword: 564 | active: true 565 | OptionalUnit: 566 | active: false 567 | OptionalWhenBraces: 568 | active: false 569 | PreferToOverPairSyntax: 570 | active: false 571 | ProtectedMemberInFinalClass: 572 | active: true 573 | RedundantExplicitType: 574 | active: false 575 | RedundantHigherOrderMapUsage: 576 | active: false 577 | RedundantVisibilityModifierRule: 578 | active: false 579 | ReturnCount: 580 | active: true 581 | max: 2 582 | excludedFunctions: 'equals' 583 | excludeLabeled: false 584 | excludeReturnFromLambda: true 585 | excludeGuardClauses: false 586 | SafeCast: 587 | active: true 588 | SerialVersionUIDInSerializableClass: 589 | active: true 590 | SpacingBetweenPackageAndImports: 591 | active: false 592 | ThrowsCount: 593 | active: true 594 | max: 2 595 | excludeGuardClauses: false 596 | TrailingWhitespace: 597 | active: false 598 | UnderscoresInNumericLiterals: 599 | active: false 600 | acceptableLength: 4 601 | allowNonStandardGrouping: false 602 | UnnecessaryAbstractClass: 603 | active: true 604 | UnnecessaryAnnotationUseSiteTarget: 605 | active: false 606 | UnnecessaryApply: 607 | active: true 608 | UnnecessaryFilter: 609 | active: false 610 | UnnecessaryInheritance: 611 | active: true 612 | UnnecessaryInnerClass: 613 | active: false 614 | UnnecessaryLet: 615 | active: false 616 | UnnecessaryParentheses: 617 | active: false 618 | UntilInsteadOfRangeTo: 619 | active: false 620 | UnusedImports: 621 | active: true 622 | UnusedPrivateClass: 623 | active: true 624 | UnusedPrivateMember: 625 | active: true 626 | allowedNames: '(_|ignored|expected|serialVersionUID|i|j)' 627 | UseAnyOrNoneInsteadOfFind: 628 | active: false 629 | UseArrayLiteralsInAnnotations: 630 | active: false 631 | UseCheckNotNull: 632 | active: false 633 | UseCheckOrError: 634 | active: false 635 | UseDataClass: 636 | active: false 637 | allowVars: false 638 | UseEmptyCounterpart: 639 | active: false 640 | UseIfEmptyOrIfBlank: 641 | active: false 642 | UseIfInsteadOfWhen: 643 | active: false 644 | UseIsNullOrEmpty: 645 | active: false 646 | UseOrEmpty: 647 | active: false 648 | UseRequire: 649 | active: false 650 | UseRequireNotNull: 651 | active: false 652 | UselessCallOnNotNull: 653 | active: true 654 | UtilityClassWithPublicConstructor: 655 | active: true 656 | VarCouldBeVal: 657 | active: true 658 | WildcardImport: 659 | active: true 660 | excludes: ['**/test/**'] 661 | excludeImports: 662 | - 'java.util.*' 663 | --------------------------------------------------------------------------------