diff --git a/.github/workflows/test-and-build-workflow.yml b/.github/workflows/test-and-build-workflow.yml index 50e050e88..2d2764612 100644 --- a/.github/workflows/test-and-build-workflow.yml +++ b/.github/workflows/test-and-build-workflow.yml @@ -15,6 +15,7 @@ jobs: test-and-build-linux: needs: Get-CI-Image-Tag + timeout-minutes: 25 env: TEST_FILTER: ${{ matrix.test_filter }} strategy: @@ -51,9 +52,22 @@ jobs: # This is a hack, but this step creates a link to the X: mounted drive, which makes the path # short enough to work on Windows - name: Build with Gradle + timeout-minutes: 20 run: | chown -R 1000:1000 `pwd` su `id -un 1000` -c "./gradlew build ${{ env.TEST_FILTER }}" + - name: Generate Jacoco Test Report + if: always() && matrix.java == 21 + run: | + su `id -un 1000` -c "./gradlew jacocoTestReport" + - name: Upload coverage XML + uses: actions/upload-artifact@v4 + if: always() && matrix.java == 21 + with: + name: coverage-xml-${{ matrix.java }}-${{ matrix.feature }} + path: build/reports/jacoco/**/jacocoTestReport.xml + if-no-files-found: warn + overwrite: 'true' - name: Upload failed logs uses: actions/upload-artifact@v4 if: ${{ failure() }} @@ -61,14 +75,17 @@ jobs: name: logs-${{ matrix.java }}-${{ matrix.feature }} path: build/testclusters/integTest-*/logs/* overwrite: 'true' + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: test-reports-linux-${{ matrix.java }}-${{ matrix.feature }} + path: build/reports/ + overwrite: 'true' - name: Create Artifact Path run: | mkdir -p index-management-artifacts cp ./build/distributions/*.zip index-management-artifacts - - name: Uploads coverage - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} # This step uses the upload-artifact Github action: https://github.com/actions/upload-artifact - name: Upload Artifacts # v4 requires node.js 20 which is not supported @@ -134,3 +151,41 @@ jobs: name: index-management-plugin-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.feature }} path: index-management-artifacts overwrite: 'true' + + report-coverage: + needs: ["test-and-build-linux"] + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: downloaded-artifacts + pattern: coverage-xml-* + + - name: Display structure of downloaded files + run: ls -R + working-directory: downloaded-artifacts + + - name: Check if coverage files exist + id: check_coverage + run: | + if find downloaded-artifacts -name "*.xml" -type f | grep -q .; then + echo "Coverage XML files found" + echo "has_coverage=true" >> $GITHUB_OUTPUT + else + echo "No coverage XML files found" + echo "has_coverage=false" >> $GITHUB_OUTPUT + fi + + - name: Upload Coverage with retry + if: steps.check_coverage.outputs.has_coverage == 'true' + uses: Wandalen/wretry.action@v3.8.0 + with: + attempt_limit: 5 + attempt_delay: 2000 + action: codecov/codecov-action@v4 + with: | + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + verbose: true diff --git a/.gitignore b/.gitignore index c97a071cd..743a1ef29 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ bin/ spi/bin/ src/test/resources/notifications* +.claude +CLAUDE.md diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 690727245..1c3f002ba 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -5,6 +5,7 @@ - [Setup](#setup) - [Build](#build) - [Building from the command line](#building-from-the-command-line) + - [Code Coverage](#code-coverage) - [Debugging](#debugging) - [Using IntelliJ IDEA](#using-intellij-idea) - [Submitting Changes](#submitting-changes) @@ -63,6 +64,31 @@ However, to build the `index management` plugin project, we also use the OpenSea When launching a cluster using one of the above commands, logs are placed in `build/testclusters/integTest-0/logs`. Though the logs are teed to the console, in practices it's best to check the actual log file. +### Code Coverage + +The project supports generating code coverage reports using JaCoCo. + +#### Generating Coverage Reports + +To generate code coverage reports + +```bash +./gradlew check -Dtests.coverage=true +``` + +Or run any tests, and generate test report + +```bash +./gradlew test +./gradlew jacocoTestReport +``` + +#### Coverage Report Locations + +After running with coverage enabled, reports are generated in: +- **HTML Report**: `build/reports/jacoco/test/html/index.html` (human readable) +- **XML Report**: `build/reports/jacoco/test/jacocoTestReport.xml` (for tools like Codecov) + ### Debugging Sometimes it is useful to attach a debugger to either the OpenSearch cluster or the integ tests to see what's going on. When running unit tests, hit **Debug** from the IDE's gutter to debug the tests. For the OpenSearch cluster or the integ tests, first, make sure start a debugger listening on port `5005`. diff --git a/build-tools/coverage.gradle b/build-tools/coverage.gradle index 8abac12a1..684e140f2 100644 --- a/build-tools/coverage.gradle +++ b/build-tools/coverage.gradle @@ -19,7 +19,7 @@ // Get gradle to generate the required jvm agent arg for us using a dummy tasks of type Test. Unfortunately Elastic's // testing tasks don't derive from Test so the jacoco plugin can't do this automatically. -def jacocoDir = "${buildDir}/jacoco" +def jacocoDir = layout.buildDirectory.dir("jacoco").get().asFile.absolutePath tasks.register("dummyTest", Test) { enabled = false @@ -42,12 +42,28 @@ tasks.register("dummyIntegTest", Test) { } integTest { - systemProperty 'jacoco.dir', "${jacocoDir}" + systemProperty 'jacoco.dir', jacocoDir } jacocoTestReport { - dependsOn integTest, test - executionData dummyTest.jacoco.destinationFile, dummyIntegTest.jacoco.destinationFile + // Use mustRunAfter instead of dependsOn to avoid re-running tests + // This ensures proper ordering without forcing test execution + mustRunAfter test, integTest + + // Configure execution data to use any available .exec files + executionData.from fileTree(dir: jacocoDir, include: '*.exec') + + // Make the task skip only if no execution data exists at all + // Using a closure to defer the check until execution time + onlyIf { + def execFiles = fileTree(dir: jacocoDir, include: '*.exec').files + def hasExecFiles = execFiles.any { it.exists() } + if (!hasExecFiles) { + println("Skipping jacocoTestReport: No execution data files found in ${jacocoDir}") + } + hasExecFiles + } + sourceDirectories.from = "src/main/kotlin" classDirectories.from = sourceSets.main.output reports { @@ -58,8 +74,6 @@ jacocoTestReport { allprojects{ afterEvaluate { - jacocoTestReport.dependsOn integTest - testClusters.integTest { jvmArgs " ${dummyIntegTest.jacoco.getAsJvmArg()}".replace('javaagent:','javaagent:/') systemProperty 'com.sun.management.jmxremote', "true" @@ -69,4 +83,11 @@ allprojects{ systemProperty 'java.rmi.server.hostname', "127.0.0.1" } } +} + +// Attach code coverage report task to Gradle check task when coverage is enabled +if (System.getProperty("tests.coverage")) { + project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME).configure { + dependsOn tasks.named('jacocoTestReport', JacocoReport) + } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 223c57ecd..62a7b4a6f 100644 --- a/build.gradle +++ b/build.gradle @@ -134,7 +134,6 @@ def usingMultiNode = project.properties.containsKey('numNodes') if (!usingRemoteCluster && !usingMultiNode) { apply from: 'build-tools/coverage.gradle' } -check.dependsOn jacocoTestReport opensearchplugin { name 'opensearch-index-management' diff --git a/spi/build.gradle b/spi/build.gradle index ec58e5ad2..dbdabb622 100644 --- a/spi/build.gradle +++ b/spi/build.gradle @@ -44,7 +44,6 @@ jacocoTestReport { html.destination file("${buildDir}/jacoco/") } } -check.dependsOn jacocoTestReport repositories { mavenLocal()