Skip to content

Commit 3f9f781

Browse files
authored
build: DH-18477: Java coverage generated conditionally (deephaven#6586)
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
1 parent 0aa1d2c commit 3f9f781

9 files changed

+141
-34
lines changed

buildSrc/src/main/groovy/TestTools.groovy

-22
Original file line numberDiff line numberDiff line change
@@ -116,28 +116,6 @@ By default only runs in CI; to run locally:
116116
.replace "${separator}test$separator", "$separator$type$separator"
117117
(report as SimpleReport).outputLocation.set new File(rebased)
118118
}
119-
// this is not part of the standard class; it is glued on later by jacoco plugin;
120-
// we want to give each test it's own output files for jacoco analysis,
121-
// so we don't accidentally stomp on previous output.
122-
// TODO: verify jenkins is analyzing _all_ information here.
123-
if (project.findProperty('jacoco.enabled') == "true") {
124-
(t['jacoco'] as JacocoTaskExtension).with {
125-
destinationFile = project.provider({ new File(project.buildDir, "jacoco/${type}.exec".toString()) } as Callable<File>)
126-
classDumpDir = new File(project.buildDir, "jacoco/${type}Dumps".toString())
127-
}
128-
(project['jacocoTestReport'] as JacocoReport).with {
129-
reports {
130-
JacocoReportsContainer c ->
131-
c.xml.enabled = true
132-
c.csv.enabled = true
133-
c.html.enabled = true
134-
}
135-
}
136-
}
137-
138-
}
139-
if (project.findProperty('jacoco.enabled') == "true") {
140-
project.tasks.findByName('jacocoTestReport').mustRunAfter(t)
141119
}
142120

143121
return t

buildSrc/src/main/groovy/io.deephaven.java-jacoco-conventions.gradle

+8-6
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ plugins {
44
}
55

66
jacoco {
7-
toolVersion = '0.8.8'
7+
toolVersion = '0.8.12'
88
}
99

1010
jacocoTestReport {
11+
sourceSets sourceSets.main
12+
executionData = fileTree(buildDir).include("/jacoco/*.exec")
1113
reports {
12-
xml.enabled true
13-
csv.enabled true
14-
html.enabled true
14+
csv.required = true
15+
xml.required = true
16+
html.required = true
1517
}
1618
}
1719

