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 ae53f35eb5 Issue #2664 : Harden pipeline unit tests (replacement 
notices, numeric compare, rename) (#8274)
ae53f35eb5 is described below

commit ae53f35eb5ac8b4ab428df702b7f239df9083178
Author: Matt Casters <[email protected]>
AuthorDate: Tue Sep 8 08:58:08 2026 +0200

    Issue #2664 : Harden pipeline unit tests (replacement notices, numeric 
compare, rename) (#8274)
    
    * Issue #2664 : Harden pipeline unit tests (replacement notices, numeric 
compare, rename)
    
    When attaching an input or golden data set, inform the user that the
    transform is replaced with an Injector or Dummy during the test.
    Compare Number/BigNumber golden values with a length/precision format
    mask, and keep data set locations in sync when a transform is renamed.
    
    Fixes #2664
    
    * Issue #2664 : Keep 1e-6 fuzzy compare and defer unit-test rename save
    
    When a Number/BigNumber field has no declared length/precision, restore
    the historical 1e-6 fuzzy equals so existing tests do not tighten on
    upgrade. Declared-precision formatting is unchanged.
    
    Do not persist transform-name updates on the unit test until the
    pipeline is saved. Undo restores the old transform name and rolls the
    in-memory locations back, so the data set attachment is not orphaned.
---
 .../hop/core/extension/HopExtensionPoint.java      |   2 +
 ...unit-testing-set-golden-dataset-explanation.png | Bin 0 -> 104848 bytes
 ...-unit-testing-set-input-dataset-explanation.png | Bin 0 -> 68592 bytes
 .../ROOT/pages/metadata-types/data-set.adoc        |   5 +
 .../pages/metadata-types/pipeline-unit-test.adoc   |   5 +-
 .../ROOT/pages/pipeline/pipeline-unit-testing.adoc |  56 ++++++--
 .../ROOT/pages/pipeline/transforms/dummy.adoc      |   3 +
 .../ROOT/pages/pipeline/transforms/injector.adoc   |   3 +
 .../java/org/apache/hop/pipeline/PipelineMeta.java |  18 +++
 .../apache/hop/pipeline/TransformNameChange.java   |  38 ++++++
 .../org/apache/hop/testing/PipelineUnitTest.java   |  33 +++++
 .../apache/hop/testing/gui/TestingGuiPlugin.java   |  75 ++++++++++-
 .../org/apache/hop/testing/util/DataSetConst.java  | 130 ++++++++++++++++--
 .../hop/testing/util/UnitTestTransformRenames.java | 147 +++++++++++++++++++++
 .../hop/testing/xp/HopGuiPipelineAfterClose.java   |   5 +
 ...a => PersistUnitTestRenamesExtensionPoint.java} |  45 +++++--
 ... => RenameUnitTestLocationsExtensionPoint.java} |  50 ++++---
 .../xp/UpdateUnitTestButtonsExtensionPoint.java    |  11 ++
 .../testing/gui/messages/messages_en_US.properties |  12 +-
 .../PipelineUnitTestRenameTransformTest.java       |  76 +++++++++++
 .../TestingGuiPluginReplacementWarningTest.java    |  51 +++++++
 .../util/DataSetConstNumericCompareTest.java       | 124 +++++++++++++++++
 .../testing/util/UnitTestTransformRenamesTest.java | 102 ++++++++++++++
 23 files changed, 934 insertions(+), 57 deletions(-)

