This is an automated email from the ASF dual-hosted git repository.

bamaer 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 e32491e99b Issue #2420 : Allow custom pipeline unit test types (#8676)
e32491e99b is described below

commit e32491e99b64159015c46ae0e7fa17cd511dc227
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 09:08:00 2026 +0200

    Issue #2420 : Allow custom pipeline unit test types (#8676)
---
 .../pages/metadata-types/pipeline-unit-test.adoc   |   2 +-
 .../ROOT/pages/pipeline/pipeline-unit-testing.adoc |   2 +-
 .../ROOT/pages/pipeline/transforms/exectests.adoc  |   2 +-
 .../org/apache/hop/testing/PipelineUnitTest.java   |  10 +-
 .../PipelineUnitTestSearchableAnalyser.java        |   8 +-
 .../main/java/org/apache/hop/testing/TestType.java |  23 --
 .../testing/transforms/exectests/ExecuteTests.java |   4 +-
 .../transforms/exectests/ExecuteTestsDialog.java   |   4 +-
 .../transforms/exectests/ExecuteTestsMeta.java     |   7 +-
 .../org/apache/hop/testing/util/DataSetConst.java  | 118 ++++++++--
 .../xp/HopGuiUnitTestCreateBeforeDialog.java       |   4 +-
 .../hop/ui/testing/PipelineUnitTestEditor.java     |   4 +-
 .../exectests/messages/messages_en_US.properties   |   1 +
 .../ui/testing/messages/messages_en_US.properties  |   1 +
 .../hop/testing/PipelineUnitTestTypeTest.java      | 255 +++++++++++++++++++++
 15 files changed, 373 insertions(+), 72 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/pipeline-unit-test.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/pipeline-unit-test.adoc
index 4956235643..76055c7efc 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/pipeline-unit-test.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/pipeline-unit-test.adoc
@@ -48,7 +48,7 @@ None/All
 |Option|Default|Description
 |Name||the name to use for this unit test
 |Description||the description to use for this unit test
-|Type of test|Development|'Development' or 'unit test'
+|Type of test|Development|Development, Unit test, or a custom type. The list 
includes every type already used by unit tests in the project. Development and 
Unit test are stored as DEVELOPMENT and UNIT_TEST; any other type is stored as 
entered.
 |The pipeline to test||the pipeline that will be tested by this unit test
 |Test pipeline filename (Optional)||
 |Base test path (or use HOP_UNIT_TESTS_FOLDER)||
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-unit-testing.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-unit-testing.adoc
index 5094c5ec5b..90ab0d9ebe 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-unit-testing.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-unit-testing.adoc
@@ -139,7 +139,7 @@ The options in this dialog are:
 |===
 |Name|name to use for this unit test
 |Description|a description for this unit test
-|Type of test|'Unit test' or 'Development'
+|Type of test|Development, Unit test, or a custom type. The list includes 
every type already used in the project. Type a new value to add one. 
Development and Unit test are stored as DEVELOPMENT and UNIT_TEST.
 |The pipeline to test|the pipeline this test applies to.
 By default, you should see the active pipeline filename here.
 |Test pipeline filename (Optional)|the filename to use for this unit test.
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/exectests.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/exectests.adoc
index 1ea0db689d..5beb2feaeb 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/exectests.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/exectests.adoc
@@ -33,7 +33,7 @@ The Execute unit tests transform fetches and executes the 
available xref:pipelin
 |Transform name|name for this transform
 |Test name input field|name of a field to get the unit test name from to 
determine which transforms to execute.
 This option is only available when the transform receives input.
-|Type of tests to run|Development or Unit Test
+|Type of tests to run|Development, Unit test, or a custom type from the 
project. Only unit tests of that type are executed. The list includes every 
type already used in the project.
 |Pipeline output field name|the pipeline that is tested
 |Unit test output field name|the unit test that is executed
 |Data set output field name|the (golden) data set used for this test
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/PipelineUnitTest.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/PipelineUnitTest.java
index 91ed6c8526..b9dd2c041a 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/PipelineUnitTest.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/PipelineUnitTest.java
@@ -68,7 +68,7 @@ public class PipelineUnitTest extends HopMetadataBase 
implements Cloneable, IHop
   protected List<PipelineUnitTestTweak> tweaks;
 
   @HopMetadataProperty(key = "test_type")
