diff --git a/.github/workflows/sql-test-and-build-workflow.yml b/.github/workflows/sql-test-and-build-workflow.yml index ccc7f11d605..18b0cdfc232 100644 --- a/.github/workflows/sql-test-and-build-workflow.yml +++ b/.github/workflows/sql-test-and-build-workflow.yml @@ -106,7 +106,7 @@ jobs: - name: Build and Test run: | if [ "${{ matrix.test-type }}" = "unit" ]; then - ./gradlew --continue build -x spotlessCheck -x integTest -x tracingIntegTest -x yamlRestTest -x doctest + ./gradlew --continue build jacocoAggregatedReport -x spotlessCheck -x integTest -x tracingIntegTest -x yamlRestTest -x doctest elif [ "${{ matrix.test-type }}" = "integration" ]; then ./gradlew --continue integTest tracingIntegTest yamlRestTest else @@ -124,7 +124,7 @@ jobs: uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: jacoco-coverage-reports - path: '**/build/reports/jacoco/test/jacocoTestReport.xml' + path: build/reports/jacoco/jacocoAggregatedReport/jacocoAggregatedReport.xml retention-days: 1 if-no-files-found: error diff --git a/DEVELOPER_GUIDE.rst b/DEVELOPER_GUIDE.rst index a179b1fc64d..4c75cfdeaa7 100644 --- a/DEVELOPER_GUIDE.rst +++ b/DEVELOPER_GUIDE.rst @@ -243,6 +243,8 @@ For faster local iterations, skip integration tests. ``./gradlew build -x integT - Compile all Java source files. * - ./gradlew test - Run all unit tests. + * - ./gradlew test jacocoAggregatedReport + - Run all unit tests and generate a code coverage report across all modules in build/reports/jacoco/jacocoAggregatedReport. * - ./gradlew :integ-test:integTest - Run all integration test (this takes time). * - ./gradlew :integ-test:yamlRestTest @@ -357,7 +359,7 @@ Here is a sample for integration test for your reference: } } -Finally thanks to JaCoCo library, you can check out the test coverage in ``/build/reports/jacoco`` for your changes easily. +Finally thanks to JaCoCo library, you can check out the test coverage in ``/build/reports/jacoco`` for your changes easily. A module's report only counts the tests of that module. To also count tests in other modules (for example ``ppl`` tests of ``core`` classes), which is the coverage that Codecov reports, run ``./gradlew test jacocoAggregatedReport`` and open ``build/reports/jacoco/jacocoAggregatedReport/html/index.html``. Deploying Locally ----------------- diff --git a/build.gradle b/build.gradle index 348ff34563b..be38845404d 100644 --- a/build.gradle +++ b/build.gradle @@ -300,6 +300,56 @@ jacocoTestCoverageVerification { } check.dependsOn jacocoTestCoverageVerification +// Each module's jacocoTestReport credits a class only with the tests of its own module, but many +// classes are mostly tested from other modules (for example core's CalciteRelNodeVisitor is tested +// by ppl's CalcitePPL*Test). This report merges the unit test execution data of every module that +// applies jacoco and reports it against the classes of the modules below, with the same excludes as +// their own jacocoTestReport. CI uploads it to Codecov. The per-module +// jacocoTestCoverageVerification rules still measure each module's own tests only. +def aggregatedCoverageModules = [ + ':api', + ':async-query', + ':async-query-core', + ':core', + ':datasources', + ':direct-query', + ':direct-query-core', + ':opensearch', + ':ppl', + ':prometheus', + ':protocol', + ':sql', +] +def jacocoAggregatedReport = tasks.register('jacocoAggregatedReport', JacocoReport) { + group = 'verification' + description = 'Generates a code coverage report of all modules from the unit tests of all modules. ' + + 'Does not run the tests; use ./gradlew test jacocoAggregatedReport.' + reports { + html.required = true + xml.required = true + csv.required = false + } +} +// The module reports set their excludes in afterEvaluate, so read them once every project is evaluated. +gradle.projectsEvaluated { + jacocoAggregatedReport.configure { + subprojects.findAll { it.plugins.hasPlugin('jacoco') }.each { p -> + // 'junit4' is the JUnit 4 test task of async-query, direct-query and direct-query-core. + p.tasks.withType(Test).matching { it.name in ['test', 'junit4'] }.each { testTask -> + // Like each module's jacocoTestReport, only run after the tests rather than depend on + // them, so that a failing test still leaves a report for CI to upload. + mustRunAfter testTask + executionData testTask + } + } + aggregatedCoverageModules.each { path -> + def moduleReport = project(path).tasks.named('jacocoTestReport', JacocoReport).get() + classDirectories.from(moduleReport.classDirectories) + sourceDirectories.from(moduleReport.sourceDirectories) + } + } +} + configurations.all { resolutionStrategy.force 'junit:junit:4.13.2'