Skip to content

Commit

Permalink
build: DH-18477: Java coverage generated conditionally (deephaven#6586)
Browse files Browse the repository at this point in the history
Java coverage in the DHC build with Jacoco was broken at some point.
Fixed that and added coverage merge. Full coverage can be run and merged
with the following:
```
./gradlew -Pcoverage.enabled=true check
./gradlew -Pcoverage.enabled=true testSerial
./gradlew -Pcoverage.enabled=true testParallel
./gradlew -Pcoverage.enabled=true testOutOfBand
./gradlew -Pcoverage.enabled=true jacocoTestReport
./gradlew -Pcoverage.enabled=true coverage-merge
```
This produces HTML, CSV, XML coverage artifacts at the Java project
level. It also produces a merged HTML site in the root "build/reports"
directory.

If the "coverage.enabled" property is not supplied or is false, "check"
will run as usual without instrumentation.

Jira: https://deephaven.atlassian.net/browse/DH-18477
  • Loading branch information
stanbrub authored Feb 6, 2025
1 parent 0aa1d2c commit 3f9f781
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 34 deletions.
22 changes: 0 additions & 22 deletions buildSrc/src/main/groovy/TestTools.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -116,28 +116,6 @@ By default only runs in CI; to run locally:
.replace "${separator}test$separator", "$separator$type$separator"
(report as SimpleReport).outputLocation.set new File(rebased)
}
// this is not part of the standard class; it is glued on later by jacoco plugin;
// we want to give each test it's own output files for jacoco analysis,
// so we don't accidentally stomp on previous output.
// TODO: verify jenkins is analyzing _all_ information here.
if (project.findProperty('jacoco.enabled') == "true") {
(t['jacoco'] as JacocoTaskExtension).with {
destinationFile = project.provider({ new File(project.buildDir, "jacoco/${type}.exec".toString()) } as Callable<File>)
classDumpDir = new File(project.buildDir, "jacoco/${type}Dumps".toString())
}
(project['jacocoTestReport'] as JacocoReport).with {
reports {
JacocoReportsContainer c ->
c.xml.enabled = true
c.csv.enabled = true
c.html.enabled = true
}
}
}

}
if (project.findProperty('jacoco.enabled') == "true") {
project.tasks.findByName('jacocoTestReport').mustRunAfter(t)
}

return t
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ plugins {
}

jacoco {
toolVersion = '0.8.8'
toolVersion = '0.8.12'
}

jacocoTestReport {
sourceSets sourceSets.main
executionData = fileTree(buildDir).include("/jacoco/*.exec")
reports {
xml.enabled true
csv.enabled true
html.enabled true
csv.required = true
xml.required = true
html.required = true
}
}

