diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index e5f000e2..00518a5f 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -33,19 +33,19 @@ jobs: # Execute forgerock-core debug unit tests - name: Run forgerock-core debug unit tests - run: ./gradlew :forgerock-core:testDebugUnitTest --stacktrace --no-daemon + run: ./gradlew :forgerock-core:testDebugUnitTestCoverage --stacktrace --no-daemon # Execute forgerock-auth debug unit tests - name: Run forgerock-auth debug unit tests - run: ./gradlew :forgerock-auth:testDebugUnitTest --stacktrace --no-daemon + run: ./gradlew :forgerock-auth:testDebugUnitTestCoverage --stacktrace --no-daemon # Execute forgerock-authenticator debug unit tests - name: Run forgerock-authenticator debug unit tests - run: ./gradlew :forgerock-authenticator:testDebugUnitTest --stacktrace --no-daemon + run: ./gradlew :forgerock-authenticator:testDebugUnitTestCoverage --stacktrace --no-daemon # Execute forgerock-authenticator debug unit tests - name: Run ping-protect debug unit tests - run: ./gradlew :ping-protect:testDebugUnitTest --stacktrace --no-daemon + run: ./gradlew :ping-protect:testDebugUnitTestCoverage --stacktrace --no-daemon # Publish test reports for the unit tests - name: Publish test results @@ -53,12 +53,27 @@ jobs: uses: dorny/test-reporter@v1 with: name: Unit tests results - path: 'forgerock-core/build/test-results/**/TEST-*.xml,forgerock-auth/build/test-results/**/TEST-*.xml,forgerock-authenticator/build/test-results/**/TEST-*.xml,ping-protect/build/test-results/**/TEST-*.xml' + path: | + 'forgerock-core/build/test-results/**/TEST-*.xml, + forgerock-auth/build/test-results/**/TEST-*.xml, + forgerock-authenticator/build/test-results/**/TEST-*.xml, + ping-protect/build/test-results/**/TEST-*.xml' list-suites: 'all' list-tests: 'all' fail-on-error: 'true' reporter: java-junit + # Publish test coverage report + - name: Jacoco Report to PR + id: jacoco + uses: madrapps/jacoco-report@v1.6.1 + with: + paths: | + ${{ github.workspace }}/**/build/reports/jacoco/testDebugUnitTestCoverage/testDebugUnitTestCoverage.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 40 + min-coverage-changed-files: 60 + # Send slack notification with result status - uses: 8398a7/action-slack@v3 with: diff --git a/build.gradle.kts b/build.gradle.kts index 25b0e0fa..41899e20 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -103,9 +103,9 @@ subprojects { tasks.withType().configureEach { - jvmArgs = jvmArgs?.plus("--add-opens=java.base/java.lang=ALL-UNNAMED") - jvmArgs = jvmArgs?.plus("--add-opens=java.base/java.security=ALL-UNNAMED") - jvmArgs = jvmArgs?.plus("--add-opens=java.base/java.security.cert=ALL-UNNAMED") + jvmArgs = jvmArgs?.plus("--add-opens=java.base/java.lang=ALL-UNNAMED") as MutableList + jvmArgs = jvmArgs?.plus("--add-opens=java.base/java.security=ALL-UNNAMED") as MutableList + jvmArgs = jvmArgs?.plus("--add-opens=java.base/java.security.cert=ALL-UNNAMED") as MutableList } } diff --git a/config/jacoco.gradle b/config/jacoco.gradle new file mode 100644 index 00000000..7e1defdd --- /dev/null +++ b/config/jacoco.gradle @@ -0,0 +1,151 @@ +apply plugin: 'jacoco' + +jacoco { + toolVersion = "0.8.12" +} + +ext { + limits = [ + 'instruction': 0, + 'branch' : 0, + 'line' : 0, + 'complexity' : 0, + 'method' : 0, + 'class' : 0 + ] +} + +project.afterEvaluate { + def buildTypes = android.buildTypes.collect { type -> type.name } + def productFlavors = android.productFlavors.collect { flavor -> flavor.name } + + if (!productFlavors) productFlavors.add('') + + productFlavors.each { productFlavorName -> + buildTypes.each { buildTypeName -> + def sourceName, sourcePath + if (!productFlavorName) { + sourceName = sourcePath = "${buildTypeName}" + } else { + sourceName = "${productFlavorName}${buildTypeName.capitalize()}" + sourcePath = "${productFlavorName}/${buildTypeName}" + } + def testTaskName = "test${sourceName.capitalize()}UnitTest" + + task "${testTaskName}Coverage"(type: JacocoReport, dependsOn: "$testTaskName") { + group = "Reporting" + description = "Generate Jacoco coverage reports on the ${sourceName.capitalize()} build." + + + def javaDirectories = fileTree( + dir: "${project.buildDir}/intermediates/javac/${sourcePath}/compile${sourceName.capitalize()}JavaWithJavac/classes", + excludes: ['**/R.class', + '**/R$*.class', + '**/*$ViewInjector*.*', + '**/*$ViewBinder*.*', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Factory*', + '**/*_MembersInjector*', + '**/*Module*', + '**/*Component*', + '**android**', + '**/BR.class'] + ) + def kotlinDirectories = fileTree( + dir: "${project.buildDir}/tmp/kotlin-classes/${sourcePath}", + excludes: ['**/R.class', + '**/R$*.class', + '**/*$ViewInjector*.*', + '**/*$ViewBinder*.*', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Factory*', + '**/*_MembersInjector*', + '**/*Module*', + '**/*Component*', + '**android**', + '**/BR.class'] + ) + +// logger.lifecycle("javaDirectories: $javaDirectories") +// logger.lifecycle("kotlinDirectories: $kotlinDirectories") + + + getClassDirectories().setFrom(files(javaDirectories, kotlinDirectories)) + + def coverageSourceDirs = [ + "src/main/java", + "src/$productFlavorName/java", + "src/$buildTypeName/java" + ] + getAdditionalSourceDirs().setFrom(files(coverageSourceDirs)) + getSourceDirectories().setFrom(files(coverageSourceDirs)) + getExecutionData().setFrom(files("${project.buildDir}/jacoco/${testTaskName}.exec")) + + reports { + xml.required.set(true) + html.required.set(false) + } + + doLast { + jacocoTestReport("${testTaskName}Coverage") + } + } + } + } +} + + +def jacocoTestReport(testTaskName) { + def report = file("${buildDir}/reports/jacoco/${testTaskName}/${testTaskName}.xml") + logger.lifecycle("Checking coverage results: ${report}") + + def parser = new XmlParser() + parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) + parser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false) + def results = parser.parse(report) + + def percentage = { + def covered = it.'@covered' as Double + def missed = it.'@missed' as Double + ((covered / (covered + missed)) * 100).round(2) + } + + def counters = results.counter + def metrics = [:] + metrics << [ + 'instruction': percentage(counters.find { it.'@type'.equals('INSTRUCTION') }), + 'branch' : percentage(counters.find { it.'@type'.equals('BRANCH') }), + 'line' : percentage(counters.find { it.'@type'.equals('LINE') }), + 'complexity' : percentage(counters.find { it.'@type'.equals('COMPLEXITY') }), + 'method' : percentage(counters.find { it.'@type'.equals('METHOD') }), + 'class' : percentage(counters.find { it.'@type'.equals('CLASS') }) + ] + + def failures = [] + def success = [] + metrics.each { + def limit = limits[it.key] + if (it.value < limit) { + failures.add("- ${it.key} coverage rate is: ${it.value}%, minimum is ${limit}%") + }else{ + success.add("- ${it.key} coverage rate is: ${it.value}%") + } + } + + if (failures) { + logger.quiet("------------------ Code Coverage Failed -----------------------") + failures.each { + logger.quiet(it) + } + logger.quiet("---------------------------------------------------------------") + throw new GradleException("Code coverage failed") + } else { + logger.quiet("------------------ Code Coverage Success -----------------------") + success.each { + logger.quiet(it) + } + logger.quiet("---------------------------------------------------------------") + } +} \ No newline at end of file diff --git a/forgerock-auth/build.gradle.kts b/forgerock-auth/build.gradle.kts index 590c9bad..34fc77d4 100644 --- a/forgerock-auth/build.gradle.kts +++ b/forgerock-auth/build.gradle.kts @@ -40,6 +40,7 @@ android { apply("../config/logger.gradle") apply("../config/kdoc.gradle") +apply("../config/jacoco.gradle") apply("../config/publish.gradle") /** * Dependencies diff --git a/forgerock-authenticator/build.gradle.kts b/forgerock-authenticator/build.gradle.kts index 11b92f52..a1774312 100644 --- a/forgerock-authenticator/build.gradle.kts +++ b/forgerock-authenticator/build.gradle.kts @@ -50,6 +50,7 @@ tasks { } } +apply("../config/jacoco.gradle") apply("../config/logger.gradle") apply("../config/publish.gradle") diff --git a/forgerock-core/build.gradle.kts b/forgerock-core/build.gradle.kts index f6f8e83e..757f96b2 100644 --- a/forgerock-core/build.gradle.kts +++ b/forgerock-core/build.gradle.kts @@ -47,6 +47,7 @@ android { apply("../config/logger.gradle") apply("../config/kdoc.gradle") +apply("../config/jacoco.gradle") apply("../config/publish.gradle") val delombok by configurations.creating { diff --git a/ping-protect/build.gradle.kts b/ping-protect/build.gradle.kts index 862096e2..1feb4835 100644 --- a/ping-protect/build.gradle.kts +++ b/ping-protect/build.gradle.kts @@ -49,6 +49,7 @@ tasks { } } +apply("../config/jacoco.gradle") apply("../config/logger.gradle") apply("../config/publish.gradle")