Skip to content

Commit

Permalink
Development: Exclude gradle source files from jacoco test report veri…
Browse files Browse the repository at this point in the history
…fication (#10240)
  • Loading branch information
ole-ve authored Feb 9, 2025
1 parent 881d8d0 commit 721970e
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 89 deletions.
214 changes: 126 additions & 88 deletions gradle/jacoco.gradle
Original file line number Diff line number Diff line change
@@ -1,92 +1,37 @@
ext {
AggregatedCoverageThresholds = [
"INSTRUCTION": 0.89,
"CLASS": 56
];
// TODO: this should become 90% for INSTRUCTION and 0 for CLASS
AggregatedCoverageThresholds = ["INSTRUCTION": 0.888, "CLASS": 69]
// (Isolated) thresholds when executing each module on its own
// TODO: each module should achieve 90% for INSTRUCTION and 0 for CLASS
ModuleCoverageThresholds = [
"assessment" : [
"INSTRUCTION": 0.77,
"CLASS": 8
],
"athena" : [
"INSTRUCTION": 0.85,
"CLASS": 2
],
"atlas" : [
"INSTRUCTION": 0.85,
"CLASS": 12
],
"buildagent" : [
"INSTRUCTION": 0.31,
"CLASS": 13
],
"communication": [
"INSTRUCTION": 0.89,
"CLASS": 7
],
"core" : [
"INSTRUCTION": 0.65,
"CLASS": 69
],
"exam" : [
"INSTRUCTION": 0.91,
"CLASS": 1
],
"exercise" : [
"INSTRUCTION": 0.64,
"CLASS": 9
],
"fileupload" : [
"INSTRUCTION": 0.90,
"CLASS": 1
],
"iris" : [
"INSTRUCTION": 0.74,
"CLASS": 26
],
"lecture" : [
"INSTRUCTION": 0.86,
"CLASS": 0
],
"lti" : [
"INSTRUCTION": 0.77,
"CLASS": 3
],
"modeling" : [
"INSTRUCTION": 0.89,
"CLASS": 2
],
"plagiarism" : [
"INSTRUCTION": 0.76,
"CLASS": 1
],
"programming" : [
"INSTRUCTION": 0.86,
"CLASS": 12
],
"quiz" : [
"INSTRUCTION": 0.78,
"CLASS": 6
],
"text" : [
"INSTRUCTION": 0.84,
"CLASS": 0
],
"tutorialgroup": [
"INSTRUCTION": 0.91,
"CLASS": 0
],
"assessment" : ["INSTRUCTION": 0.929, "CLASS": 0 ],
"athena" : ["INSTRUCTION": 0.872, "CLASS": 2 ],
"atlas" : ["INSTRUCTION": 0.908, "CLASS": 5 ],
"buildagent" : ["INSTRUCTION": 0.797, "CLASS": 0 ],
"communication" : ["INSTRUCTION": 0.926, "CLASS": 1 ],
"core" : ["INSTRUCTION": 0.861, "CLASS": 17],
"exam" : ["INSTRUCTION": 0.939, "CLASS": 0 ],
"exercise" : ["INSTRUCTION": 0.929, "CLASS": 0 ],
"fileupload" : ["INSTRUCTION": 0.927, "CLASS": 1 ],
"iris" : ["INSTRUCTION": 0.753, "CLASS": 26],
"lecture" : ["INSTRUCTION": 0.910, "CLASS": 0 ],
"lti" : ["INSTRUCTION": 0.894, "CLASS": 2 ],
"modeling" : ["INSTRUCTION": 0.916, "CLASS": 2 ],
"plagiarism" : ["INSTRUCTION": 0.911, "CLASS": 0 ],
"programming" : ["INSTRUCTION": 0.865, "CLASS": 9 ],
"quiz" : ["INSTRUCTION": 0.904, "CLASS": 4 ],
"text" : ["INSTRUCTION": 0.954, "CLASS": 0 ],
"tutorialgroup" : ["INSTRUCTION": 0.923, "CLASS": 0 ],
]
// If no explicit modules defined -> generate reports and validate for each module
reportedModules = includedModules.size() == 0
? ModuleCoverageThresholds.collect {element -> element.key}
? ModuleCoverageThresholds.collect {element -> element.key} + ["aggregated"]
: includedModules as ArrayList

ignoredDirectories = [
"**/$BasePath/**/domain/**/*_*",
"**/$BasePath/core/config/migration/entries/**",
"**/org/gradle/**",
"$BasePath/**/domain/**/*_*",
"$BasePath/core/config/migration/entries/**",
"org/gradle/**",
"**/gradle-wrapper.jar/**"
]
}
Expand All @@ -99,9 +44,20 @@ jacocoTestReport {
// For the aggregated report
reports {
xml.required = true
xml.outputLocation = file("build/reports/jacoco/test/jacocoTestReport.xml")
xml.outputLocation = file("build/reports/jacoco/aggregated/jacocoTestReport.xml")
html.required = true
html.outputLocation = file("build/reports/jacoco/test/html")
html.outputLocation = file("build/reports/jacoco/aggregated/html")
}

afterEvaluate {
classDirectories.setFrom(
files(classDirectories.files.collect { classDir ->
project.fileTree(classDir) {
includes=["$BasePath/**/*.class"]
excludes=ignoredDirectories
}
})
)
}

finalizedBy reportedModules
Expand All @@ -110,13 +66,22 @@ jacocoTestReport {
}

jacocoTestCoverageVerification {
// Only run full coverage when no specific modules set
enabled = reportedModules.size() == 0
// Only run full coverage when no specific/all modules set
enabled = reportedModules.size() == 0 || reportedModules.size() == ModuleCoverageThresholds.size()

classDirectories.setFrom(
files(classDirectories.files.collect { classDir ->
project.fileTree(classDir) {
excludes=ignoredDirectories
}
})
)

def minInstructionCoveredRatio = AggregatedCoverageThresholds["INSTRUCTION"] as double
def maxNumberUncoveredClasses = AggregatedCoverageThresholds["CLASS"] as int
applyVerificationRule(jacocoTestCoverageVerification, minInstructionCoveredRatio, maxNumberUncoveredClasses)

// TODO: somehow the report order is in reversed alphabetical order, it would be great if it could be from A to Z
finalizedBy reportedModules
.collect { module -> registerJacocoTestCoverageVerification(module as String, jacocoTestCoverageVerification) }
.findAll { task -> task != null}
Expand Down Expand Up @@ -158,10 +123,15 @@ private JacocoReport registerJacocoReportTask(String moduleName, JacocoReport ro
private JacocoCoverageVerification registerJacocoTestCoverageVerification(String moduleName, JacocoCoverageVerification rootTask) {
def taskName = "jacocoTestCoverageVerification-$moduleName"

def thresholds = ModuleCoverageThresholds[moduleName]
if (thresholds == null) {
println "No coverage thresholds defined for module '$moduleName'. Skipping verification for this module..."
return null
def thresholds
if (moduleName == "aggregated") {
thresholds = AggregatedCoverageThresholds
} else {
thresholds = ModuleCoverageThresholds[moduleName]
if (thresholds == null) {
println "No coverage thresholds defined for module '$moduleName'. Skipping verification for this module..."
return null
}
}
def minInstructionCoveredRatio = thresholds["INSTRUCTION"] as double
def maxNumberUncoveredClasses = thresholds["CLASS"] as int
Expand Down Expand Up @@ -199,6 +169,8 @@ private void prepareJacocoReportTask(JacocoReportBase task, String moduleName, J
)
}

import javax.xml.parsers.DocumentBuilderFactory

private static void applyVerificationRule(JacocoCoverageVerification task, double minInstructionCoveredRatio, int maxNumberUncoveredClasses) {
task.violationRules {
rule {
Expand All @@ -214,4 +186,70 @@ private static void applyVerificationRule(JacocoCoverageVerification task, doubl
}
}
}

task.doLast {
def moduleName = task.name.replace('jacocoTestCoverageVerification-', '')

// Handle aggregated total coverage stored in 'test'
if (moduleName == "aggregated") {
moduleName = "Aggregated Code Coverage"
}

def reportsFile = project.file("${task.project.layout.buildDirectory.get().asFile}/reports/jacoco/${task.name.replace('jacocoTestCoverageVerification-', '')}/jacocoTestReport.xml")

if (!reportsFile.exists()) {
println "⚠️ Jacoco report not found for ${task.name.replace('jacocoTestCoverageVerification-', '')}"
return
}

try {
// Read file as text and remove DOCTYPE
def xmlContent = reportsFile.text.replaceAll(/<!DOCTYPE[^>]*>/, "")

// Secure XML parsing without DOCTYPE
def factory = DocumentBuilderFactory.newInstance()

def builder = factory.newDocumentBuilder()
def document = builder.parse(new ByteArrayInputStream(xmlContent.getBytes("UTF-8")))

def counters = document.getElementsByTagName("counter")

def missedInstructions = 0
def coveredInstructions = 0
def missedClasses = 0

for (int i = 0; i < counters.getLength(); i++) {
def node = counters.item(i)
def type = node.getAttributes().getNamedItem("type").getTextContent()
def missed = node.getAttributes().getNamedItem("missed").getTextContent().toInteger()
def covered = node.getAttributes().getNamedItem("covered").getTextContent().toInteger()

if (type == "INSTRUCTION") {
missedInstructions = missed
coveredInstructions = covered
} else if (type == "CLASS") {
missedClasses = missed
}
}

def totalInstructions = missedInstructions + coveredInstructions
def actualInstructionCoverage = totalInstructions == 0 ? 1.0 : (coveredInstructions / totalInstructions)

println ""
println "📊 Module: ${moduleName}"
println " 🔍 Measured Instruction Coverage: ${String.format('%.2f', actualInstructionCoverage * 100)}% (Required: ${String.format('%.2f', minInstructionCoveredRatio * 100)}%)"
println " 🏛️ Uncovered Classes: ${missedClasses} (Allowed: ${maxNumberUncoveredClasses})"

if (actualInstructionCoverage < minInstructionCoveredRatio || missedClasses > maxNumberUncoveredClasses) {
println "❌ Coverage requirements not met!"
throw new GradleException("❌ Build failed: Coverage requirements for ${moduleName} not met!")
} else {
println "✅ Coverage requirements met."
}

} catch (Exception e) {
println "⚠️ Error parsing Jacoco XML: ${e.message}"
throw new GradleException("❌ Build failed: Error parsing Jacoco XML for ${task.name.replace('jacocoTestCoverageVerification-', '')}")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package de.tum.cit.aet.artemis.lti;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.testcontainers.shaded.org.awaitility.Awaitility.await;

import java.time.ZonedDateTime;

Expand Down Expand Up @@ -86,7 +88,7 @@ void testLtiServicesAreCalledUponQuizSubmission(boolean isSubmitted) throws Exce

quizSubmissionService.calculateAllResults(quizExercise.getId());

verify(lti13Service).onNewResult(any());
await().atMost(2, SECONDS).untilAsserted(() -> lti13Service.onNewResult(any()));
}

@Test
Expand Down

0 comments on commit 721970e

Please sign in to comment.