Skip to content
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-18477: Java coverage generated conditionally #6586

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,20 @@ plugins {
}

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

jacocoTestReport {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to look into hooking up integration tests as part of the coverage path. Right now, it seems like only the "test" task is included for the reporting. https://docs.gradle.org/current/userguide/jacoco_plugin.html#sec:jacoco_tasks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the README.md I have the expected command line for running everything. Are there more than check, testSerial, testParallel, testOutOfBand? How to I run anything that's missing?

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
task coverage {
dependsOn jacocoTestReport
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ plugins {
id 'java'
}

if (project.findProperty('jacoco.enabled') == 'true') {
// Only load if jacoco enabled; otherwise
// instrumentation of the code is still done.
// Apply Jacoco instrumentation and coverage reporting
if (project.findProperty('coverage.enabled') == 'true') {
Comment on lines +5 to +6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@niloc132; I wonder if we should unconditionally apply the jacoco plugin.

Copy link
Contributor Author

@stanbrub stanbrub Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that affect things like JMH because of instrumentation?

project.apply plugin: 'io.deephaven.java-jacoco-conventions'
}

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 coverage
./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-report-aggregation'
}

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