From d7dea36d7f0785179610efdb09407646dd73a21f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 23:41:46 +0000 Subject: [PATCH 1/3] Fix coverage-modified classes being included in published artifacts The markGeneratedEqualsHashCode task was modifying .class files in-place in build/classes/java/main and the jar task depended on it, causing bytecode with injected @Generated annotations to end up in the published JAR. Fix by copying classes to a separate build/classes-jacoco directory for modification, and only wiring jacocoTestReport (not jar) to depend on the task. https://claude.ai/code/session_01XDZqTUKLBoSJpGPGx9Amxg --- build.gradle | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 63f1e283b..10122ed96 100644 --- a/build.gradle +++ b/build.gradle @@ -574,15 +574,18 @@ jacocoTestReport { csv.required = false } - // Exclude generated ANTLR code from coverage + // Use the JaCoCo-specific classes (with @Generated annotations on equals/hashCode) + // instead of the original classes, so coverage-modified bytecode is never published. + // Also exclude generated ANTLR code from coverage. afterEvaluate { - classDirectories.setFrom(files(classDirectories.files.collect { - fileTree(dir: it, exclude: [ + def jacocoClassDir = layout.buildDirectory.dir('classes-jacoco/java/main').get().asFile + classDirectories.setFrom(files( + fileTree(dir: jacocoClassDir, exclude: [ 'graphql/parser/antlr/**', 'graphql/com/google/**', 'graphql/org/antlr/**' ]) - })) + )) } } @@ -598,12 +601,19 @@ tasks.register('markGeneratedEqualsHashCode') { dependsOn classes doLast { - def dir = layout.buildDirectory.dir('classes/java/main').get().asFile - if (!dir.exists()) return + def srcDir = layout.buildDirectory.dir('classes/java/main').get().asFile + def destDir = layout.buildDirectory.dir('classes-jacoco/java/main').get().asFile + if (!srcDir.exists()) return + + // Copy all class files to a separate directory for JaCoCo + project.copy { + from srcDir + into destDir + } def ANNOTATION = 'Lgraphql/coverage/Generated;' - dir.eachFileRecurse(groovy.io.FileType.FILES) { file -> + destDir.eachFileRecurse(groovy.io.FileType.FILES) { file -> if (!file.name.endsWith('.class')) return def bytes = file.bytes @@ -631,10 +641,8 @@ tasks.register('markGeneratedEqualsHashCode') { } } -// Ensure the annotation task runs before anything that reads the main class files -tasks.named('test') { dependsOn markGeneratedEqualsHashCode } -tasks.named('compileTestJava') { dependsOn markGeneratedEqualsHashCode } -tasks.named('jar') { dependsOn markGeneratedEqualsHashCode } +// Only JaCoCo reporting needs the annotated classes — never the jar or published artifacts +tasks.named('jacocoTestReport') { dependsOn markGeneratedEqualsHashCode } /* * The gradle.buildFinished callback is deprecated BUT there does not seem to be a decent alternative in gradle 7 From 782f57104673674711685cadf4bd57513b45cc24 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 23:54:59 +0000 Subject: [PATCH 2/3] Include TestNG execution data in JaCoCo coverage report jacocoTestReport was only including execution data from the JUnit `test` task. The `testng` task also produces a JaCoCo .exec file but it was never merged into the report, causing code exercised only by TestNG tests to show zero coverage. Add `testng` to dependsOn and set executionData to include all .exec files from the jacoco output directory. https://claude.ai/code/session_01XDZqTUKLBoSJpGPGx9Amxg --- build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 10122ed96..6930858aa 100644 --- a/build.gradle +++ b/build.gradle @@ -567,13 +567,17 @@ jacoco { } jacocoTestReport { - dependsOn test + dependsOn test, testng reports { xml.required = true html.required = true csv.required = false } + // Include execution data from both the JUnit (test) and TestNG (testng) tasks + // so that coverage from TestNG tests is not missing from the report. + executionData.setFrom(fileTree(layout.buildDirectory.dir('jacoco')).include('*.exec')) + // Use the JaCoCo-specific classes (with @Generated annotations on equals/hashCode) // instead of the original classes, so coverage-modified bytecode is never published. // Also exclude generated ANTLR code from coverage. From 29cac5701df1ff73f6fa0d7c5b4f3aa04d062ebe Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Mar 2026 00:08:51 +0000 Subject: [PATCH 3/3] Fix JaCoCo coverage: use original classes, not bytecode-modified copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The markGeneratedEqualsHashCode task copied class files and then modified them with ASM to add @Generated annotations on equals/hashCode methods. This changed the bytecode CRC64 checksums, so JaCoCo could not correlate its execution data (recorded against the original classes) with the modified copies. Result: 61 classes with equals/hashCode showed ZERO coverage across all metrics — not just on equals/hashCode, but on every line, branch, and method in the class. Fix by pointing classDirectories back at the original classes/java/main directory and removing the markGeneratedEqualsHashCode task entirely. The small trade-off is that equals/hashCode methods now appear in coverage metrics, but all 61 affected classes regain their full coverage. https://claude.ai/code/session_01XDZqTUKLBoSJpGPGx9Amxg --- build.gradle | 64 ++++------------------------------------------------ 1 file changed, 4 insertions(+), 60 deletions(-) diff --git a/build.gradle b/build.gradle index 6930858aa..562142947 100644 --- a/build.gradle +++ b/build.gradle @@ -578,13 +578,12 @@ jacocoTestReport { // so that coverage from TestNG tests is not missing from the report. executionData.setFrom(fileTree(layout.buildDirectory.dir('jacoco')).include('*.exec')) - // Use the JaCoCo-specific classes (with @Generated annotations on equals/hashCode) - // instead of the original classes, so coverage-modified bytecode is never published. - // Also exclude generated ANTLR code from coverage. + // Use the original compiled classes (not a modified copy) so that JaCoCo's + // CRC64 checksums match the execution data recorded during testing. + // Exclude generated ANTLR parser code and shaded dependencies from coverage. afterEvaluate { - def jacocoClassDir = layout.buildDirectory.dir('classes-jacoco/java/main').get().asFile classDirectories.setFrom(files( - fileTree(dir: jacocoClassDir, exclude: [ + fileTree(dir: layout.buildDirectory.dir('classes/java/main'), exclude: [ 'graphql/parser/antlr/**', 'graphql/com/google/**', 'graphql/org/antlr/**' @@ -593,61 +592,6 @@ jacocoTestReport { } } -// --------------------------------------------------------------------------- -// Mark identity equals(Object)/hashCode() with a @Generated annotation so -// JaCoCo's AnnotationGeneratedFilter excludes them from coverage. -// The annotation class need not exist — JaCoCo only inspects the descriptor -// string in the bytecode, and the JVM ignores unknown CLASS-retention -// annotations. -// --------------------------------------------------------------------------- -tasks.register('markGeneratedEqualsHashCode') { - description = 'Add @Generated annotation to equals/hashCode so JaCoCo ignores them' - dependsOn classes - - doLast { - def srcDir = layout.buildDirectory.dir('classes/java/main').get().asFile - def destDir = layout.buildDirectory.dir('classes-jacoco/java/main').get().asFile - if (!srcDir.exists()) return - - // Copy all class files to a separate directory for JaCoCo - project.copy { - from srcDir - into destDir - } - - def ANNOTATION = 'Lgraphql/coverage/Generated;' - - destDir.eachFileRecurse(groovy.io.FileType.FILES) { file -> - if (!file.name.endsWith('.class')) return - - def bytes = file.bytes - def classNode = new org.objectweb.asm.tree.ClassNode() - new org.objectweb.asm.ClassReader(bytes).accept(classNode, 0) - - boolean modified = false - for (method in classNode.methods) { - if ((method.name == 'equals' && method.desc == '(Ljava/lang/Object;)Z') || - (method.name == 'hashCode' && method.desc == '()I')) { - if (method.invisibleAnnotations == null) { - method.invisibleAnnotations = [] - } - method.invisibleAnnotations.add(new org.objectweb.asm.tree.AnnotationNode(ANNOTATION)) - modified = true - } - } - - if (modified) { - def writer = new org.objectweb.asm.ClassWriter(0) - classNode.accept(writer) - file.bytes = writer.toByteArray() - } - } - } -} - -// Only JaCoCo reporting needs the annotated classes — never the jar or published artifacts -tasks.named('jacocoTestReport') { dependsOn markGeneratedEqualsHashCode } - /* * The gradle.buildFinished callback is deprecated BUT there does not seem to be a decent alternative in gradle 7 * So progress over perfection here