mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-01 20:10:35 +01: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:
parent
c7b713ac80
commit
251ad63ea6
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -97,6 +97,7 @@ jobs:
|
||||
with:
|
||||
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
|
||||
path: |
|
||||
app/**/build/reports/jacoco/test
|
||||
app/**/build/reports/tests/
|
||||
app/**/build/test-results/
|
||||
app/**/build/reports/problems/
|
||||
@ -104,6 +105,17 @@ jobs:
|
||||
retention-days: 3
|
||||
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:
|
||||
if: needs.files-changed.outputs.openapi == 'true'
|
||||
needs: [files-changed]
|
||||
|
||||
130
build.gradle
130
build.gradle
@ -14,6 +14,8 @@ plugins {
|
||||
import com.github.jk1.license.render.*
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.xml.XmlSlurper
|
||||
import org.gradle.api.tasks.testing.Test
|
||||
|
||||
ext {
|
||||
springBootVersion = "3.5.7"
|
||||
@ -183,30 +185,142 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.compilerArgs << "-parameters"
|
||||
tasks.named("compileJava", JavaCompile).configure {
|
||||
options.compilerArgs.add("-parameters")
|
||||
}
|
||||
|
||||
test {
|
||||
def jacocoReport = tasks.named("jacocoTestReport")
|
||||
|
||||
tasks.withType(Test).configureEach {
|
||||
useJUnitPlatform()
|
||||
finalizedBy jacocoTestReport
|
||||
finalizedBy(jacocoReport)
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn test
|
||||
jacocoReport.configure {
|
||||
dependsOn(tasks.named("test"))
|
||||
reports {
|
||||
xml.required.set(true)
|
||||
csv.required.set(false)
|
||||
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 {
|
||||
dependsOn jacocoTestReport
|
||||
dependsOn jacocoReport
|
||||
violationRules {
|
||||
rule {
|
||||
enabled = true
|
||||
element = 'BUNDLE'
|
||||
// Bytecode-Anweisungen abgedeckt
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user