diff --git 
a/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java 
b/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java
index 8a6271b4ae..74be646186 100644
--- a/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java
+++ b/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java
@@ -51,6 +51,8 @@ public enum HopExtensionPoint {
   PipelineGraphMouseMoved("The mouse was moved on the canvas"),
   PipelineGraphMouseDoubleClick("A left or right button was double-clicked in 
a Pipeline"),
   PipelineBeforeDeleteTransforms("Pipeline transforms about to be deleted"),
+  PipelineTransformRenamed(
+      "A pipeline transform was renamed (TransformNameChange: pipeline, old 
name, new name)"),
 
   HopGuiPipelineMetaExecutionStart("Hop GUI initiates the execution of a 
pipeline (PipelineMeta)"),
   HopGuiPipelineExecutionConfiguration(
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline-unit-testing-set-golden-dataset-explanation.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline-unit-testing-set-golden-dataset-explanation.png
new file mode 100644
index 0000000000..0270070301
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline-unit-testing-set-golden-dataset-explanation.png
 differ
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline-unit-testing-set-input-dataset-explanation.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline-unit-testing-set-input-dataset-explanation.png
new file mode 100644
index 0000000000..c1386db38c
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline-unit-testing-set-input-dataset-explanation.png
 differ
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/data-set.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/data-set.adoc
index 602d166976..9c8203b59d 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/data-set.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/data-set.adoc
@@ -26,6 +26,11 @@ A Data Set is a static pre-defined collection of rows, used 
in xref:metadata-typ
 This input set is used to be processed in a unit testing pipeline.
 After processing the result of the pipeline is compared to a data set that has 
been specified as the 'golden data set' for the test.
 
+When a data set is attached as *input*, the marked transform is replaced with 
an Injector and the static data set rows are injected (so the original 
transform does not read from a file or a database).
+When a data set is attached as *golden*, the marked transform is replaced with 
a Dummy and the rows flowing into it are compared to the data set (so the 
original transform does not write to a file or a database).
+
+See xref:pipeline/pipeline-unit-testing.adoc[Pipeline Unit Testing] for 
details.
+
 == Related Plugins
 
 xref:pipeline/transforms/datasetinput.adoc[Data set input], 
xref:pipeline/transforms/datasetoutput.adoc[Data set output]
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 8d38a5eb82..4956235643 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
@@ -28,7 +28,10 @@ Describes a test for a pipeline. During the test, 
alternative data sets are used
 
 When the result set produced by the pipeline execution matches the expected 
result (golden data set), the test succeeds. If the generated results do not 
match what was expected, the test fails.
 
-Specific transforms in a pipeline can be bypassed or removed for testing for 
addtional flexibility.
+Transforms with an input data set are replaced with an Injector (the static 
data set rows are injected).
+Transforms with a golden data set are replaced with a Dummy so the test does 
not write to a file or a database; the rows flowing *into* that transform are 
compared to the golden data set.
+
+Specific transforms in a pipeline can be bypassed or removed for testing for 
additional flexibility.
 
 Additionally, unit tests can be executed in bulk.
 
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 4dc86cd1e0..fdeae55e61 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
@@ -57,16 +57,44 @@ By default, the value for the 
`{openvar}HOP_DATASETS_FOLDER{closevar}` variable
 
 When a pipeline is executed in Hop Gui and a unit test is selected the 
following happens:
 
-* all transforms marked with an input data set are replaced with an Injector 
transform
-* all transforms marked with a golden data set are replaced with a dummy 
transform (does nothing).
-* all transforms marked with a "Bypass" tweak are replaced with a dummy.
+* all transforms marked with an input data set are replaced with an 
xref:pipeline/transforms/injector.adoc[Injector] transform
+* all transforms marked with a golden data set are replaced with a 
xref:pipeline/transforms/dummy.adoc[Dummy] transform (does nothing).
+* all transforms marked with a "Bypass" tweak are replaced with a Dummy.
 * all transforms marked with a "Remove" tweak are removed
 
 These operations take place on a copy of the pipeline, in memory only unless 
you specify a hpl file location in the unit test dialog.
+The original pipeline on disk is not changed.
 
-After execution, transform output is validated against golden data and logged.
+After execution, the rows that flowed *into* each golden-data-set transform 
are compared to the golden data set and the result is logged.
 In case of errors in the test, a dialog will pop up when running in Hop Gui.
 
+=== Input data sets replace the transform with an Injector
+
+When you attach an input data set to a transform, that transform is replaced 
with an Injector during the test.
+The static rows from the data set are injected instead of executing the 
original transform.
+
+This is typically used so the unit test does not read from a file, a database, 
or another external source.
+
+Hop Gui shows a notice (with a *Don't show this message again* option) the 
first time you attach an input data set to a transform.
+
+image::pipeline-unit-testing-set-input-dataset-explanation.png[Input data set 
replacement notice,width="90%"]
+
+=== Golden data sets replace the transform with a Dummy
+
+When you attach a golden data set to a transform, that transform is replaced 
with a Dummy during the test.
+The rows that would have been sent *into* that transform are captured and 
compared to the golden data set.
+
+This is typically used to prevent the unit test from writing to a file or a 
database.
+Attach the golden data set to the *output* transform (for example Text File 
Output or Table Output), not to the transform whose results you want to verify.
+
+IMPORTANT: The transform you attach the golden data set to is *not* executed.
+If you want to validate the output of a Calculator, attach the golden data set 
to the transform *after* the Calculator.
+Attaching it to the Calculator itself would skip the Calculator and compare 
the Calculator's input instead.
+
+Hop Gui shows a notice (with a *Don't show this message again* option) the 
first time you attach a golden data set to a transform.
+
+image::pipeline-unit-testing-set-golden-dataset-explanation.png[Golden data 
set replacement notice,width="90%"]
+
 == Create unit tests
 
 === Unit test and data set options
@@ -75,10 +103,10 @@ The 'Unit Testing' category in the transform context 
dialog (click on transform
 
 image::pipeline-unit-testing-data-sets.png[Pipeline Unit Testing - Data 
Sets,width=90%]
 
-* **Set input data set**: For the active unit test, it defines which data set 
to use instead of the output of the transform
+* **Set input data set**: For the active unit test, replace this transform 
with an Injector that injects the selected data set instead of executing the 
transform (so the test does not read from a file or a database).
 * **Clear input data set**: Remove a defined input data set from this 
transform unit test
-* **Set golden data set**: The input to this transform is taken and compared 
to the golden data set you are selecting.
-* **Clear golden data set**: Remove a defined input data set for this 
transform unit test
+* **Set golden data set**: Compare the rows flowing *into* this transform with 
the selected golden data set. During the test the transform is replaced with a 
Dummy so it is not executed (for example it will not write to a file or a 
database).
+* **Clear golden data set**: Remove a defined golden data set from this 
transform unit test
 * **Create data set**: Create an empty data set with the output fields of this 
transform
 * **Write rows to data set**: Run the current pipeline and write the data to a 
data set
 * **Remove from test**: When this unit test is run, do not include this 
transform
@@ -86,6 +114,8 @@ image::pipeline-unit-testing-data-sets.png[Pipeline Unit 
Testing - Data Sets,wid
 * **Bypass in test**: When this unit test is run, bypass this transform 
(replace with a dummy)
 * **Remove bypass in test**: Do not bypass this transform in the current 
pipeline during testing
 
+If a unit test is active and you rename a transform that has an input data 
set, a golden data set, or a tweak, Hop updates that location on the unit test 
so the attachment stays on the renamed transform.
+
 TIP: creating data sets is also possible from the 'New' context menu or 
metadata perspective.
 
 You can also generate or regenerate a data set from a pipeline with the 
xref:pipeline/transforms/datasetoutput.adoc[Data set output] transform.
@@ -142,11 +172,14 @@ For this example, just click 'guess'.
 Repeat for the 'customer by year out' transform and data set.
 
 Click the 'read customers' transform icon again, select 'set input data set'.
-Select the data set and accept the sort order.
+Select the data set.
+Hop Gui shows a notice that this transform will be replaced with an Injector 
during the test (you can check *Don't show this message again* if you do not 
want to see it next time).
+Then map the fields and accept the sort order.
 
 Repeat for 'customers by year out', but use 'Set golden data set'.
+Hop Gui shows a similar notice that this transform will be replaced with a 
Dummy during the test, so the pipeline does not write the output file.
 
-Your pipeline now has two new indicators for ths input and output data set.
+Your pipeline now has two new indicators for the input and output data set.
 
 image::pipeline-unit-testing-data-sets-enabled.png[Unit Test - data set 
enabled,width="90%"]
 
@@ -180,6 +213,11 @@ While successful test show 'Test passed succesfully 
against golden data set' and
 * `Incorrect number of rows received from transform, golden data set 
<GOLDEN_DATASET_NAME> has <GOLDEN_DATASET_ROWS> rows in it and we received 
<NB_ROWS_FOUND>`
 * `Validation against golden data failed for row number <ROW_NUMBER>, field 
<FIELD_NAME>: transform value [<FIELD_VALUE>] does not correspond to data set 
value [<GOLDEN_DATASET_VALUE>]`
 
+Number and BigNumber fields are compared by converting both values to a string 
with a format mask derived from the field length and precision.
+For example, length 7 and precision 2 uses `00000.00;-0000.00`.
+Values that round to the same string at that precision are considered equal 
(so `sqrt(2)` matches golden `1.4142` when precision is 4).
+If length or precision is not set, values are compared with a tolerance of 
`0.000001`, the same as previous Hop versions.
+
 === Remove and bypass transforms in unit tests
 
 While developing pipelines, you'll often remove or disable transforms in a 
pipeline.
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dummy.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dummy.adoc
index 91c250450a..e36fe164f8 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dummy.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dummy.adoc
@@ -29,6 +29,9 @@ The Dummy transform passes data without making any 
modifications.
 
 It functions as a placeholder for testing purposes or a way to combine 
multiple streams with the same field layout.
 
+Pipeline unit tests also replace a transform that has a golden data set 
attached with a Dummy, so the test does not write to a file or a database.
+See xref:pipeline/pipeline-unit-testing.adoc[Pipeline Unit Testing].
+
 |
 == Supported Engines
 [%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/injector.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/injector.adoc
index 442a32411e..07a49ea104 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/injector.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/injector.adoc
@@ -29,6 +29,9 @@ The Injector transform is used for special purpose pipelines 
where you  want to
 
 Among other things you can build 'headless' pipelines with it: pipelines that 
have no input at design time: do not read from file or database.
 
+Pipeline unit tests replace a transform that has an input data set attached 
with an Injector, and inject the static data set rows instead of executing the 
original transform.
+See xref:pipeline/pipeline-unit-testing.adoc[Pipeline Unit Testing].
+
 |
 == Supported Engines
 [%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
diff --git a/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java 
b/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
index 70a57b1902..4387e9f908 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
@@ -68,6 +68,7 @@ import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.util.StringUtil;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.core.xml.IXml;
 import org.apache.hop.core.xml.XmlFormatter;
@@ -3372,6 +3373,23 @@ public class PipelineMeta extends AbstractMeta
     for (ITransformMetaChangeListener listener : transformChangeListeners) {
       listener.onTransformChange(this, oldMeta, newMeta);
     }
+    if (oldMeta == null || newMeta == null) {
+      return;
+    }
+    String oldName = oldMeta.getName();
+    String newName = newMeta.getName();
+    if (oldName == null || oldName.equals(newName)) {
+      return;
+    }
+    try {
+      ExtensionPointHandler.callExtensionPoint(
+          LogChannel.GENERAL,
+          Variables.getADefaultVariableSpace(),
+          HopExtensionPoint.PipelineTransformRenamed.id,
+          new TransformNameChange(this, oldName, newName));
+    } catch (HopException e) {
+      LogChannel.GENERAL.logError("Error calling extension point 
PipelineTransformRenamed", e);
+    }
   }
 
   public boolean containsTransformMeta(TransformMeta transformMeta) {
diff --git 
a/engine/src/main/java/org/apache/hop/pipeline/TransformNameChange.java 
b/engine/src/main/java/org/apache/hop/pipeline/TransformNameChange.java
new file mode 100644
index 0000000000..5dd31dd765
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/pipeline/TransformNameChange.java
@@ -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.
+ */
+
+package org.apache.hop.pipeline;
+
+import lombok.Getter;
+
+/**
+ * Payload for {@link 
org.apache.hop.core.extension.HopExtensionPoint#PipelineTransformRenamed}.
+ * Fired when a transform dialog commits a new name, before the live {@link
+ * org.apache.hop.pipeline.transform.TransformMeta} is updated.
+ */
+@Getter
+public class TransformNameChange {
+  private final PipelineMeta pipelineMeta;
+  private final String oldName;
+  private final String newName;
+
+  public TransformNameChange(PipelineMeta pipelineMeta, String oldName, String 
newName) {
+    this.pipelineMeta = pipelineMeta;
+    this.oldName = oldName;
+    this.newName = newName;
+  }
+}
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 d518f96817..91ed6c8526 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
@@ -196,6 +196,39 @@ public class PipelineUnitTest extends HopMetadataBase 
implements Cloneable, IHop
     return null;
   }
 
+  /**
+   * Point input/golden data set locations and tweaks at {@code newName} when 
the transform they
+   * were attached to is renamed. Names are matched case-insensitively, same 
as {@link
+   * #findInputLocation(String)}.
+   *
+   * @return true when at least one location or tweak was updated
+   */
+  public boolean renameTransform(String oldName, String newName) {
+    if (Utils.isEmpty(oldName) || Utils.isEmpty(newName) || 
oldName.equals(newName)) {
+      return false;
+    }
+    boolean changed = false;
+    for (PipelineUnitTestSetLocation location : inputDataSets) {
+      if (oldName.equalsIgnoreCase(location.getTransformName())) {
+        location.setTransformName(newName);
+        changed = true;
+      }
+    }
+    for (PipelineUnitTestSetLocation location : goldenDataSets) {
+      if (oldName.equalsIgnoreCase(location.getTransformName())) {
+        location.setTransformName(newName);
+        changed = true;
+      }
+    }
+    for (PipelineUnitTestTweak tweak : tweaks) {
+      if (tweak.getTransformName() != null && 
oldName.equalsIgnoreCase(tweak.getTransformName())) {
+        tweak.setTransformName(newName);
+        changed = true;
+      }
+    }
+    return changed;
+  }
+
   /**
    * Remove all input and golden data sets on the transform with the provided 
name
    *
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/gui/TestingGuiPlugin.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/gui/TestingGuiPlugin.java
index c8d8a3b534..2ee7296b6a 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/gui/TestingGuiPlugin.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/gui/TestingGuiPlugin.java
@@ -65,13 +65,16 @@ import 
org.apache.hop.testing.actions.runtests.RunPipelineTests;
 import org.apache.hop.testing.actions.runtests.RunPipelineTestsField;
 import org.apache.hop.testing.util.DataSetConst;
 import org.apache.hop.testing.util.UnitTestGraphVariables;
+import org.apache.hop.testing.util.UnitTestTransformRenames;
 import org.apache.hop.testing.xp.PipelineMetaModifier;
 import org.apache.hop.testing.xp.WriteToDataSetExtensionPoint;
+import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.dialog.EnterMappingDialog;
 import org.apache.hop.ui.core.dialog.EnterSelectionDialog;
 import org.apache.hop.ui.core.dialog.EnterStringDialog;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.core.dialog.MessageDialogWithToggle;
 import org.apache.hop.ui.core.dialog.SelectRowDialog;
 import org.apache.hop.ui.core.metadata.MetadataManager;
 import org.apache.hop.ui.core.widget.ColumnInfo;
@@ -133,6 +136,17 @@ public class TestingGuiPlugin {
   public static final String 
ACTION_ID_PIPELINE_GRAPH_COPY_TEST_ACTION_CLIPBOARD =
       "pipeline-graph-transform-10400-copy-pipeline-action";
 
+  /**
+   * GUI custom parameter: show the golden data set Dummy-replacement warning. 
Default {@code Y}.
+   */
+  public static final String STRING_GOLDEN_DATASET_WARNING_PARAMETER =
+      "UnitTestGoldenDataSetWarning";
+
+  /**
+   * GUI custom parameter: show the input data set Injector-replacement 
warning. Default {@code Y}.
+   */
+  public static final String STRING_INPUT_DATASET_WARNING_PARAMETER = 
"UnitTestInputDataSetWarning";
+
   private static TestingGuiPlugin instance = null;
 
   private static final ILogChannel log = LogChannel.GENERAL;
@@ -236,6 +250,12 @@ public class TestingGuiPlugin {
       throws HopException {
     HopGui hopGui = HopGui.getInstance();
 
+    showDataSetReplacementWarning(
+        hopGui.getShell(),
+        STRING_INPUT_DATASET_WARNING_PARAMETER,
+        "TestingGuiPlugin.InputDataSetReplacement.Title",
+        "TestingGuiPlugin.InputDataSetReplacement.Message");
+
     // Now we need to map the fields from the input data set to the 
transform...
     //
     IRowMeta setFields = dataSet.getSetRowMeta();
@@ -364,6 +384,45 @@ public class TestingGuiPlugin {
     }
   }
 
+  /**
+   * Inform the user that attaching a data set replaces the transform at test 
execution time. The
+   * "don't show this again" choice is stored as a GUI custom parameter.
+   */
+  private void showDataSetReplacementWarning(
+      Shell shell, String parameterName, String titleKey, String messageKey) {
+    PropsUi props = HopGui.getInstance().getProps();
+    if (!shouldShowReplacementWarning(props.getCustomParameter(parameterName, 
"Y"))) {
+      return;
+    }
+    MessageDialogWithToggle md =
+        new MessageDialogWithToggle(
+            shell,
+            BaseMessages.getString(PKG, titleKey),
+            BaseMessages.getString(PKG, messageKey, Const.CR) + Const.CR,
+            SWT.ICON_WARNING,
+            new String[] {BaseMessages.getString(PKG, 
"TestingGuiPlugin.DataSetReplacement.Close")},
+            BaseMessages.getString(PKG, 
"TestingGuiPlugin.DataSetReplacement.DontShowAgain"),
+            "N".equalsIgnoreCase(props.getCustomParameter(parameterName, 
"Y")));
+    md.open();
+    props.setCustomParameter(parameterName, 
replacementWarningStoredValue(md.getToggleState()));
+  }
+
+  /**
+   * @param storedValue GUI custom parameter value, {@code Y} (default) to 
show the warning
+   * @return true when the replacement warning dialog should be shown
+   */
+  static boolean shouldShowReplacementWarning(String storedValue) {
+    return "Y".equalsIgnoreCase(Const.NVL(storedValue, "Y"));
+  }
+
+  /**
+   * @param dontShowAgain true when the user checked "Don't show this message 
again"
+   * @return {@code N} to suppress the warning, {@code Y} to keep showing it
+   */
+  static String replacementWarningStoredValue(boolean dontShowAgain) {
+    return dontShowAgain ? "N" : "Y";
+  }
+
   private boolean checkTestPresent(HopGui hopGui, 
HopGuiPipelineTransformContext context) {
     // Get the unit test directly from the pipeline graph context (works in 
web/RAP mode)
     PipelineUnitTest activeTest = getUnitTestFromContext(context);
@@ -454,6 +513,13 @@ public class TestingGuiPlugin {
       PipelineUnitTest unitTest,
       DataSet dataSet)
       throws HopException {
+    HopGui hopGui = HopGui.getInstance();
+    showDataSetReplacementWarning(
+        hopGui.getShell(),
+        STRING_GOLDEN_DATASET_WARNING_PARAMETER,
+        "TestingGuiPlugin.GoldenDataSetReplacement.Title",
+        "TestingGuiPlugin.GoldenDataSetReplacement.Message");
+
     // Now we need to map the fields from the transform to golden data set 
fields...
     //
     IRowMeta transformFields;
@@ -471,7 +537,7 @@ public class TestingGuiPlugin {
     String[] setFieldNames = setFields.getFieldNames();
 
     EnterMappingDialog mappingDialog =
-        new EnterMappingDialog(HopGui.getInstance().getShell(), 
transformFieldNames, setFieldNames);
+        new EnterMappingDialog(hopGui.getShell(), transformFieldNames, 
setFieldNames);
     List<SourceToTargetMapping> mappings = mappingDialog.open();
     if (mappings == null) {
       return false;
@@ -490,7 +556,7 @@ public class TestingGuiPlugin {
     }
     EditRowsDialog orderDialog =
         new EditRowsDialog(
-            HopGui.getInstance().getShell(),
+            hopGui.getShell(),
             SWT.NONE,
             BaseMessages.getString(PKG, "TestingGuiPlugin.SortOrder.Title"),
             BaseMessages.getString(PKG, "TestingGuiPlugin.SortOrder.Message"),
@@ -888,6 +954,11 @@ public class TestingGuiPlugin {
       // Clear unit-test sample variables from the graph variable space, then 
drop state.
       //
       Map<String, Object> stateMap = getStateMap(pipelineMeta);
+      if (stateMap != null) {
+        PipelineUnitTest unitTest =
+            (PipelineUnitTest) 
stateMap.get(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+        UnitTestTransformRenames.revertAll(unitTest, stateMap);
+      }
       UnitTestGraphVariables.clear(pipelineGraph.getVariables(), stateMap);
       if (stateMap != null) {
         stateMap.clear();
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 05ec260798..085104d1f6 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
@@ -18,9 +18,13 @@
 package org.apache.hop.testing.util;
 
 import com.google.common.math.DoubleMath;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.core.Const;
@@ -80,6 +84,26 @@ public class DataSetConst {
    */
   public static final String STATE_KEY_APPLIED_UNIT_TEST_VARIABLES = 
"AppliedUnitTestVariables";
 
+  /**
+   * In-memory transform renames that have not been saved to the unit test 
metadata yet. Value type:
+   * {@code List<UnitTestTransformRenames.Rename>}.
+   */
+  public static final String STATE_KEY_PENDING_TRANSFORM_RENAMES =
+      "UnitTestPendingTransformRenames";
+
+  /**
+   * Fallback mask when a Number/BigNumber field has no length/precision. 
Optional digits so 1 and
+   * 1.0 compare equal, with enough fraction digits to keep values 
distinguishable.
+   */
+  public static final String NUMERIC_COMPARE_MASK_DEFAULT =
+      "##########.#########;-###########.########";
+
+  /**
+   * Historical tolerance used when a Number/BigNumber field has no declared 
length/precision.
+   * Matches the previous {@code DoubleMath.fuzzyEquals} epsilon of 1 
millionth.
+   */
+  public static final double NUMERIC_COMPARE_EPSILON = 0.000001d;
+
   private static final String[] tweakDesc =
       new String[] {
         BaseMessages.getString(PKG, "DataSetConst.Tweak.NONE.Desc"),
@@ -381,18 +405,12 @@ public class DataSetConst {
               int cmp =
                   transformValueMeta.compare(transformValue, goldenValueMeta, 
goldenValueConverted);
               if (cmp != 0
-                  && transformValueMeta.isNumber()
+                  && (transformValueMeta.isNumber() || 
transformValueMeta.isBigNumber())
                   && !transformValueMeta.isNull(transformValue)
-                  && !transformValueMeta.isNull(goldenValueConverted)) {
-
-                // See if it's a floating point issue...
-                // Convert to an epsilon of 1 millionth.
-                Double d1 = transformValueMeta.getNumber(transformValue);
-                Double d2 = transformValueMeta.getNumber(goldenValueConverted);
-
-                if (DoubleMath.fuzzyEquals(d1, d2, 0.000001d)) {
-                  cmp = 0;
-                }
+                  && !transformValueMeta.isNull(goldenValueConverted)
+                  && numericValuesEqualForUnitTest(
+                      transformValueMeta, transformValue, goldenValueMeta, 
goldenValueConverted)) {
+                cmp = 0;
               }
               if (cmp != 0) {
                 if (log.isDebug()) {
@@ -459,6 +477,96 @@ public class DataSetConst {
     return nrErrors;
   }
 
+  /**
+   * Build a DecimalFormat pattern from field length and precision so two 
floating-point values can
+   * be compared as strings with no leftover binary precision.
+   *
+   * <p>For length 7 and precision 2 this is {@code 00000.00;-0000.00}: {@code 
length - precision}
+   * integer digits on the positive side, one fewer on the negative side to 
make room for the minus
+   * sign. When length is missing or precision is negative, {@link 
#NUMERIC_COMPARE_MASK_DEFAULT} is
+   * used.
+   *
+   * @param length total digit count (integer + fraction), or &lt; 1 when 
unspecified
+   * @param precision number of fraction digits, or &lt; 0 when unspecified
+   * @return a positive;negative DecimalFormat pattern
+   */
+  public static String buildNumericCompareMask(int length, int precision) {
+    if (length < 1 || precision < 0) {
+      return NUMERIC_COMPARE_MASK_DEFAULT;
+    }
+    int integerDigits = length - precision;
+    if (integerDigits < 1) {
+      integerDigits = 1;
+    }
+    StringBuilder positive = new StringBuilder(integerDigits + precision + 1);
+    positive.append("0".repeat(integerDigits));
+    if (precision > 0) {
+      positive.append('.').append("0".repeat(precision));
+    }
+    int negativeIntegerDigits = Math.max(integerDigits - 1, 1);
+    StringBuilder negative = new StringBuilder(negativeIntegerDigits + 
precision + 2);
+    negative.append('-').append("0".repeat(negativeIntegerDigits));
+    if (precision > 0) {
+      negative.append('.').append("0".repeat(precision));
+    }
+    return positive.append(';').append(negative).toString();
+  }
+
+  /**
+   * Create a locale-independent formatter for {@link 
#buildNumericCompareMask(int, int)} patterns.
+   * Always uses {@code '.'} as decimal separator and half-up rounding.
+   */
+  public static DecimalFormat createNumericCompareFormat(String mask) {
+    DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.US);
+    symbols.setDecimalSeparator('.');
+    DecimalFormat format = new DecimalFormat(mask, symbols);
+    format.setGroupingUsed(false);
+    format.setRoundingMode(RoundingMode.HALF_UP);
+    return format;
+  }
+
+  /**
+   * Compare two numeric values for a unit test. When length and precision are 
declared, both sides
+   * are formatted with {@link #buildNumericCompareMask(int, int)}. Otherwise 
the historical 1e-6
+   * fuzzy equals is used so existing tests without field precision keep 
passing.
+   */
+  static boolean numericValuesEqualForUnitTest(
+      IValueMeta transformMeta, Object transformValue, IValueMeta goldenMeta, 
Object goldenValue)
+      throws HopValueException {
+    IValueMeta spec = goldenMeta.getLength() > 0 ? goldenMeta : transformMeta;
+    if (hasDeclaredNumericPrecision(spec)) {
+      DecimalFormat format =
+          createNumericCompareFormat(
+              buildNumericCompareMask(spec.getLength(), spec.getPrecision()));
+      return formattedNumericValuesEqual(format, transformMeta, 
transformValue, goldenValue);
+    }
+    Double d1 = transformMeta.getNumber(transformValue);
+    Double d2 = transformMeta.getNumber(goldenValue);
+    return DoubleMath.fuzzyEquals(d1, d2, NUMERIC_COMPARE_EPSILON);
+  }
+
+  static boolean hasDeclaredNumericPrecision(IValueMeta meta) {
+    return meta != null && meta.getLength() > 0 && meta.getPrecision() >= 0;
+  }
+
+  /**
+   * @return true when both non-null numeric values format to the same string 
with {@code format}
+   */
+  static boolean formattedNumericValuesEqual(
+      DecimalFormat format, IValueMeta valueMeta, Object left, Object right)
+      throws HopValueException {
+    return format
+        .format(toComparableNumber(valueMeta, left))
+        .equals(format.format(toComparableNumber(valueMeta, right)));
+  }
+
+  static Number toComparableNumber(IValueMeta valueMeta, Object value) throws 
HopValueException {
+    if (valueMeta.isBigNumber()) {
+      return valueMeta.getBigNumber(value);
+    }
+    return valueMeta.getNumber(value);
+  }
+
   public static String getDirectoryFromPath(String path) {
     int lastSlashIndex = path.lastIndexOf('/');
     if (lastSlashIndex >= 0) {
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/UnitTestTransformRenames.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/UnitTestTransformRenames.java
new file mode 100644
index 0000000000..f5457e0af3
--- /dev/null
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/UnitTestTransformRenames.java
@@ -0,0 +1,147 @@
+/*
+ * 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.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.testing.PipelineUnitTest;
+
+/**
+ * Tracks in-memory unit-test transform renames that have not been saved yet, 
so Ctrl+Z can restore
+ * the previous location names and a pipeline save can persist them.
+ */
+public final class UnitTestTransformRenames {
+
+  private UnitTestTransformRenames() {
+    // utility
+  }
+
+  public static final class Rename {
+    private final String oldName;
+    private final String newName;
+
+    public Rename(String oldName, String newName) {
+      this.oldName = oldName;
+      this.newName = newName;
+    }
+
+    public String getOldName() {
+      return oldName;
+    }
+
+    public String getNewName() {
+      return newName;
+    }
+  }
+
+  public static List<Rename> pending(Map<String, Object> stateMap) {
+    List<Rename> pending = mutablePending(stateMap);
+    return pending != null ? pending : Collections.emptyList();
+  }
+
+  public static void record(Map<String, Object> stateMap, String oldName, 
String newName) {
+    if (stateMap == null) {
+      return;
+    }
+    List<Rename> list = mutablePending(stateMap);
+    if (list == null) {
+      list = new ArrayList<>();
+      stateMap.put(DataSetConst.STATE_KEY_PENDING_TRANSFORM_RENAMES, list);
+    }
+    list.add(new Rename(oldName, newName));
+  }
+
+  public static void clear(Map<String, Object> stateMap) {
+    if (stateMap != null) {
+      stateMap.remove(DataSetConst.STATE_KEY_PENDING_TRANSFORM_RENAMES);
+    }
+  }
+
+  public static boolean hasPending(Map<String, Object> stateMap) {
+    List<Rename> pending = mutablePending(stateMap);
+    return pending != null && !pending.isEmpty();
+  }
+
+  @SuppressWarnings("unchecked")
+  private static List<Rename> mutablePending(Map<String, Object> stateMap) {
+    if (stateMap == null) {
+      return null;
+    }
+    Object value = 
stateMap.get(DataSetConst.STATE_KEY_PENDING_TRANSFORM_RENAMES);
+    if (value instanceof List<?> list) {
+      return (List<Rename>) list;
+    }
+    return null;
+  }
+
+  /**
+   * If undo restored the old transform name, roll the unit test locations 
back. Walks newest-first
+   * so chained renames unwind in order.
+   *
+   * @return true when at least one location was reverted
+   */
+  public static boolean revertIfUndoRestoredOldNames(
+      PipelineMeta pipelineMeta, PipelineUnitTest unitTest, Map<String, 
Object> stateMap) {
+    if (pipelineMeta == null || unitTest == null || stateMap == null) {
+      return false;
+    }
+    List<Rename> pending = pending(stateMap);
+    if (pending.isEmpty()) {
+      return false;
+    }
+    boolean changed = false;
+    for (int i = pending.size() - 1; i >= 0; i--) {
+      Rename rename = pending.get(i);
+      if (pipelineMeta.findTransform(rename.getNewName()) == null
+          && pipelineMeta.findTransform(rename.getOldName()) != null) {
+        if (unitTest.renameTransform(rename.getNewName(), 
rename.getOldName())) {
+          changed = true;
+        }
+        pending.remove(i);
+      }
+    }
+    return changed;
+  }
+
+  /**
+   * Undo every pending rename on the unit test so a cached metadata object 
matches what is on disk.
+   *
+   * @return true when at least one location was reverted
+   */
+  public static boolean revertAll(PipelineUnitTest unitTest, Map<String, 
Object> stateMap) {
+    if (unitTest == null || stateMap == null) {
+      return false;
+    }
+    List<Rename> pending = pending(stateMap);
+    if (pending.isEmpty()) {
+      return false;
+    }
+    boolean changed = false;
+    for (int i = pending.size() - 1; i >= 0; i--) {
+      Rename rename = pending.get(i);
+      if (unitTest.renameTransform(rename.getNewName(), rename.getOldName())) {
+        changed = true;
+      }
+    }
+    pending.clear();
+    return changed;
+  }
+}
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
index bfa827e2e9..fa0ac2e0f9 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
@@ -24,9 +24,11 @@ import org.apache.hop.core.extension.IExtensionPoint;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.testing.PipelineUnitTest;
 import org.apache.hop.testing.gui.TestingGuiPlugin;
 import org.apache.hop.testing.util.DataSetConst;
 import org.apache.hop.testing.util.UnitTestGraphVariables;
+import org.apache.hop.testing.util.UnitTestTransformRenames;
 import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
 
 @ExtensionPoint(
@@ -49,6 +51,9 @@ public class HopGuiPipelineAfterClose implements 
IExtensionPoint<PipelineMeta> {
       UnitTestGraphVariables.clear(variables, stateMap);
     }
     if (stateMap != null) {
+      PipelineUnitTest unitTest =
+          (PipelineUnitTest) 
stateMap.get(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+      UnitTestTransformRenames.revertAll(unitTest, stateMap);
       stateMap.remove(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
     }
   }
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/PersistUnitTestRenamesExtensionPoint.java
similarity index 54%
copy from 
plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
copy to 
plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/PersistUnitTestRenamesExtensionPoint.java
index bfa827e2e9..fb0df3eed1 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/PersistUnitTestRenamesExtensionPoint.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -24,32 +24,53 @@ import org.apache.hop.core.extension.IExtensionPoint;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.testing.PipelineUnitTest;
 import org.apache.hop.testing.gui.TestingGuiPlugin;
 import org.apache.hop.testing.util.DataSetConst;
-import org.apache.hop.testing.util.UnitTestGraphVariables;
+import org.apache.hop.testing.util.UnitTestTransformRenames;
+import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
 
 @ExtensionPoint(
-    extensionPointId = "HopGuiPipelineAfterClose",
-    id = "HopGuiPipelineAfterClose",
-    description = "Cleanup the active unit test for the closed pipeline")
-public class HopGuiPipelineAfterClose implements IExtensionPoint<PipelineMeta> 
{
+    id = "PersistUnitTestRenamesExtensionPoint",
+    extensionPointId = "PipelineAfterSave",
+    description = "Persist in-memory unit test transform-name updates when the 
pipeline is saved")
+public class PersistUnitTestRenamesExtensionPoint implements 
IExtensionPoint<PipelineMeta> {
 
   @Override
   public void callExtensionPoint(ILogChannel log, IVariables variables, 
PipelineMeta pipelineMeta)
       throws HopException {
+    if (pipelineMeta == null) {
+      return;
+    }
     HopGuiPipelineGraph pipelineGraph = 
TestingGuiPlugin.getPipelineGraph(pipelineMeta);
     Map<String, Object> stateMap =
         pipelineGraph != null
             ? pipelineGraph.getStateMap()
             : TestingGuiPlugin.getStateMap(pipelineMeta);
-    if (pipelineGraph != null) {
-      UnitTestGraphVariables.clear(pipelineGraph.getVariables(), stateMap);
-    } else {
-      UnitTestGraphVariables.clear(variables, stateMap);
+    if (!UnitTestTransformRenames.hasPending(stateMap)) {
+      return;
+    }
+    PipelineUnitTest unitTest =
+        (PipelineUnitTest) 
stateMap.get(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+    if (unitTest == null) {
+      return;
     }
-    if (stateMap != null) {
-      stateMap.remove(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+    try {
+      HopGui hopGui = HopGui.getInstance();
+      if (hopGui == null || hopGui.getMetadataProvider() == null) {
+        return;
+      }
+      IVariables graphVariables = pipelineGraph != null ? 
pipelineGraph.getVariables() : variables;
+      unitTest.setRelativeFilename(graphVariables, pipelineMeta.getFilename());
+      
hopGui.getMetadataProvider().getSerializer(PipelineUnitTest.class).save(unitTest);
+      UnitTestTransformRenames.clear(stateMap);
+    } catch (Exception e) {
+      log.logError(
+          "Error saving unit test '"
+              + unitTest.getName()
+              + "' after pipeline save with renamed transforms",
+          e);
     }
   }
 }
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/RenameUnitTestLocationsExtensionPoint.java
similarity index 51%
copy from 
plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
copy to 
plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/RenameUnitTestLocationsExtensionPoint.java
index bfa827e2e9..4adf48cd0a 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiPipelineAfterClose.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/RenameUnitTestLocationsExtensionPoint.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -23,33 +23,45 @@ import org.apache.hop.core.extension.ExtensionPoint;
 import org.apache.hop.core.extension.IExtensionPoint;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.variables.IVariables;
-import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.TransformNameChange;
+import org.apache.hop.testing.PipelineUnitTest;
 import org.apache.hop.testing.gui.TestingGuiPlugin;
 import org.apache.hop.testing.util.DataSetConst;
-import org.apache.hop.testing.util.UnitTestGraphVariables;
+import org.apache.hop.testing.util.UnitTestTransformRenames;
 import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
 
 @ExtensionPoint(
-    extensionPointId = "HopGuiPipelineAfterClose",
-    id = "HopGuiPipelineAfterClose",
-    description = "Cleanup the active unit test for the closed pipeline")
-public class HopGuiPipelineAfterClose implements IExtensionPoint<PipelineMeta> 
{
+    id = "RenameUnitTestLocationsExtensionPoint",
+    extensionPointId = "PipelineTransformRenamed",
+    description =
+        "Keep unit test input/golden data set locations and tweaks in sync 
when a transform is renamed")
+public class RenameUnitTestLocationsExtensionPoint implements 
IExtensionPoint<TransformNameChange> {
 
   @Override
-  public void callExtensionPoint(ILogChannel log, IVariables variables, 
PipelineMeta pipelineMeta)
+  public void callExtensionPoint(ILogChannel log, IVariables variables, 
TransformNameChange change)
       throws HopException {
-    HopGuiPipelineGraph pipelineGraph = 
TestingGuiPlugin.getPipelineGraph(pipelineMeta);
-    Map<String, Object> stateMap =
-        pipelineGraph != null
-            ? pipelineGraph.getStateMap()
-            : TestingGuiPlugin.getStateMap(pipelineMeta);
-    if (pipelineGraph != null) {
-      UnitTestGraphVariables.clear(pipelineGraph.getVariables(), stateMap);
-    } else {
-      UnitTestGraphVariables.clear(variables, stateMap);
+    if (change == null || change.getPipelineMeta() == null) {
+      return;
     }
-    if (stateMap != null) {
-      stateMap.remove(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+
+    HopGuiPipelineGraph pipelineGraph = 
TestingGuiPlugin.getPipelineGraph(change.getPipelineMeta());
+    if (pipelineGraph == null) {
+      return;
+    }
+    Map<String, Object> stateMap = pipelineGraph.getStateMap();
+    if (stateMap == null) {
+      return;
+    }
+    PipelineUnitTest unitTest =
+        (PipelineUnitTest) 
stateMap.get(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+    if (unitTest == null) {
+      return;
+    }
+    if (!unitTest.renameTransform(change.getOldName(), change.getNewName())) {
+      return;
     }
+    // Keep the rename in memory until the pipeline is saved, so Ctrl+Z can 
restore the old
+    // transform name without orphaning the data set attachment on disk.
+    UnitTestTransformRenames.record(stateMap, change.getOldName(), 
change.getNewName());
   }
 }
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/UpdateUnitTestButtonsExtensionPoint.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/UpdateUnitTestButtonsExtensionPoint.java
index e3a5f63bbb..a32eb0d89e 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/UpdateUnitTestButtonsExtensionPoint.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/UpdateUnitTestButtonsExtensionPoint.java
@@ -22,7 +22,10 @@ import org.apache.hop.core.extension.ExtensionPoint;
 import org.apache.hop.core.extension.IExtensionPoint;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.testing.PipelineUnitTest;
 import org.apache.hop.testing.gui.TestingGuiPlugin;
+import org.apache.hop.testing.util.DataSetConst;
+import org.apache.hop.testing.util.UnitTestTransformRenames;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
 import org.eclipse.swt.widgets.Display;
@@ -43,6 +46,14 @@ public class UpdateUnitTestButtonsExtensionPoint implements 
IExtensionPoint<HopG
       ILogChannel log, IVariables variables, HopGuiPipelineGraph pipelineGraph)
       throws HopException {
 
+    if (pipelineGraph != null && pipelineGraph.getStateMap() != null) {
+      PipelineUnitTest unitTest =
+          (PipelineUnitTest)
+              
pipelineGraph.getStateMap().get(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+      UnitTestTransformRenames.revertIfUndoRestoredOldNames(
+          pipelineGraph.getPipelineMeta(), unitTest, 
pipelineGraph.getStateMap());
+    }
+
     // Update the unit test button states
     // Use asyncExec to ensure this runs after any async combo population
     Display display = HopGui.getInstance().getDisplay();
diff --git 
a/plugins/misc/testing/src/main/resources/org/apache/hop/testing/gui/messages/messages_en_US.properties
 
b/plugins/misc/testing/src/main/resources/org/apache/hop/testing/gui/messages/messages_en_US.properties
index 7bb979dd90..cbfe91395b 100644
--- 
a/plugins/misc/testing/src/main/resources/org/apache/hop/testing/gui/messages/messages_en_US.properties
+++ 
b/plugins/misc/testing/src/main/resources/org/apache/hop/testing/gui/messages/messages_en_US.properties
@@ -28,7 +28,7 @@ 
TestingGuiPlugin.ContextAction.CheckTestPresent.Message=Please create a test-cas
 TestingGuiPlugin.ContextAction.ClearGoldenDataset.Error.Header=Error
 TestingGuiPlugin.ContextAction.ClearGoldenDataset.Error.Message=Error saving 
unit test
 TestingGuiPlugin.ContextAction.ClearGoldenDataset.Name=Clear golden data set
-TestingGuiPlugin.ContextAction.ClearGoldenDataset.Tooltip=Remove a defined 
input data set from this transform unit test
+TestingGuiPlugin.ContextAction.ClearGoldenDataset.Tooltip=Remove a defined 
golden data set from this transform unit test
 TestingGuiPlugin.ContextAction.ClearInputDataset.Error.Header=Error
 TestingGuiPlugin.ContextAction.ClearInputDataset.Error.Message=Error saving 
unit test
 TestingGuiPlugin.ContextAction.ClearInputDataset.Name=Clear input data set
@@ -61,13 +61,19 @@ 
TestingGuiPlugin.ContextAction.SetGoldenDataset.Error.Message=Error retrieving t
 TestingGuiPlugin.ContextAction.SetGoldenDataset.Header=Select the golden data 
set
 TestingGuiPlugin.ContextAction.SetGoldenDataset.Message=Select the golden data 
set...
 TestingGuiPlugin.ContextAction.SetGoldenDataset.Name=Set golden data set
-TestingGuiPlugin.ContextAction.SetGoldenDataset.Tooltip=The input to this 
transform is taken and compared to the golden data set you are selecting. The 
transform itself is not executed during testing.
+TestingGuiPlugin.ContextAction.SetGoldenDataset.Tooltip=Compare the rows 
flowing into this transform with the selected golden data set. During the test 
the transform is replaced with a Dummy so it is not executed (for example it 
will not write to a file or a database).
 TestingGuiPlugin.ContextAction.SetInputDataset.Error.Header=Error
 TestingGuiPlugin.ContextAction.SetInputDataset.Error.Message=Error retrieving 
the list of data set groups
 TestingGuiPlugin.ContextAction.SetInputDataset.Header=Select the data set
 TestingGuiPlugin.ContextAction.SetInputDataset.Message=Select the data set to 
edit...
 TestingGuiPlugin.ContextAction.SetInputDataset.Name=Set input data set
-TestingGuiPlugin.ContextAction.SetInputDataset.Tooltip=For the active unit 
test it defines which data to use instead of the output of the transform
+TestingGuiPlugin.ContextAction.SetInputDataset.Tooltip=For the active unit 
test, replace this transform with an Injector that injects the selected data 
set during test execution.
+TestingGuiPlugin.DataSetReplacement.Close=Close
+TestingGuiPlugin.DataSetReplacement.DontShowAgain=Don''t show this message 
again
+TestingGuiPlugin.GoldenDataSetReplacement.Title=Golden data set
+TestingGuiPlugin.GoldenDataSetReplacement.Message=When you attach a golden 
data set to a transform, that transform is replaced with a Dummy transform 
during unit test execution.{0}{0}The rows that would have been sent into the 
transform are captured and compared to the golden data set. The transform 
itself is not executed.{0}{0}This is typically done to prevent the unit test 
pipeline from writing to a file or a database.{0}Attach the golden data set to 
the output transform (for example T [...]
+TestingGuiPlugin.InputDataSetReplacement.Title=Input data set
+TestingGuiPlugin.InputDataSetReplacement.Message=When you attach an input data 
set to a transform, that transform is replaced with an Injector transform 
during unit test execution.{0}{0}The static content of the data set is injected 
as rows instead of running the original transform.{0}{0}This is typically done 
so the unit test does not read from a file, a database, or another external 
source.
 TestingGuiPlugin.DataSet.ADataSetWithNameExists.Message=A data set named 
''{0}'' already exists, please pick another one.
 TestingGuiPlugin.DataSet.NoNameSpecified.Message=A data set needs to have a 
name
 TestingGuiPlugin.DataSet.RenamingOfADataSetsNotSupported.Message=Renaming a 
data set is currently not supported
diff --git 
a/plugins/misc/testing/src/test/java/org/apache/hop/testing/PipelineUnitTestRenameTransformTest.java
 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/PipelineUnitTestRenameTransformTest.java
new file mode 100644
index 0000000000..b13c8b3f72
--- /dev/null
+++ 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/PipelineUnitTestRenameTransformTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class PipelineUnitTestRenameTransformTest {
+
+  @Test
+  void renamesInputAndGoldenLocationsAndTweaks() {
+    PipelineUnitTest unitTest = new PipelineUnitTest();
+    unitTest.getInputDataSets().add(location("reader"));
+    unitTest.getGoldenDataSets().add(location("writer"));
+    unitTest.getTweaks().add(new 
PipelineUnitTestTweak(PipelineTweak.BYPASS_TRANSFORM, "skip-me"));
+
+    assertTrue(unitTest.renameTransform("reader", "csv-input"));
+    assertTrue(unitTest.renameTransform("writer", "file-output"));
+    assertTrue(unitTest.renameTransform("skip-me", "bypass"));
+
+    assertNotNull(unitTest.findInputLocation("csv-input"));
+    assertNull(unitTest.findInputLocation("reader"));
+    assertNotNull(unitTest.findGoldenLocation("file-output"));
+    assertNull(unitTest.findGoldenLocation("writer"));
+    assertEquals("bypass", unitTest.findTweak("bypass").getTransformName());
+    assertNull(unitTest.findTweak("skip-me"));
+  }
+
+  @Test
+  void renameIsCaseInsensitiveOnTheOldName() {
+    PipelineUnitTest unitTest = new PipelineUnitTest();
+    unitTest.getInputDataSets().add(location("Read Customers"));
+
+    assertTrue(unitTest.renameTransform("read customers", "customers"));
+    assertEquals("customers", 
unitTest.findInputLocation("customers").getTransformName());
+  }
+
+  @Test
+  void noChangeWhenNamesAreEqualOrNothingIsAttached() {
+    PipelineUnitTest unitTest = new PipelineUnitTest();
+    unitTest.getGoldenDataSets().add(location("out"));
+
+    assertFalse(unitTest.renameTransform("out", "out"));
+    assertFalse(unitTest.renameTransform("missing", "other"));
+    assertFalse(unitTest.renameTransform(null, "other"));
+    assertFalse(unitTest.renameTransform("out", null));
+    assertEquals("out", unitTest.findGoldenLocation("out").getTransformName());
+  }
+
+  private static PipelineUnitTestSetLocation location(String transformName) {
+    PipelineUnitTestSetLocation location = new PipelineUnitTestSetLocation();
+    location.setTransformName(transformName);
+    location.setDataSetName(transformName + "-set");
+    return location;
+  }
+}
diff --git 
a/plugins/misc/testing/src/test/java/org/apache/hop/testing/gui/TestingGuiPluginReplacementWarningTest.java
 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/gui/TestingGuiPluginReplacementWarningTest.java
new file mode 100644
index 0000000000..aa3a2c626d
--- /dev/null
+++ 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/gui/TestingGuiPluginReplacementWarningTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.gui;
+
+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 org.junit.jupiter.api.Test;
+
+/**
+ * Preference helpers for the input/golden data set replacement warnings 
(issue #2664). Dialog
+ * display itself requires SWT and is not covered here.
+ */
+class TestingGuiPluginReplacementWarningTest {
+
+  @Test
+  void warningShownByDefaultWhenParameterMissing() {
+    assertTrue(TestingGuiPlugin.shouldShowReplacementWarning(null));
+    assertTrue(TestingGuiPlugin.shouldShowReplacementWarning(""));
+    assertTrue(TestingGuiPlugin.shouldShowReplacementWarning("Y"));
+    assertTrue(TestingGuiPlugin.shouldShowReplacementWarning("y"));
+  }
+
+  @Test
+  void warningSuppressedWhenUserChoseNotToShowAgain() {
+    assertFalse(TestingGuiPlugin.shouldShowReplacementWarning("N"));
+    assertFalse(TestingGuiPlugin.shouldShowReplacementWarning("n"));
+  }
+
+  @Test
+  void toggleStatePersistsAsCustomParameter() {
+    assertEquals("N", TestingGuiPlugin.replacementWarningStoredValue(true));
+    assertEquals("Y", TestingGuiPlugin.replacementWarningStoredValue(false));
+  }
+}
diff --git 
a/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/DataSetConstNumericCompareTest.java
 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/DataSetConstNumericCompareTest.java
new file mode 100644
index 0000000000..5f6ba94f34
--- /dev/null
+++ 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/DataSetConstNumericCompareTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.util;
+
+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.math.BigDecimal;
+import java.text.DecimalFormat;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaBigNumber;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class DataSetConstNumericCompareTest {
+
+  @BeforeAll
+  static void initHop() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void maskForLength7Precision2MatchesDocumentedPattern() {
+    assertEquals("00000.00;-0000.00", DataSetConst.buildNumericCompareMask(7, 
2));
+  }
+
+  @Test
+  void maskForLength7Precision4HasThreeIntegerDigits() {
+    assertEquals("000.0000;-00.0000", DataSetConst.buildNumericCompareMask(7, 
4));
+  }
+
+  @Test
+  void maskFallsBackWhenLengthOrPrecisionMissing() {
+    assertEquals(
+        DataSetConst.NUMERIC_COMPARE_MASK_DEFAULT, 
DataSetConst.buildNumericCompareMask(-1, -1));
+    assertEquals(
+        DataSetConst.NUMERIC_COMPARE_MASK_DEFAULT, 
DataSetConst.buildNumericCompareMask(0, 2));
+    assertEquals(
+        DataSetConst.NUMERIC_COMPARE_MASK_DEFAULT, 
DataSetConst.buildNumericCompareMask(7, -1));
+  }
+
+  @Test
+  void sqrtOfTwoMatchesGoldenAtDeclaredPrecision() throws HopValueException {
+    IValueMeta meta = new ValueMetaNumber("sqrt", 7, 4);
+    DecimalFormat format =
+        DataSetConst.createNumericCompareFormat(
+            DataSetConst.buildNumericCompareMask(meta.getLength(), 
meta.getPrecision()));
+
+    assertTrue(
+        DataSetConst.formattedNumericValuesEqual(format, meta, Math.sqrt(2), 
1.4142),
+        "sqrt(2) and 1.4142 must compare equal at length 7 precision 4");
+    assertEquals("001.4142", format.format(Math.sqrt(2)));
+    assertEquals("001.4142", format.format(1.4142));
+  }
+
+  @Test
+  void sqrtOfTwoDoesNotMatchGoldenWithoutPrecision() throws HopValueException {
+    IValueMeta meta = new ValueMetaNumber("sqrt");
+
+    assertFalse(
+        DataSetConst.numericValuesEqualForUnitTest(meta, Math.sqrt(2), meta, 
1.4142),
+        "without length/precision sqrt(2) is still farther than the 1e-6 fuzzy 
tolerance");
+  }
+
+  @Test
+  void withoutPrecisionKeepsHistoricalFuzzyTolerance() throws 
HopValueException {
+    IValueMeta meta = new ValueMetaNumber("n");
+
+    assertTrue(
+        DataSetConst.numericValuesEqualForUnitTest(meta, 1.0000001d, meta, 
1.0000002d),
+        "values within 1e-6 must still match when length/precision is not 
declared");
+    assertFalse(DataSetConst.numericValuesEqualForUnitTest(meta, 1.0d, meta, 
1.01d));
+  }
+
+  @Test
+  void valuesThatDifferAtDeclaredPrecisionAreNotEqual() throws 
HopValueException {
+    IValueMeta meta = new ValueMetaNumber("amount", 7, 2);
+    DecimalFormat format =
+        
DataSetConst.createNumericCompareFormat(DataSetConst.buildNumericCompareMask(7, 
2));
+
+    assertFalse(DataSetConst.formattedNumericValuesEqual(format, meta, 12.34, 
12.35));
+    assertTrue(DataSetConst.formattedNumericValuesEqual(format, meta, 12.344, 
12.336));
+  }
+
+  @Test
+  void bigNumberUsesSameMask() throws HopValueException {
+    IValueMeta meta = new ValueMetaBigNumber("sqrt", 7, 4);
+    DecimalFormat format =
+        
DataSetConst.createNumericCompareFormat(DataSetConst.buildNumericCompareMask(7, 
4));
+
+    assertTrue(
+        DataSetConst.formattedNumericValuesEqual(
+            format, meta, new BigDecimal("1.4142135623730951"), new 
BigDecimal("1.4142")));
+  }
+
+  @Test
+  void defaultMaskTreatsOneAndOnePointZeroAsEqual() throws HopValueException {
+    IValueMeta meta = new ValueMetaNumber("n");
+    DecimalFormat format =
+        
DataSetConst.createNumericCompareFormat(DataSetConst.NUMERIC_COMPARE_MASK_DEFAULT);
+
+    assertTrue(DataSetConst.formattedNumericValuesEqual(format, meta, 1.0d, 
1.00d));
+  }
+}
diff --git 
a/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/UnitTestTransformRenamesTest.java
 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/UnitTestTransformRenamesTest.java
new file mode 100644
index 0000000000..f79ea0aee7
--- /dev/null
+++ 
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/UnitTestTransformRenamesTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.util;
+
+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.util.HashMap;
+import java.util.Map;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.testing.PipelineUnitTest;
+import org.apache.hop.testing.PipelineUnitTestSetLocation;
+import org.junit.jupiter.api.Test;
+
+class UnitTestTransformRenamesTest {
+
+  @Test
+  void revertWhenUndoRestoredTheOldTransformName() {
+    PipelineMeta pipelineMeta = pipelineWithTransform("reader");
+    PipelineUnitTest unitTest = unitTestOn("csv-input");
+    Map<String, Object> stateMap = new HashMap<>();
+    UnitTestTransformRenames.record(stateMap, "reader", "csv-input");
+
+    assertTrue(
+        UnitTestTransformRenames.revertIfUndoRestoredOldNames(pipelineMeta, 
unitTest, stateMap));
+    assertEquals("reader", 
unitTest.findInputLocation("reader").getTransformName());
+    assertFalse(UnitTestTransformRenames.hasPending(stateMap));
+  }
+
+  @Test
+  void leaveRenameInPlaceWhenNewNameStillExists() {
+    PipelineMeta pipelineMeta = pipelineWithTransform("csv-input");
+    PipelineUnitTest unitTest = unitTestOn("csv-input");
+    Map<String, Object> stateMap = new HashMap<>();
+    UnitTestTransformRenames.record(stateMap, "reader", "csv-input");
+
+    assertFalse(
+        UnitTestTransformRenames.revertIfUndoRestoredOldNames(pipelineMeta, 
unitTest, stateMap));
+    assertEquals("csv-input", 
unitTest.findInputLocation("csv-input").getTransformName());
+    assertTrue(UnitTestTransformRenames.hasPending(stateMap));
+  }
+
+  @Test
+  void revertAllRestoresDiskStateOnClose() {
+    PipelineUnitTest unitTest = unitTestOn("csv-input");
+    Map<String, Object> stateMap = new HashMap<>();
+    UnitTestTransformRenames.record(stateMap, "reader", "csv-input");
+
+    assertTrue(UnitTestTransformRenames.revertAll(unitTest, stateMap));
+    assertEquals("reader", 
unitTest.findInputLocation("reader").getTransformName());
+    assertFalse(UnitTestTransformRenames.hasPending(stateMap));
+  }
+
+  @Test
+  void chainedRenamesUnwindNewestFirst() {
+    PipelineMeta pipelineMeta = pipelineWithTransform("b");
+    PipelineUnitTest unitTest = unitTestOn("c");
+    Map<String, Object> stateMap = new HashMap<>();
+    UnitTestTransformRenames.record(stateMap, "a", "b");
+    UnitTestTransformRenames.record(stateMap, "b", "c");
+
+    assertTrue(
+        UnitTestTransformRenames.revertIfUndoRestoredOldNames(pipelineMeta, 
unitTest, stateMap));
+    assertEquals("b", unitTest.findInputLocation("b").getTransformName());
+    assertEquals(1, UnitTestTransformRenames.pending(stateMap).size());
+    assertEquals("a", 
UnitTestTransformRenames.pending(stateMap).get(0).getOldName());
+  }
+
+  private static PipelineMeta pipelineWithTransform(String name) {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    TransformMeta transformMeta = new TransformMeta();
+    transformMeta.setName(name);
+    pipelineMeta.addTransform(transformMeta);
+    return pipelineMeta;
+  }
+
+  private static PipelineUnitTest unitTestOn(String transformName) {
+    PipelineUnitTest unitTest = new PipelineUnitTest();
+    PipelineUnitTestSetLocation location = new PipelineUnitTestSetLocation();
+    location.setTransformName(transformName);
+    location.setDataSetName("set");
+    unitTest.getInputDataSets().add(location);
+    return unitTest;
+  }
+}

Reply via email to