mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
feat(build): enhance JaCoCo reporting with coverage summary and enforce thresholds (#5352)
# Description of Changes ### What was changed - Refactored Gradle task configuration to use `tasks.named` and `configureEach` for better lazy configuration and compatibility. - Centralized JaCoCo report handling by introducing a single `jacocoReport` task reference. - Added a post-processing step to the JaCoCo XML report to: - Parse coverage metrics (LINE, INSTRUCTION, BRANCH). - Calculate coverage ratios. - Print a formatted coverage summary table directly to the build logs. - Enabled and aligned `jacocoTestCoverageVerification` rules with defined minimum coverage thresholds. - Ensured the `build` task depends on the JaCoCo report to always generate coverage output. ### Why the change was made - To improve visibility of test coverage results directly in CI and local builds without manually opening the HTML report. - To enforce consistent and explicit coverage thresholds for key metrics. - To modernize Gradle task configuration and avoid eager task realization. --- > Task :proprietary:jacocoTestReport ==== JaCoCo Coverage Summary ==== Metric | Coverage | Covered/Total | Status | Target ------------|----------|---------------|--------|---------- LINE | 9.01% | 759/8426 | FAIL | >= 16.00% INSTRUCTION | 8.41% | 2741/32590 | FAIL | >= 14.00% BRANCH | 6.04% | 248/4103 | FAIL | >= 9.00% --- > Task :common:jacocoTestReport ==== JaCoCo Coverage Summary ==== | Metric | Coverage | Covered/Total | Status | Target |------------|----------|---------------|--------|---------- LINE | 39.47% | 2996/7591 | PASS | >= 16.00% INSTRUCTION | 41.05% | 12868/31345 | PASS | >= 14.00% BRANCH | 33.43% | 1166/3488 | PASS | >= 9.00% --- > Task :stirling-pdf:jacocoTestReport ==== JaCoCo Coverage Summary ==== Metric | Coverage | Covered/Total | Status | Target ------------|----------|---------------|--------|---------- LINE | 13.63% | 2554/18741 | FAIL | >= 16.00% INSTRUCTION | 14.59% | 11459/78532 | PASS | >= 14.00% BRANCH | 10.68% | 868/8124 | PASS | >= 9.00% --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -97,6 +97,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
|
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
|
||||||
path: |
|
path: |
|
||||||
|
app/**/build/reports/jacoco/test
|
||||||
app/**/build/reports/tests/
|
app/**/build/reports/tests/
|
||||||
app/**/build/test-results/
|
app/**/build/test-results/
|
||||||
app/**/build/reports/problems/
|
app/**/build/reports/problems/
|
||||||
@@ -104,6 +105,17 @@ jobs:
|
|||||||
retention-days: 3
|
retention-days: 3
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
- name: Add coverage to PR with spring security ${{ matrix.spring-security }} and JDK ${{ matrix.jdk-version }}
|
||||||
|
id: jacoco
|
||||||
|
uses: madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2
|
||||||
|
with:
|
||||||
|
paths: |
|
||||||
|
${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
min-coverage-overall: 10
|
||||||
|
min-coverage-changed-files: 0
|
||||||
|
comment-type: summary
|
||||||
|
|
||||||
check-generateOpenApiDocs:
|
check-generateOpenApiDocs:
|
||||||
if: needs.files-changed.outputs.openapi == 'true'
|
if: needs.files-changed.outputs.openapi == 'true'
|
||||||
needs: [files-changed]
|
needs: [files-changed]
|
||||||
|
|||||||
+122
-8
@@ -14,6 +14,8 @@ plugins {
|
|||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
|
import groovy.xml.XmlSlurper
|
||||||
|
import org.gradle.api.tasks.testing.Test
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.5.7"
|
springBootVersion = "3.5.7"
|
||||||
@@ -183,30 +185,142 @@ subprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileJava {
|
tasks.named("compileJava", JavaCompile).configure {
|
||||||
options.compilerArgs << "-parameters"
|
options.compilerArgs.add("-parameters")
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
def jacocoReport = tasks.named("jacocoTestReport")
|
||||||
|
|
||||||
|
tasks.withType(Test).configureEach {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
finalizedBy jacocoTestReport
|
finalizedBy(jacocoReport)
|
||||||
}
|
}
|
||||||
|
|
||||||
jacocoTestReport {
|
jacocoReport.configure {
|
||||||
dependsOn test
|
dependsOn(tasks.named("test"))
|
||||||
reports {
|
reports {
|
||||||
xml.required.set(true)
|
xml.required.set(true)
|
||||||
csv.required.set(false)
|
csv.required.set(false)
|
||||||
html.required.set(true)
|
html.required.set(true)
|
||||||
}
|
}
|
||||||
|
doLast {
|
||||||
|
def xmlReport = reports.xml.outputLocation.get().asFile
|
||||||
|
if (!xmlReport.exists()) {
|
||||||
|
logger.lifecycle("Jacoco coverage report not found at ${xmlReport}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
def xmlContent = xmlReport.getText("UTF-8")
|
||||||
|
xmlContent = xmlContent.replaceFirst('(?s)<!DOCTYPE.*?>', '')
|
||||||
|
def report = new XmlSlurper(false, false).parseText(xmlContent)
|
||||||
|
def counters = report.counter.collectEntries { counter ->
|
||||||
|
def type = counter.@type.text()
|
||||||
|
def covered = counter.@covered.text() as BigDecimal
|
||||||
|
def missed = counter.@missed.text() as BigDecimal
|
||||||
|
[(type): [covered: covered, missed: missed]]
|
||||||
|
}
|
||||||
|
|
||||||
|
def thresholds = [
|
||||||
|
LINE : 0.16,
|
||||||
|
INSTRUCTION: 0.14,
|
||||||
|
BRANCH : 0.09
|
||||||
|
]
|
||||||
|
|
||||||
|
def types = ["LINE", "INSTRUCTION", "BRANCH"]
|
||||||
|
def headers = ["Metric", "Coverage", "Covered/Total", "Status", "Target"]
|
||||||
|
|
||||||
|
def rows = types.collect { String type ->
|
||||||
|
def data = counters[type]
|
||||||
|
if (!data) {
|
||||||
|
return [type, "—", "—", "No data", ""]
|
||||||
|
}
|
||||||
|
|
||||||
|
def total = data.covered + data.missed
|
||||||
|
if (total == 0) {
|
||||||
|
return [type, "—", "0/${total.toBigInteger()}", "No executions", ""]
|
||||||
|
}
|
||||||
|
|
||||||
|
def ratio = data.covered / total * 100
|
||||||
|
def coverageText = String.format(Locale.ROOT, "%.2f%%", ratio)
|
||||||
|
def coveredText = String.format(Locale.ROOT, "%d/%d",
|
||||||
|
data.covered.toBigInteger(),
|
||||||
|
total.toBigInteger())
|
||||||
|
|
||||||
|
def threshold = thresholds[type]
|
||||||
|
def thresholdPercent = threshold != null ? threshold * 100 : null
|
||||||
|
def targetText = thresholdPercent != null ?
|
||||||
|
String.format(Locale.ROOT, ">= %.2f%%", thresholdPercent) : ""
|
||||||
|
def passed = thresholdPercent != null ? ratio >= thresholdPercent : null
|
||||||
|
def statusText = passed == null ? "" : (passed ? "PASS" : "FAIL")
|
||||||
|
|
||||||
|
return [type, coverageText, coveredText, statusText, targetText]
|
||||||
|
}
|
||||||
|
|
||||||
|
def columnIndexes = (0..<headers.size())
|
||||||
|
def columnWidths = columnIndexes.collect { idx ->
|
||||||
|
Math.max(headers[idx].length(), rows.collect { row ->
|
||||||
|
row[idx] != null ? row[idx].toString().length() : 0
|
||||||
|
}.max() ?: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
def formatRow = { List<String> values ->
|
||||||
|
columnIndexes.collect { idx ->
|
||||||
|
def value = values[idx] ?: ""
|
||||||
|
value.padRight(columnWidths[idx])
|
||||||
|
}.join(" | ")
|
||||||
|
}
|
||||||
|
|
||||||
|
def separator = columnIndexes.collect { idx ->
|
||||||
|
''.padRight(columnWidths[idx], '-')
|
||||||
|
}.join("-+-")
|
||||||
|
|
||||||
|
logger.lifecycle("")
|
||||||
|
logger.lifecycle("==== JaCoCo Coverage Summary ====")
|
||||||
|
logger.lifecycle(formatRow(headers))
|
||||||
|
logger.lifecycle(separator)
|
||||||
|
rows.each { row ->
|
||||||
|
logger.lifecycle(formatRow(row))
|
||||||
|
}
|
||||||
|
logger.lifecycle(separator)
|
||||||
|
|
||||||
|
def htmlReport = reports.html.outputLocation.get().asFile
|
||||||
|
logger.lifecycle("Detailed HTML report available at: ${htmlReport}")
|
||||||
|
if (rows.any { it[3] == "FAIL" }) {
|
||||||
|
logger.lifecycle("Some coverage targets were missed. Please review the detailed report above.")
|
||||||
|
} else if (rows.any { it[3] == "PASS" }) {
|
||||||
|
logger.lifecycle("Great job! All tracked coverage metrics meet their targets.")
|
||||||
|
}
|
||||||
|
logger.lifecycle("=================================\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("build") {
|
||||||
|
dependsOn jacocoReport
|
||||||
}
|
}
|
||||||
|
|
||||||
jacocoTestCoverageVerification {
|
jacocoTestCoverageVerification {
|
||||||
dependsOn jacocoTestReport
|
dependsOn jacocoReport
|
||||||
violationRules {
|
violationRules {
|
||||||
rule {
|
rule {
|
||||||
|
enabled = true
|
||||||
|
element = 'BUNDLE'
|
||||||
|
// Bytecode-Anweisungen abgedeckt
|
||||||
limit {
|
limit {
|
||||||
minimum = 0.0
|
counter = 'INSTRUCTION'
|
||||||
|
value = 'COVEREDRATIO'
|
||||||
|
minimum = 0.14
|
||||||
|
}
|
||||||
|
// wie viele Quellcode-Zeilen abgedeckt
|
||||||
|
limit {
|
||||||
|
counter = 'LINE'
|
||||||
|
value = 'COVEREDRATIO'
|
||||||
|
minimum = 0.16
|
||||||
|
}
|
||||||
|
// Verzweigungen (if/else, switch) abgedeckt; misst Logik-Abdeckung
|
||||||
|
limit {
|
||||||
|
counter = 'BRANCH'
|
||||||
|
value = 'COVEREDRATIO'
|
||||||
|
minimum = 0.09
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user