From b2f086a20bceeb780c0070566d785bdae8176dfe Mon Sep 17 00:00:00 2001 From: stanbrub Date: Fri, 17 Jan 2025 13:10:35 -0700 Subject: [PATCH 1/2] Added optional Java coverage to the gradle build --- build.gradle | 30 ++++++++++++++++++++++++ coverage/all-coverage.py | 43 +++++++++++++++++++++++++++++++++++ coverage/exclude-packages.txt | 5 ++++ coverage/jacoco.gradle | 20 ++++++++++++++++ coverage/java-coverage.py | 21 +++++++++++++++++ 5 files changed, 119 insertions(+) create mode 100644 coverage/all-coverage.py create mode 100644 coverage/exclude-packages.txt create mode 100644 coverage/jacoco.gradle create mode 100644 coverage/java-coverage.py diff --git a/build.gradle b/build.gradle index a5086daf053..118a3fc9add 100644 --- a/build.gradle +++ b/build.gradle @@ -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" + 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 { + 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) + } +} + tasks.wrapper { Wrapper w -> w.distributionType = 'ALL' diff --git a/coverage/all-coverage.py b/coverage/all-coverage.py new file mode 100644 index 00000000000..d6ab6ff1d77 --- /dev/null +++ b/coverage/all-coverage.py @@ -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) + diff --git a/coverage/exclude-packages.txt b/coverage/exclude-packages.txt new file mode 100644 index 00000000000..d240b595cbb --- /dev/null +++ b/coverage/exclude-packages.txt @@ -0,0 +1,5 @@ +io.deephaven.tuple.generated +io.deephaven.proto.backplane.grpc +io.deephaven.proto.backplane.script.grpc +io.deephaven.proto + diff --git a/coverage/jacoco.gradle b/coverage/jacoco.gradle new file mode 100644 index 00000000000..ecae7a90269 --- /dev/null +++ b/coverage/jacoco.gradle @@ -0,0 +1,20 @@ + +if (Boolean.getBoolean("coverageEnabled")) { + apply plugin: 'jacoco' + 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') + } + } +} + diff --git a/coverage/java-coverage.py b/coverage/java-coverage.py new file mode 100644 index 00000000000..bafa31dc99a --- /dev/null +++ b/coverage/java-coverage.py @@ -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) From 03b985a464a8e9dd4763757b421ff1f6558904d1 Mon Sep 17 00:00:00 2001 From: stanbrub Date: Fri, 17 Jan 2025 14:58:43 -0700 Subject: [PATCH 2/2] Added README for the coverage project --- coverage/README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 coverage/README.md diff --git a/coverage/README.md b/coverage/README.md new file mode 100644 index 00000000000..d63ba5d31ba --- /dev/null +++ b/coverage/README.md @@ -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) +