project.tasks.withType(Test).all { Test t ->
finalizedBy jacocoTestReport
project.tasks.withType(Test).configureEach { Test t ->
t.jacoco.enabled = (project.findProperty('coverage.enabled') == 'true')
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
plugins {
id 'java'
}

if (project.findProperty('jacoco.enabled') == 'true') {
// Only load if jacoco enabled; otherwise
// instrumentation of the code is still done.
project.apply plugin: 'io.deephaven.java-jacoco-conventions'
id 'io.deephaven.java-jacoco-conventions'
}

def testJar = project.tasks.register 'testJar', Jar, { Jar jar ->
Expand Down
47 changes: 47 additions & 0 deletions coverage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Overview

Code coverage for Deephaven Community Core manages multiple languages like Java, Python, R, Go and C++. This is handled in the gradle build at the individual project level but also supports collection of normalized results rolled up to the top level. For convenience, both top-level Java HTML and a top-level all-language CSV are created.

## Running for Coverage

A typical run looks like the following that is run from the root of the multi-project build
```
./gradlew -Pcoverage.enabled=true check
./gradlew -Pcoverage.enabled=true testSerial
./gradlew -Pcoverage.enabled=true testParallel
./gradlew -Pcoverage.enabled=true testOutOfBand
./gradlew -Pcoverage.enabled=true jacocoTestReport
./gradlew -Pcoverage.enabled=true coverage-merge
```
Running the second command is not contingent upon the first command succeeding. It merely collects what coverage is available.

## Result Files

Results for individual project coverage are stored in the project's _build_ output directory. Depending on the language and coverage tools, there will be different result files with slightly different locations and names. For example, Java coverage could produce a binary _jacoco.exec_ file, while python coverage produces a tabbed text file.

Aggregated results produce a merged CSV file for each language under the top-level _build_ directory. Those CSV files are further merged into one _all-coverage.csv_.

## Exclusion Filters

In some cases, there may be a need to exclude some packages from coverage, even though they may be used during testing. For example, some Java classes used in GRPC are generated. The expectation is that the generator mechanism has already been tested and should produce viable classes. Including coverage for those classes in the results as zero coverage causes unnecessary noise and makes it harder to track coverage overall.

To avoid unneeded coverage, the file _exclude-packages.txt_ can be used. This is a list of values to be excluded if they match the "Package" column in the coverage CSV. These are exact values and not wildcards.

## File Layout

Top-level Build Directory (Some languages TBD)
- `coverage/` This project's directory
- `gather-coverage.py` Gather and normalize coverage for all languages
- `exclude-packages.txt` A list of packages to exclude from aggregated results
- `buildSrc/src/main/groovy/`
- `io.deephaven.java-jacoco-conventions.gradle` Applied to run coverage on Java projects
- `io.deephaven.java-test-conventions.gradle` Applies the above conditionally base on the _coverage.enabled_ property
- `coverage/build/reports/coverage/`
- `java-coverage.csv` Normalized coverage from all Java projects
- `python-coverage.py` Normalized coverage from all Python projects
- `cplus-coverage.py` Normalized coverage from all C++ projects
- `r-coverage.py` Normalized coverage from all R projects
- `go-coverage.oy` Normalized coverage from all Go projects
- `all-coverage.csv` Normalized and filtered coverage from all covered projects
- `coverage/build/reports/jacoco/jacoco-merge/html/`
- `index.html` Root file to view Java coverage down to the branch level (not filtered)
32 changes: 32 additions & 0 deletions coverage/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
plugins {
id 'io.deephaven.project.register'
id 'java'
id 'jacoco'
}

jacoco {
toolVersion = '0.8.12'
}

tasks.register("jacoco-merge", JacocoReport) {
def jprojects = rootProject.allprojects.findAll { p-> p.plugins.hasPlugin('java') }
additionalSourceDirs = files(jprojects.sourceSets.main.allSource.srcDirs)
sourceDirectories = files(jprojects.sourceSets.main.allSource.srcDirs)
classDirectories = files(jprojects.sourceSets.main.output)
reports {
html.required = true
csv.required = true
xml.required = false
}
def projRootDir = rootProject.rootDir.absolutePath
executionData fileTree(projRootDir).include("**/build/jacoco/*.exec")
}

tasks.register("coverage-merge", Exec) {
dependsOn("jacoco-merge")
def projDir = projectDir.absolutePath
def script = projDir + '/gather-coverage.py'
commandLine 'python', script, projDir
standardOutput = System.out
}

5 changes: 5 additions & 0 deletions coverage/exclude-packages.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
io.deephaven.tuple.generated
io.deephaven.engine.table.impl.tuplesource.generated
io.deephaven.proto.backplane.grpc
io.deephaven.proto.backplane.script.grpc
io.deephaven.proto
46 changes: 46 additions & 0 deletions coverage/gather-coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#
# Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending
#
import sys, glob, csv, os, shutil

# Aggregate coverage data for all languages. Each language has a different way of doing
# coverage and each normalization mechanism is used here. Class/file exclusions are
# handled here, since coverage tools are inconsistent or non-functional in that regard.

proj_root_dir = sys.argv[1]
script_dir = os.path.dirname(os.path.abspath(__file__))
coverage_dir = proj_root_dir + '/build/reports/coverage'
coverage_output_path = coverage_dir + '/all-coverage.csv'
coverage_input_glob = coverage_dir + '/*-coverage.csv'
exclude_path = script_dir + '/exclude-packages.txt'

if os.path.exists(coverage_dir):
shutil.rmtree(coverage_dir)
os.makedirs(coverage_dir)

# Aggregate and normalize coverage for java projects
input_glob = proj_root_dir + '/build/reports/jacoco/jacoco-merge/jacoco-merge.csv'
with open(f'{coverage_dir}/java-coverage.csv', 'w', newline='') as outfile:
csv_writer = csv.writer(outfile)
csv_writer.writerow(['Language','Project','Package','Class','Missed','Covered'])
for filename in glob.glob(input_glob, recursive = True):
with open(filename, 'r') as csv_in:
csv_reader = csv.reader(csv_in)
next(csv_reader, None)
for row in csv_reader:
new_row = ['java',row[0],row[1],row[2],row[3],row[4]]
csv_writer.writerow(new_row)

# Load packages to be excluded from the aggregated coverage CSV
with open(exclude_path) as f:
excludes = [line.strip() for line in f]

# Collect coverage CSVs into a single CSV without lines containing exclusions
with open(coverage_output_path, 'w', newline='') as outfile:
csv_writer = csv.writer(outfile)
for csv_file in glob.glob(coverage_input_glob):
with open(csv_file, 'r') as csv_in:
for row in csv.reader(csv_in):
if row[2] in excludes: continue
new_row = [row[0],row[1],row[2],row[3],row[4],row[5]]
csv_writer.writerow(new_row)
1 change: 1 addition & 0 deletions coverage/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.deephaven.project.ProjectType=BASIC
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ project(':configs').projectDir = file('props/configs')
include(':test-configs')
project(':test-configs').projectDir = file('props/test-configs')

include 'coverage'
include 'combined-javadoc'

include 'grpc-java:grpc-servlet-jakarta'
Expand Down

0 comments on commit 3f9f781

Please sign in to comment.