diff --git a/.github/actions/java-gradle/pre-merge/action.yml b/.github/actions/java-gradle/pre-merge/action.yml index df02a52fc8..7c76044f46 100644 --- a/.github/actions/java-gradle/pre-merge/action.yml +++ b/.github/actions/java-gradle/pre-merge/action.yml @@ -93,6 +93,27 @@ runs: USE_EXTERNAL_SERVER: true run: ./gradlew test + - name: Generate coverage report + if: inputs.task == 'test' + shell: bash + working-directory: foreign/java + env: + USE_EXTERNAL_SERVER: true + run: ./gradlew jacocoAggregatedReport + + - name: Copy coverage reports + if: ${{ !cancelled() && inputs.task == 'test' }} + shell: bash + run: | + if [ -f "foreign/java/build/reports/jacoco/aggregate/jacocoAggregated.xml" ]; then + echo "Found aggregated coverage report" + mkdir -p reports/java-coverage + cp foreign/java/build/reports/jacoco/aggregate/jacocoAggregated.xml reports/java-coverage/ + if [ -d "foreign/java/build/reports/jacoco/aggregate/html" ]; then + cp -r foreign/java/build/reports/jacoco/aggregate/html reports/java-coverage/ + fi + fi + - name: Copy test reports if: ${{ !cancelled() && inputs.task == 'test' }} shell: bash diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index d83ee8add6..b05626676b 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -104,6 +104,18 @@ jobs: with: task: ${{ inputs.task }} + - name: Upload Java coverage to Codecov + if: inputs.component == 'sdk-java' && inputs.task == 'test' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: reports/java-coverage/jacocoAggregated.xml + disable_search: true + flags: java + fail_ci_if_error: false + verbose: true + override_pr: ${{ github.event.pull_request.number }} + # C# SDK - name: Run C# SDK task if: inputs.component == 'sdk-csharp' diff --git a/codecov.yml b/codecov.yml index 0cdb369b59..1926f1853c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -48,6 +48,13 @@ ignore: - "core/harness_derive/**" - "bdd/**" - "examples/**" - - "foreign/**" + - "foreign/csharp/**" + - "foreign/go/**" + - "foreign/node/**" + - "foreign/python/**" - "web/**" - "**/tests/**" + - "**/*Test.java" + - "**/test/**" + - "**/build/**" + - "**/target/**" diff --git a/foreign/java/build.gradle.kts b/foreign/java/build.gradle.kts index d1d66fb3be..a0693eb039 100644 --- a/foreign/java/build.gradle.kts +++ b/foreign/java/build.gradle.kts @@ -21,6 +21,11 @@ import com.diffplug.gradle.spotless.SpotlessExtension plugins { alias(libs.plugins.spotless) apply false + jacoco +} + +repositories { + mavenCentral() } subprojects { @@ -103,3 +108,37 @@ subprojects { } } } + +tasks.register("jacocoAggregatedReport") { + description = "Generates aggregated code coverage report for all modules" + group = "verification" + + dependsOn(subprojects.map { it.tasks.named("test") }) + + // Aggregate execution data from all subprojects + executionData.setFrom(files(subprojects.mapNotNull { + val testTask = it.tasks.withType().findByName("test") + if (testTask != null && it.plugins.hasPlugin("java")) { + it.layout.buildDirectory.file("jacoco/test.exec").get().asFile + } else { + null + } + }.filter { it.exists() })) + + // Aggregate source and class files + subprojects.forEach { subproject -> + if (subproject.plugins.hasPlugin("java")) { + val sourceSets = subproject.extensions.getByType() + sourceDirectories.from(sourceSets.getByName("main").allSource.srcDirs) + classDirectories.from(files(sourceSets.getByName("main").output)) + } + } + + reports { + xml.required.set(true) + xml.outputLocation.set(layout.buildDirectory.file("reports/jacoco/aggregate/jacocoAggregated.xml")) + html.required.set(true) + html.outputLocation.set(layout.buildDirectory.dir("reports/jacoco/aggregate/html")) + csv.required.set(false) + } +} diff --git a/foreign/java/buildSrc/src/main/kotlin/iggy.java-common-conventions.gradle.kts b/foreign/java/buildSrc/src/main/kotlin/iggy.java-common-conventions.gradle.kts index bb86082b93..ba057c6b61 100644 --- a/foreign/java/buildSrc/src/main/kotlin/iggy.java-common-conventions.gradle.kts +++ b/foreign/java/buildSrc/src/main/kotlin/iggy.java-common-conventions.gradle.kts @@ -17,8 +17,11 @@ * under the License. */ +import org.gradle.api.artifacts.VersionCatalogsExtension + plugins { java + jacoco } repositories { @@ -39,6 +42,34 @@ tasks.withType { options.encoding = "UTF-8" } +jacoco { + toolVersion = extensions.getByType() + .named("libs") + .findVersion("jacoco") + .get() + .requiredVersion +} + tasks.withType { useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) + reports { + xml.required.set(true) + html.required.set(true) + csv.required.set(false) + } +} + +tasks.jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = "0.0".toBigDecimal() + } + } + } } diff --git a/foreign/java/gradle/libs.versions.toml b/foreign/java/gradle/libs.versions.toml index dd3e8408cb..108b5909f8 100644 --- a/foreign/java/gradle/libs.versions.toml +++ b/foreign/java/gradle/libs.versions.toml @@ -58,6 +58,7 @@ typesafe-config = "1.4.5" spotless = "8.1.0" shadow = "9.2.2" checkstyle = "12.1.2" +jacoco = "0.8.14" [libraries] # Jackson diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java index c9c3171895..c4354719c6 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java @@ -207,7 +207,8 @@ public static ByteBuf toBytes(TopicPermissions permissions) { } public static ByteBuf toBytes(String value) { - ByteBuf buffer = Unpooled.buffer(1 + value.length()); + int bufferLength = 1 + value.length(); + ByteBuf buffer = Unpooled.buffer(bufferLength); byte[] stringBytes = value.getBytes(StandardCharsets.UTF_8); buffer.writeByte(stringBytes.length); buffer.writeBytes(stringBytes);