-  protected TestType type;
+  protected String type;
 
   @HopMetadataProperty(key = "persist_filename")
   protected String filename;
@@ -86,7 +86,7 @@ public class PipelineUnitTest extends HopMetadataBase 
implements Cloneable, IHop
     inputDataSets = new ArrayList<>();
     goldenDataSets = new ArrayList<>();
     tweaks = new ArrayList<>();
-    type = TestType.DEVELOPMENT;
+    type = DataSetConst.TEST_TYPE_DEVELOPMENT;
     databaseReplacements = new ArrayList<>();
     variableValues = new ArrayList<>();
     basePath = "${" + DataSetConst.VARIABLE_HOP_UNIT_TESTS_FOLDER + "}";
@@ -100,7 +100,7 @@ public class PipelineUnitTest extends HopMetadataBase 
implements Cloneable, IHop
       List<PipelineUnitTestSetLocation> inputDataSets,
       List<PipelineUnitTestSetLocation> goldenDataSets,
       List<PipelineUnitTestTweak> tweaks,
-      TestType type,
+      String type,
       String filename,
       List<PipelineUnitTestDatabaseReplacement> databaseReplacements,
       boolean autoOpening) {
@@ -390,14 +390,14 @@ public class PipelineUnitTest extends HopMetadataBase 
implements Cloneable, IHop
    *
    * @return value of type
    */
-  public TestType getType() {
+  public String getType() {
     return type;
   }
 
   /**
    * @param type The type to set
    */
-  public void setType(TestType type) {
+  public void setType(String type) {
     this.type = type;
   }
 
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/PipelineUnitTestSearchableAnalyser.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/PipelineUnitTestSearchableAnalyser.java
index 895a4fd6b9..a74e9144d8 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/PipelineUnitTestSearchableAnalyser.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/PipelineUnitTestSearchableAnalyser.java
@@ -47,13 +47,7 @@ public class PipelineUnitTestSearchableAnalyser
 
     matchProperty(searchable, results, searchQuery, "name", 
unitTest.getName(), null);
     matchProperty(searchable, results, searchQuery, "description", 
unitTest.getDescription(), null);
-    matchProperty(
-        searchable,
-        results,
-        searchQuery,
-        "type",
-        unitTest.getType() != null ? unitTest.getType().name() : null,
-        null);
+    matchProperty(searchable, results, searchQuery, "type", 
unitTest.getType(), null);
     matchProperty(
         searchable,
         results,
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/TestType.java 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/TestType.java
deleted file mode 100644
index f7d0e1a54e..0000000000
--- a/plugins/misc/testing/src/main/java/org/apache/hop/testing/TestType.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.testing;
-
-public enum TestType {
-  DEVELOPMENT,
-  UNIT_TEST;
-}
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTests.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTests.java
index 4e2ac2d878..43fc1bf826 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTests.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTests.java
@@ -32,6 +32,7 @@ import org.apache.hop.pipeline.transform.TransformMeta;
 import org.apache.hop.testing.PipelineUnitTest;
 import org.apache.hop.testing.UnitTestResult;
 import org.apache.hop.testing.gui.TestingGuiPlugin;
+import org.apache.hop.testing.util.DataSetConst;
 import org.apache.hop.testing.util.UnitTestUtil;
 
 public class ExecuteTests extends BaseTransform<ExecuteTestsMeta, 
ExecuteTestsData> {
@@ -119,8 +120,7 @@ public class ExecuteTests extends 
BaseTransform<ExecuteTestsMeta, ExecuteTestsDa
           data.tests = new ArrayList<>();
           for (String testName : testSerializer.listObjectNames()) {
             PipelineUnitTest pipelineUnitTest = testSerializer.load(testName);
-            if (meta.getTypeToExecute() == null
-                || meta.getTypeToExecute() == pipelineUnitTest.getType()) {
+            if (DataSetConst.matchesTestType(meta.getTypeToExecute(), 
pipelineUnitTest.getType())) {
               data.tests.add(pipelineUnitTest);
             }
           }
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTestsDialog.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTestsDialog.java
index 3ed816b4bd..b7245a5a82 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTestsDialog.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTestsDialog.java
@@ -145,7 +145,9 @@ public class ExecuteTestsDialog extends BaseTransformDialog 
{
     fdlTypeToExecute.top = new FormAttachment(lastControl, margin);
     wlTypeToExecute.setLayoutData(fdlTypeToExecute);
     wTypeToExecute = new Combo(wContent, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
-    wTypeToExecute.setItems(DataSetConst.getTestTypeDescriptions());
+    
wTypeToExecute.setItems(DataSetConst.getTestTypeDescriptions(metadataProvider));
+    wTypeToExecute.setToolTipText(
+        BaseMessages.getString(PKG, 
"ExecuteTestsDialog.TypeToExecute.Tooltip"));
     PropsUi.setLook(wTypeToExecute);
     FormData fdTypeToExecute = new FormData();
     fdTypeToExecute.left = new FormAttachment(middle, 0);
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTestsMeta.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTestsMeta.java
index 7555845e2f..d5655b4996 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTestsMeta.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/transforms/exectests/ExecuteTestsMeta.java
@@ -24,7 +24,6 @@ import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.pipeline.transform.BaseTransformMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
-import org.apache.hop.testing.TestType;
 import org.apache.hop.testing.UnitTestResult;
 
 @Transform(
@@ -50,7 +49,7 @@ public class ExecuteTestsMeta extends 
BaseTransformMeta<ExecuteTests, ExecuteTes
   private String testNameInputField;
 
   @HopMetadataProperty(key = TAG_TYPE_TO_EXECUTE)
-  private TestType typeToExecute;
+  private String typeToExecute;
 
   @HopMetadataProperty(key = TAG_PIPELINE_NAME_FIELD)
   private String pipelineNameField;
@@ -127,14 +126,14 @@ public class ExecuteTestsMeta extends 
BaseTransformMeta<ExecuteTests, ExecuteTes
    *
    * @return value of typeToExecute
    */
-  public TestType getTypeToExecute() {
+  public String getTypeToExecute() {
     return typeToExecute;
   }
 
   /**
    * @param typeToExecute The typeToExecute to set
    */
-  public void setTypeToExecute(TestType typeToExecute) {
+  public void setTypeToExecute(String typeToExecute) {
     this.typeToExecute = typeToExecute;
   }
 
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/DataSetConst.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/DataSetConst.java
index 085104d1f6..a12677c160 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/DataSetConst.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/DataSetConst.java
@@ -23,6 +23,7 @@ import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -32,12 +33,14 @@ import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.exception.HopRuntimeException;
 import org.apache.hop.core.exception.HopValueException;
 import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.logging.LogChannel;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.api.IHopMetadataSerializer;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.engine.IPipelineEngine;
 import org.apache.hop.testing.DataSet;
@@ -45,7 +48,6 @@ import org.apache.hop.testing.PipelineTweak;
 import org.apache.hop.testing.PipelineUnitTest;
 import org.apache.hop.testing.PipelineUnitTestFieldMapping;
 import org.apache.hop.testing.PipelineUnitTestSetLocation;
-import org.apache.hop.testing.TestType;
 import org.apache.hop.testing.UnitTestResult;
 import org.apache.hop.testing.xp.RowCollection;
 
@@ -111,6 +113,14 @@ public class DataSetConst {
         BaseMessages.getString(PKG, 
"DataSetConst.Tweak.REMOVE_TRANSFORM.Desc"),
       };
 
+  /**
+   * Legacy stored codes. Dialogs show the translated label and write this 
code back. Any other type
+   * is stored exactly as entered.
+   */
+  public static final String TEST_TYPE_DEVELOPMENT = "DEVELOPMENT";
+
+  public static final String TEST_TYPE_UNIT_TEST = "UNIT_TEST";
+
   private static final String[] testTypeDesc =
       new String[] {
         BaseMessages.getString(PKG, "DataSetConst.TestType.DEVELOPMENT.Desc"),
@@ -602,40 +612,100 @@ public class DataSetConst {
     return outputRowMeta;
   }
 
-  public static String getTestTypeDescription(TestType testType) {
-    int index = 0; // DEVELOPMENT
-    if (testType != null) {
-      TestType[] testTypes = TestType.values();
-      for (int i = 0; i < testTypes.length; i++) {
-        if (testTypes[i] == testType) {
-          index = i;
-          break;
-        }
-      }
+  /**
+   * Dialog label for a stored test type. {@code DEVELOPMENT}, {@code 
UNIT_TEST}, and a missing type
+   * use the translated labels. Every other value is shown unchanged.
+   *
+   * @param testType stored type, or null
+   * @return label to put in the combo
+   */
+  public static String getTestTypeDescription(String testType) {
+    if (TEST_TYPE_UNIT_TEST.equals(testType)) {
+      return testTypeDesc[1];
     }
-
-    return testTypeDesc[index];
+    if (StringUtils.isEmpty(testType) || 
TEST_TYPE_DEVELOPMENT.equals(testType)) {
+      return testTypeDesc[0];
+    }
+    return testType;
   }
 
   /**
-   * Get the TestType for a tweak description (from the dialog)
+   * Stored type for a dialog label. The Development and Unit test labels map 
back to {@code
+   * DEVELOPMENT} and {@code UNIT_TEST}. An empty label maps to {@code 
DEVELOPMENT}. Anything else
+   * is stored as entered.
    *
-   * @param testTypeDescription The description to look for
-   * @return the test type or NONE if nothing matched
+   * @param testTypeDescription label from the combo
+   * @return value written to metadata
    */
-  public static TestType getTestTypeForDescription(String testTypeDescription) 
{
-    if (StringUtils.isEmpty(testTypeDescription)) {
-      return TestType.DEVELOPMENT;
+  public static String getTestTypeForDescription(String testTypeDescription) {
+    if (StringUtils.isEmpty(testTypeDescription)
+        || testTypeDescription.equalsIgnoreCase(testTypeDesc[0])) {
+      return TEST_TYPE_DEVELOPMENT;
     }
-    int index = Const.indexOfString(testTypeDescription, testTypeDesc);
-    if (index < 0) {
-      return TestType.DEVELOPMENT;
+    if (testTypeDescription.equalsIgnoreCase(testTypeDesc[1])) {
+      return TEST_TYPE_UNIT_TEST;
     }
-    return TestType.values()[index];
+    return testTypeDescription;
   }
 
+  /**
+   * @return translated Development and Unit test labels
+   */
   public static String[] getTestTypeDescriptions() {
-    return testTypeDesc;
+    return getTestTypeDescriptions(null);
+  }
+
+  /**
+   * Development, Unit test, then every other type already stored on a 
pipeline unit test in the
+   * project. A metadata error leaves the two built-in labels in place.
+   *
+   * @param metadataProvider project metadata, or null for the built-in labels 
only
+   * @return combo items
+   */
+  public static String[] getTestTypeDescriptions(IHopMetadataProvider 
metadataProvider) {
+    LinkedHashSet<String> descriptions = new LinkedHashSet<>();
+    descriptions.add(testTypeDesc[0]);
+    descriptions.add(testTypeDesc[1]);
+    if (metadataProvider == null) {
+      return descriptions.toArray(new String[0]);
+    }
+    try {
+      IHopMetadataSerializer<PipelineUnitTest> serializer =
+          metadataProvider.getSerializer(PipelineUnitTest.class);
+      LinkedHashSet<String> custom = new LinkedHashSet<>();
+      for (String name : serializer.listObjectNames()) {
+        try {
+          PipelineUnitTest unitTest = serializer.load(name);
+          if (unitTest == null) {
+            continue;
+          }
+          String description = getTestTypeDescription(unitTest.getType());
+          if (StringUtils.isNotEmpty(description) && 
!descriptions.contains(description)) {
+            custom.add(description);
+          }
+        } catch (Exception e) {
+          LogChannel.GENERAL.logError(
+              "Unable to load pipeline unit test '" + name + "' while listing 
test types", e);
+        }
+      }
+      List<String> sorted = new ArrayList<>(custom);
+      sorted.sort(String.CASE_INSENSITIVE_ORDER);
+      descriptions.addAll(sorted);
+    } catch (Exception e) {
+      LogChannel.GENERAL.logError("Unable to list pipeline unit test types", 
e);
+    }
+    return descriptions.toArray(new String[0]);
+  }
+
+  /**
+   * An unset type runs every unit test. A set type matches the stored type 
exactly.
+   *
+   * @param typeToExecute type selected on Execute unit tests, or null when it 
was never set
+   * @param unitTestType type stored on the pipeline unit test
+   * @return true when this unit test should run
+   */
+  public static boolean matchesTestType(String typeToExecute, String 
unitTestType) {
+    return typeToExecute == null || typeToExecute.equals(unitTestType);
   }
 
   /**
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestCreateBeforeDialog.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestCreateBeforeDialog.java
index ca0d98a630..a1c0e30951 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestCreateBeforeDialog.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestCreateBeforeDialog.java
@@ -27,8 +27,8 @@ import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.metadata.api.IHopMetadataSerializer;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.testing.PipelineUnitTest;
-import org.apache.hop.testing.TestType;
 import org.apache.hop.testing.gui.TestingGuiPlugin;
+import org.apache.hop.testing.util.DataSetConst;
 import org.apache.hop.ui.hopgui.HopGui;
 
 @ExtensionPoint(
@@ -54,7 +54,7 @@ public class HopGuiUnitTestCreateBeforeDialog extends 
HopGuiUnitTestChanged
 
     HopGui hopGui = HopGui.getInstance();
     test.setName(uniqueUnitTestName(pipelineMeta.getName(), 
hopGui.getMetadataProvider()));
-    test.setType(TestType.UNIT_TEST);
+    test.setType(DataSetConst.TEST_TYPE_UNIT_TEST);
     test.setRelativeFilename(hopGui.getVariables(), 
pipelineMeta.getFilename());
   }
 
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/ui/testing/PipelineUnitTestEditor.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/ui/testing/PipelineUnitTestEditor.java
index 487ae88c4c..3aca6161c2 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/ui/testing/PipelineUnitTestEditor.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/ui/testing/PipelineUnitTestEditor.java
@@ -198,7 +198,9 @@ public class PipelineUnitTestEditor extends 
MetadataEditor<PipelineUnitTest> {
     fdTestType.left = new FormAttachment(middle, 0);
     fdTestType.right = new FormAttachment(100, 0);
     wTestType.setLayoutData(fdTestType);
-    wTestType.setItems(DataSetConst.getTestTypeDescriptions());
+    wTestType.setItems(DataSetConst.getTestTypeDescriptions(metadataProvider));
+    wTestType.setToolTipText(
+        BaseMessages.getString(PKG, 
"PipelineUnitTestDialog.TestType.Tooltip"));
     lastControl = wTestType;
 
     // The filename of the pipeline to test
diff --git 
a/plugins/misc/testing/src/main/resources/org/apache/hop/testing/transforms/exectests/messages/messages_en_US.properties
 
b/plugins/misc/testing/src/main/resources/org/apache/hop/testing/transforms/exectests/messages/messages_en_US.properties
index 3ddf817d3c..0b84810abe 100644
--- 
a/plugins/misc/testing/src/main/resources/org/apache/hop/testing/transforms/exectests/messages/messages_en_US.properties
+++ 
b/plugins/misc/testing/src/main/resources/org/apache/hop/testing/transforms/exectests/messages/messages_en_US.properties
@@ -26,5 +26,6 @@ ExecuteTestsDialog.TestNameInputField.Label=Test name input 
field
 ExecuteTestsDialog.TransformName.Label=Transform name
 ExecuteTestsDialog.TransformNameField.Label=transform output field name
 ExecuteTestsDialog.TypeToExecute.Label=Type of tests to run
+ExecuteTestsDialog.TypeToExecute.Tooltip=Run unit tests of this type. The list 
includes Development, Unit test, and every other type already used in this 
project. Type a value that is not listed yet to use it.
 ExecuteTestsDialog.UnitTestNameField.Label=Unit test output field name
 ExecuteTestsMeta.keyword=test,unit,run,validate,assert
diff --git 
a/plugins/misc/testing/src/main/resources/org/apache/hop/ui/testing/messages/messages_en_US.properties
 
b/plugins/misc/testing/src/main/resources/org/apache/hop/ui/testing/messages/messages_en_US.properties
index 7304fbe298..16b09c895f 100644
--- 
a/plugins/misc/testing/src/main/resources/org/apache/hop/ui/testing/messages/messages_en_US.properties
+++ 
b/plugins/misc/testing/src/main/resources/org/apache/hop/ui/testing/messages/messages_en_US.properties
@@ -55,6 +55,7 @@ PipelineUnitTestDialog.Tab.General=General
 PipelineUnitTestDialog.Tab.Tweaks=Tweaks
 PipelineUnitTestDialog.Tab.Variables=Parameters & Variables
 PipelineUnitTestDialog.TestType.Label=Type of test
+PipelineUnitTestDialog.TestType.Tooltip=Development, Unit test, or a custom 
type. The list includes types already used in this project. Type a new value to 
add one.
 PipelineUnitTestDialog.Tweaks.ColumnInfo.TransformName=Transform name
 PipelineUnitTestDialog.Tweaks.ColumnInfo.Tweak=Tweak
 PipelineUnitTestDialog.Tweaks.Label=Transform tweaks applied when this unit 
test runs:
diff --git 
a/plugins/misc/testing/src/test/java/org/apache/hop/testing/PipelineUnitTestTypeTest.java
 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/PipelineUnitTestTypeTest.java
new file mode 100644
index 0000000000..949da9e2b6
--- /dev/null
+++ 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/PipelineUnitTestTypeTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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.testing;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.encryption.HopTwoWayPasswordEncoder;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.api.IHopMetadataSerializer;
+import org.apache.hop.metadata.serializer.json.JsonMetadataProvider;
+import org.apache.hop.metadata.serializer.json.JsonMetadataSerializer;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil;
+import org.apache.hop.testing.transforms.exectests.ExecuteTestsMeta;
+import org.apache.hop.testing.util.DataSetConst;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.w3c.dom.Node;
+
+class PipelineUnitTestTypeTest {
+
+  @BeforeAll
+  static void initHop() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void legacyCodesRoundTripThroughDialogLabels() {
+    String development = 
DataSetConst.getTestTypeDescription(DataSetConst.TEST_TYPE_DEVELOPMENT);
+    String unitTest = 
DataSetConst.getTestTypeDescription(DataSetConst.TEST_TYPE_UNIT_TEST);
+
+    assertEquals(development, DataSetConst.getTestTypeDescription(null));
+    assertEquals(development, DataSetConst.getTestTypeDescription(""));
+    assertEquals(DataSetConst.TEST_TYPE_DEVELOPMENT, 
DataSetConst.getTestTypeForDescription(null));
+    assertEquals(DataSetConst.TEST_TYPE_DEVELOPMENT, 
DataSetConst.getTestTypeForDescription(""));
+    assertEquals(
+        DataSetConst.TEST_TYPE_DEVELOPMENT, 
DataSetConst.getTestTypeForDescription(development));
+    assertEquals(
+        DataSetConst.TEST_TYPE_DEVELOPMENT,
+        
DataSetConst.getTestTypeForDescription(development.toLowerCase(Locale.ROOT)));
+    assertEquals(
+        DataSetConst.TEST_TYPE_UNIT_TEST, 
DataSetConst.getTestTypeForDescription(unitTest));
+    assertEquals(
+        DataSetConst.TEST_TYPE_UNIT_TEST,
+        
DataSetConst.getTestTypeForDescription(unitTest.toUpperCase(Locale.ROOT)));
+
+    assertEquals("Smoke", DataSetConst.getTestTypeDescription("Smoke"));
+    assertEquals("Smoke", DataSetConst.getTestTypeForDescription("Smoke"));
+    assertEquals("  ", DataSetConst.getTestTypeForDescription("  "));
+  }
+
+  @Test
+  void unsetTypeRunsEveryTestAndASetTypeMatchesExactly() {
+    assertTrue(DataSetConst.matchesTestType(null, "Smoke"));
+    assertTrue(DataSetConst.matchesTestType(null, null));
+    assertTrue(DataSetConst.matchesTestType("Smoke", "Smoke"));
+    assertTrue(
+        DataSetConst.matchesTestType(
+            DataSetConst.TEST_TYPE_UNIT_TEST, 
DataSetConst.TEST_TYPE_UNIT_TEST));
+    assertFalse(DataSetConst.matchesTestType("Smoke", 
DataSetConst.TEST_TYPE_UNIT_TEST));
+    assertFalse(DataSetConst.matchesTestType("Smoke", null));
+    assertFalse(DataSetConst.matchesTestType("", "Smoke"));
+    assertFalse(DataSetConst.matchesTestType("Unit test", 
DataSetConst.TEST_TYPE_UNIT_TEST));
+  }
+
+  @Test
+  void newUnitTestDefaultsToDevelopment() {
+    assertEquals(DataSetConst.TEST_TYPE_DEVELOPMENT, new 
PipelineUnitTest().getType());
+  }
+
+  @Test
+  void descriptionsListBuiltInTypesAndProjectTypes() throws Exception {
+    MemoryMetadataProvider provider = new MemoryMetadataProvider();
+    IHopMetadataSerializer<PipelineUnitTest> serializer =
+        provider.getSerializer(PipelineUnitTest.class);
+    save(serializer, "dev", DataSetConst.TEST_TYPE_DEVELOPMENT);
+    save(serializer, "unit", DataSetConst.TEST_TYPE_UNIT_TEST);
+    save(serializer, "smoke", "Smoke");
+    save(serializer, "smoke-again", "Smoke");
+    save(serializer, "regression", "Regression");
+    save(serializer, "blank", "");
+
+    String[] items = DataSetConst.getTestTypeDescriptions(provider);
+
+    
assertEquals(DataSetConst.getTestTypeDescription(DataSetConst.TEST_TYPE_DEVELOPMENT),
 items[0]);
+    
assertEquals(DataSetConst.getTestTypeDescription(DataSetConst.TEST_TYPE_UNIT_TEST),
 items[1]);
+    assertEquals(List.of(items[0], items[1], "Regression", "Smoke"), 
Arrays.asList(items));
+    assertEquals(2, DataSetConst.getTestTypeDescriptions().length);
+    assertEquals(2, DataSetConst.getTestTypeDescriptions(null).length);
+  }
+
+  @Test
+  void descriptionsStayUsableWhenMetadataCannotBeListed() throws Exception {
+    IHopMetadataProvider broken = mock(IHopMetadataProvider.class);
+    when(broken.getSerializer(PipelineUnitTest.class)).thenThrow(new 
HopException("nope"));
+
+    assertEquals(2, DataSetConst.getTestTypeDescriptions(broken).length);
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void descriptionsSkipAUnitTestThatFailsToLoad() throws Exception {
+    IHopMetadataSerializer<PipelineUnitTest> serializer = 
mock(IHopMetadataSerializer.class);
+    when(serializer.listObjectNames()).thenReturn(List.of("ok", "bad"));
+    PipelineUnitTest ok = new PipelineUnitTest();
+    ok.setName("ok");
+    ok.setType("Smoke");
+    when(serializer.load("ok")).thenReturn(ok);
+    when(serializer.load("bad")).thenThrow(new HopException("bad"));
+    IHopMetadataProvider provider = mock(IHopMetadataProvider.class);
+    
when(provider.getSerializer(PipelineUnitTest.class)).thenReturn(serializer);
+
+    String[] items = DataSetConst.getTestTypeDescriptions(provider);
+
+    assertEquals(3, items.length);
+    assertEquals("Smoke", items[2]);
+  }
+
+  @Test
+  void legacyJsonKeepsStoredCodesAndCustomTypesAreStoredAsEntered(@TempDir 
Path folder)
+      throws Exception {
+    writeUnitTest(folder, "legacy-unit", DataSetConst.TEST_TYPE_UNIT_TEST);
+    writeUnitTest(folder, "legacy-dev", DataSetConst.TEST_TYPE_DEVELOPMENT);
+
+    JsonMetadataSerializer<PipelineUnitTest> serializer = serializer(folder);
+
+    PipelineUnitTest legacyUnit = serializer.load("legacy-unit");
+    PipelineUnitTest legacyDev = serializer.load("legacy-dev");
+    assertEquals(DataSetConst.TEST_TYPE_UNIT_TEST, legacyUnit.getType());
+    assertEquals(DataSetConst.TEST_TYPE_DEVELOPMENT, legacyDev.getType());
+
+    serializer.save(legacyUnit);
+    String savedLegacy = read(serializer.calculateFilename("legacy-unit"));
+    
assertTrue(withoutSpaces(savedLegacy).contains("\"test_type\":\"UNIT_TEST\""));
+    assertFalse(
+        savedLegacy.contains(
+            
DataSetConst.getTestTypeDescription(DataSetConst.TEST_TYPE_UNIT_TEST)));
+
+    PipelineUnitTest custom = new PipelineUnitTest();
+    custom.setName("custom");
+    custom.setType("Smoke");
+    serializer.save(custom);
+
+    assertEquals("Smoke", serializer.load("custom").getType());
+    String savedCustom = read(serializer.calculateFilename("custom"));
+    assertTrue(withoutSpaces(savedCustom).contains("\"test_type\":\"Smoke\""));
+    
assertFalse(withoutSpaces(savedCustom).contains("\"test_type\":\"DEVELOPMENT\""));
+  }
+
+  @Test
+  void executeTestsKeepsLegacyAndCustomTypesInXml() throws Exception {
+    assertNull(loadExecuteTests(null).getTypeToExecute());
+    assertEquals(
+        DataSetConst.TEST_TYPE_DEVELOPMENT, 
loadExecuteTests("DEVELOPMENT").getTypeToExecute());
+    assertEquals(
+        DataSetConst.TEST_TYPE_UNIT_TEST, 
loadExecuteTests("UNIT_TEST").getTypeToExecute());
+
+    ExecuteTestsMeta smoke = loadExecuteTests("Smoke");
+    assertEquals("Smoke", smoke.getTypeToExecute());
+    String xml = smoke.getXml();
+    assertTrue(xml.contains("<type_to_execute>Smoke</type_to_execute>"));
+
+    Node copyNode = XmlHandler.loadXmlString("<transform>" + xml + 
"</transform>", "transform");
+    ExecuteTestsMeta copy =
+        XmlMetadataUtil.deSerializeFromXml(
+            copyNode, ExecuteTestsMeta.class, new MemoryMetadataProvider());
+    assertEquals("Smoke", copy.getTypeToExecute());
+  }
+
+  private static void save(
+      IHopMetadataSerializer<PipelineUnitTest> serializer, String name, String 
type)
+      throws HopException {
+    PipelineUnitTest unitTest = new PipelineUnitTest();
+    unitTest.setName(name);
+    unitTest.setType(type);
+    serializer.save(unitTest);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static JsonMetadataSerializer<PipelineUnitTest> serializer(Path 
folder)
+      throws HopException {
+    JsonMetadataProvider provider =
+        new JsonMetadataProvider(
+            new HopTwoWayPasswordEncoder(),
+            folder.toString(),
+            Variables.getADefaultVariableSpace());
+    return (JsonMetadataSerializer<PipelineUnitTest>)
+        provider.getSerializer(PipelineUnitTest.class);
+  }
+
+  private static void writeUnitTest(Path folder, String name, String testType) 
throws Exception {
+    String directory = folder.resolve("unit-test").toString();
+    HopVfs.getFileObject(directory).createFolder();
+    String filename = directory + "/" + name + ".json";
+    String json = "{\"name\":\"" + name + "\",\"test_type\":\"" + testType + 
"\"}";
+    try (OutputStream out = HopVfs.getOutputStream(filename, false)) {
+      out.write(json.getBytes(UTF_8));
+    }
+  }
+
+  private static String read(String filename) throws Exception {
+    try (InputStream in = HopVfs.getInputStream(filename)) {
+      return new String(in.readAllBytes(), UTF_8);
+    }
+  }
+
+  private static String withoutSpaces(String value) {
+    return value.replace(" ", "");
+  }
+
+  private static ExecuteTestsMeta loadExecuteTests(String typeToExecute) 
throws Exception {
+    String xml =
+        "<transform>"
+            + (typeToExecute == null
+                ? ""
+                : "<type_to_execute>" + typeToExecute + "</type_to_execute>")
+            + "</transform>";
+    Node node = XmlHandler.loadXmlString(xml, "transform");
+    return XmlMetadataUtil.deSerializeFromXml(
+        node, ExecuteTestsMeta.class, new MemoryMetadataProvider());
+  }
+}

Reply via email to