-
Notifications
You must be signed in to change notification settings - Fork 80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
build: DH-18379: Java Code Coverage #6576
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
plugins { | ||
id 'base' | ||
id 'io.deephaven.project.register' | ||
id 'jacoco-report-aggregation' | ||
} | ||
|
||
import org.gradle.internal.jvm.Jvm | ||
|
@@ -84,6 +85,35 @@ tasks.register('smoke') { | |
it.dependsOn project(':Generators').tasks.findByName(LifecycleBasePlugin.CHECK_TASK_NAME) | ||
} | ||
|
||
tasks.register("coverage") { | ||
System.setProperty "coverageEnabled", "true" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not be configuring system properties based on when this task is executed... we should be using gradle properties for parts of the build system we want configurable. |
||
allprojects.findAll { p-> p.plugins.hasPlugin('java') }.each { | ||
it.apply(['from':rootProject.file("coverage/jacoco.gradle")]) | ||
} | ||
} | ||
|
||
tasks.register("coverage-merge", JacocoReport) { | ||
def jprojects = 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 = project.rootDir.absolutePath | ||
executionData fileTree(projRootDir).include("**/build/jacoco/*.exec") | ||
doLast { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is probably bad to attach a doLast to an existing Task type; likely, you'll want a separate task to depend on the output of JacocoReport. |
||
def stdout = new StringBuilder(), stderr = new StringBuilder() | ||
def task = ('python ' + projRootDir + '/coverage/all-coverage.py ' + projRootDir).execute() | ||
task.consumeProcessOutput(stdout, stderr) | ||
task.waitFor() | ||
println(stdout) | ||
println(stderr) | ||
Comment on lines
+112
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here on purpose? |
||
} | ||
} | ||
Comment on lines
+90
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not be building in this logic at the root build.gradle level; we have buildSrc plugins to do this. |
||
|
||
tasks.wrapper { | ||
Wrapper w -> | ||
w.distributionType = 'ALL' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# Overview | ||
|
||
This project is a collection of gradle builds and scripts for running and gathering code coverage over projects using different languages. The Gradle workflow for this allows project coverage to be run optionally. Since coverage is a separate step, the "check" task runs normally with no instrumentation. This tool is intended to be run from the top down and not against individual projects. After "check" runs with coverage turned on, the _coverage-merge_ task can be used to aggregate individual project coverage into the top-level build directory. | ||
|
||
## Running for Coverage | ||
|
||
A typical run looks like the following that is run from the root of the multi-project build | ||
``` | ||
./gradlew coverage check | ||
./gradlew 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 | ||
- `java-coverage.py` Gather and normalize coverage for Java projects | ||
- `python-coverage.py` Gather and normalize coverage for Python projects | ||
- `cplus-coverage.py` Gather and normalize coverage for C++ projects | ||
- `r-coverage.py` Gather and normalize coverage for R projects | ||
- `go-coverage.oy` Gather and normalize coverage for Go projects | ||
- `all-coverage.py` Merged all normalized coverage and apply exclusions | ||
- `exclude-packages.txt` A list of packages to exclude from aggregated results | ||
- `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 | ||
- `build/reports/jacoco/converage-merge/html` | ||
- `index.html` Root file to view Java coverage down to the branch level (not filtered) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# | ||
# Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending | ||
# | ||
import sys, glob, csv, os, subprocess | ||
|
||
# Aggregate coverage data for all languages. Each language has a different way of doing | ||
# coverage and each normalization mechanism is called 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_output_path): | ||
os.remove(coverage_output_path) | ||
|
||
def pycall(lang): | ||
lang_cov = f'{lang}-coverage' | ||
cmd = f'python {script_dir}/{lang_cov}.py {proj_root_dir} {coverage_dir}/{lang_cov}.csv' | ||
result = subprocess.check_output(cmd, shell=True, text=True) | ||
print(result) | ||
|
||
# Aggregate and normalize coverage for projects that use each language | ||
pycall('java') | ||
#pycall('python') | ||
|
||
# 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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
io.deephaven.tuple.generated | ||
io.deephaven.proto.backplane.grpc | ||
io.deephaven.proto.backplane.script.grpc | ||
io.deephaven.proto | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
|
||
if (Boolean.getBoolean("coverageEnabled")) { | ||
apply plugin: 'jacoco' | ||
Comment on lines
+2
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already having something like this as a buildSrc plugin. If the gradle property |
||
jacoco { | ||
toolVersion = '0.8.12' | ||
} | ||
test { | ||
finalizedBy jacocoTestReport | ||
} | ||
jacocoTestReport { | ||
dependsOn test | ||
reports { | ||
csv.required = true | ||
csv.destination = layout.buildDirectory.file('reports/jacoco/java-coverage.csv').get().asFile | ||
xml.required = false | ||
html.outputLocation = layout.buildDirectory.dir('reports/jacoco/html') | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# | ||
# Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending | ||
# | ||
import sys, glob, csv | ||
|
||
# Convert java coverage CSV files to the normalized form from a multi-project | ||
# root, merge, and write to the given CSV output path | ||
|
||
input_glob = sys.argv[1] + '/**/build/reports/jacoco/java-coverage.csv' | ||
output_path = sys.argv[2] | ||
|
||
with open(output_path, '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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused... is this plugin actually being used?