18-
project.tasks.withType(Test).all { Test t ->
19-
finalizedBy jacocoTestReport
20+
project.tasks.withType(Test).configureEach { Test t ->
21+
t.jacoco.enabled = (project.findProperty('coverage.enabled') == 'true')
2022
}

buildSrc/src/main/groovy/io.deephaven.java-test-conventions.gradle

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
plugins {
22
id 'java'
3-
}
4-
5-
if (project.findProperty('jacoco.enabled') == 'true') {
6-
// Only load if jacoco enabled; otherwise
7-
// instrumentation of the code is still done.
8-
project.apply plugin: 'io.deephaven.java-jacoco-conventions'
3+
id 'io.deephaven.java-jacoco-conventions'
94
}
105

116
def testJar = project.tasks.register 'testJar', Jar, { Jar jar ->

coverage/README.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Overview
2+
3+
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.
4+
5+
## Running for Coverage
6+
7+
A typical run looks like the following that is run from the root of the multi-project build
8+
```
9+
./gradlew -Pcoverage.enabled=true check
10+
./gradlew -Pcoverage.enabled=true testSerial
11+
./gradlew -Pcoverage.enabled=true testParallel
12+
./gradlew -Pcoverage.enabled=true testOutOfBand
13+
./gradlew -Pcoverage.enabled=true jacocoTestReport
14+
./gradlew -Pcoverage.enabled=true coverage-merge
15+
```
16+
Running the second command is not contingent upon the first command succeeding. It merely collects what coverage is available.
17+
18+
## Result Files
19+
20+
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.
21+
22+
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_.
23+
24+
## Exclusion Filters
25+
26+
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.
27+
28+
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.
29+
30+
## File Layout
31+
32+
Top-level Build Directory (Some languages TBD)
33+
- `coverage/` This project's directory
34+
- `gather-coverage.py` Gather and normalize coverage for all languages
35+
- `exclude-packages.txt` A list of packages to exclude from aggregated results
36+
- `buildSrc/src/main/groovy/`
37+
- `io.deephaven.java-jacoco-conventions.gradle` Applied to run coverage on Java projects
38+
- `io.deephaven.java-test-conventions.gradle` Applies the above conditionally base on the _coverage.enabled_ property
39+
- `coverage/build/reports/coverage/`
40+
- `java-coverage.csv` Normalized coverage from all Java projects
41+
- `python-coverage.py` Normalized coverage from all Python projects
42+
- `cplus-coverage.py` Normalized coverage from all C++ projects
43+
- `r-coverage.py` Normalized coverage from all R projects
44+
- `go-coverage.oy` Normalized coverage from all Go projects
45+
- `all-coverage.csv` Normalized and filtered coverage from all covered projects
46+
- `coverage/build/reports/jacoco/jacoco-merge/html/`
47+
- `index.html` Root file to view Java coverage down to the branch level (not filtered)

coverage/build.gradle

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
plugins {
2+
id 'io.deephaven.project.register'
3+
id 'java'
4+
id 'jacoco'
5+
}
6+
7+
jacoco {
8+
toolVersion = '0.8.12'
9+
}
10+
11+
tasks.register("jacoco-merge", JacocoReport) {
12+
def jprojects = rootProject.allprojects.findAll { p-> p.plugins.hasPlugin('java') }
13+
additionalSourceDirs = files(jprojects.sourceSets.main.allSource.srcDirs)
14+
sourceDirectories = files(jprojects.sourceSets.main.allSource.srcDirs)
15+
classDirectories = files(jprojects.sourceSets.main.output)
16+
reports {
17+
html.required = true
18+
csv.required = true
19+
xml.required = false
20+
}
21+
def projRootDir = rootProject.rootDir.absolutePath
22+
executionData fileTree(projRootDir).include("**/build/jacoco/*.exec")
23+
}
24+
25+
tasks.register("coverage-merge", Exec) {
26+
dependsOn("jacoco-merge")
27+
def projDir = projectDir.absolutePath
28+
def script = projDir + '/gather-coverage.py'
29+
commandLine 'python', script, projDir
30+
standardOutput = System.out
31+
}
32+

coverage/exclude-packages.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
io.deephaven.tuple.generated
2+
io.deephaven.engine.table.impl.tuplesource.generated
3+
io.deephaven.proto.backplane.grpc
4+
io.deephaven.proto.backplane.script.grpc
5+
io.deephaven.proto

coverage/gather-coverage.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#
2+
# Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending
3+
#
4+
import sys, glob, csv, os, shutil
5+
6+
# Aggregate coverage data for all languages. Each language has a different way of doing
7+
# coverage and each normalization mechanism is used here. Class/file exclusions are
8+
# handled here, since coverage tools are inconsistent or non-functional in that regard.
9+
10+
proj_root_dir = sys.argv[1]
11+
script_dir = os.path.dirname(os.path.abspath(__file__))
12+
coverage_dir = proj_root_dir + '/build/reports/coverage'
13+
coverage_output_path = coverage_dir + '/all-coverage.csv'
14+
coverage_input_glob = coverage_dir + '/*-coverage.csv'
15+
exclude_path = script_dir + '/exclude-packages.txt'
16+
17+
if os.path.exists(coverage_dir):
18+
shutil.rmtree(coverage_dir)
19+
os.makedirs(coverage_dir)
20+
21+
# Aggregate and normalize coverage for java projects
22+
input_glob = proj_root_dir + '/build/reports/jacoco/jacoco-merge/jacoco-merge.csv'
23+
with open(f'{coverage_dir}/java-coverage.csv', 'w', newline='') as outfile:
24+
csv_writer = csv.writer(outfile)
25+
csv_writer.writerow(['Language','Project','Package','Class','Missed','Covered'])
26+
for filename in glob.glob(input_glob, recursive = True):
27+
with open(filename, 'r') as csv_in:
28+
csv_reader = csv.reader(csv_in)
29+
next(csv_reader, None)
30+
for row in csv_reader:
31+
new_row = ['java',row[0],row[1],row[2],row[3],row[4]]
32+
csv_writer.writerow(new_row)
33+
34+
# Load packages to be excluded from the aggregated coverage CSV
35+
with open(exclude_path) as f:
36+
excludes = [line.strip() for line in f]
37+
38+
# Collect coverage CSVs into a single CSV without lines containing exclusions
39+
with open(coverage_output_path, 'w', newline='') as outfile:
40+
csv_writer = csv.writer(outfile)
41+
for csv_file in glob.glob(coverage_input_glob):
42+
with open(csv_file, 'r') as csv_in:
43+
for row in csv.reader(csv_in):
44+
if row[2] in excludes: continue
45+
new_row = [row[0],row[1],row[2],row[3],row[4],row[5]]
46+
csv_writer.writerow(new_row)

coverage/gradle.properties

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.deephaven.project.ProjectType=BASIC

settings.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ project(':configs').projectDir = file('props/configs')
5151
include(':test-configs')
5252
project(':test-configs').projectDir = file('props/test-configs')
5353

54+
include 'coverage'
5455
include 'combined-javadoc'
5556

5657
include 'grpc-java:grpc-servlet-jakarta'

0 commit comments

Comments
 (0)