This is an automated email from the ASF dual-hosted git repository.
mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 23e37b145b issue #7494 : speed up integration tests with single-JVM
runner and Surefire Report Output (#7506)
23e37b145b is described below
commit 23e37b145bce74fc2e6a55f4e96486155949dca0
Author: Matt Casters <[email protected]>
AuthorDate: Sun Jul 12 16:36:41 2026 +0200
issue #7494 : speed up integration tests with single-JVM runner and
Surefire Report Output (#7506)
Run project main-*.hwf suites in one hop-run via a shared runner pipeline
and
a new Surefire Report Output transform, cutting cold-start overhead (e.g.
transforms ~13m to ~4m). WorkflowExecutor keeps input fields on the
execution
result stream so test names reach the report. Keep classic per-test JVM mode
behind HOP_IT_PER_TEST_JVM=true. Also pin 0059 Get Files Rows Count to its
original four files after an extra .txt broke the wildcard count.
---
assemblies/plugins/pom.xml | 6 +
integration-tests/scripts/run-project-tests.hpl | 165 +++++++++
integration-tests/scripts/run-tests.sh | 237 ++++++++-----
integration-tests/scripts/run-workflow.hwf | 115 ++++++
.../transforms/0059-get-files-rows-count.hpl | 25 +-
plugins/transforms/pom.xml | 1 +
plugins/transforms/surefirereport/pom.xml | 32 ++
.../surefirereport/src/assembly/assembly.xml | 38 ++
.../surefirereport/SurefireReportOutput.java | 287 +++++++++++++++
.../surefirereport/SurefireReportOutputData.java | 45 +++
.../surefirereport/SurefireReportOutputDialog.java | 385 +++++++++++++++++++++
.../surefirereport/SurefireReportOutputMeta.java | 191 ++++++++++
.../surefirereport/SurefireReportWriter.java | 211 +++++++++++
.../surefirereport/SurefireTestCase.java | 81 +++++
.../messages/messages_en_US.properties | 71 ++++
.../src/main/resources/surefirereport.svg | 183 ++++++++++
.../surefirereport/src/main/resources/version.xml | 20 ++
.../SurefireReportOutputMetaTest.java | 58 ++++
.../surefirereport/SurefireReportWriterTest.java | 101 ++++++
.../workflowexecutor/WorkflowExecutor.java | 8 +
.../workflowexecutor/WorkflowExecutorMeta.java | 142 ++++----
21 files changed, 2256 insertions(+), 146 deletions(-)
diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml
index d53fd7e6c5..ef42a43597 100644
--- a/assemblies/plugins/pom.xml
+++ b/assemblies/plugins/pom.xml
@@ -1525,6 +1525,12 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-transform-surefirereport</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-switchcase</artifactId>
diff --git a/integration-tests/scripts/run-project-tests.hpl
b/integration-tests/scripts/run-project-tests.hpl
new file mode 100644
index 0000000000..520c735d45
--- /dev/null
+++ b/integration-tests/scripts/run-project-tests.hpl
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>run-project-tests</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Run all main-*.hwf integration tests in one JVM and write a
Surefire report</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/07/11 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/11 00:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>get main workflows</from>
+ <to>run-workflow.hwf</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>run-workflow.hwf</from>
+ <to>Surefire Report Output</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>get main workflows</name>
+ <type>GetFileNames</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <doNotFailIfNoFile>N</doNotFailIfNoFile>
+ <dynamic_include_subfolders>N</dynamic_include_subfolders>
+ <file>
+ <file_required>N</file_required>
+ <filemask>main.*\.hwf$</filemask>
+ <include_subfolders>N</include_subfolders>
+ <name>${PROJECT_HOME}</name>
+ </file>
+ <filefield>N</filefield>
+ <filter>
+ <filterfiletype>all_files</filterfiletype>
+ </filter>
+ <isaddresult>N</isaddresult>
+ <limit>0</limit>
+ <raiseAnExceptionIfNoFile>N</raiseAnExceptionIfNoFile>
+ <rownum>N</rownum>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>run-workflow.hwf</name>
+ <type>WorkflowExecutor</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <run_configuration>local</run_configuration>
+ <filename>${Internal.Entry.Current.Folder}/run-workflow.hwf</filename>
+ <group_size>1</group_size>
+ <group_field/>
+ <group_time/>
+ <parameters>
+ <variablemapping>
+ <variable>WORKFLOW_FILE</variable>
+ <field>filename</field>
+ <input/>
+ </variablemapping>
+ <inherit_all_vars>Y</inherit_all_vars>
+ </parameters>
+ <execution_result_target_transform>Surefire Report
Output</execution_result_target_transform>
+ <execution_time_field>ExecutionTime</execution_time_field>
+ <execution_result_field>ExecutionResult</execution_result_field>
+ <execution_errors_field>ExecutionNrErrors</execution_errors_field>
+ <execution_lines_read_field>ExecutionLinesRead</execution_lines_read_field>
+
<execution_lines_written_field>ExecutionLinesWritten</execution_lines_written_field>
+
<execution_lines_input_field>ExecutionLinesInput</execution_lines_input_field>
+
<execution_lines_output_field>ExecutionLinesOutput</execution_lines_output_field>
+
<execution_lines_rejected_field>ExecutionLinesRejected</execution_lines_rejected_field>
+
<execution_lines_updated_field>ExecutionLinesUpdated</execution_lines_updated_field>
+
<execution_lines_deleted_field>ExecutionLinesDeleted</execution_lines_deleted_field>
+
<execution_files_retrieved_field>ExecutionFilesRetrieved</execution_files_retrieved_field>
+
<execution_exit_status_field>ExecutionExitStatus</execution_exit_status_field>
+ <execution_log_text_field>ExecutionLogText</execution_log_text_field>
+
<execution_log_channelid_field>ExecutionLogChannelId</execution_log_channelid_field>
+ <result_rows_target_transform/>
+ <result_files_target_transform/>
+ <result_files_file_name_field>FileName</result_files_file_name_field>
+ <attributes/>
+ <GUI>
+ <xloc>304</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Surefire Report Output</name>
+ <type>SurefireReportOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <filename>${IT_SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml</filename>
+ <suite_name>${PROJECT_NAME}</suite_name>
+ <create_parent_folder>Y</create_parent_folder>
+ <test_name_field>filename</test_name_field>
+ <duration_field>ExecutionTime</duration_field>
+ <duration_in_milliseconds>Y</duration_in_milliseconds>
+ <result_field>ExecutionResult</result_field>
+ <system_out_field>ExecutionLogText</system_out_field>
+ <system_err_field/>
+ <failure_message_field/>
+ <failure_type_field/>
+ <fail_on_test_failure>Y</fail_on_test_failure>
+ <attributes/>
+ <GUI>
+ <xloc>544</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/scripts/run-tests.sh
b/integration-tests/scripts/run-tests.sh
index 4c465295fa..c2072ca497 100755
--- a/integration-tests/scripts/run-tests.sh
+++ b/integration-tests/scripts/run-tests.sh
@@ -31,6 +31,13 @@ if [ -z "${SUREFIRE_REPORT}" ]; then
SUREFIRE_REPORT="true"
fi
+# When false/unset (default), run all main-*.hwf tests in a single hop-run JVM
via
+# run-project-tests.hpl. Set HOP_IT_PER_TEST_JVM=true to restore the classic
isolation model
+# (one hop-run process per main workflow).
+if [ -z "${HOP_IT_PER_TEST_JVM}" ]; then
+ HOP_IT_PER_TEST_JVM="false"
+fi
+
# Install any JDBC drivers required by this test set, using the Hop
driver-download CLI.
# Driven by HOP_DRIVERS_DOWNLOAD (comma separated driver ids, each optionally
with a version, e.g.
# "vertica,mysql:9.2.0"), set per test in the integration-tests-*.yaml compose
files. Restricted
@@ -127,6 +134,25 @@ mkdir -p "${HOP_AUDIT_FOLDER}"
# Store current HOP_CONFIG_FOLDER
TMP_CONFIG_FOLDER="${HOP_CONFIG_FOLDER}"
+SUREFIRE_DIR="$(cd "${CURRENT_DIR}/../surefire-reports" && pwd)"
+RUNNER_PIPELINE="${CURRENT_DIR}/run-project-tests.hpl"
+
+# Shared hop-run parameters for both single-JVM and per-test modes
+HOP_RUN_COMMON_ARGS=(
+ -r "local"
+ -e "dev"
+ -p "POSTGRES_HOST=${POSTGRES_HOST}"
+ -p "POSTGRES_DATABASE=${POSTGRES_DATABASE}"
+ -p "POSTGRES_PORT=${POSTGRES_PORT}"
+ -p "POSTGRES_USER=${POSTGRES_USER}"
+ -p "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}"
+ -p "SSH_TUNNEL_HOST=${SSH_TUNNEL_HOST}"
+ -p "SSH_TUNNEL_PORT=${SSH_TUNNEL_PORT}"
+ -p "SSH_TUNNEL_USER=${SSH_TUNNEL_USER}"
+ -p "SSH_TUNNEL_PASSWORD=${SSH_TUNNEL_PASSWORD}"
+ -p "BOOTSTRAP_SERVERS=${BOOTSTRAP_SERVERS}"
+)
+
#Loop over project folders
for d in "${CURRENT_DIR}"/../${PROJECT_NAME}/; do
#cleanup project testcases
@@ -145,117 +171,158 @@ for d in "${CURRENT_DIR}"/../${PROJECT_NAME}/; do
skipped_counter=0
failures_counter=0
- PROJECT_NAME=$(basename $d)
+ PROJECT_NAME=$(basename "$d")
echo ${SPACER}
echo "Starting Tests in project: ${PROJECT_NAME}"
echo ${SPACER}
- # Increment timer and set test start time
- test_counter=$((test_counter + 1))
-
# Create New Project
export HOP_CONFIG_FOLDER="$d"
- # Find main hwf files in the test sub-folder
- #
- # TODO: add hpl support when result is returned correctly
- #
- find $d -name 'main*.hwf' | sort | while read f; do
+ # Prefer single-JVM suite runner when available (unless isolation mode
is requested)
+ if [ "${HOP_IT_PER_TEST_JVM}" != "true" ] && [ -f "${RUNNER_PIPELINE}"
]; then
- #cleanup temp files
- rm -f /tmp/test_output
- rm -f /tmp/test_output_err
-
- #set file and test name
- hop_file="$(realpath $f)"
- test_name=$(basename $f)
- test_name=${test_name//'main_'/}
- test_name=${test_name//'main-'/}
- test_name=${test_name//'.hwf'/}
-
- #Starting Test
echo ${SPACER}
- echo "Starting Test: $test_name"
+ echo "Running project tests in single JVM via run-project-tests.hpl"
echo ${SPACER}
- #Start time test
start_time_test=$SECONDS
- #Run Test
$HOP_LOCATION/hop-run.sh \
- -r "local" \
- -e "dev" \
- -p POSTGRES_HOST=${POSTGRES_HOST} \
- -p POSTGRES_DATABASE=${POSTGRES_DATABASE} \
- -p POSTGRES_PORT=${POSTGRES_PORT} \
- -p POSTGRES_USER=${POSTGRES_USER} \
- -p POSTGRES_PASSWORD=${POSTGRES_PASSWORD} \
- -p SSH_TUNNEL_HOST=${SSH_TUNNEL_HOST} \
- -p SSH_TUNNEL_PORT=${SSH_TUNNEL_PORT} \
- -p SSH_TUNNEL_USER=${SSH_TUNNEL_USER} \
- -p SSH_TUNNEL_PASSWORD=${SSH_TUNNEL_PASSWORD} \
- -p BOOTSTRAP_SERVERS=${BOOTSTRAP_SERVERS} \
- -f $hop_file > >(tee /tmp/test_output) 2> >(tee /tmp/test_output_err
>&1)
-
- #Capture exit code
- exit_code=${PIPESTATUS[0]}
+ "${HOP_RUN_COMMON_ARGS[@]}" \
+ -p "PROJECT_NAME=${PROJECT_NAME}" \
+ -p "IT_SUREFIRE_DIR=${SUREFIRE_DIR}" \
+ -f "${RUNNER_PIPELINE}" > >(tee /tmp/test_output) 2> >(tee
/tmp/test_output_err >&1)
- #Test time duration
+ exit_code=${PIPESTATUS[0]}
test_duration=$((SECONDS - start_time_test))
+ total_duration=$((SECONDS - start_time))
if (($exit_code >= 1)); then
- errors_counter=$((errors_counter + 1))
- failures_counter=$((failures_counter + 1))
- #Write single line to overview file
- echo $test_name >>"${CURRENT_DIR}"/../surefire-reports/failed_tests
- #Create surefire xml failure
- echo "<testcase name=\"$test_name\" time=\"$test_duration\">"
>>${TMP_TESTCASES}
- echo "<failure type=\"$test_name\"></failure>" >>${TMP_TESTCASES}
- echo "<system-out>" >>${TMP_TESTCASES}
- echo "<![CDATA[" >>${TMP_TESTCASES}
- cat /tmp/test_output >>${TMP_TESTCASES}
- echo "]]>" >>${TMP_TESTCASES}
- echo "</system-out>" >>${TMP_TESTCASES}
- echo "<system-err>" >>${TMP_TESTCASES}
- echo "<![CDATA[" >>${TMP_TESTCASES}
- cat /tmp/test_output_err >>${TMP_TESTCASES}
- echo "]]>" >>${TMP_TESTCASES}
- echo "</system-err>" >>${TMP_TESTCASES}
- echo "</testcase>" >>${TMP_TESTCASES}
-
+ errors_counter=1
+ failures_counter=1
+ echo "${PROJECT_NAME}"
>>"${CURRENT_DIR}"/../surefire-reports/failed_tests
else
- #Write single line to overview file
- echo $test_name >>"${CURRENT_DIR}"/../surefire-reports/passed_tests
- #Create surefire xml success
- echo "<testcase name=\"$test_name\" time=\"$test_duration\">"
>>${TMP_TESTCASES}
- echo "<system-out>" >>${TMP_TESTCASES}
- echo "<![CDATA[" >>${TMP_TESTCASES}
- cat /tmp/test_output >>${TMP_TESTCASES}
- echo "]]>" >>${TMP_TESTCASES}
- echo "</system-out>" >>${TMP_TESTCASES}
- echo "</testcase>" >>${TMP_TESTCASES}
+ echo "${PROJECT_NAME}"
>>"${CURRENT_DIR}"/../surefire-reports/passed_tests
fi
- #Print results to console
echo ${SPACER}
- echo "Test Result"
+ echo "Project suite result"
echo ${SPACER}
- echo "Test duration: $test_duration"
- echo "Test Exit Code: $exit_code"
-
- done
-
- total_duration=$((SECONDS - start_time))
-
- #create final report
- if [ "${SUREFIRE_REPORT}" = "true" ]; then
+ echo "Duration: $test_duration"
+ echo "Exit Code: $exit_code"
+
+ # Surefire XML is written by the Surefire Report Output transform.
+ # If the transform never ran (startup failure), write a minimal
failure suite.
+ if [ "${SUREFIRE_REPORT}" = "true" ]; then
+ if [ ! -f "${SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml" ]; then
+ echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
>"${SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml"
+ echo "<testsuite
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
xsi:noNamespaceSchemaLocation=\"https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report-3.0.xsd\"
version=\"3.0\" name=\"${PROJECT_NAME}\" time=\"$total_duration\" tests=\"1\"
errors=\"1\" skipped=\"0\" failures=\"0\">"
>>"${SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml"
+ echo "<testcase name=\"suite_startup\"
time=\"$test_duration\"><failure
type=\"suite_startup\"></failure><system-out><![CDATA["
>>"${SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml"
+ cat /tmp/test_output
>>"${SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml"
+ echo "]]></system-out><system-err><![CDATA["
>>"${SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml"
+ cat /tmp/test_output_err
>>"${SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml"
+ echo "]]></system-err></testcase></testsuite>"
>>"${SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml"
+ fi
+ fi
- echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml
- echo "<testsuite
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
xsi:noNamespaceSchemaLocation=\"https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report-3.0.xsd\"
version=\"3.0\" name=\"${PROJECT_NAME}\" time=\"$total_duration\"
tests=\"$test_counter\" errors=\"$errors_counter\" skipped=\"$skipped_counter\"
failures=\"$failures_counter\">"
>>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml
- cat ${TMP_TESTCASES}
>>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml
- echo "</testsuite>"
>>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml
+ else
+
+ # Classic path: one hop-run JVM per main-*.hwf
+ #
+ find "$d" -name 'main*.hwf' | sort | while read -r f; do
+
+ #cleanup temp files
+ rm -f /tmp/test_output
+ rm -f /tmp/test_output_err
+
+ #set file and test name
+ hop_file="$(realpath "$f")"
+ test_name=$(basename "$f")
+ test_name=${test_name//'main_'/}
+ test_name=${test_name//'main-'/}
+ test_name=${test_name//'.hwf'/}
+
+ #Starting Test
+ echo ${SPACER}
+ echo "Starting Test: $test_name"
+ echo ${SPACER}
+
+ #Start time test
+ start_time_test=$SECONDS
+
+ #Run Test
+ $HOP_LOCATION/hop-run.sh \
+ "${HOP_RUN_COMMON_ARGS[@]}" \
+ -f "$hop_file" > >(tee /tmp/test_output) 2> >(tee
/tmp/test_output_err >&1)
+
+ #Capture exit code
+ exit_code=${PIPESTATUS[0]}
+
+ #Test time duration
+ test_duration=$((SECONDS - start_time_test))
+
+ if (($exit_code >= 1)); then
+ #Write single line to overview file
+ echo "$test_name"
>>"${CURRENT_DIR}"/../surefire-reports/failed_tests
+ #Create surefire xml failure
+ echo "<testcase name=\"$test_name\" time=\"$test_duration\">"
>>${TMP_TESTCASES}
+ echo "<failure type=\"$test_name\"></failure>" >>${TMP_TESTCASES}
+ echo "<system-out>" >>${TMP_TESTCASES}
+ echo "<![CDATA[" >>${TMP_TESTCASES}
+ cat /tmp/test_output >>${TMP_TESTCASES}
+ echo "]]>" >>${TMP_TESTCASES}
+ echo "</system-out>" >>${TMP_TESTCASES}
+ echo "<system-err>" >>${TMP_TESTCASES}
+ echo "<![CDATA[" >>${TMP_TESTCASES}
+ cat /tmp/test_output_err >>${TMP_TESTCASES}
+ echo "]]>" >>${TMP_TESTCASES}
+ echo "</system-err>" >>${TMP_TESTCASES}
+ echo "</testcase>" >>${TMP_TESTCASES}
+
+ else
+ #Write single line to overview file
+ echo "$test_name"
>>"${CURRENT_DIR}"/../surefire-reports/passed_tests
+ #Create surefire xml success
+ echo "<testcase name=\"$test_name\" time=\"$test_duration\">"
>>${TMP_TESTCASES}
+ echo "<system-out>" >>${TMP_TESTCASES}
+ echo "<![CDATA[" >>${TMP_TESTCASES}
+ cat /tmp/test_output >>${TMP_TESTCASES}
+ echo "]]>" >>${TMP_TESTCASES}
+ echo "</system-out>" >>${TMP_TESTCASES}
+ echo "</testcase>" >>${TMP_TESTCASES}
+ fi
+
+ #Print results to console
+ echo ${SPACER}
+ echo "Test Result"
+ echo ${SPACER}
+ echo "Test duration: $test_duration"
+ echo "Test Exit Code: $exit_code"
+
+ done
+
+ total_duration=$((SECONDS - start_time))
+
+ #create final report
+ if [ "${SUREFIRE_REPORT}" = "true" ]; then
+
+ # Count testcases written (subshell-safe)
+ if [ -f "${TMP_TESTCASES}" ]; then
+ test_counter=$(grep -c '<testcase ' "${TMP_TESTCASES}" || true)
+ failures_counter=$(grep -c '<failure ' "${TMP_TESTCASES}" || true)
+ errors_counter=${failures_counter}
+ fi
+
+ echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml
+ echo "<testsuite
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
xsi:noNamespaceSchemaLocation=\"https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report-3.0.xsd\"
version=\"3.0\" name=\"${PROJECT_NAME}\" time=\"$total_duration\"
tests=\"$test_counter\" errors=\"$errors_counter\" skipped=\"$skipped_counter\"
failures=\"$failures_counter\">"
>>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml
+ if [ -f "${TMP_TESTCASES}" ]; then
+ cat ${TMP_TESTCASES}
>>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml
+ fi
+ echo "</testsuite>"
>>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml
+ fi
fi
fi
fi
diff --git a/integration-tests/scripts/run-workflow.hwf
b/integration-tests/scripts/run-workflow.hwf
new file mode 100644
index 0000000000..6d0cce8dc7
--- /dev/null
+++ b/integration-tests/scripts/run-workflow.hwf
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>run-workflow</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Run a single main-*.hwf workflow for integration
tests</description>
+ <extended_description/>
+ <workflow_version/>
+ <workflow_status>0</workflow_status>
+ <created_user>-</created_user>
+ <created_date>2026/07/11 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/11 00:00:00.000</modified_date>
+ <parameters>
+ <parameter>
+ <name>WORKFLOW_FILE</name>
+ <default_value/>
+ <description>The filename of the workflow to execute</description>
+ </parameter>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>144</xloc>
+ <yloc>112</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Run ${WORKFLOW_FILE}</name>
+ <description/>
+ <type>WORKFLOW</type>
+ <attributes/>
+ <run_configuration>local</run_configuration>
+ <filename>${WORKFLOW_FILE}</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <wait_until_finished>Y</wait_until_finished>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <set_append_logfile>N</set_append_logfile>
+ <parallel>N</parallel>
+ <xloc>304</xloc>
+ <yloc>112</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Abort workflow</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <always_log_rows>N</always_log_rows>
+ <parallel>N</parallel>
+ <xloc>480</xloc>
+ <yloc>112</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Run ${WORKFLOW_FILE}</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>Run ${WORKFLOW_FILE}</from>
+ <to>Abort workflow</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/transforms/0059-get-files-rows-count.hpl
b/integration-tests/transforms/0059-get-files-rows-count.hpl
index d162b0a948..0c83ff3edb 100644
--- a/integration-tests/transforms/0059-get-files-rows-count.hpl
+++ b/integration-tests/transforms/0059-get-files-rows-count.hpl
@@ -65,8 +65,29 @@ limitations under the License.
<filename_Field/>
<smartCount>Y</smartCount>
<file>
- <name>${PROJECT_HOME}/files/</name>
- <filemask>.*\.txt$</filemask>
+ <name>${PROJECT_HOME}/files/customers-100.txt</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ <file>
+ <name>${PROJECT_HOME}/files/sample-file1.txt</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ <file>
+ <name>${PROJECT_HOME}/files/sample-file2.txt</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ <file>
+ <name>${PROJECT_HOME}/files/sample-file3.txt</name>
+ <filemask/>
<exclude_filemask/>
<file_required>N</file_required>
<include_subfolders>N</include_subfolders>
diff --git a/plugins/transforms/pom.xml b/plugins/transforms/pom.xml
index ab621dd3f2..f2f3e86993 100644
--- a/plugins/transforms/pom.xml
+++ b/plugins/transforms/pom.xml
@@ -149,6 +149,7 @@
<module>streamschemamerge</module>
<module>stringcut</module>
<module>stringoperations</module>
+ <module>surefirereport</module>
<module>switchcase</module>
<module>synchronizeaftermerge</module>
<module>systemdata</module>
diff --git a/plugins/transforms/surefirereport/pom.xml
b/plugins/transforms/surefirereport/pom.xml
new file mode 100644
index 0000000000..66a0e294f4
--- /dev/null
+++ b/plugins/transforms/surefirereport/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-plugins-transforms</artifactId>
+ <version>2.19.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>hop-transform-surefirereport</artifactId>
+ <packaging>jar</packaging>
+ <name>Hop Plugins Transforms Surefire Report Output</name>
+
+</project>
diff --git a/plugins/transforms/surefirereport/src/assembly/assembly.xml
b/plugins/transforms/surefirereport/src/assembly/assembly.xml
new file mode 100644
index 0000000000..b517cb04be
--- /dev/null
+++ b/plugins/transforms/surefirereport/src/assembly/assembly.xml
@@ -0,0 +1,38 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0
http://maven.apache.org/xsd/assembly-2.2.0.xsd">
+ <id>hop-transform-surefirereport</id>
+ <formats>
+ <format>zip</format>
+ </formats>
+ <baseDirectory>.</baseDirectory>
+ <files>
+ <file>
+ <source>${project.basedir}/src/main/resources/version.xml</source>
+ <outputDirectory>${hop.plugin.libdir}</outputDirectory>
+ <filtered>true</filtered>
+ </file>
+ </files>
+
+ <componentDescriptors>
+
<componentDescriptor>${maven.multiModuleProjectDirectory}/assemblies/shared/hop-plugin-libs.xml</componentDescriptor>
+ </componentDescriptors>
+</assembly>
diff --git
a/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutput.java
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutput.java
new file mode 100644
index 0000000000..3a44dd68ac
--- /dev/null
+++
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutput.java
@@ -0,0 +1,287 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.surefirereport;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.BaseTransform;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import
org.apache.hop.pipeline.transforms.surefirereport.SurefireTestCase.Status;
+
+/**
+ * Accumulates test-result rows and writes a Maven Surefire-compatible XML
report at end of stream.
+ */
+public class SurefireReportOutput
+ extends BaseTransform<SurefireReportOutputMeta, SurefireReportOutputData> {
+
+ private static final Class<?> PKG = SurefireReportOutputMeta.class;
+
+ public SurefireReportOutput(
+ TransformMeta transformMeta,
+ SurefireReportOutputMeta meta,
+ SurefireReportOutputData data,
+ int copyNr,
+ PipelineMeta pipelineMeta,
+ Pipeline pipeline) {
+ super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline);
+ }
+
+ @Override
+ public boolean processRow() throws HopException {
+ Object[] row = getRow();
+ if (row == null) {
+ writeReport();
+ setOutputDone();
+ return false;
+ }
+
+ if (first) {
+ first = false;
+ data.outputRowMeta = getInputRowMeta().clone();
+ data.suiteStartMs = System.currentTimeMillis();
+ resolveFieldIndexes(getInputRowMeta());
+ }
+
+ SurefireTestCase testCase = buildTestCase(getInputRowMeta(), row);
+ synchronized (data.lock) {
+ data.testCases.add(testCase);
+ }
+
+ putRow(data.outputRowMeta, row);
+ return true;
+ }
+
+ private void resolveFieldIndexes(IRowMeta rowMeta) throws
HopTransformException {
+ data.indexTestName = requireField(rowMeta, meta.getTestNameField());
+ data.indexDuration = optionalField(rowMeta, meta.getDurationField());
+ data.indexResult = optionalField(rowMeta, meta.getResultField());
+ data.indexSystemOut = optionalField(rowMeta, meta.getSystemOutField());
+ data.indexSystemErr = optionalField(rowMeta, meta.getSystemErrField());
+ data.indexFailureMessage = optionalField(rowMeta,
meta.getFailureMessageField());
+ data.indexFailureType = optionalField(rowMeta, meta.getFailureTypeField());
+ }
+
+ private int requireField(IRowMeta rowMeta, String fieldName) throws
HopTransformException {
+ if (Utils.isEmpty(fieldName)) {
+ throw new HopTransformException(
+ BaseMessages.getString(PKG,
"SurefireReportOutput.Error.TestNameFieldMissing"));
+ }
+ int index = rowMeta.indexOfValue(fieldName);
+ if (index < 0) {
+ throw new HopTransformException(
+ BaseMessages.getString(PKG,
"SurefireReportOutput.Error.CouldNotFindField", fieldName));
+ }
+ return index;
+ }
+
+ private int optionalField(IRowMeta rowMeta, String fieldName) throws
HopTransformException {
+ if (Utils.isEmpty(fieldName)) {
+ return -1;
+ }
+ int index = rowMeta.indexOfValue(fieldName);
+ if (index < 0) {
+ throw new HopTransformException(
+ BaseMessages.getString(PKG,
"SurefireReportOutput.Error.CouldNotFindField", fieldName));
+ }
+ return index;
+ }
+
+ private SurefireTestCase buildTestCase(IRowMeta rowMeta, Object[] row)
throws HopException {
+ String name = normalizeTestName(ConstString(rowMeta.getString(row,
data.indexTestName)));
+ if (Utils.isEmpty(name)) {
+ name = "unknown";
+ }
+
+ double durationSeconds = 0.0;
+ if (data.indexDuration >= 0) {
+ IValueMeta vm = rowMeta.getValueMeta(data.indexDuration);
+ Object value = row[data.indexDuration];
+ if (value != null) {
+ double raw = vm.getNumber(value);
+ durationSeconds = meta.isDurationInMilliseconds() ? raw / 1000.0 : raw;
+ if (durationSeconds < 0) {
+ durationSeconds = 0;
+ }
+ }
+ }
+
+ Status status = Status.PASS;
+ if (data.indexResult >= 0) {
+ status = parseStatus(rowMeta, row, data.indexResult);
+ }
+
+ String systemOut =
+ data.indexSystemOut >= 0 ? ConstString(rowMeta.getString(row,
data.indexSystemOut)) : null;
+ String systemErr =
+ data.indexSystemErr >= 0 ? ConstString(rowMeta.getString(row,
data.indexSystemErr)) : null;
+ String failureMessage =
+ data.indexFailureMessage >= 0
+ ? ConstString(rowMeta.getString(row, data.indexFailureMessage))
+ : null;
+ String failureType =
+ data.indexFailureType >= 0
+ ? ConstString(rowMeta.getString(row, data.indexFailureType))
+ : null;
+
+ if ((status == Status.FAIL || status == Status.ERROR)
+ && Utils.isEmpty(failureType)
+ && !Utils.isEmpty(name)) {
+ failureType = name;
+ }
+
+ return new SurefireTestCase(
+ name, durationSeconds, status, systemOut, systemErr, failureMessage,
failureType);
+ }
+
+ static Status parseStatus(IRowMeta rowMeta, Object[] row, int index) throws
HopException {
+ IValueMeta vm = rowMeta.getValueMeta(index);
+ Object value = row[index];
+ if (value == null) {
+ return Status.FAIL;
+ }
+ if (vm.isBoolean()) {
+ return vm.getBoolean(value) ? Status.PASS : Status.FAIL;
+ }
+ if (vm.isNumeric()) {
+ // Treat as exit-style: 0 = success, non-zero = failure
+ double n = vm.getNumber(value);
+ return n == 0.0 ? Status.PASS : Status.FAIL;
+ }
+ String text = vm.getString(value);
+ if (text == null) {
+ return Status.FAIL;
+ }
+ String normalized = text.trim().toUpperCase();
+ return switch (normalized) {
+ case "Y", "YES", "TRUE", "PASS", "PASSED", "SUCCESS", "OK" ->
Status.PASS;
+ case "SKIP", "SKIPPED" -> Status.SKIP;
+ case "ERROR" -> Status.ERROR;
+ case "N", "NO", "FALSE", "FAIL", "FAILED", "FAILURE" -> Status.FAIL;
+ default -> Status.FAIL;
+ };
+ }
+
+ private static String ConstString(String value) {
+ return value == null ? "" : value;
+ }
+
+ /**
+ * Normalize workflow filenames to the short test names used by the classic
IT runner: strip path,
+ * optional {@code main-}/{@code main_} prefix, and {@code .hwf} extension.
+ */
+ static String normalizeTestName(String raw) {
+ if (raw == null || raw.isEmpty()) {
+ return raw;
+ }
+ String name = raw;
+ int slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
+ if (slash >= 0 && slash < name.length() - 1) {
+ name = name.substring(slash + 1);
+ }
+ if (name.startsWith("main-") || name.startsWith("main_")) {
+ name = name.substring(5);
+ }
+ if (name.endsWith(".hwf") || name.endsWith(".HWF")) {
+ name = name.substring(0, name.length() - 4);
+ }
+ return name;
+ }
+
+ private void writeReport() throws HopException {
+ String filename = resolve(meta.getFilename());
+ if (Utils.isEmpty(filename)) {
+ throw new HopException(
+ BaseMessages.getString(PKG,
"SurefireReportOutput.Error.FilenameMissing"));
+ }
+
+ String suiteName = resolve(meta.getSuiteName());
+ if (Utils.isEmpty(suiteName)) {
+ suiteName = "tests";
+ }
+
+ Path reportPath = Paths.get(filename);
+ if (meta.isCreateParentFolder() && reportPath.getParent() != null) {
+ try {
+ java.nio.file.Files.createDirectories(reportPath.getParent());
+ } catch (Exception e) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG, "SurefireReportOutput.Error.WriteFailed", filename,
e.getMessage()),
+ e);
+ }
+ }
+
+ int failedOrErrored;
+ try {
+ synchronized (data.lock) {
+ SurefireReportWriter.write(reportPath, suiteName, data.testCases);
+ failedOrErrored = 0;
+ for (SurefireTestCase testCase : data.testCases) {
+ if (testCase.getStatus() == Status.FAIL || testCase.getStatus() ==
Status.ERROR) {
+ failedOrErrored++;
+ }
+ }
+ if (isBasic()) {
+ logBasic(
+ BaseMessages.getString(
+ PKG,
+ "SurefireReportOutput.Log.ReportWritten",
+ Integer.toString(data.testCases.size()),
+ filename));
+ }
+ }
+ } catch (Exception e) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG, "SurefireReportOutput.Error.WriteFailed", filename,
e.getMessage()),
+ e);
+ }
+
+ if (meta.isFailOnTestFailure() && failedOrErrored > 0) {
+ logError(
+ BaseMessages.getString(
+ PKG, "SurefireReportOutput.Error.TestsFailed",
Integer.toString(failedOrErrored)));
+ setErrors(failedOrErrored);
+ stopAll();
+ }
+ }
+
+ @Override
+ public boolean init() {
+ if (super.init()) {
+ if (Utils.isEmpty(meta.getFilename())) {
+ logError(BaseMessages.getString(PKG,
"SurefireReportOutput.Error.FilenameMissing"));
+ return false;
+ }
+ if (Utils.isEmpty(meta.getTestNameField())) {
+ logError(BaseMessages.getString(PKG,
"SurefireReportOutput.Error.TestNameFieldMissing"));
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+}
diff --git
a/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputData.java
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputData.java
new file mode 100644
index 0000000000..df3541debf
--- /dev/null
+++
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputData.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.surefirereport;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.pipeline.transform.BaseTransformData;
+import org.apache.hop.pipeline.transform.ITransformData;
+
+public class SurefireReportOutputData extends BaseTransformData implements
ITransformData {
+ public IRowMeta outputRowMeta;
+
+ public int indexTestName = -1;
+ public int indexDuration = -1;
+ public int indexResult = -1;
+ public int indexSystemOut = -1;
+ public int indexSystemErr = -1;
+ public int indexFailureMessage = -1;
+ public int indexFailureType = -1;
+
+ public final List<SurefireTestCase> testCases = new ArrayList<>();
+ public final Object lock = new Object();
+
+ public long suiteStartMs;
+
+ public SurefireReportOutputData() {
+ super();
+ }
+}
diff --git
a/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputDialog.java
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputDialog.java
new file mode 100644
index 0000000000..81201594e0
--- /dev/null
+++
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputDialog.java
@@ -0,0 +1,385 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.surefirereport;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.apache.hop.ui.pipeline.transform.ComponentSelectionListener;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Cursor;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+public class SurefireReportOutputDialog extends BaseTransformDialog {
+ private static final Class<?> PKG = SurefireReportOutputMeta.class;
+
+ private final SurefireReportOutputMeta input;
+ private boolean gotPreviousFields;
+
+ private TextVar wFilename;
+ private TextVar wSuiteName;
+ private Button wCreateParentFolder;
+ private CCombo wTestNameField;
+ private CCombo wDurationField;
+ private Button wDurationInMilliseconds;
+ private CCombo wResultField;
+ private CCombo wSystemOutField;
+ private CCombo wSystemErrField;
+ private CCombo wFailureMessageField;
+ private CCombo wFailureTypeField;
+ private Button wFailOnTestFailure;
+
+ public SurefireReportOutputDialog(
+ Shell parent,
+ IVariables variables,
+ SurefireReportOutputMeta transformMeta,
+ PipelineMeta pipelineMeta) {
+ super(parent, variables, transformMeta, pipelineMeta);
+ input = transformMeta;
+ }
+
+ @Override
+ public String open() {
+ createShell(BaseMessages.getString(PKG,
"SurefireReportOutputDialog.Shell.Title"));
+
+ buildButtonBar().ok(e -> ok()).cancel(e -> cancel()).build();
+
+ ModifyListener lsMod = e -> input.setChanged();
+ changed = input.hasChanged();
+
+ Control lastControl = wSpacer;
+
+ // Report filename
+ Label wlFilename = new Label(shell, SWT.RIGHT);
+ wlFilename.setText(BaseMessages.getString(PKG,
"SurefireReportOutputDialog.Filename.Label"));
+ PropsUi.setLook(wlFilename);
+ FormData fdlFilename = new FormData();
+ fdlFilename.left = new FormAttachment(0, 0);
+ fdlFilename.right = new FormAttachment(middle, -margin);
+ fdlFilename.top = new FormAttachment(lastControl, margin);
+ wlFilename.setLayoutData(fdlFilename);
+
+ wFilename = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ wFilename.setToolTipText(
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.Filename.Tooltip"));
+ PropsUi.setLook(wFilename);
+ wFilename.addModifyListener(lsMod);
+ FormData fdFilename = new FormData();
+ fdFilename.left = new FormAttachment(middle, 0);
+ fdFilename.top = new FormAttachment(lastControl, margin);
+ fdFilename.right = new FormAttachment(100, 0);
+ wFilename.setLayoutData(fdFilename);
+ lastControl = wFilename;
+
+ // Suite name
+ Label wlSuiteName = new Label(shell, SWT.RIGHT);
+ wlSuiteName.setText(BaseMessages.getString(PKG,
"SurefireReportOutputDialog.SuiteName.Label"));
+ PropsUi.setLook(wlSuiteName);
+ FormData fdlSuiteName = new FormData();
+ fdlSuiteName.left = new FormAttachment(0, 0);
+ fdlSuiteName.right = new FormAttachment(middle, -margin);
+ fdlSuiteName.top = new FormAttachment(lastControl, margin);
+ wlSuiteName.setLayoutData(fdlSuiteName);
+
+ wSuiteName = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ wSuiteName.setToolTipText(
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.SuiteName.Tooltip"));
+ PropsUi.setLook(wSuiteName);
+ wSuiteName.addModifyListener(lsMod);
+ FormData fdSuiteName = new FormData();
+ fdSuiteName.left = new FormAttachment(middle, 0);
+ fdSuiteName.top = new FormAttachment(lastControl, margin);
+ fdSuiteName.right = new FormAttachment(100, 0);
+ wSuiteName.setLayoutData(fdSuiteName);
+ lastControl = wSuiteName;
+
+ // Create parent folder
+ Label wlCreateParentFolder = new Label(shell, SWT.RIGHT);
+ wlCreateParentFolder.setText(
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.CreateParentFolder.Label"));
+ PropsUi.setLook(wlCreateParentFolder);
+ FormData fdlCreateParentFolder = new FormData();
+ fdlCreateParentFolder.left = new FormAttachment(0, 0);
+ fdlCreateParentFolder.right = new FormAttachment(middle, -margin);
+ fdlCreateParentFolder.top = new FormAttachment(lastControl, margin);
+ wlCreateParentFolder.setLayoutData(fdlCreateParentFolder);
+
+ wCreateParentFolder = new Button(shell, SWT.CHECK);
+ PropsUi.setLook(wCreateParentFolder);
+ wCreateParentFolder.setToolTipText(
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.CreateParentFolder.Tooltip"));
+ FormData fdCreateParentFolder = new FormData();
+ fdCreateParentFolder.left = new FormAttachment(middle, 0);
+ fdCreateParentFolder.top = new FormAttachment(wlCreateParentFolder, 0,
SWT.CENTER);
+ wCreateParentFolder.setLayoutData(fdCreateParentFolder);
+ wCreateParentFolder.addSelectionListener(new
ComponentSelectionListener(input));
+ lastControl = wlCreateParentFolder;
+
+ // Fields group
+ Group wFields = new Group(shell, SWT.SHADOW_NONE);
+ PropsUi.setLook(wFields);
+ wFields.setText(BaseMessages.getString(PKG,
"SurefireReportOutputDialog.Fields.Group.Label"));
+ FormLayout fieldsLayout = new FormLayout();
+ fieldsLayout.marginWidth = 10;
+ fieldsLayout.marginHeight = 10;
+ wFields.setLayout(fieldsLayout);
+ FormData fdFields = new FormData();
+ fdFields.left = new FormAttachment(0, 0);
+ fdFields.top = new FormAttachment(lastControl, margin * 2);
+ fdFields.right = new FormAttachment(100, 0);
+ fdFields.bottom = new FormAttachment(wOk, -margin);
+ wFields.setLayoutData(fdFields);
+
+ Control fieldLast = null;
+
+ fieldLast =
+ addFieldRow(
+ wFields, fieldLast,
"SurefireReportOutputDialog.TestNameField.Label", lsMod, true);
+ wTestNameField = (CCombo) fieldLast;
+
+ fieldLast =
+ addFieldRow(
+ wFields, fieldLast,
"SurefireReportOutputDialog.DurationField.Label", lsMod, false);
+ wDurationField = (CCombo) fieldLast;
+
+ Label wlDurationMs = new Label(wFields, SWT.RIGHT);
+ wlDurationMs.setText(
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.DurationInMilliseconds.Label"));
+ PropsUi.setLook(wlDurationMs);
+ FormData fdlDurationMs = new FormData();
+ fdlDurationMs.left = new FormAttachment(0, 0);
+ fdlDurationMs.right = new FormAttachment(middle, -margin);
+ fdlDurationMs.top = new FormAttachment(fieldLast, margin);
+ wlDurationMs.setLayoutData(fdlDurationMs);
+
+ wDurationInMilliseconds = new Button(wFields, SWT.CHECK);
+ PropsUi.setLook(wDurationInMilliseconds);
+ wDurationInMilliseconds.setToolTipText(
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.DurationInMilliseconds.Tooltip"));
+ FormData fdDurationMs = new FormData();
+ fdDurationMs.left = new FormAttachment(middle, 0);
+ fdDurationMs.top = new FormAttachment(wlDurationMs, 0, SWT.CENTER);
+ wDurationInMilliseconds.setLayoutData(fdDurationMs);
+ wDurationInMilliseconds.addSelectionListener(new
ComponentSelectionListener(input));
+ fieldLast = wlDurationMs;
+
+ fieldLast =
+ addFieldRow(
+ wFields, fieldLast,
"SurefireReportOutputDialog.ResultField.Label", lsMod, false);
+ wResultField = (CCombo) fieldLast;
+ wResultField.setToolTipText(
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.ResultField.Tooltip"));
+
+ fieldLast =
+ addFieldRow(
+ wFields, fieldLast,
"SurefireReportOutputDialog.SystemOutField.Label", lsMod, false);
+ wSystemOutField = (CCombo) fieldLast;
+
+ fieldLast =
+ addFieldRow(
+ wFields, fieldLast,
"SurefireReportOutputDialog.SystemErrField.Label", lsMod, false);
+ wSystemErrField = (CCombo) fieldLast;
+
+ fieldLast =
+ addFieldRow(
+ wFields,
+ fieldLast,
+ "SurefireReportOutputDialog.FailureMessageField.Label",
+ lsMod,
+ false);
+ wFailureMessageField = (CCombo) fieldLast;
+
+ fieldLast =
+ addFieldRow(
+ wFields, fieldLast,
"SurefireReportOutputDialog.FailureTypeField.Label", lsMod, false);
+ wFailureTypeField = (CCombo) fieldLast;
+
+ Label wlFailOnTestFailure = new Label(wFields, SWT.RIGHT);
+ wlFailOnTestFailure.setText(
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.FailOnTestFailure.Label"));
+ PropsUi.setLook(wlFailOnTestFailure);
+ FormData fdlFailOnTestFailure = new FormData();
+ fdlFailOnTestFailure.left = new FormAttachment(0, 0);
+ fdlFailOnTestFailure.right = new FormAttachment(middle, -margin);
+ fdlFailOnTestFailure.top = new FormAttachment(fieldLast, margin);
+ wlFailOnTestFailure.setLayoutData(fdlFailOnTestFailure);
+
+ wFailOnTestFailure = new Button(wFields, SWT.CHECK);
+ PropsUi.setLook(wFailOnTestFailure);
+ wFailOnTestFailure.setToolTipText(
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.FailOnTestFailure.Tooltip"));
+ FormData fdFailOnTestFailure = new FormData();
+ fdFailOnTestFailure.left = new FormAttachment(middle, 0);
+ fdFailOnTestFailure.top = new FormAttachment(wlFailOnTestFailure, 0,
SWT.CENTER);
+ wFailOnTestFailure.setLayoutData(fdFailOnTestFailure);
+ wFailOnTestFailure.addSelectionListener(new
ComponentSelectionListener(input));
+
+ getData();
+ focusTransformName();
+ BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+ return transformName;
+ }
+
+ private CCombo addFieldRow(
+ Group parent, Control above, String labelKey, ModifyListener lsMod,
boolean first) {
+ Label wl = new Label(parent, SWT.RIGHT);
+ wl.setText(BaseMessages.getString(PKG, labelKey));
+ PropsUi.setLook(wl);
+ FormData fdl = new FormData();
+ fdl.left = new FormAttachment(0, 0);
+ fdl.right = new FormAttachment(middle, -margin);
+ if (above == null) {
+ fdl.top = new FormAttachment(0, margin);
+ } else {
+ fdl.top = new FormAttachment(above, margin);
+ }
+ wl.setLayoutData(fdl);
+
+ CCombo w = new CCombo(parent, SWT.BORDER | SWT.READ_ONLY);
+ PropsUi.setLook(w);
+ w.addModifyListener(lsMod);
+ FormData fd = new FormData();
+ fd.left = new FormAttachment(middle, 0);
+ if (above == null) {
+ fd.top = new FormAttachment(0, margin);
+ } else {
+ fd.top = new FormAttachment(above, margin);
+ }
+ fd.right = new FormAttachment(100, 0);
+ w.setLayoutData(fd);
+ w.addFocusListener(
+ new FocusListener() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ // no-op
+ }
+
+ @Override
+ public void focusGained(FocusEvent e) {
+ Cursor busy = new Cursor(shell.getDisplay(), SWT.CURSOR_WAIT);
+ shell.setCursor(busy);
+ getFields();
+ shell.setCursor(null);
+ busy.dispose();
+ }
+ });
+ return w;
+ }
+
+ private void getFields() {
+ if (gotPreviousFields) {
+ return;
+ }
+ try {
+ String[] fields = new String[0];
+ IRowMeta r = pipelineMeta.getPrevTransformFields(variables,
transformName);
+ if (r != null) {
+ fields = r.getFieldNames();
+ }
+ gotPreviousFields = true;
+ setComboValues(wTestNameField, fields);
+ setComboValues(wDurationField, fields);
+ setComboValues(wResultField, fields);
+ setComboValues(wSystemOutField, fields);
+ setComboValues(wSystemErrField, fields);
+ setComboValues(wFailureMessageField, fields);
+ setComboValues(wFailureTypeField, fields);
+ } catch (HopException ke) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.FailedToGetFields.DialogTitle"),
+ BaseMessages.getString(PKG,
"SurefireReportOutputDialog.FailedToGetFields.DialogMessage"),
+ ke);
+ }
+ }
+
+ private void setComboValues(CCombo combo, String[] fields) {
+ if (combo == null) {
+ return;
+ }
+ String current = combo.getText();
+ combo.setItems(fields);
+ if (!Utils.isEmpty(current)) {
+ combo.setText(current);
+ }
+ }
+
+ public void getData() {
+ wTransformName.setText(Const.NVL(transformName, ""));
+ wFilename.setText(Const.NVL(input.getFilename(), ""));
+ wSuiteName.setText(Const.NVL(input.getSuiteName(), ""));
+ wCreateParentFolder.setSelection(input.isCreateParentFolder());
+ wTestNameField.setText(Const.NVL(input.getTestNameField(), ""));
+ wDurationField.setText(Const.NVL(input.getDurationField(), ""));
+ wDurationInMilliseconds.setSelection(input.isDurationInMilliseconds());
+ wResultField.setText(Const.NVL(input.getResultField(), ""));
+ wSystemOutField.setText(Const.NVL(input.getSystemOutField(), ""));
+ wSystemErrField.setText(Const.NVL(input.getSystemErrField(), ""));
+ wFailureMessageField.setText(Const.NVL(input.getFailureMessageField(),
""));
+ wFailureTypeField.setText(Const.NVL(input.getFailureTypeField(), ""));
+ wFailOnTestFailure.setSelection(input.isFailOnTestFailure());
+ wTransformName.selectAll();
+ wTransformName.setFocus();
+ }
+
+ private void cancel() {
+ transformName = null;
+ input.setChanged(changed);
+ dispose();
+ }
+
+ private void ok() {
+ if (Utils.isEmpty(wTransformName.getText())) {
+ return;
+ }
+ transformName = wTransformName.getText();
+ input.setFilename(wFilename.getText());
+ input.setSuiteName(wSuiteName.getText());
+ input.setCreateParentFolder(wCreateParentFolder.getSelection());
+ input.setTestNameField(wTestNameField.getText());
+ input.setDurationField(wDurationField.getText());
+ input.setDurationInMilliseconds(wDurationInMilliseconds.getSelection());
+ input.setResultField(wResultField.getText());
+ input.setSystemOutField(wSystemOutField.getText());
+ input.setSystemErrField(wSystemErrField.getText());
+ input.setFailureMessageField(wFailureMessageField.getText());
+ input.setFailureTypeField(wFailureTypeField.getText());
+ input.setFailOnTestFailure(wFailOnTestFailure.getSelection());
+ dispose();
+ }
+}
diff --git
a/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputMeta.java
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputMeta.java
new file mode 100644
index 0000000000..e16c7c3599
--- /dev/null
+++
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputMeta.java
@@ -0,0 +1,191 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.surefirereport;
+
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.annotations.Transform;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.BaseTransformMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+
+@Transform(
+ id = "SurefireReportOutput",
+ image = "surefirereport.svg",
+ name = "i18n::SurefireReportOutput.Name",
+ description = "i18n::SurefireReportOutput.Description",
+ categoryDescription =
"i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Output",
+ keywords = "i18n::SurefireReportOutputMeta.keyword",
+ documentationUrl = "/pipeline/transforms/surefirereport.html")
+@Getter
+@Setter
+public class SurefireReportOutputMeta
+ extends BaseTransformMeta<SurefireReportOutput, SurefireReportOutputData> {
+ private static final Class<?> PKG = SurefireReportOutputMeta.class;
+
+ @HopMetadataProperty(
+ key = "filename",
+ injectionKeyDescription = "SurefireReportOutput.Injection.Filename")
+ private String filename;
+
+ @HopMetadataProperty(
+ key = "suite_name",
+ injectionKeyDescription = "SurefireReportOutput.Injection.SuiteName")
+ private String suiteName;
+
+ @HopMetadataProperty(
+ key = "create_parent_folder",
+ injectionKeyDescription =
"SurefireReportOutput.Injection.CreateParentFolder")
+ private boolean createParentFolder;
+
+ @HopMetadataProperty(
+ key = "test_name_field",
+ injectionKeyDescription = "SurefireReportOutput.Injection.TestNameField")
+ private String testNameField;
+
+ @HopMetadataProperty(
+ key = "duration_field",
+ injectionKeyDescription = "SurefireReportOutput.Injection.DurationField")
+ private String durationField;
+
+ @HopMetadataProperty(
+ key = "duration_in_milliseconds",
+ injectionKeyDescription =
"SurefireReportOutput.Injection.DurationInMilliseconds")
+ private boolean durationInMilliseconds;
+
+ @HopMetadataProperty(
+ key = "result_field",
+ injectionKeyDescription = "SurefireReportOutput.Injection.ResultField")
+ private String resultField;
+
+ @HopMetadataProperty(
+ key = "system_out_field",
+ injectionKeyDescription =
"SurefireReportOutput.Injection.SystemOutField")
+ private String systemOutField;
+
+ @HopMetadataProperty(
+ key = "system_err_field",
+ injectionKeyDescription =
"SurefireReportOutput.Injection.SystemErrField")
+ private String systemErrField;
+
+ @HopMetadataProperty(
+ key = "failure_message_field",
+ injectionKeyDescription =
"SurefireReportOutput.Injection.FailureMessageField")
+ private String failureMessageField;
+
+ @HopMetadataProperty(
+ key = "failure_type_field",
+ injectionKeyDescription =
"SurefireReportOutput.Injection.FailureTypeField")
+ private String failureTypeField;
+
+ /** When true, the transform fails the pipeline if any test case failed or
errored. */
+ @HopMetadataProperty(
+ key = "fail_on_test_failure",
+ injectionKeyDescription =
"SurefireReportOutput.Injection.FailOnTestFailure")
+ private boolean failOnTestFailure;
+
+ public SurefireReportOutputMeta() {
+ super();
+ }
+
+ @Override
+ public Object clone() {
+ return super.clone();
+ }
+
+ @Override
+ public void setDefault() {
+ filename = "${IT_SUREFIRE_DIR}/surefile_${PROJECT_NAME}.xml";
+ suiteName = "${PROJECT_NAME}";
+ createParentFolder = true;
+ testNameField = "testName";
+ durationField = "ExecutionTime";
+ durationInMilliseconds = true;
+ resultField = "ExecutionResult";
+ systemOutField = "ExecutionLogText";
+ systemErrField = "";
+ failureMessageField = "";
+ failureTypeField = "";
+ failOnTestFailure = true;
+ }
+
+ @Override
+ public void check(
+ List<ICheckResult> remarks,
+ PipelineMeta pipelineMeta,
+ TransformMeta transformMeta,
+ IRowMeta prev,
+ String[] input,
+ String[] output,
+ IRowMeta info,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider) {
+ if (Utils.isEmpty(filename)) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(PKG,
"SurefireReportOutputMeta.CheckResult.FilenameMissing"),
+ transformMeta));
+ } else {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(PKG,
"SurefireReportOutputMeta.CheckResult.FilenameOK"),
+ transformMeta));
+ }
+
+ if (Utils.isEmpty(testNameField)) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG,
"SurefireReportOutputMeta.CheckResult.TestNameFieldMissing"),
+ transformMeta));
+ } else {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(PKG,
"SurefireReportOutputMeta.CheckResult.TestNameFieldOK"),
+ transformMeta));
+ }
+
+ if (input.length > 0) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(
+ PKG,
"SurefireReportOutputMeta.CheckResult.ReceivingInfoFromOtherTransforms"),
+ transformMeta));
+ } else {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(PKG,
"SurefireReportOutputMeta.CheckResult.NoInputReceived"),
+ transformMeta));
+ }
+ }
+}
diff --git
a/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriter.java
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriter.java
new file mode 100644
index 0000000000..dea912edff
--- /dev/null
+++
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriter.java
@@ -0,0 +1,211 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.surefirereport;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.Locale;
+import
org.apache.hop.pipeline.transforms.surefirereport.SurefireTestCase.Status;
+
+/** Writes Maven Surefire 3.0-compatible XML reports. */
+public final class SurefireReportWriter {
+
+ private static final String XSI =
"http://www.w3.org/2001/XMLSchema-instance";
+ private static final String XSD =
+
"https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report-3.0.xsd";
+
+ private SurefireReportWriter() {}
+
+ public static void write(Path reportFile, String suiteName,
List<SurefireTestCase> cases)
+ throws IOException {
+ int tests = cases.size();
+ int failures = 0;
+ int errors = 0;
+ int skipped = 0;
+ double totalTime = 0.0;
+ for (SurefireTestCase c : cases) {
+ totalTime += c.getDurationSeconds();
+ switch (c.getStatus()) {
+ case FAIL -> failures++;
+ case ERROR -> errors++;
+ case SKIP -> skipped++;
+ default -> {
+ // PASS
+ }
+ }
+ }
+
+ Path parent = reportFile.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
+
+ Path tmp = reportFile.resolveSibling(reportFile.getFileName().toString() +
".tmp");
+ try (BufferedWriter out =
+ new BufferedWriter(
+ new OutputStreamWriter(Files.newOutputStream(tmp),
StandardCharsets.UTF_8))) {
+ out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ out.newLine();
+ out.write("<testsuite xmlns:xsi=\"");
+ out.write(XSI);
+ out.write("\" xsi:noNamespaceSchemaLocation=\"");
+ out.write(XSD);
+ out.write("\" version=\"3.0\" name=\"");
+ out.write(escapeAttribute(suiteName));
+ out.write("\" time=\"");
+ out.write(formatTime(totalTime));
+ out.write("\" tests=\"");
+ out.write(Integer.toString(tests));
+ out.write("\" errors=\"");
+ out.write(Integer.toString(errors));
+ out.write("\" skipped=\"");
+ out.write(Integer.toString(skipped));
+ out.write("\" failures=\"");
+ out.write(Integer.toString(failures));
+ out.write("\">");
+ out.newLine();
+
+ for (SurefireTestCase c : cases) {
+ writeTestCase(out, c);
+ }
+
+ out.write("</testsuite>");
+ out.newLine();
+ }
+
+ try {
+ Files.move(
+ tmp, reportFile, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
+ } catch (IOException atomicFailed) {
+ Files.move(tmp, reportFile, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ private static void writeTestCase(BufferedWriter out, SurefireTestCase c)
throws IOException {
+ out.write("<testcase name=\"");
+ out.write(escapeAttribute(c.getName()));
+ out.write("\" time=\"");
+ out.write(formatTime(c.getDurationSeconds()));
+ out.write("\">");
+ out.newLine();
+
+ if (c.getStatus() == Status.FAIL || c.getStatus() == Status.ERROR) {
+ String element = c.getStatus() == Status.ERROR ? "error" : "failure";
+ String type =
+ c.getFailureType() != null && !c.getFailureType().isEmpty()
+ ? c.getFailureType()
+ : c.getName();
+ out.write("<");
+ out.write(element);
+ out.write(" type=\"");
+ out.write(escapeAttribute(type));
+ out.write("\"");
+ if (c.getFailureMessage() != null && !c.getFailureMessage().isEmpty()) {
+ out.write(" message=\"");
+ out.write(escapeAttribute(c.getFailureMessage()));
+ out.write("\"");
+ }
+ out.write("></");
+ out.write(element);
+ out.write(">");
+ out.newLine();
+ } else if (c.getStatus() == Status.SKIP) {
+ out.write("<skipped");
+ if (c.getFailureMessage() != null && !c.getFailureMessage().isEmpty()) {
+ out.write(" message=\"");
+ out.write(escapeAttribute(c.getFailureMessage()));
+ out.write("\"");
+ }
+ out.write("/>");
+ out.newLine();
+ }
+
+ if (c.getSystemOut() != null && !c.getSystemOut().isEmpty()) {
+ out.write("<system-out>");
+ out.newLine();
+ writeCdata(out, c.getSystemOut());
+ out.write("</system-out>");
+ out.newLine();
+ }
+
+ if (c.getSystemErr() != null && !c.getSystemErr().isEmpty()) {
+ out.write("<system-err>");
+ out.newLine();
+ writeCdata(out, c.getSystemErr());
+ out.write("</system-err>");
+ out.newLine();
+ }
+
+ out.write("</testcase>");
+ out.newLine();
+ }
+
+ static void writeCdata(BufferedWriter out, String text) throws IOException {
+ // Split CDATA if the text contains the terminator sequence.
+ String remaining = text == null ? "" : text;
+ int idx;
+ out.write("<![CDATA[");
+ while ((idx = remaining.indexOf("]]>")) >= 0) {
+ out.write(remaining.substring(0, idx));
+ out.write("]]]]><![CDATA[>");
+ remaining = remaining.substring(idx + 3);
+ }
+ out.write(remaining);
+ out.write("]]>");
+ out.newLine();
+ }
+
+ static String escapeAttribute(String value) {
+ if (value == null) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder(value.length() + 16);
+ for (int i = 0; i < value.length(); i++) {
+ char ch = value.charAt(i);
+ switch (ch) {
+ case '&' -> sb.append("&");
+ case '<' -> sb.append("<");
+ case '>' -> sb.append(">");
+ case '"' -> sb.append(""");
+ case '\'' -> sb.append("'");
+ default -> {
+ if (ch < 0x20 && ch != '\t' && ch != '\n' && ch != '\r') {
+ // skip illegal XML 1.0 control characters
+ } else {
+ sb.append(ch);
+ }
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ static String formatTime(double seconds) {
+ // Surefire commonly uses integer seconds; keep one decimal when needed.
+ if (Math.abs(seconds - Math.rint(seconds)) < 0.0005) {
+ return Long.toString(Math.round(seconds));
+ }
+ return String.format(Locale.US, "%.3f", seconds);
+ }
+}
diff --git
a/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireTestCase.java
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireTestCase.java
new file mode 100644
index 0000000000..53c7e8e68f
--- /dev/null
+++
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireTestCase.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.surefirereport;
+
+/** One Surefire testcase row accumulated by {@link SurefireReportOutput}. */
+public class SurefireTestCase {
+ public enum Status {
+ PASS,
+ FAIL,
+ ERROR,
+ SKIP
+ }
+
+ private final String name;
+ private final double durationSeconds;
+ private final Status status;
+ private final String systemOut;
+ private final String systemErr;
+ private final String failureMessage;
+ private final String failureType;
+
+ public SurefireTestCase(
+ String name,
+ double durationSeconds,
+ Status status,
+ String systemOut,
+ String systemErr,
+ String failureMessage,
+ String failureType) {
+ this.name = name;
+ this.durationSeconds = durationSeconds;
+ this.status = status;
+ this.systemOut = systemOut;
+ this.systemErr = systemErr;
+ this.failureMessage = failureMessage;
+ this.failureType = failureType;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public double getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public String getSystemOut() {
+ return systemOut;
+ }
+
+ public String getSystemErr() {
+ return systemErr;
+ }
+
+ public String getFailureMessage() {
+ return failureMessage;
+ }
+
+ public String getFailureType() {
+ return failureType;
+ }
+}
diff --git
a/plugins/transforms/surefirereport/src/main/resources/org/apache/hop/pipeline/transforms/surefirereport/messages/messages_en_US.properties
b/plugins/transforms/surefirereport/src/main/resources/org/apache/hop/pipeline/transforms/surefirereport/messages/messages_en_US.properties
new file mode 100644
index 0000000000..e3848041e6
--- /dev/null
+++
b/plugins/transforms/surefirereport/src/main/resources/org/apache/hop/pipeline/transforms/surefirereport/messages/messages_en_US.properties
@@ -0,0 +1,71 @@
+#
+# 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.
+#
+
+SurefireReportOutput.Name=Surefire Report Output
+SurefireReportOutput.Description=Write a Maven Surefire/JUnit XML report from
test result rows
+SurefireReportOutputMeta.keyword=surefire,junit,test,report,xml,jenkins
+
+SurefireReportOutput.Error.FilenameMissing=Report filename is missing\!
+SurefireReportOutput.Error.TestNameFieldMissing=Test name field is missing\!
+SurefireReportOutput.Error.CouldNotFindField=Couldn''t find field ''{0}'' in
the input row\!
+SurefireReportOutput.Error.WriteFailed=Unable to write Surefire report to
''{0}'': {1}
+SurefireReportOutput.Error.TestsFailed={0} test case(s) failed or errored
+SurefireReportOutput.Log.ReportWritten=Wrote Surefire report with {0} test
case(s) to {1}
+
+SurefireReportOutput.Injection.Filename=Report filename (Surefire XML)
+SurefireReportOutput.Injection.SuiteName=Test suite name
+SurefireReportOutput.Injection.CreateParentFolder=Create parent folder (Y/N)
+SurefireReportOutput.Injection.TestNameField=Field containing the test name
+SurefireReportOutput.Injection.DurationField=Field containing the test duration
+SurefireReportOutput.Injection.DurationInMilliseconds=Duration field is in
milliseconds (Y/N)
+SurefireReportOutput.Injection.ResultField=Field containing the test result
(Boolean or status)
+SurefireReportOutput.Injection.SystemOutField=Field containing system-out log
text
+SurefireReportOutput.Injection.SystemErrField=Field containing system-err log
text
+SurefireReportOutput.Injection.FailureMessageField=Field containing the
failure message
+SurefireReportOutput.Injection.FailureTypeField=Field containing the failure
type
+SurefireReportOutput.Injection.FailOnTestFailure=Fail the pipeline when any
test fails (Y/N)
+SurefireReportOutputDialog.FailOnTestFailure.Label=Fail pipeline on test
failure
+SurefireReportOutputDialog.FailOnTestFailure.Tooltip=When enabled, the
pipeline fails after writing the report if any test case failed or errored
+
+SurefireReportOutputMeta.CheckResult.FilenameMissing=Report filename is
missing\!
+SurefireReportOutputMeta.CheckResult.FilenameOK=Report filename is specified
+SurefireReportOutputMeta.CheckResult.TestNameFieldMissing=Test name field is
missing\!
+SurefireReportOutputMeta.CheckResult.TestNameFieldOK=Test name field is
specified
+SurefireReportOutputMeta.CheckResult.ReceivingInfoFromOtherTransforms=Transform
is receiving info from other transforms.
+SurefireReportOutputMeta.CheckResult.NoInputReceived=No input received from
other transforms\!
+
+SurefireReportOutputDialog.Shell.Title=Surefire Report Output
+SurefireReportOutputDialog.TransformName.Label=Transform name
+SurefireReportOutputDialog.Filename.Label=Report filename
+SurefireReportOutputDialog.Filename.Tooltip=Path of the Surefire XML report to
write (variables allowed)
+SurefireReportOutputDialog.SuiteName.Label=Test suite name
+SurefireReportOutputDialog.SuiteName.Tooltip=Name attribute of the testsuite
element (variables allowed)
+SurefireReportOutputDialog.CreateParentFolder.Label=Create parent folder
+SurefireReportOutputDialog.CreateParentFolder.Tooltip=Create the parent folder
of the report file if it does not exist
+SurefireReportOutputDialog.Fields.Group.Label=Input fields
+SurefireReportOutputDialog.TestNameField.Label=Test name field
+SurefireReportOutputDialog.DurationField.Label=Duration field
+SurefireReportOutputDialog.DurationInMilliseconds.Label=Duration is in
milliseconds
+SurefireReportOutputDialog.DurationInMilliseconds.Tooltip=When checked,
duration values are converted from milliseconds to seconds for Surefire
+SurefireReportOutputDialog.ResultField.Label=Result field
+SurefireReportOutputDialog.ResultField.Tooltip=Boolean true/false, or
PASS/FAIL/ERROR/SKIP (or Y/N)
+SurefireReportOutputDialog.SystemOutField.Label=System-out field
+SurefireReportOutputDialog.SystemErrField.Label=System-err field
+SurefireReportOutputDialog.FailureMessageField.Label=Failure message field
+SurefireReportOutputDialog.FailureTypeField.Label=Failure type field
+SurefireReportOutputDialog.FailedToGetFields.DialogTitle=Get fields failed
+SurefireReportOutputDialog.FailedToGetFields.DialogMessage=Unable to get
fields from previous transforms because of an error
diff --git
a/plugins/transforms/surefirereport/src/main/resources/surefirereport.svg
b/plugins/transforms/surefirereport/src/main/resources/surefirereport.svg
new file mode 100644
index 0000000000..6b7df2f1b2
--- /dev/null
+++ b/plugins/transforms/surefirereport/src/main/resources/surefirereport.svg
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version:
6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+ width="42px" height="42px" viewBox="0 0 42 42" enable-background="new 0 0
42 42">
+ <g>
+ <polygon fill="#0E3A5A" points="5.534,11.804 11.924,11.802
11.924,5.475 10.023,5.475 10.023,8.559 4.798,3.334 3.454,4.678
+ 8.68,9.903 5.534,9.903 "/>
+ <path fill="#C9E8FB"
d="M21.367,21.796c-0.624,0-0.827,0.237-0.827,0.732v2.048c0,0.496,0.203,0.732,0.827,0.732
+
s0.827-0.236,0.827-0.732v-2.048C22.195,22.033,21.991,21.796,21.367,21.796z"/>
+ <path fill="#C9E8FB"
d="M23.521,27.196c-0.624,0-0.827,0.237-0.827,0.733v2.047c0,0.496,0.203,0.732,0.827,0.732
+
c0.624,0,0.827-0.236,0.827-0.732v-2.047C24.347,27.433,24.144,27.196,23.521,27.196z"/>
+ <path fill="#C9E8FB"
d="M21.367,14.507c0.624,0,0.827-0.237,0.827-0.732v-2.048c0-0.495-0.203-0.732-0.827-0.732
+
s-0.827,0.237-0.827,0.732v2.048C20.54,14.269,20.743,14.507,21.367,14.507z"/>
+ <path fill="#C9E8FB"
d="M15.378,14.507c0.624,0,0.827-0.237,0.827-0.732v-2.048c0-0.069-0.015-0.122-0.023-0.181l-1.631,1.631
+ v0.598C14.55,14.269,14.754,14.507,15.378,14.507z"/>
+ <path fill="#C9E8FB"
d="M15.378,25.308c0.624,0,0.827-0.236,0.827-0.732v-2.048c0-0.495-0.203-0.732-0.827-0.732
+
s-0.828,0.237-0.828,0.732v2.048C14.55,25.072,14.754,25.308,15.378,25.308z"/>
+ <path fill="#C9E8FB"
d="M17.531,19.908c0.624,0,0.827-0.238,0.827-0.733v-2.048c0-0.495-0.203-0.731-0.827-0.731
+
s-0.828,0.236-0.828,0.731v2.048C16.703,19.67,16.907,19.908,17.531,19.908z"/>
+ <path fill="#C9E8FB"
d="M23.521,19.908c0.624,0,0.827-0.238,0.827-0.733v-2.048c0-0.495-0.203-0.731-0.827-0.731
+
c-0.624,0-0.827,0.236-0.827,0.731v2.048C22.694,19.67,22.897,19.908,23.521,19.908z"/>
+ <path fill="#C9E8FB"
d="M30.037,33.222L29.979,13.07h-4.257V8.812h-6.808l-2.266,2.266c0.084,0.17,0.126,0.367,0.126,0.586v2.171
+
c0,0.678-0.346,1.173-1.396,1.173c-1.052,0-1.397-0.495-1.397-1.173v-0.089l-1.637,1.637v17.839H30.037z
M18.928,30.038
+
c0,0.679-0.346,1.173-1.397,1.173c-1.051,0-1.397-0.494-1.397-1.173v-2.17c0-0.678,0.346-1.174,1.397-1.174
+ c1.051,0,1.397,0.496,1.397,1.174V30.038z
M18.204,21.863c-0.11,0.057-0.306,0.14-0.345,0.14c-0.055,0-0.085-0.017-0.091-0.051
+
c-0.004-0.02-0.008-0.04-0.012-0.061c-0.014-0.067-0.028-0.137-0.028-0.199c0-0.06,0.036-0.098,0.134-0.141
+
c0.185-0.083,0.569-0.258,0.815-0.258c0.141,0,0.162,0.107,0.162,0.173v4.162c0,0.132-0.043,0.182-0.162,0.182h-0.18
+ c-0.177,0-0.229-0.024-0.229-0.182v-3.797L18.204,21.863z
M24.918,30.038c0,0.679-0.346,1.173-1.397,1.173
+
s-1.397-0.494-1.397-1.173v-2.17c0-0.678,0.346-1.174,1.397-1.174s1.397,0.496,1.397,1.174V30.038z
M21.367,25.81
+
c-1.051,0-1.397-0.495-1.397-1.173v-2.17c0-0.679,0.346-1.174,1.397-1.174s1.397,0.495,1.397,1.174v2.17
+ C22.764,25.315,22.419,25.81,21.367,25.81z
M24.194,21.863c-0.111,0.057-0.305,0.14-0.346,0.14c-0.055,0-0.085-0.017-0.091-0.051
+
c-0.003-0.02-0.008-0.04-0.012-0.061c-0.014-0.067-0.029-0.137-0.029-0.199c0-0.06,0.037-0.098,0.134-0.141
+
c0.184-0.083,0.568-0.258,0.814-0.258c0.141,0,0.162,0.107,0.162,0.173v4.162c0,0.132-0.043,0.182-0.162,0.182h-0.179
+ c-0.178,0-0.229-0.024-0.229-0.182v-3.797L24.194,21.863z
M23.851,10.751c0.184-0.084,0.568-0.258,0.814-0.258
+
c0.141,0,0.162,0.107,0.162,0.171v4.162c0,0.134-0.043,0.183-0.162,0.183h-0.179c-0.178,0-0.229-0.023-0.229-0.183V11.03
+
l-0.063,0.032c-0.111,0.056-0.305,0.14-0.346,0.14c-0.055,0-0.085-0.018-0.091-0.052c-0.003-0.019-0.008-0.04-0.012-0.06
+
c-0.014-0.066-0.029-0.137-0.029-0.199C23.717,10.831,23.754,10.794,23.851,10.751z
M24.918,17.065v2.171
+
c0,0.678-0.346,1.173-1.397,1.173s-1.397-0.495-1.397-1.173v-2.171c0-0.678,0.346-1.172,1.397-1.172S24.918,16.387,24.918,17.065z
+
M19.97,11.665c0-0.678,0.346-1.172,1.397-1.172s1.397,0.494,1.397,1.172v2.171c0,0.678-0.346,1.173-1.397,1.173
+ s-1.397-0.495-1.397-1.173V11.665z
M20.014,16.151c0.185-0.084,0.569-0.258,0.815-0.258c0.141,0,0.162,0.107,0.162,0.172v4.162
+
c0,0.133-0.044,0.182-0.162,0.182h-0.18c-0.177,0-0.229-0.023-0.229-0.182V16.43l-0.063,0.032c-0.111,0.057-0.306,0.14-0.346,0.14
+
c-0.055,0-0.085-0.018-0.09-0.052c-0.004-0.018-0.008-0.04-0.012-0.06c-0.014-0.067-0.028-0.137-0.028-0.198
+ C19.881,16.231,19.917,16.194,20.014,16.151z
M20.014,26.952c0.185-0.083,0.569-0.258,0.815-0.258c0.141,0,0.162,0.107,0.162,0.173
+
v4.162c0,0.132-0.044,0.182-0.162,0.182h-0.18c-0.177,0-0.229-0.023-0.229-0.182v-3.796l-0.063,0.031
+
c-0.111,0.056-0.306,0.139-0.346,0.139c-0.055,0-0.085-0.016-0.09-0.051c-0.004-0.019-0.008-0.04-0.012-0.061
+
c-0.014-0.067-0.028-0.137-0.028-0.199C19.881,27.033,19.917,26.995,20.014,26.952z
M17.861,10.751
+
c0.185-0.084,0.569-0.258,0.815-0.258c0.141,0,0.162,0.107,0.162,0.171v4.162c0,0.134-0.043,0.183-0.162,0.183h-0.18
+
c-0.177,0-0.229-0.023-0.229-0.183V11.03l-0.063,0.032c-0.11,0.056-0.306,0.14-0.345,0.14c-0.055,0-0.085-0.018-0.091-0.052
+
c-0.004-0.019-0.008-0.04-0.012-0.06c-0.014-0.066-0.028-0.137-0.028-0.199C17.727,10.831,17.763,10.794,17.861,10.751z
+
M16.134,17.065c0-0.678,0.346-1.172,1.397-1.172c1.051,0,1.397,0.494,1.397,1.172v2.171c0,0.678-0.346,1.173-1.397,1.173
+ c-1.051,0-1.397-0.495-1.397-1.173V17.065z
M16.774,22.467v2.17c0,0.678-0.346,1.173-1.396,1.173c-1.052,0-1.397-0.495-1.397-1.173
+
v-2.17c0-0.679,0.346-1.174,1.397-1.174C16.428,21.294,16.774,21.789,16.774,22.467z
M14.024,16.151
+
c0.185-0.084,0.568-0.258,0.815-0.258c0.141,0,0.162,0.107,0.162,0.172v4.162c0,0.133-0.044,0.182-0.162,0.182h-0.18
+
c-0.177,0-0.229-0.023-0.229-0.182V16.43l-0.064,0.032c-0.111,0.057-0.305,0.14-0.345,0.14c-0.055,0-0.085-0.018-0.091-0.052
+
c-0.003-0.018-0.008-0.04-0.012-0.06c-0.014-0.067-0.028-0.137-0.028-0.198C13.89,16.231,13.927,16.194,14.024,16.151z
+
M14.024,26.952c0.185-0.083,0.568-0.258,0.815-0.258c0.141,0,0.162,0.107,0.162,0.173v4.162c0,0.132-0.044,0.182-0.162,0.182
+
h-0.18c-0.177,0-0.229-0.023-0.229-0.182v-3.796l-0.064,0.031c-0.111,0.056-0.305,0.139-0.345,0.139
+
c-0.055,0-0.085-0.016-0.091-0.051c-0.003-0.019-0.008-0.04-0.012-0.061c-0.014-0.067-0.028-0.137-0.028-0.199
+ C13.89,27.033,13.927,26.995,14.024,26.952z"/>
+ <path fill="#C9E8FB"
d="M17.531,27.196c-0.624,0-0.828,0.237-0.828,0.733v2.047c0,0.496,0.204,0.732,0.828,0.732
+
s0.827-0.236,0.827-0.732v-2.047C18.358,27.433,18.155,27.196,17.531,27.196z"/>
+ <polygon fill="#0E3A5A" points="31.679,13.08 29.97,11.371
27.422,11.371 27.422,8.823 25.712,7.113 20.614,7.113 18.914,8.812
+ 25.722,8.812 25.722,13.07 29.979,13.07 30.037,33.222
12.343,33.222 12.343,15.383 10.643,17.084 10.643,34.922 31.742,34.922
+ "/>
+ <path fill="#C9E8FB"
d="M15.378,15.008c1.051,0,1.396-0.495,1.396-1.173v-2.171c0-0.219-0.042-0.416-0.126-0.586l-0.467,0.467
+
c0.009,0.059,0.023,0.112,0.023,0.181v2.048c0,0.495-0.203,0.732-0.827,0.732s-0.828-0.237-0.828-0.732v-0.598l-0.57,0.57v0.089
+ C13.98,14.513,14.326,15.008,15.378,15.008z"/>
+ <path fill="#0E3A5A"
d="M15.378,15.008c1.051,0,1.396-0.495,1.396-1.173v-2.171c0-0.219-0.042-0.416-0.126-0.586l-0.467,0.467
+
c0.009,0.059,0.023,0.112,0.023,0.181v2.048c0,0.495-0.203,0.732-0.827,0.732s-0.828-0.237-0.828-0.732v-0.598l-0.57,0.57v0.089
+ C13.98,14.513,14.326,15.008,15.378,15.008z"/>
+ <path fill="#C9E8FB"
d="M17.768,11.15c0.005,0.034,0.036,0.052,0.091,0.052c0.04,0,0.235-0.084,0.345-0.14l0.063-0.032v3.796
+
c0,0.159,0.052,0.183,0.229,0.183h0.18c0.119,0,0.162-0.049,0.162-0.183v-4.162c0-0.064-0.021-0.171-0.162-0.171
+
c-0.246,0-0.63,0.174-0.815,0.258c-0.098,0.043-0.134,0.08-0.134,0.14c0,0.063,0.014,0.133,0.028,0.199
+ C17.76,11.11,17.764,11.131,17.768,11.15z"/>
+ <path fill="#0E3A5A"
d="M17.768,11.15c0.005,0.034,0.036,0.052,0.091,0.052c0.04,0,0.235-0.084,0.345-0.14l0.063-0.032v3.796
+
c0,0.159,0.052,0.183,0.229,0.183h0.18c0.119,0,0.162-0.049,0.162-0.183v-4.162c0-0.064-0.021-0.171-0.162-0.171
+
c-0.246,0-0.63,0.174-0.815,0.258c-0.098,0.043-0.134,0.08-0.134,0.14c0,0.063,0.014,0.133,0.028,0.199
+ C17.76,11.11,17.764,11.131,17.768,11.15z"/>
+ <path fill="#C9E8FB"
d="M21.367,15.008c1.051,0,1.397-0.495,1.397-1.173v-2.171c0-0.678-0.346-1.172-1.397-1.172
+
s-1.397,0.494-1.397,1.172v2.171C19.97,14.513,20.316,15.008,21.367,15.008z
M20.54,11.726c0-0.495,0.203-0.732,0.827-0.732
+
s0.827,0.237,0.827,0.732v2.048c0,0.495-0.203,0.732-0.827,0.732s-0.827-0.237-0.827-0.732V11.726z"/>
+ <path fill="#0E3A5A"
d="M21.367,15.008c1.051,0,1.397-0.495,1.397-1.173v-2.171c0-0.678-0.346-1.172-1.397-1.172
+
s-1.397,0.494-1.397,1.172v2.171C19.97,14.513,20.316,15.008,21.367,15.008z
M20.54,11.726c0-0.495,0.203-0.732,0.827-0.732
+
s0.827,0.237,0.827,0.732v2.048c0,0.495-0.203,0.732-0.827,0.732s-0.827-0.237-0.827-0.732V11.726z"/>
+ <path fill="#C9E8FB"
d="M23.758,11.15c0.005,0.034,0.036,0.052,0.091,0.052c0.041,0,0.234-0.084,0.346-0.14l0.063-0.032v3.796
+
c0,0.159,0.051,0.183,0.229,0.183h0.179c0.119,0,0.162-0.049,0.162-0.183v-4.162c0-0.064-0.021-0.171-0.162-0.171
+
c-0.246,0-0.63,0.174-0.814,0.258c-0.098,0.043-0.134,0.08-0.134,0.14c0,0.063,0.015,0.133,0.029,0.199
+ C23.75,11.11,23.755,11.131,23.758,11.15z"/>
+ <path fill="#0E3A5A"
d="M23.758,11.15c0.005,0.034,0.036,0.052,0.091,0.052c0.041,0,0.234-0.084,0.346-0.14l0.063-0.032v3.796
+
c0,0.159,0.051,0.183,0.229,0.183h0.179c0.119,0,0.162-0.049,0.162-0.183v-4.162c0-0.064-0.021-0.171-0.162-0.171
+
c-0.246,0-0.63,0.174-0.814,0.258c-0.098,0.043-0.134,0.08-0.134,0.14c0,0.063,0.015,0.133,0.029,0.199
+ C23.75,11.11,23.755,11.131,23.758,11.15z"/>
+ <path fill="#C9E8FB"
d="M13.98,22.467v2.17c0,0.678,0.346,1.173,1.397,1.173c1.051,0,1.396-0.495,1.396-1.173v-2.17
+
c0-0.679-0.346-1.174-1.396-1.174C14.326,21.294,13.98,21.789,13.98,22.467z
M14.55,22.528c0-0.495,0.204-0.732,0.828-0.732
+
s0.827,0.237,0.827,0.732v2.048c0,0.496-0.203,0.732-0.827,0.732s-0.828-0.236-0.828-0.732V22.528z"/>
+ <path fill="#0E3A5A"
d="M13.98,22.467v2.17c0,0.678,0.346,1.173,1.397,1.173c1.051,0,1.396-0.495,1.396-1.173v-2.17
+
c0-0.679-0.346-1.174-1.396-1.174C14.326,21.294,13.98,21.789,13.98,22.467z
M14.55,22.528c0-0.495,0.204-0.732,0.828-0.732
+
s0.827,0.237,0.827,0.732v2.048c0,0.496-0.203,0.732-0.827,0.732s-0.828-0.236-0.828-0.732V22.528z"/>
+ <path fill="#C9E8FB"
d="M18.496,25.81h0.18c0.119,0,0.162-0.05,0.162-0.182v-4.162c0-0.065-0.021-0.173-0.162-0.173
+
c-0.246,0-0.63,0.175-0.815,0.258c-0.098,0.043-0.134,0.081-0.134,0.141c0,0.063,0.014,0.132,0.028,0.199
+
c0.004,0.021,0.008,0.041,0.012,0.061c0.005,0.034,0.036,0.051,0.091,0.051c0.04,0,0.235-0.083,0.345-0.14l0.063-0.031v3.797
+ C18.267,25.786,18.319,25.81,18.496,25.81z"/>
+ <path fill="#0E3A5A"
d="M18.496,25.81h0.18c0.119,0,0.162-0.05,0.162-0.182v-4.162c0-0.065-0.021-0.173-0.162-0.173
+
c-0.246,0-0.63,0.175-0.815,0.258c-0.098,0.043-0.134,0.081-0.134,0.141c0,0.063,0.014,0.132,0.028,0.199
+
c0.004,0.021,0.008,0.041,0.012,0.061c0.005,0.034,0.036,0.051,0.091,0.051c0.04,0,0.235-0.083,0.345-0.14l0.063-0.031v3.797
+ C18.267,25.786,18.319,25.81,18.496,25.81z"/>
+ <path fill="#C9E8FB"
d="M22.764,24.637v-2.17c0-0.679-0.346-1.174-1.397-1.174s-1.397,0.495-1.397,1.174v2.17
+ c0,0.678,0.346,1.173,1.397,1.173S22.764,25.315,22.764,24.637z
M22.195,24.576c0,0.496-0.203,0.732-0.827,0.732
+
s-0.827-0.236-0.827-0.732v-2.048c0-0.495,0.203-0.732,0.827-0.732s0.827,0.237,0.827,0.732V24.576z"/>
+ <path fill="#0E3A5A"
d="M22.764,24.637v-2.17c0-0.679-0.346-1.174-1.397-1.174s-1.397,0.495-1.397,1.174v2.17
+ c0,0.678,0.346,1.173,1.397,1.173S22.764,25.315,22.764,24.637z
M22.195,24.576c0,0.496-0.203,0.732-0.827,0.732
+
s-0.827-0.236-0.827-0.732v-2.048c0-0.495,0.203-0.732,0.827-0.732s0.827,0.237,0.827,0.732V24.576z"/>
+ <path fill="#C9E8FB"
d="M24.487,25.81h0.179c0.119,0,0.162-0.05,0.162-0.182v-4.162c0-0.065-0.021-0.173-0.162-0.173
+
c-0.246,0-0.63,0.175-0.814,0.258c-0.098,0.043-0.134,0.081-0.134,0.141c0,0.063,0.015,0.132,0.029,0.199
+
c0.004,0.021,0.009,0.041,0.012,0.061c0.005,0.034,0.036,0.051,0.091,0.051c0.041,0,0.234-0.083,0.346-0.14l0.063-0.031v3.797
+ C24.258,25.786,24.309,25.81,24.487,25.81z"/>
+ <path fill="#0E3A5A"
d="M24.487,25.81h0.179c0.119,0,0.162-0.05,0.162-0.182v-4.162c0-0.065-0.021-0.173-0.162-0.173
+
c-0.246,0-0.63,0.175-0.814,0.258c-0.098,0.043-0.134,0.081-0.134,0.141c0,0.063,0.015,0.132,0.029,0.199
+
c0.004,0.021,0.009,0.041,0.012,0.061c0.005,0.034,0.036,0.051,0.091,0.051c0.041,0,0.234-0.083,0.346-0.14l0.063-0.031v3.797
+ C24.258,25.786,24.309,25.81,24.487,25.81z"/>
+ <path fill="#C9E8FB"
d="M13.931,16.55c0.005,0.034,0.036,0.052,0.091,0.052c0.04,0,0.234-0.083,0.345-0.14l0.064-0.032v3.797
+
c0,0.158,0.051,0.182,0.229,0.182h0.18c0.118,0,0.162-0.049,0.162-0.182v-4.162c0-0.065-0.021-0.172-0.162-0.172
+
c-0.247,0-0.631,0.174-0.815,0.258c-0.098,0.043-0.134,0.08-0.134,0.141c0,0.062,0.015,0.131,0.028,0.198
+ C13.923,16.51,13.928,16.532,13.931,16.55z"/>
+ <path fill="#0E3A5A"
d="M13.931,16.55c0.005,0.034,0.036,0.052,0.091,0.052c0.04,0,0.234-0.083,0.345-0.14l0.064-0.032v3.797
+
c0,0.158,0.051,0.182,0.229,0.182h0.18c0.118,0,0.162-0.049,0.162-0.182v-4.162c0-0.065-0.021-0.172-0.162-0.172
+
c-0.247,0-0.631,0.174-0.815,0.258c-0.098,0.043-0.134,0.08-0.134,0.141c0,0.062,0.015,0.131,0.028,0.198
+ C13.923,16.51,13.928,16.532,13.931,16.55z"/>
+ <path fill="#C9E8FB"
d="M17.531,20.409c1.051,0,1.397-0.495,1.397-1.173v-2.171c0-0.678-0.346-1.172-1.397-1.172
+
c-1.051,0-1.397,0.494-1.397,1.172v2.171C16.134,19.914,16.479,20.409,17.531,20.409z
M16.703,17.127
+
c0-0.495,0.204-0.731,0.828-0.731s0.827,0.236,0.827,0.731v2.048c0,0.495-0.203,0.733-0.827,0.733s-0.828-0.238-0.828-0.733V17.127
+ z"/>
+ <path fill="#0E3A5A"
d="M17.531,20.409c1.051,0,1.397-0.495,1.397-1.173v-2.171c0-0.678-0.346-1.172-1.397-1.172
+
c-1.051,0-1.397,0.494-1.397,1.172v2.171C16.134,19.914,16.479,20.409,17.531,20.409z
M16.703,17.127
+
c0-0.495,0.204-0.731,0.828-0.731s0.827,0.236,0.827,0.731v2.048c0,0.495-0.203,0.733-0.827,0.733s-0.828-0.238-0.828-0.733V17.127
+ z"/>
+ <path fill="#C9E8FB"
d="M19.921,16.55c0.005,0.034,0.036,0.052,0.09,0.052c0.04,0,0.235-0.083,0.346-0.14l0.063-0.032v3.797
+
c0,0.158,0.052,0.182,0.229,0.182h0.18c0.118,0,0.162-0.049,0.162-0.182v-4.162c0-0.065-0.021-0.172-0.162-0.172
+
c-0.246,0-0.63,0.174-0.815,0.258c-0.098,0.043-0.134,0.08-0.134,0.141c0,0.062,0.014,0.131,0.028,0.198
+ C19.913,16.51,19.917,16.532,19.921,16.55z"/>
+ <path fill="#0E3A5A"
d="M19.921,16.55c0.005,0.034,0.036,0.052,0.09,0.052c0.04,0,0.235-0.083,0.346-0.14l0.063-0.032v3.797
+
c0,0.158,0.052,0.182,0.229,0.182h0.18c0.118,0,0.162-0.049,0.162-0.182v-4.162c0-0.065-0.021-0.172-0.162-0.172
+
c-0.246,0-0.63,0.174-0.815,0.258c-0.098,0.043-0.134,0.08-0.134,0.141c0,0.062,0.014,0.131,0.028,0.198
+ C19.913,16.51,19.917,16.532,19.921,16.55z"/>
+ <path fill="#C9E8FB"
d="M22.123,17.065v2.171c0,0.678,0.346,1.173,1.397,1.173s1.397-0.495,1.397-1.173v-2.171
+ c0-0.678-0.346-1.172-1.397-1.172S22.123,16.387,22.123,17.065z
M22.694,17.127c0-0.495,0.203-0.731,0.827-0.731
+
c0.624,0,0.827,0.236,0.827,0.731v2.048c0,0.495-0.203,0.733-0.827,0.733c-0.624,0-0.827-0.238-0.827-0.733V17.127z"/>
+ <path fill="#0E3A5A"
d="M22.123,17.065v2.171c0,0.678,0.346,1.173,1.397,1.173s1.397-0.495,1.397-1.173v-2.171
+ c0-0.678-0.346-1.172-1.397-1.172S22.123,16.387,22.123,17.065z
M22.694,17.127c0-0.495,0.203-0.731,0.827-0.731
+
c0.624,0,0.827,0.236,0.827,0.731v2.048c0,0.495-0.203,0.733-0.827,0.733c-0.624,0-0.827-0.238-0.827-0.733V17.127z"/>
+ <path fill="#C9E8FB"
d="M13.931,27.352c0.005,0.035,0.036,0.051,0.091,0.051c0.04,0,0.234-0.083,0.345-0.139l0.064-0.031v3.796
+
c0,0.158,0.051,0.182,0.229,0.182h0.18c0.118,0,0.162-0.05,0.162-0.182v-4.162c0-0.065-0.021-0.173-0.162-0.173
+
c-0.247,0-0.631,0.175-0.815,0.258c-0.098,0.043-0.134,0.081-0.134,0.141c0,0.063,0.015,0.132,0.028,0.199
+ C13.923,27.312,13.928,27.333,13.931,27.352z"/>
+ <path fill="#0E3A5A"
d="M13.931,27.352c0.005,0.035,0.036,0.051,0.091,0.051c0.04,0,0.234-0.083,0.345-0.139l0.064-0.031v3.796
+
c0,0.158,0.051,0.182,0.229,0.182h0.18c0.118,0,0.162-0.05,0.162-0.182v-4.162c0-0.065-0.021-0.173-0.162-0.173
+
c-0.247,0-0.631,0.175-0.815,0.258c-0.098,0.043-0.134,0.081-0.134,0.141c0,0.063,0.015,0.132,0.028,0.199
+ C13.923,27.312,13.928,27.333,13.931,27.352z"/>
+ <path fill="#C9E8FB"
d="M17.531,26.694c-1.051,0-1.397,0.496-1.397,1.174v2.17c0,0.679,0.346,1.173,1.397,1.173
+
c1.051,0,1.397-0.494,1.397-1.173v-2.17C18.928,27.19,18.581,26.694,17.531,26.694z
M18.358,29.976
+
c0,0.496-0.203,0.732-0.827,0.732s-0.828-0.236-0.828-0.732v-2.047c0-0.496,0.204-0.733,0.828-0.733s0.827,0.237,0.827,0.733
+ V29.976z"/>
+ <path fill="#0E3A5A"
d="M17.531,26.694c-1.051,0-1.397,0.496-1.397,1.174v2.17c0,0.679,0.346,1.173,1.397,1.173
+
c1.051,0,1.397-0.494,1.397-1.173v-2.17C18.928,27.19,18.581,26.694,17.531,26.694z
M18.358,29.976
+
c0,0.496-0.203,0.732-0.827,0.732s-0.828-0.236-0.828-0.732v-2.047c0-0.496,0.204-0.733,0.828-0.733s0.827,0.237,0.827,0.733
+ V29.976z"/>
+ <path fill="#C9E8FB"
d="M19.921,27.352c0.005,0.035,0.036,0.051,0.09,0.051c0.04,0,0.235-0.083,0.346-0.139l0.063-0.031v3.796
+
c0,0.158,0.052,0.182,0.229,0.182h0.18c0.118,0,0.162-0.05,0.162-0.182v-4.162c0-0.065-0.021-0.173-0.162-0.173
+
c-0.246,0-0.63,0.175-0.815,0.258c-0.098,0.043-0.134,0.081-0.134,0.141c0,0.063,0.014,0.132,0.028,0.199
+ C19.913,27.312,19.917,27.333,19.921,27.352z"/>
+ <path fill="#0E3A5A"
d="M19.921,27.352c0.005,0.035,0.036,0.051,0.09,0.051c0.04,0,0.235-0.083,0.346-0.139l0.063-0.031v3.796
+
c0,0.158,0.052,0.182,0.229,0.182h0.18c0.118,0,0.162-0.05,0.162-0.182v-4.162c0-0.065-0.021-0.173-0.162-0.173
+
c-0.246,0-0.63,0.175-0.815,0.258c-0.098,0.043-0.134,0.081-0.134,0.141c0,0.063,0.014,0.132,0.028,0.199
+ C19.913,27.312,19.917,27.333,19.921,27.352z"/>
+ <path fill="#C9E8FB"
d="M23.521,26.694c-1.051,0-1.397,0.496-1.397,1.174v2.17c0,0.679,0.346,1.173,1.397,1.173
+
s1.397-0.494,1.397-1.173v-2.17C24.918,27.19,24.572,26.694,23.521,26.694z
M24.347,29.976c0,0.496-0.203,0.732-0.827,0.732
+
c-0.624,0-0.827-0.236-0.827-0.732v-2.047c0-0.496,0.203-0.733,0.827-0.733c0.624,0,0.827,0.237,0.827,0.733V29.976z"/>
+ <path fill="#0E3A5A"
d="M23.521,26.694c-1.051,0-1.397,0.496-1.397,1.174v2.17c0,0.679,0.346,1.173,1.397,1.173
+
s1.397-0.494,1.397-1.173v-2.17C24.918,27.19,24.572,26.694,23.521,26.694z
M24.347,29.976c0,0.496-0.203,0.732-0.827,0.732
+
c-0.624,0-0.827-0.236-0.827-0.732v-2.047c0-0.496,0.203-0.733,0.827-0.733c0.624,0,0.827,0.237,0.827,0.733V29.976z"/>
+ </g>
+</svg>
diff --git a/plugins/transforms/surefirereport/src/main/resources/version.xml
b/plugins/transforms/surefirereport/src/main/resources/version.xml
new file mode 100644
index 0000000000..36ab20e22e
--- /dev/null
+++ b/plugins/transforms/surefirereport/src/main/resources/version.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+
+<version>${project.version}</version>
diff --git
a/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputMetaTest.java
b/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputMetaTest.java
new file mode 100644
index 0000000000..c5cba77612
--- /dev/null
+++
b/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportOutputMetaTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.surefirereport;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.pipeline.transforms.loadsave.LoadSaveTester;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class SurefireReportOutputMetaTest {
+ @RegisterExtension
+ static RestoreHopEngineEnvironmentExtension env = new
RestoreHopEngineEnvironmentExtension();
+
+ @Test
+ void testTransformMeta() throws HopException {
+ List<String> attributes =
+ Arrays.asList(
+ "filename",
+ "suiteName",
+ "createParentFolder",
+ "testNameField",
+ "durationField",
+ "durationInMilliseconds",
+ "resultField",
+ "systemOutField",
+ "systemErrField",
+ "failureMessageField",
+ "failureTypeField",
+ "failOnTestFailure");
+
+ Map<String, String> getterMap = new HashMap<>();
+ Map<String, String> setterMap = new HashMap<>();
+
+ LoadSaveTester loadSaveTester =
+ new LoadSaveTester(SurefireReportOutputMeta.class, attributes,
getterMap, setterMap);
+ loadSaveTester.testSerialization();
+ }
+}
diff --git
a/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriterTest.java
b/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriterTest.java
new file mode 100644
index 0000000000..f480a1de8b
--- /dev/null
+++
b/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriterTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.surefirereport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import
org.apache.hop.pipeline.transforms.surefirereport.SurefireTestCase.Status;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class SurefireReportWriterTest {
+
+ @TempDir Path tempDir;
+
+ @Test
+ void writesEmptySuite() throws Exception {
+ Path report = tempDir.resolve("surefile_empty.xml");
+ SurefireReportWriter.write(report, "empty", List.of());
+
+ String xml = Files.readString(report, StandardCharsets.UTF_8);
+ assertTrue(xml.contains("name=\"empty\""));
+ assertTrue(xml.contains("tests=\"0\""));
+ assertTrue(xml.contains("failures=\"0\""));
+ assertTrue(xml.contains("errors=\"0\""));
+ assertTrue(xml.contains("skipped=\"0\""));
+ assertFalse(xml.contains("<testcase"));
+ }
+
+ @Test
+ void writesMixedResultsAndEscapes() throws Exception {
+ Path report = tempDir.resolve("nested/surefile_mixed.xml");
+ List<SurefireTestCase> cases = new ArrayList<>();
+ cases.add(
+ new SurefireTestCase("pass-test", 1.5, Status.PASS, "hello ]]> world",
null, null, null));
+ cases.add(
+ new SurefireTestCase(
+ "fail\"test", 2.0, Status.FAIL, "out", "err", "boom & more",
"AssertionError"));
+ cases.add(new SurefireTestCase("skip-test", 0.0, Status.SKIP, null, null,
"not now", null));
+ cases.add(new SurefireTestCase("error-test", 3.0, Status.ERROR, null,
null, "oops", "Error"));
+
+ SurefireReportWriter.write(report, "suite & name", cases);
+
+ String xml = Files.readString(report, StandardCharsets.UTF_8);
+ assertTrue(xml.contains("name=\"suite & name\""));
+ assertTrue(xml.contains("tests=\"4\""));
+ assertTrue(xml.contains("failures=\"1\""));
+ assertTrue(xml.contains("errors=\"1\""));
+ assertTrue(xml.contains("skipped=\"1\""));
+ assertTrue(xml.contains("name=\"pass-test\""));
+ assertTrue(xml.contains("name=\"fail"test\""));
+ assertTrue(xml.contains("<failure type=\"AssertionError\" message=\"boom
& more\""));
+ assertTrue(xml.contains("<error type=\"Error\" message=\"oops\""));
+ assertTrue(xml.contains("<skipped message=\"not now\"/>"));
+ // CDATA terminator must be split safely
+ assertTrue(xml.contains("]]]]><![CDATA[>"));
+ assertTrue(xml.contains("hello "));
+ assertTrue(xml.contains(" world"));
+ }
+
+ @Test
+ void formatTimeUsesIntegerWhenWhole() {
+ assertEquals("6", SurefireReportWriter.formatTime(6.0));
+ assertEquals("1.500", SurefireReportWriter.formatTime(1.5));
+ }
+
+ @Test
+ void escapeAttributeHandlesSpecialChars() {
+ assertEquals("&<>"'",
SurefireReportWriter.escapeAttribute("&<>\"'"));
+ }
+
+ @Test
+ void normalizeTestNameStripsMainWorkflowFilename() {
+ assertEquals(
+ "0001-add-sequence",
+
SurefireReportOutput.normalizeTestName("/files/transforms/main-0001-add-sequence.hwf"));
+ assertEquals("simple",
SurefireReportOutput.normalizeTestName("main_simple.hwf"));
+ assertEquals("already-clean",
SurefireReportOutput.normalizeTestName("already-clean"));
+ }
+}
diff --git
a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutor.java
b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutor.java
index 826e0100bf..b30485e566 100644
---
a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutor.java
+++
b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutor.java
@@ -220,11 +220,19 @@ public class WorkflowExecutor extends
BaseTransform<WorkflowExecutorMeta, Workfl
Result result = data.executorWorkflow.startExecution();
// First the natural output...
+ // Execution-result rows keep the first input row fields (e.g. filename)
and append metrics.
//
if (meta.getExecutionResultTargetTransformMeta() != null) {
Object[] outputRow =
RowDataUtil.allocateRowData(data.executionResultsOutputRowMeta.size());
int idx = 0;
+ // Copy fields from the first buffered input row
+ Object[] inputData = data.groupBuffer.get(0).getData();
+ int nrInputFields = data.inputRowMeta.size();
+ for (int i = 0; i < nrInputFields; i++) {
+ outputRow[idx++] = inputData[i];
+ }
+
if (!Utils.isEmpty(meta.getExecutionTimeField())) {
outputRow[idx++] = System.currentTimeMillis() - data.groupTimeStart;
}
diff --git
a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java
b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java
index c3dd047b72..f03da0b27d 100644
---
a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java
+++
b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java
@@ -286,9 +286,8 @@ public class WorkflowExecutorMeta
IHopMetadataProvider metadataProvider)
throws HopTransformException {
- row.clear();
-
if (nextTransform != null &&
nextTransform.equals(resultRowsTargetTransformMeta)) {
+ row.clear();
for (WorkflowExecutorResultRows workflowExecutorResultRows :
resultRowsField) {
IValueMeta value;
try {
@@ -303,71 +302,96 @@ public class WorkflowExecutorMeta
value.setLength(
workflowExecutorResultRows.getLength(),
workflowExecutorResultRows.getPrecision());
}
+ value.setOrigin(origin);
row.addValueMeta(value);
}
} else if (nextTransform != null &&
nextTransform.equals(resultFilesTargetTransformMeta)) {
+ row.clear();
if (!Utils.isEmpty(resultFilesFileNameField)) {
IValueMeta value = new ValueMetaString("filename", 255, 0);
+ value.setOrigin(origin);
row.addValueMeta(value);
}
} else if (nextTransform != null &&
nextTransform.equals(executionResultTargetTransformMeta)) {
- if (!Utils.isEmpty(executionTimeField)) {
- IValueMeta value = new ValueMetaInteger(executionTimeField, 15, 0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionResultField)) {
- IValueMeta value = new ValueMetaBoolean(executionResultField);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionNrErrorsField)) {
- IValueMeta value = new ValueMetaInteger(executionNrErrorsField, 9, 0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionLinesReadField)) {
- IValueMeta value = new ValueMetaInteger(executionLinesReadField, 9, 0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionLinesWrittenField)) {
- IValueMeta value = new ValueMetaInteger(executionLinesWrittenField, 9,
0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionLinesInputField)) {
- IValueMeta value = new ValueMetaInteger(executionLinesInputField, 9,
0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionLinesOutputField)) {
- IValueMeta value = new ValueMetaInteger(executionLinesOutputField, 9,
0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionLinesRejectedField)) {
- IValueMeta value = new ValueMetaInteger(executionLinesRejectedField,
9, 0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionLinesUpdatedField)) {
- IValueMeta value = new ValueMetaInteger(executionLinesUpdatedField, 9,
0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionLinesDeletedField)) {
- IValueMeta value = new ValueMetaInteger(executionLinesDeletedField, 9,
0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionFilesRetrievedField)) {
- IValueMeta value = new ValueMetaInteger(executionFilesRetrievedField,
9, 0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionExitStatusField)) {
- IValueMeta value = new ValueMetaInteger(executionExitStatusField, 3,
0);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionLogTextField)) {
- IValueMeta value = new ValueMetaString(executionLogTextField);
- value.setLargeTextField(true);
- row.addValueMeta(value);
- }
- if (!Utils.isEmpty(executionLogChannelIdField)) {
- IValueMeta value = new ValueMetaString(executionLogChannelIdField, 50,
0);
- row.addValueMeta(value);
- }
+ // Keep the incoming fields (e.g. filename from Get File Names) and
append execution metrics.
+ addExecutionResultFields(row, origin);
+ } else {
+ row.clear();
+ }
+ }
+
+ /** Append execution-result value metas to the given row meta (does not
clear existing fields). */
+ void addExecutionResultFields(IRowMeta row, String origin) {
+ if (!Utils.isEmpty(executionTimeField)) {
+ IValueMeta value = new ValueMetaInteger(executionTimeField, 15, 0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionResultField)) {
+ IValueMeta value = new ValueMetaBoolean(executionResultField);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionNrErrorsField)) {
+ IValueMeta value = new ValueMetaInteger(executionNrErrorsField, 9, 0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionLinesReadField)) {
+ IValueMeta value = new ValueMetaInteger(executionLinesReadField, 9, 0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionLinesWrittenField)) {
+ IValueMeta value = new ValueMetaInteger(executionLinesWrittenField, 9,
0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionLinesInputField)) {
+ IValueMeta value = new ValueMetaInteger(executionLinesInputField, 9, 0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionLinesOutputField)) {
+ IValueMeta value = new ValueMetaInteger(executionLinesOutputField, 9, 0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionLinesRejectedField)) {
+ IValueMeta value = new ValueMetaInteger(executionLinesRejectedField, 9,
0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionLinesUpdatedField)) {
+ IValueMeta value = new ValueMetaInteger(executionLinesUpdatedField, 9,
0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionLinesDeletedField)) {
+ IValueMeta value = new ValueMetaInteger(executionLinesDeletedField, 9,
0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionFilesRetrievedField)) {
+ IValueMeta value = new ValueMetaInteger(executionFilesRetrievedField, 9,
0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionExitStatusField)) {
+ IValueMeta value = new ValueMetaInteger(executionExitStatusField, 3, 0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionLogTextField)) {
+ IValueMeta value = new ValueMetaString(executionLogTextField);
+ value.setLargeTextField(true);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
+ }
+ if (!Utils.isEmpty(executionLogChannelIdField)) {
+ IValueMeta value = new ValueMetaString(executionLogChannelIdField, 50,
0);
+ value.setOrigin(origin);
+ row.addValueMeta(value);
}
}