This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new b52727eaaa NIFI-15432 Add system tests to code coverage workflow
(#10735)
b52727eaaa is described below
commit b52727eaaa2cc4f54ebfaaf1ce25b57c48389c6e
Author: Pierre Villard <[email protected]>
AuthorDate: Wed Jan 21 03:06:37 2026 +0100
NIFI-15432 Add system tests to code coverage workflow (#10735)
- Add system-tests job to collect coverage from spawned NiFi instances
- Configure JaCoCo agent injection via JACOCO_AGENT_PATH environment
variable
- Modify SpawnedStandaloneNiFiInstanceFactory to configure JaCoCo agent in
bootstrap.conf
- Collect and merge jacoco.exec files from system tests and stateless tests
- Convert merged coverage data to XML format for Codecov upload
- Dynamically extract module list from nifi-code-coverage/pom.xml
- Add nifi-registry-integration-tests profile to integration tests job
- Add jacoco.version property to root pom.xml for workflow compatibility
Signed-off-by: David Handermann <[email protected]>
---
.github/scripts/collect-system-tests-coverage.sh | 95 ++++++++++++++++++++++
.github/workflows/code-coverage.yml | 86 +++++++++++++++++++-
.../SpawnedStandaloneNiFiInstanceFactory.java | 68 +++++++++++++++-
pom.xml | 3 +-
4 files changed, 248 insertions(+), 4 deletions(-)
diff --git a/.github/scripts/collect-system-tests-coverage.sh
b/.github/scripts/collect-system-tests-coverage.sh
new file mode 100755
index 0000000000..ad02cdfba0
--- /dev/null
+++ b/.github/scripts/collect-system-tests-coverage.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Collects JaCoCo coverage files from system tests, merges them, and generates
an XML report.
+# This script is used by the code-coverage GitHub Actions workflow.
+
+set -e
+
+# Collect all jacoco.exec files from spawned NiFi instance directories
+EXEC_FILES=""
+for dir in nifi-system-tests/nifi-system-test-suite/target/*/coverage; do
+ if [ -f "$dir/jacoco.exec" ]; then
+ EXEC_FILES="$EXEC_FILES $dir/jacoco.exec"
+ echo "Found: $dir/jacoco.exec"
+ fi
+done
+
+# Also collect jacoco-it.exec from stateless tests (runs in-process, captured
by JaCoCo agent)
+if [ -f
"nifi-system-tests/nifi-stateless-system-test-suite/target/jacoco-it.exec" ];
then
+ EXEC_FILES="$EXEC_FILES
nifi-system-tests/nifi-stateless-system-test-suite/target/jacoco-it.exec"
+ echo "Found:
nifi-system-tests/nifi-stateless-system-test-suite/target/jacoco-it.exec"
+fi
+
+if [ -z "$EXEC_FILES" ]; then
+ echo "No coverage files found"
+ exit 0
+fi
+
+# Download JaCoCo CLI
+JACOCO_VERSION=$(./mvnw help:evaluate -Dexpression=jacoco.version -q
-DforceStdout 2>/dev/null)
+./mvnw dependency:copy
-Dartifact=org.jacoco:org.jacoco.cli:${JACOCO_VERSION}:jar:nodeps
-DoutputDirectory=target/jacoco-cli
+
+# Merge all exec files into one
+mkdir -p target/system-tests-coverage
+echo "Merging coverage files..."
+java -jar target/jacoco-cli/org.jacoco.cli-${JACOCO_VERSION}-nodeps.jar merge \
+ $EXEC_FILES \
+ --destfile target/system-tests-coverage/jacoco-merged.exec
+
+# Build classfiles by merging all module classes into a single directory
+# This avoids duplicate class errors that occur when same class exists in
multiple modules
+mkdir -p target/merged-classes
+
+# Extract module names from nifi-code-coverage/pom.xml (nifi-*, minifi-*, c2-*
only)
+# Exclude nifi-mock as it has class files that JaCoCo cannot analyze
+MODULES=$(grep '<artifactId>' nifi-code-coverage/pom.xml | sed
's/.*<artifactId>//' | sed 's/<.*//' | grep -E '^(nifi-|minifi-|c2-)' | grep -v
'nifi-code-coverage' | grep -v 'nifi-mock' | sort -u)
+
+SOURCEFILES_ARGS=""
+MODULE_COUNT=0
+
+for module in $MODULES; do
+ classdir=$(find . -type d -path "*/${module}/target/classes" 2>/dev/null |
head -1)
+ if [ -n "$classdir" ] && [ -d "$classdir" ]; then
+ # Copy classes to merged directory (later copies overwrite, deduplicating)
+ cp -r "$classdir"/* target/merged-classes/ 2>/dev/null || true
+ MODULE_COUNT=$((MODULE_COUNT + 1))
+ # Also add corresponding source directory
+ srcdir=$(echo "$classdir" | sed 's|/target/classes|/src/main/java|')
+ if [ -d "$srcdir" ]; then
+ SOURCEFILES_ARGS="$SOURCEFILES_ARGS --sourcefiles $srcdir"
+ fi
+ fi
+done
+
+echo "Merged classes from $MODULE_COUNT modules"
+
+echo "Generating XML report from merged classes..."
+java -jar target/jacoco-cli/org.jacoco.cli-${JACOCO_VERSION}-nodeps.jar report
\
+ target/system-tests-coverage/jacoco-merged.exec \
+ --classfiles target/merged-classes \
+ $SOURCEFILES_ARGS \
+ --xml target/system-tests-coverage/jacoco.xml 2>&1 || true
+
+# Check if report was generated
+if [ -f "target/system-tests-coverage/jacoco.xml" ]; then
+ echo "=== Generated XML report ==="
+ ls -la target/system-tests-coverage/jacoco.xml
+ head -20 target/system-tests-coverage/jacoco.xml
+else
+ echo "WARNING: No XML report was generated"
+fi
+
diff --git a/.github/workflows/code-coverage.yml
b/.github/workflows/code-coverage.yml
index a31cb4c581..da851caff8 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -58,7 +58,6 @@ jobs:
run: >
./mvnw --fail-fast --no-snapshot-updates --no-transfer-progress
--show-version
--threads 1C
- -D include-python-integration-tests=true
-P report-code-coverage
jacoco:prepare-agent
verify
@@ -69,6 +68,7 @@ jobs:
files: ./nifi-code-coverage/target/site/jacoco-aggregate/jacoco.xml
flags: unit-tests
token: ${{ secrets.CODECOV_TOKEN }}
+ disable_search: true
integration-tests:
timeout-minutes: 120
@@ -79,6 +79,7 @@ jobs:
verify
-P skip-unit-tests
-P integration-tests
+ -P nifi-registry-integration-tests
-P report-code-coverage
MAVEN_BUILD_EXCLUDE_PROJECTS: >-
-pl -:minifi-assembly
@@ -122,3 +123,86 @@ jobs:
files: ./nifi-code-coverage/target/site/jacoco-aggregate/jacoco.xml
flags: integration-tests
token: ${{ secrets.CODECOV_TOKEN }}
+ disable_search: true
+
+ system-tests:
+ timeout-minutes: 120
+ runs-on: ubuntu-24.04
+ name: Run System Tests
+ env:
+ MAVEN_BUILD_ARGUMENTS: >-
+ install
+ -D skipTests
+ -pl nifi-code-coverage
+ -am
+ MAVEN_RUN_ARGUMENTS: >-
+ verify
+ -P integration-tests
+ -P report-code-coverage
+ -D include-python-integration-tests=true
+ MAVEN_PROJECTS: >-
+ -pl :nifi-python-framework
+ -pl :nifi-python-extension-api
+ -pl :nifi-python-test-extensions
+ -pl :nifi-py4j-integration-tests
+ -pl nifi-system-tests/nifi-system-test-suite
+ -pl nifi-system-tests/nifi-stateless-system-test-suite
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v6
+ - name: Set up Java 21
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 21
+ cache: 'maven'
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.10'
+ - name: Set up Astral uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ python-version: '3.10'
+ enable-cache: false
+ - name: Build Dependencies
+ env:
+ MAVEN_OPTS: >-
+ ${{ env.DEFAULT_MAVEN_OPTS }}
+ run: >
+ ./mvnw
+ -V
+ -nsu
+ -ntp
+ -ff
+ ${{ env.MAVEN_BUILD_ARGUMENTS }}
+ ${{ env.MAVEN_PROJECTS }}
+ - name: Download JaCoCo Agent
+ run: |
+ JACOCO_VERSION=$(./mvnw help:evaluate -Dexpression=jacoco.version -q
-DforceStdout 2>/dev/null)
+ ./mvnw dependency:copy
-Dartifact=org.jacoco:org.jacoco.agent:${JACOCO_VERSION}:jar:runtime
-DoutputDirectory=target/jacoco-agent
+ echo
"JACOCO_AGENT_PATH=$(pwd)/target/jacoco-agent/org.jacoco.agent-${JACOCO_VERSION}-runtime.jar"
>> $GITHUB_ENV
+ - name: Run System Tests with Coverage
+ env:
+ MAVEN_OPTS: >-
+ ${{ env.DEFAULT_MAVEN_OPTS }}
+ JACOCO_AGENT_PATH: ${{ env.JACOCO_AGENT_PATH }}
+ run: >
+ ./mvnw
+ -V
+ -nsu
+ -ntp
+ -ff
+ ${{ env.MAVEN_RUN_ARGUMENTS }}
+ ${{ env.MAVEN_PROJECTS }}
+ - name: Collect and Convert Coverage Files
+ if: always()
+ run: .github/scripts/collect-system-tests-coverage.sh
+ - name: Upload Coverage to Codecov
+ uses: codecov/codecov-action@v5
+ if: github.repository_owner == 'apache'
+ with:
+ files: target/system-tests-coverage/jacoco.xml
+ flags: system-tests
+ token: ${{ secrets.CODECOV_TOKEN }}
+ disable_search: true
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/SpawnedStandaloneNiFiInstanceFactory.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/SpawnedStandaloneNiFiInstanceFactory.java
index bc3c17887f..5474a54a46 100644
---
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/SpawnedStandaloneNiFiInstanceFactory.java
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/SpawnedStandaloneNiFiInstanceFactory.java
@@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -51,6 +52,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class SpawnedStandaloneNiFiInstanceFactory implements
NiFiInstanceFactory {
private static final Logger logger =
LoggerFactory.getLogger(SpawnedStandaloneNiFiInstanceFactory.class);
+
+ /**
+ * Environment variable name for JaCoCo agent JAR path.
+ * When set, enables code coverage collection for spawned NiFi instances.
+ */
+ private static final String JACOCO_AGENT_PATH_ENV = "JACOCO_AGENT_PATH";
+
private final InstanceConfiguration instanceConfig;
public SpawnedStandaloneNiFiInstanceFactory(final InstanceConfiguration
instanceConfig) {
@@ -158,6 +166,9 @@ public class SpawnedStandaloneNiFiInstanceFactory
implements NiFiInstanceFactory
copyContents(bootstrapConfigFile.getParentFile(), destinationConf);
bootstrapConfigFile = new File(destinationConf,
bootstrapConfigFile.getName());
+ // Configure JaCoCo agent for code coverage if environment
variable is set
+ configureJacocoAgent(bootstrapConfigFile);
+
final File destinationLib = new File(instanceDirectory, "lib");
copyContents(new File("target/nifi-lib-assembly/lib"),
destinationLib);
@@ -241,6 +252,56 @@ public class SpawnedStandaloneNiFiInstanceFactory
implements NiFiInstanceFactory
}
}
+ /**
+ * Configures JaCoCo agent for code coverage collection if
JACOCO_AGENT_PATH environment variable is set.
+ * Appends JaCoCo agent JVM argument to the bootstrap configuration
file with a unique destfile
+ * based on the instance directory name.
+ *
+ * @param bootstrapConfig The bootstrap configuration file to modify
+ * @throws IOException if unable to write to the bootstrap
configuration file
+ */
+ private void configureJacocoAgent(final File bootstrapConfig) throws
IOException {
+ final String jacocoAgentPath =
System.getenv(JACOCO_AGENT_PATH_ENV);
+ if (jacocoAgentPath == null || jacocoAgentPath.isBlank()) {
+ return;
+ }
+
+ final File jacocoAgentFile = new File(jacocoAgentPath);
+ if (!jacocoAgentFile.exists()) {
+ logger.warn("JaCoCo agent path specified but file not found:
{}", jacocoAgentPath);
+ return;
+ }
+
+ // Create coverage output directory in instance directory
+ final File coverageDir = new File(instanceDirectory, "coverage");
+ if (!coverageDir.exists()) {
+ assertTrue(coverageDir.mkdirs());
+ }
+
+ // Use instance directory name to create unique coverage file
+ final String instanceName = instanceDirectory.getName();
+ final File coverageFile = new File(coverageDir, "jacoco.exec");
+
+ // Build JaCoCo agent argument with options:
+ // - destfile: unique file per instance to avoid conflicts
+ // - output=file: write coverage on JVM exit
+ // - append=true: append to existing coverage data if file exists
+ final String jacocoArg = String.format(
+ "-javaagent:%s=destfile=%s,output=file,append=true",
+ jacocoAgentFile.getAbsolutePath(),
+ coverageFile.getAbsolutePath()
+ );
+
+ // Append JaCoCo agent argument to bootstrap.conf
+ try (final FileWriter writer = new FileWriter(bootstrapConfig,
true)) {
+ writer.write(System.lineSeparator());
+ writer.write("# JaCoCo agent for code coverage
(auto-configured)" + System.lineSeparator());
+ writer.write("java.arg.jacoco=" + jacocoArg +
System.lineSeparator());
+ }
+
+ logger.info("Configured JaCoCo agent for NiFi instance [{}]:
destfile={}", instanceName, coverageFile.getAbsolutePath());
+ }
+
@Override
public boolean isAccessible() {
if (process == null) {
@@ -377,9 +438,12 @@ public class SpawnedStandaloneNiFiInstanceFactory
implements NiFiInstanceFactory
@Override
public void quarantineTroubleshootingInfo(final File destinationDir,
final Throwable cause) throws IOException {
- final String[] dirsToCopy = new String[] {"conf", "logs"};
+ final String[] dirsToCopy = new String[] {"conf", "logs",
"coverage"};
for (final String dirToCopy : dirsToCopy) {
- copyContents(new File(getInstanceDirectory(), dirToCopy), new
File(destinationDir, dirToCopy));
+ final File sourceDir = new File(getInstanceDirectory(),
dirToCopy);
+ if (sourceDir.exists()) {
+ copyContents(sourceDir, new File(destinationDir,
dirToCopy));
+ }
}
if (process == null) {
diff --git a/pom.xml b/pom.xml
index 6200fa35c3..a237d4adf2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -206,6 +206,7 @@
<pmd.version>7.20.0</pmd.version>
<checkstyle.version>13.0.0</checkstyle.version>
<testcontainers.version>2.0.3</testcontainers.version>
+ <jacoco.version>0.8.14</jacoco.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -821,7 +822,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
- <version>0.8.14</version>
+ <version>${jacoco.version}</version>
</plugin>
<plugin>
<groupId>io.swagger.core.v3</groupId>