Skip to content

Commit 251ad63

Browse files
authored
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.
1 parent c7b713a commit 251ad63

2 files changed

Lines changed: 134 additions & 8 deletions

File tree

.github/workflows/build.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,25 @@ jobs:
9797
with:
9898
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
9999
path: |
100+
app/**/build/reports/jacoco/test
100101
app/**/build/reports/tests/
101102
app/**/build/test-results/
102103
app/**/build/reports/problems/
103104
build/reports/problems/
104105
retention-days: 3
105106
if-no-files-found: warn
106107

108+
- name: Add coverage to PR with spring security ${{ matrix.spring-security }} and JDK ${{ matrix.jdk-version }}
109+
id: jacoco
110+
uses: madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2
111+
with:
112+
paths: |
113+
${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
114+
token: ${{ secrets.GITHUB_TOKEN }}
115+
min-coverage-overall: 10
116+
min-coverage-changed-files: 0
117+
comment-type: summary
118+
107119
check-generateOpenApiDocs:
108120
if: needs.files-changed.outputs.openapi == 'true'
109121
needs: [files-changed]

build.gradle

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ plugins {
1414
import com.github.jk1.license.render.*
1515
import groovy.json.JsonOutput
1616
import groovy.json.JsonSlurper
17+
import groovy.xml.XmlSlurper
18+
import org.gradle.api.tasks.testing.Test
1719

1820
ext {
1921
springBootVersion = "3.5.7"
@@ -183,30 +185,142 @@ subprojects {
183185
}
184186
}
185187

186-
compileJava {
187-
options.compilerArgs << "-parameters"
188+
tasks.named("compileJava", JavaCompile).configure {
189+
options.compilerArgs.add("-parameters")
188190
}
189191

190-
test {
192+
def jacocoReport = tasks.named("jacocoTestReport")
193+
194+
tasks.withType(Test).configureEach {
191195
useJUnitPlatform()
192-
finalizedBy jacocoTestReport
196+
finalizedBy(jacocoReport)
193197
}
194198

195-
jacocoTestReport {
196-
dependsOn test
199+
jacocoReport.configure {
200+
dependsOn(tasks.named("test"))
197201
reports {
198202
xml.required.set(true)
199203
csv.required.set(false)
200204
html.required.set(true)
201205
}
206+
doLast {
207+
def xmlReport = reports.xml.outputLocation.get().asFile
208+
if (!xmlReport.exists()) {
209+
logger.lifecycle("Jacoco coverage report not found at ${xmlReport}")
210+
return
211+
}
212+
213+
def xmlContent = xmlReport.getText("UTF-8")
214+
xmlContent = xmlContent.replaceFirst('(?s)<!DOCTYPE.*?>', '')
215+
def report = new XmlSlurper(false, false).parseText(xmlContent)
216+
def counters = report.counter.collectEntries { counter ->
217+
def type = counter.@type.text()
218+
def covered = counter.@covered.text() as BigDecimal
219+
def missed = counter.@missed.text() as BigDecimal
220+
[(type): [covered: covered, missed: missed]]
221+
}
222+
223+
def thresholds = [
224+
LINE : 0.16,
225+
INSTRUCTION: 0.14,
226+
BRANCH : 0.09
227+
]
228+
229+
def types = ["LINE", "INSTRUCTION", "BRANCH"]
230+
def headers = ["Metric", "Coverage", "Covered/Total", "Status", "Target"]
231+
232+
def rows = types.collect { String type ->
233+
def data = counters[type]
234+
if (!data) {
235+
return [type, "", "", "No data", ""]
236+
}
237+
238+
def total = data.covered + data.missed
239+
if (total == 0) {
240+
return [type, "", "0/${total.toBigInteger()}", "No executions", ""]
241+
}
242+
243+
def ratio = data.covered / total * 100
244+
def coverageText = String.format(Locale.ROOT, "%.2f%%", ratio)
245+
def coveredText = String.format(Locale.ROOT, "%d/%d",
246+
data.covered.toBigInteger(),
247+
total.toBigInteger())
248+
249+
def threshold = thresholds[type]
250+
def thresholdPercent = threshold != null ? threshold * 100 : null
251+
def targetText = thresholdPercent != null ?
252+
String.format(Locale.ROOT, ">= %.2f%%", thresholdPercent) : ""
253+
def passed = thresholdPercent != null ? ratio >= thresholdPercent : null
254+
def statusText = passed == null ? "" : (passed ? "PASS" : "FAIL")
255+
256+
return [type, coverageText, coveredText, statusText, targetText]
257+
}
258+
259+
def columnIndexes = (0..<headers.size())
260+
def columnWidths = columnIndexes.collect { idx ->
261+
Math.max(headers[idx].length(), rows.collect { row ->
262+
row[idx] != null ? row[idx].toString().length() : 0
263+
}.max() ?: 0)
264+
}
265+
266+
def formatRow = { List<String> values ->
267+
columnIndexes.collect { idx ->
268+
def value = values[idx] ?: ""
269+
value.padRight(columnWidths[idx])
270+
}.join(" | ")
271+
}
272+
273+
def separator = columnIndexes.collect { idx ->
274+
''.padRight(columnWidths[idx], '-')
275+
}.join("-+-")
276+
277+
logger.lifecycle("")
278+
logger.lifecycle("==== JaCoCo Coverage Summary ====")
279+
logger.lifecycle(formatRow(headers))
280+
logger.lifecycle(separator)
281+
rows.each { row ->
282+
logger.lifecycle(formatRow(row))
283+
}
284+
logger.lifecycle(separator)
285+
286+
def htmlReport = reports.html.outputLocation.get().asFile
287+
logger.lifecycle("Detailed HTML report available at: ${htmlReport}")
288+
if (rows.any { it[3] == "FAIL" }) {
289+
logger.lifecycle("Some coverage targets were missed. Please review the detailed report above.")
290+
} else if (rows.any { it[3] == "PASS" }) {
291+
logger.lifecycle("Great job! All tracked coverage metrics meet their targets.")
292+
}
293+
logger.lifecycle("=================================\n")
294+
}
295+
}
296+
297+
tasks.named("build") {
298+
dependsOn jacocoReport
202299
}
203300

204301
jacocoTestCoverageVerification {
205-
dependsOn jacocoTestReport
302+
dependsOn jacocoReport
206303
violationRules {
207304
rule {
305+
enabled = true
306+
element = 'BUNDLE'
307+
// Bytecode-Anweisungen abgedeckt
308+
limit {
309+
counter = 'INSTRUCTION'
310+
value = 'COVEREDRATIO'
311+
minimum = 0.14
312+
}
313+
// wie viele Quellcode-Zeilen abgedeckt
314+
limit {
315+
counter = 'LINE'
316+
value = 'COVEREDRATIO'
317+
minimum = 0.16
318+
}
319+
// Verzweigungen (if/else, switch) abgedeckt; misst Logik-Abdeckung
208320
limit {
209-
minimum = 0.0
321+
counter = 'BRANCH'
322+
value = 'COVEREDRATIO'
323+
minimum = 0.09
210324
}
211325
}
212326
}

0 commit comments

Comments
 (0)