This is an automated email from the ASF dual-hosted git repository.
hansva 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 44c3e5ca1e issue #7719 : unit test variables at design-time and unit
test UX fixes (#7872)
44c3e5ca1e is described below
commit 44c3e5ca1e2c21b7679c4b1c6cedc1b87a70ac5c
Author: Matt Casters <[email protected]>
AuthorDate: Tue Aug 11 11:40:41 2026 +0200
issue #7719 : unit test variables at design-time and unit test UX fixes
(#7872)
* issue #7719 : apply unit test variables at design-time and harden unit
test UX
Make pipeline unit test Parameters & Variables available on the pipeline
graph
variable space when a unit test is selected, so Get Fields, Check, dialogs,
and
execution configuration use the same sample values as a unit-test run.
Also fix related friction found while testing:
- Creating metadata with a suggested default name no longer renames/deletes
an
existing object when the user changes the name (MetadataEditor.markAsNew)
- Unique default unit test names when the base name already exists
- Switching unit tests refreshes graph variables and avoids sticky
previous-run
values in the execution configuration
- Auto-open is exclusive per pipeline when saved with the flag enabled
* Fix HopVfsSingleManagerTest pollution from other core VFS tests
HopVfsTest.startsWithScheme(..., Variables) bootstraps static named-provider
variables and never cleared them. When noMetadataLookupBeforeVariablesArrive
ran later in the same JVM it incorrectly reported a metadata lookup.
Clear bootstrap variables and reset HopVfs in HopVfsTest tearDown and in
HopVfsSingleManagerTest BeforeEach/AfterEach so the suite is
order-independent.
* Initialize HopEnvironment in ExcelInputMetaTest for password decode
Serialization of Excel Input metadata decodes an encrypted password via
Encr.encoder. Initialize HopEnvironment (and restore the engine env) so
the test does not NPE when run without a prior test that set up Encr.
* Avoid HopGui/SWT init in uniqueUnitTestName unit tests
HopGuiUnitTestCreateBeforeDialog.uniqueUnitTestName() called
HopGui.getInstance(), which creates an SWT Display. That fails headless
on GitHub Actions with gtk_init_check() failed.
Pass an optional IHopMetadataProvider instead; unit tests use null and
only check base-name formatting without touching the GUI.
---
.../hop/core/vfs/HopVfsSingleManagerTest.java | 17 +++
.../java/org/apache/hop/core/vfs/HopVfsTest.java | 9 ++
.../pipeline/PipelineExecutionConfiguration.java | 18 ++-
.../apache/hop/testing/gui/TestingGuiPlugin.java | 63 ++++++++-
.../org/apache/hop/testing/util/DataSetConst.java | 6 +
.../hop/testing/util/UnitTestAutoOpening.java | 97 ++++++++++++++
.../hop/testing/util/UnitTestGraphVariables.java | 103 ++++++++++++++
.../hop/testing/xp/HopGuiPipelineAfterClose.java | 13 +-
.../hop/testing/xp/HopGuiUnitTestChanged.java | 42 +++++-
.../xp/HopGuiUnitTestCreateBeforeDialog.java | 46 ++++++-
.../hop/testing/xp/HopGuiUnitTestDeleted.java | 47 ++++++-
.../xp/HopGuiUnitTestVariablesExtensionPoint.java | 29 ++++
.../hop/testing/util/UnitTestAutoOpeningTest.java | 93 +++++++++++++
.../testing/util/UnitTestGraphVariablesTest.java | 148 +++++++++++++++++++++
.../xp/HopGuiUnitTestCreateBeforeDialogTest.java | 44 ++++++
.../transforms/excelinput/ExcelInputMetaTest.java | 13 ++
.../hop/ui/core/metadata/MetadataEditor.java | 22 ++-
.../hop/ui/core/metadata/MetadataManager.java | 17 +--
18 files changed, 795 insertions(+), 32 deletions(-)
diff --git
a/core/src/test/java/org/apache/hop/core/vfs/HopVfsSingleManagerTest.java
b/core/src/test/java/org/apache/hop/core/vfs/HopVfsSingleManagerTest.java
index dddeeef810..7baba49f80 100644
--- a/core/src/test/java/org/apache/hop/core/vfs/HopVfsSingleManagerTest.java
+++ b/core/src/test/java/org/apache/hop/core/vfs/HopVfsSingleManagerTest.java
@@ -43,6 +43,7 @@ import org.apache.hop.core.variables.Variables;
import org.apache.hop.core.vfs.plugin.IVfs;
import org.apache.hop.core.vfs.plugin.VfsPluginType;
import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -61,6 +62,18 @@ class HopVfsSingleManagerTest {
private IPlugin registeredPlugin;
+ /**
+ * Clear static HopVfs state before each method. Other core VFS tests (for
example {@link
+ * HopVfsTest#testStartsWithScheme()}) can leave {@code bootstrapVariables}
set via {@code
+ * startsWithScheme(name, variables)} / {@code
getFileSystemManager(variables)}. Without this,
+ * {@link #noMetadataLookupBeforeVariablesArrive} sees a false positive:
named providers are
+ * looked up even though this test never bootstrapped a project.
+ */
+ @BeforeEach
+ void setUp() {
+ clearHopVfsState();
+ }
+
@AfterEach
void tearDown() {
if (registeredPlugin != null) {
@@ -69,6 +82,10 @@ class HopVfsSingleManagerTest {
}
// Drop the manager built with the test providers, and the variables it
was built with, so
// other tests start clean.
+ clearHopVfsState();
+ }
+
+ private static void clearHopVfsState() {
HopVfs.setBootstrapVariables(null);
HopVfs.reset();
}
diff --git a/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
b/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
index f7d692c3b0..1087d40d98 100644
--- a/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
+++ b/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
@@ -27,11 +27,20 @@ import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
/** Unit test for {@link HopVfs} */
class HopVfsTest {
+ @AfterEach
+ void tearDown() {
+ // startsWithScheme(name, variables) bootstraps named VFS providers; clear
so other core VFS
+ // tests are not polluted by static HopVfs state.
+ HopVfs.setBootstrapVariables(null);
+ HopVfs.reset();
+ }
+
/**
* Test to validate that startsWitScheme() returns true if the fileName
starts with known protocol
* like zip: jar: then it returns true else returns false
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/PipelineExecutionConfiguration.java
b/engine/src/main/java/org/apache/hop/pipeline/PipelineExecutionConfiguration.java
index 71f3c630bf..48714ed5a3 100644
---
a/engine/src/main/java/org/apache/hop/pipeline/PipelineExecutionConfiguration.java
+++
b/engine/src/main/java/org/apache/hop/pipeline/PipelineExecutionConfiguration.java
@@ -144,8 +144,12 @@ public class PipelineExecutionConfiguration implements
IExecutionConfiguration,
HashMap<String, String> newVariables = new HashMap<>();
for (String varname : vars) {
- newVariables.put(
- varname, Const.NVL(variablesMap.get(varname),
sp.getProperty(varname, "")));
+ // Prefer live variable space over values from a previous run dialog
+ if (sp.containsKey(varname)) {
+ newVariables.put(varname, sp.getProperty(varname, ""));
+ } else {
+ newVariables.put(varname, Const.NVL(variablesMap.get(varname), ""));
+ }
}
variablesMap.putAll(newVariables);
}
@@ -176,8 +180,14 @@ public class PipelineExecutionConfiguration implements
IExecutionConfiguration,
for (String varname : vars) {
if (!varname.startsWith(Const.INTERNAL_VARIABLE_PREFIX)) {
- newVariables.put(
- varname, Const.NVL(variablesMap.get(varname),
sp.getProperty(varname, "")));
+ // Prefer the live variable space (project/env/pipeline graph,
including active unit-test
+ // values) over values remembered from a previous run dialog.
Otherwise switching unit
+ // tests leaves the old sample values sticky in the execution
configuration.
+ if (sp.containsKey(varname)) {
+ newVariables.put(varname, sp.getProperty(varname, ""));
+ } else {
+ newVariables.put(varname, Const.NVL(variablesMap.get(varname),
""));
+ }
}
}
variablesMap.putAll(newVariables);
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 7d89ad5f8f..16ba893f38 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
@@ -63,6 +63,7 @@ import org.apache.hop.testing.PipelineUnitTestTweak;
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.xp.PipelineMetaModifier;
import org.apache.hop.testing.xp.WriteToDataSetExtensionPoint;
import org.apache.hop.ui.core.dialog.EnterMappingDialog;
@@ -880,9 +881,10 @@ public class TestingGuiPlugin {
return;
}
- // Remove
+ // Clear unit-test sample variables from the graph variable space, then
drop state.
//
Map<String, Object> stateMap = getStateMap(pipelineMeta);
+ UnitTestGraphVariables.clear(pipelineGraph.getVariables(), stateMap);
if (stateMap != null) {
stateMap.clear();
}
@@ -894,7 +896,7 @@ public class TestingGuiPlugin {
combo.setText("");
}
- // Also clear the unit test variables from the pipelineGraph instance.
+ // Also clear the unit test control variables from the pipelineGraph
instance.
//
pipelineGraph.getVariables().setVariable(DataSetConst.VAR_RUN_UNIT_TEST,
"N");
pipelineGraph.getVariables().setVariable(DataSetConst.VAR_UNIT_TEST_NAME, null);
@@ -1143,6 +1145,11 @@ public class TestingGuiPlugin {
if (pipelineGraph == null) {
return;
}
+ Combo combo = getInstance().getUnitTestsCombo();
+ // Avoid re-firing the selection listener when the combo already shows
this test
+ if (combo != null && Const.NVL(name, "").equals(combo.getText())) {
+ return;
+ }
pipelineGraph.getToolBarWidgets().selectComboItem(ID_TOOLBAR_UNIT_TESTS_COMBO,
name);
}
@@ -1306,24 +1313,66 @@ public class TestingGuiPlugin {
}
public static final void selectUnitTest(PipelineMeta pipelineMeta,
PipelineUnitTest unitTest) {
- Map<String, Object> stateMap = getStateMap(pipelineMeta);
- if (stateMap == null) {
+ HopGuiPipelineGraph pipelineGraph = getPipelineGraph(pipelineMeta);
+ if (pipelineGraph == null || unitTest == null) {
// Can't select since we don't find the tab
+ return;
}
+ Map<String, Object> stateMap = pipelineGraph.getStateMap();
stateMap.put(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST, unitTest);
+
+ IVariables graphVariables = pipelineGraph.getVariables();
+ // Make unit-test sample variables available for design-time (get fields,
check, dialogs)
+ // and as the live source for the next execution configuration dialog.
+ UnitTestGraphVariables.apply(graphVariables, unitTest, stateMap);
+
+ // Keep unit-test control flags in sync on switch (not only at run start)
+ graphVariables.setVariable(DataSetConst.VAR_RUN_UNIT_TEST, "Y");
+ graphVariables.setVariable(DataSetConst.VAR_UNIT_TEST_NAME,
unitTest.getName());
+
selectUnitTestInList(unitTest.getName());
}
public static Map<String, Object> getStateMap(PipelineMeta pipelineMeta) {
+ HopGuiPipelineGraph pipelineGraph = getPipelineGraph(pipelineMeta);
+ return pipelineGraph != null ? pipelineGraph.getStateMap() : null;
+ }
+
+ /**
+ * Find the open pipeline graph tab for the given metadata, if any.
+ *
+ * <p>Prefers the active pipeline graph when it matches, then scans explorer
tabs. Matching is by
+ * identity first, then {@link PipelineMeta#equals(Object)} (filename/name).
+ *
+ * @param pipelineMeta the pipeline metadata
+ * @return the graph, or null when the pipeline is not open in the GUI
+ */
+ public static HopGuiPipelineGraph getPipelineGraph(PipelineMeta
pipelineMeta) {
+ HopGuiPipelineGraph active = HopGui.getActivePipelineGraph();
+ if (active != null && pipelineMetaMatches(active.getPipelineMeta(),
pipelineMeta)) {
+ return active;
+ }
+ if (pipelineMeta == null || HopGui.getExplorerPerspective() == null) {
+ return null;
+ }
for (TabItemHandler item : HopGui.getExplorerPerspective().getItems()) {
- if (item.getTypeHandler().getSubject().equals(pipelineMeta)) {
- HopGuiPipelineGraph pipelineGraph = (HopGuiPipelineGraph)
item.getTypeHandler();
- return pipelineGraph.getStateMap();
+ if (!(item.getTypeHandler() instanceof HopGuiPipelineGraph
pipelineGraph)) {
+ continue;
+ }
+ if (pipelineMetaMatches(pipelineGraph.getPipelineMeta(), pipelineMeta)) {
+ return pipelineGraph;
}
}
return null;
}
+ private static boolean pipelineMetaMatches(PipelineMeta a, PipelineMeta b) {
+ if (a == null || b == null) {
+ return false;
+ }
+ return a == b || a.equals(b);
+ }
+
public static final PipelineUnitTest getCurrentUnitTest(PipelineMeta
pipelineMeta) {
// When rendering a pipeline on a server status page we never have a
current unit test
//
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 e2901c36ef..05ec260798 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
@@ -74,6 +74,12 @@ public class DataSetConst {
public static final String STATE_KEY_GOLDEN_DATASET_RESULTS =
"GoldenDataSetResults";
public static final String STATE_KEY_ACTIVE_UNIT_TEST = "ActiveUnitTest";
+ /**
+ * Keys of unit-test variables currently applied to the pipeline graph
variable space
+ * (design-time). Value type: {@code Set<String>}.
+ */
+ public static final String STATE_KEY_APPLIED_UNIT_TEST_VARIABLES =
"AppliedUnitTestVariables";
+
private static final String[] tweakDesc =
new String[] {
BaseMessages.getString(PKG, "DataSetConst.Tweak.NONE.Desc"),
diff --git
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/UnitTestAutoOpening.java
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/UnitTestAutoOpening.java
new file mode 100644
index 0000000000..d0e42cdb1f
--- /dev/null
+++
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/UnitTestAutoOpening.java
@@ -0,0 +1,97 @@
+/*
+ * 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.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.api.IHopMetadataSerializer;
+import org.apache.hop.testing.PipelineUnitTest;
+
+/**
+ * Ensures at most one unit test per pipeline has the auto-open flag enabled.
When a unit test is
+ * saved with auto-open on, other tests that reference the same pipeline have
the flag cleared.
+ */
+public final class UnitTestAutoOpening {
+
+ private UnitTestAutoOpening() {
+ // utility
+ }
+
+ /**
+ * If {@code unitTest} has auto-open enabled, disable auto-open on every
other unit test that
+ * targets the same pipeline and persist those changes.
+ *
+ * @param log optional log channel
+ * @param variables variables for path resolution
+ * @param metadataProvider metadata provider (may be null)
+ * @param unitTest the unit test that was just created or saved
+ */
+ public static void enforceExclusiveAutoOpening(
+ ILogChannel log,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider,
+ PipelineUnitTest unitTest)
+ throws HopException {
+
+ if (unitTest == null || !unitTest.isAutoOpening() ||
Utils.isEmpty(unitTest.getName())) {
+ return;
+ }
+ if (metadataProvider == null || variables == null) {
+ return;
+ }
+
+ String pipelineFilename =
unitTest.calculateCompletePipelineFilename(variables);
+ if (StringUtils.isEmpty(pipelineFilename)) {
+ return;
+ }
+
+ IHopMetadataSerializer<PipelineUnitTest> serializer =
+ metadataProvider.getSerializer(PipelineUnitTest.class);
+ List<PipelineUnitTest> allTests = serializer.loadAll();
+ for (PipelineUnitTest other : allTests) {
+ if (other == null || Utils.isEmpty(other.getName())) {
+ continue;
+ }
+ if (unitTest.getName().equals(other.getName())) {
+ continue;
+ }
+ if (!other.isAutoOpening()) {
+ continue;
+ }
+ if (!other.matchesPipelineFilename(variables, pipelineFilename)) {
+ continue;
+ }
+
+ other.setAutoOpening(false);
+ serializer.save(other);
+ if (log != null && log.isDetailed()) {
+ log.logDetailed(
+ "Disabled auto-open on unit test '"
+ + other.getName()
+ + "' because '"
+ + unitTest.getName()
+ + "' is now the auto-open test for the same pipeline");
+ }
+ }
+ }
+}
diff --git
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/UnitTestGraphVariables.java
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/UnitTestGraphVariables.java
new file mode 100644
index 0000000000..45cc0204ae
--- /dev/null
+++
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/util/UnitTestGraphVariables.java
@@ -0,0 +1,103 @@
+/*
+ * 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.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.testing.PipelineUnitTest;
+import org.apache.hop.testing.VariableValue;
+
+/**
+ * Applies and clears pipeline unit test variables on the Hop GUI pipeline
graph variable space so
+ * design-time actions (get fields, check, transform dialogs) resolve the same
sample values used
+ * when the unit test is executed.
+ */
+public final class UnitTestGraphVariables {
+
+ private UnitTestGraphVariables() {
+ // utility
+ }
+
+ /**
+ * Clear any previously applied unit-test variables from the target space,
then apply the given
+ * unit test's variable values. Tracks applied keys in {@code stateMap} for
later cleanup.
+ *
+ * @param variables the pipeline graph variable space (or any target space)
+ * @param unitTest the active unit test (may be null)
+ * @param stateMap pipeline graph state map (may be null)
+ */
+ public static void apply(
+ IVariables variables, PipelineUnitTest unitTest, Map<String, Object>
stateMap) {
+ clear(variables, stateMap);
+ if (variables == null || unitTest == null || stateMap == null) {
+ return;
+ }
+
+ List<VariableValue> variableValues = unitTest.getVariableValues();
+ if (variableValues == null || variableValues.isEmpty()) {
+ return;
+ }
+
+ Set<String> applied = new HashSet<>();
+ for (VariableValue variableValue : variableValues) {
+ if (variableValue == null) {
+ continue;
+ }
+ // Resolve after clear() so a previous unit test's value for the same
key cannot leak into
+ // ${...} substitution of the new test's key/value expressions.
+ String key = variables.resolve(Const.NVL(variableValue.getKey(), ""));
+ String value = variables.resolve(Const.NVL(variableValue.getValue(),
""));
+ if (StringUtils.isEmpty(key)) {
+ continue;
+ }
+ variables.setVariable(key, value);
+ applied.add(key);
+ }
+
+ if (!applied.isEmpty()) {
+ stateMap.put(DataSetConst.STATE_KEY_APPLIED_UNIT_TEST_VARIABLES,
applied);
+ }
+ }
+
+ /**
+ * Remove unit-test variables previously applied via {@link #apply} from the
target space.
+ *
+ * @param variables the pipeline graph variable space (or any target space)
+ * @param stateMap pipeline graph state map (may be null)
+ */
+ @SuppressWarnings("unchecked")
+ public static void clear(IVariables variables, Map<String, Object> stateMap)
{
+ if (stateMap == null) {
+ return;
+ }
+ Object stored =
stateMap.remove(DataSetConst.STATE_KEY_APPLIED_UNIT_TEST_VARIABLES);
+ if (variables == null || !(stored instanceof Set)) {
+ return;
+ }
+ for (String key : (Set<String>) stored) {
+ if (StringUtils.isNotEmpty(key)) {
+ variables.setVariable(key, null);
+ }
+ }
+ }
+}
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 4692a600f1..bfa827e2e9 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
@@ -26,6 +26,8 @@ import org.apache.hop.core.variables.IVariables;
import org.apache.hop.pipeline.PipelineMeta;
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.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
@ExtensionPoint(
extensionPointId = "HopGuiPipelineAfterClose",
@@ -36,7 +38,16 @@ public class HopGuiPipelineAfterClose implements
IExtensionPoint<PipelineMeta> {
@Override
public void callExtensionPoint(ILogChannel log, IVariables variables,
PipelineMeta pipelineMeta)
throws HopException {
- Map<String, Object> stateMap = TestingGuiPlugin.getStateMap(pipelineMeta);
+ 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 (stateMap != null) {
stateMap.remove(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
}
diff --git
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestChanged.java
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestChanged.java
index bb26ea6f09..0a0b9d2f1d 100644
---
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestChanged.java
+++
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestChanged.java
@@ -20,9 +20,17 @@ package org.apache.hop.testing.xp;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.extension.IExtensionPoint;
import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.util.Utils;
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.UnitTestAutoOpening;
+import org.apache.hop.testing.util.UnitTestGraphVariables;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
+import org.apache.hop.ui.hopgui.perspective.TabItemHandler;
/** Used for create/update/delete of unit test metadata objects */
public class HopGuiUnitTestChanged implements IExtensionPoint {
@@ -32,10 +40,42 @@ public class HopGuiUnitTestChanged implements
IExtensionPoint {
throws HopException {
// We only respond to pipeline unit test changes
//
- if (!(object instanceof PipelineUnitTest)) {
+ if (!(object instanceof PipelineUnitTest unitTest)) {
return;
}
+ HopGui hopGui = HopGui.getInstance();
+
+ // When auto-open is enabled on this test, clear it on other tests for the
same pipeline
+ // (create OK / metadata editor save).
+ if (hopGui != null) {
+ UnitTestAutoOpening.enforceExclusiveAutoOpening(
+ log, variables, hopGui.getMetadataProvider(), unitTest);
+ }
+
TestingGuiPlugin.refreshUnitTestsList();
+
+ // If this unit test is active on an open pipeline graph, re-apply its
variables for
+ // design-time.
+ //
+ if (Utils.isEmpty(unitTest.getName()) || hopGui == null) {
+ return;
+ }
+ for (TabItemHandler item : HopGui.getExplorerPerspective().getItems()) {
+ if (!(item.getTypeHandler() instanceof HopGuiPipelineGraph
pipelineGraph)) {
+ continue;
+ }
+ Object subject = pipelineGraph.getSubject();
+ if (!(subject instanceof PipelineMeta pipelineMeta)) {
+ continue;
+ }
+ PipelineUnitTest active =
TestingGuiPlugin.getCurrentUnitTest(pipelineMeta);
+ if (active != null && unitTest.getName().equals(active.getName())) {
+ UnitTestGraphVariables.apply(
+ pipelineGraph.getVariables(), unitTest,
pipelineGraph.getStateMap());
+ // Keep state map pointing at the updated metadata object
+
pipelineGraph.getStateMap().put(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST,
unitTest);
+ }
+ }
}
}
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 a83086c68f..ca0d98a630 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
@@ -21,7 +21,10 @@ import org.apache.hop.core.exception.HopException;
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.util.Utils;
import org.apache.hop.core.variables.IVariables;
+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;
@@ -41,15 +44,46 @@ public class HopGuiUnitTestCreateBeforeDialog extends
HopGuiUnitTestChanged
// Ignore all other metadata object changes
//
- if (!(object instanceof PipelineUnitTest)) {
+ if (!(object instanceof PipelineUnitTest test)) {
return;
}
- PipelineUnitTest test = (PipelineUnitTest) object;
PipelineMeta pipelineMeta = TestingGuiPlugin.getActivePipelineMeta();
- if (pipelineMeta != null) {
- test.setName(pipelineMeta.getName() + " UNIT");
- test.setType(TestType.UNIT_TEST);
- test.setRelativeFilename(HopGui.getInstance().getVariables(),
pipelineMeta.getFilename());
+ if (pipelineMeta == null) {
+ return;
+ }
+
+ HopGui hopGui = HopGui.getInstance();
+ test.setName(uniqueUnitTestName(pipelineMeta.getName(),
hopGui.getMetadataProvider()));
+ test.setType(TestType.UNIT_TEST);
+ test.setRelativeFilename(hopGui.getVariables(),
pipelineMeta.getFilename());
+ }
+
+ /**
+ * Build a default unit-test name that does not collide with an existing
unit test.
+ *
+ * <p>{@code <pipeline> UNIT}, then {@code <pipeline> UNIT 2}, {@code UNIT
3}, ...
+ *
+ * @param pipelineName pipeline name used as the base (may be null/empty)
+ * @param metadataProvider metadata used to detect existing names; when
null, returns the base
+ * name without collision checks (headless / unit tests must not call
{@code
+ * HopGui.getInstance()})
+ */
+ static String uniqueUnitTestName(String pipelineName, IHopMetadataProvider
metadataProvider)
+ throws HopException {
+ String base = (Utils.isEmpty(pipelineName) ? "Pipeline" :
pipelineName.trim()) + " UNIT";
+ if (metadataProvider == null) {
+ return base;
+ }
+
+ IHopMetadataSerializer<PipelineUnitTest> serializer =
+ metadataProvider.getSerializer(PipelineUnitTest.class);
+ if (!serializer.exists(base)) {
+ return base;
+ }
+ int n = 2;
+ while (serializer.exists(base + " " + n)) {
+ n++;
}
+ return base + " " + n;
}
}
diff --git
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestDeleted.java
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestDeleted.java
index 7c82901d58..11da8b39e5 100644
---
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestDeleted.java
+++
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestDeleted.java
@@ -17,11 +17,54 @@
package org.apache.hop.testing.xp;
+import org.apache.hop.core.exception.HopException;
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.util.Utils;
+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.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
+import org.apache.hop.ui.hopgui.perspective.TabItemHandler;
@ExtensionPoint(
id = "HopGuiUnitTestDeleted",
extensionPointId = "HopGuiMetadataObjectDeleted",
- description = "When HopGui deletes a new metadata object somewhere")
-public class HopGuiUnitTestDeleted extends HopGuiUnitTestChanged implements
IExtensionPoint {}
+ description = "When HopGui deletes a pipeline unit test metadata object")
+public class HopGuiUnitTestDeleted implements IExtensionPoint {
+
+ @Override
+ public void callExtensionPoint(ILogChannel log, IVariables variables, Object
object)
+ throws HopException {
+ if (!(object instanceof PipelineUnitTest unitTest)) {
+ return;
+ }
+
+ TestingGuiPlugin.refreshUnitTestsList();
+
+ // If the deleted unit test was active, clear its design-time variables
from the graph.
+ //
+ if (Utils.isEmpty(unitTest.getName()) || HopGui.getInstance() == null) {
+ return;
+ }
+ for (TabItemHandler item : HopGui.getExplorerPerspective().getItems()) {
+ if (!(item.getTypeHandler() instanceof HopGuiPipelineGraph
pipelineGraph)) {
+ continue;
+ }
+ Object subject = pipelineGraph.getSubject();
+ if (!(subject instanceof PipelineMeta pipelineMeta)) {
+ continue;
+ }
+ PipelineUnitTest active =
TestingGuiPlugin.getCurrentUnitTest(pipelineMeta);
+ if (active != null && unitTest.getName().equals(active.getName())) {
+ UnitTestGraphVariables.clear(pipelineGraph.getVariables(),
pipelineGraph.getStateMap());
+
pipelineGraph.getStateMap().remove(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+ }
+ }
+ }
+}
diff --git
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestVariablesExtensionPoint.java
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestVariablesExtensionPoint.java
index f1fa95e171..faf27ae3ee 100644
---
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestVariablesExtensionPoint.java
+++
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiUnitTestVariablesExtensionPoint.java
@@ -31,6 +31,7 @@ import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.testing.PipelineUnitTest;
import org.apache.hop.testing.VariableValue;
import org.apache.hop.testing.util.DataSetConst;
+import org.apache.hop.testing.util.UnitTestGraphVariables;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
@@ -67,6 +68,34 @@ public class HopGuiUnitTestVariablesExtensionPoint
return;
}
+ // Reload from metadata so we never use a stale in-memory copy after
switching tests
+ String unitTestName = unitTest.getName();
+ if (StringUtils.isNotEmpty(unitTestName)) {
+ try {
+ PipelineUnitTest reloaded =
+ HopGui.getInstance()
+ .getMetadataProvider()
+ .getSerializer(PipelineUnitTest.class)
+ .load(unitTestName);
+ if (reloaded != null) {
+ unitTest = reloaded;
+ // Keep state map aligned with the fresh object
+ stateMap.put(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST, unitTest);
+ // Also refresh graph variables so design-time and execution stay in
sync
+ UnitTestGraphVariables.apply(activePipelineGraph.getVariables(),
unitTest, stateMap);
+ }
+ } catch (HopException e) {
+ // Fall back to the in-memory unit test from the state map
+ if (log.isDetailed()) {
+ log.logDetailed(
+ "Could not reload unit test '"
+ + unitTestName
+ + "' for execution variables: "
+ + e.getMessage());
+ }
+ }
+ }
+
applyUnitTestVariables(
executionConfiguration, unitTest,
activePipelineGraph.getPipelineMeta(), variables);
}
diff --git
a/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/UnitTestAutoOpeningTest.java
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/UnitTestAutoOpeningTest.java
new file mode 100644
index 0000000000..b19b82a675
--- /dev/null
+++
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/UnitTestAutoOpeningTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.api.IHopMetadataSerializer;
+import org.apache.hop.testing.PipelineUnitTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class UnitTestAutoOpeningTest {
+
+ @Mock IHopMetadataProvider metadataProvider;
+ @Mock IHopMetadataSerializer<PipelineUnitTest> serializer;
+ @Mock ILogChannel log;
+
+ @Test
+ void doesNothingWhenAutoOpeningIsFalse() throws Exception {
+ PipelineUnitTest unitTest = new PipelineUnitTest();
+ unitTest.setName("test A");
+ unitTest.setAutoOpening(false);
+ unitTest.setPipelineFilename("./pipeline.hpl");
+
+ UnitTestAutoOpening.enforceExclusiveAutoOpening(
+ log, new Variables(), metadataProvider, unitTest);
+
+ verify(metadataProvider, never()).getSerializer(any());
+ }
+
+ @Test
+ void disablesAutoOpeningOnOtherTestsForSamePipeline() throws Exception {
+ Variables variables = new Variables();
+
+ PipelineUnitTest current = new PipelineUnitTest();
+ current.setName("test A");
+ current.setAutoOpening(true);
+ current.setPipelineFilename("./pipeline.hpl");
+ current.setBasePath("/project");
+
+ PipelineUnitTest otherSame = new PipelineUnitTest();
+ otherSame.setName("test B");
+ otherSame.setAutoOpening(true);
+ otherSame.setPipelineFilename("./pipeline.hpl");
+ otherSame.setBasePath("/project");
+
+ PipelineUnitTest otherDifferent = new PipelineUnitTest();
+ otherDifferent.setName("test C");
+ otherDifferent.setAutoOpening(true);
+ otherDifferent.setPipelineFilename("./other.hpl");
+ otherDifferent.setBasePath("/project");
+
+
when(metadataProvider.getSerializer(PipelineUnitTest.class)).thenReturn(serializer);
+ when(serializer.loadAll()).thenReturn(List.of(current, otherSame,
otherDifferent));
+
+ UnitTestAutoOpening.enforceExclusiveAutoOpening(log, variables,
metadataProvider, current);
+
+ assertFalse(otherSame.isAutoOpening());
+ assertTrue(otherDifferent.isAutoOpening());
+ assertTrue(current.isAutoOpening());
+ verify(serializer).save(otherSame);
+ verify(serializer, never()).save(eq(otherDifferent));
+ verify(serializer, never()).save(eq(current));
+ }
+}
diff --git
a/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/UnitTestGraphVariablesTest.java
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/UnitTestGraphVariablesTest.java
new file mode 100644
index 0000000000..7905c87d1a
--- /dev/null
+++
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/util/UnitTestGraphVariablesTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.testing.PipelineUnitTest;
+import org.apache.hop.testing.VariableValue;
+import org.junit.jupiter.api.Test;
+
+class UnitTestGraphVariablesTest {
+
+ @Test
+ void applySetsUnitTestVariablesOnTargetSpace() {
+ Variables variables = new Variables();
+ Map<String, Object> stateMap = new HashMap<>();
+ PipelineUnitTest unitTest = new PipelineUnitTest();
+ unitTest.getVariableValues().add(new VariableValue("UNIT_VAR",
"unit-value"));
+ unitTest.getVariableValues().add(new VariableValue("OTHER",
"other-value"));
+
+ UnitTestGraphVariables.apply(variables, unitTest, stateMap);
+
+ assertEquals("unit-value", variables.getVariable("UNIT_VAR"));
+ assertEquals("other-value", variables.getVariable("OTHER"));
+
assertTrue(stateMap.containsKey(DataSetConst.STATE_KEY_APPLIED_UNIT_TEST_VARIABLES));
+ }
+
+ @Test
+ void clearRemovesAppliedVariables() {
+ Variables variables = new Variables();
+ Map<String, Object> stateMap = new HashMap<>();
+ PipelineUnitTest unitTest = new PipelineUnitTest();
+ unitTest.getVariableValues().add(new VariableValue("UNIT_VAR",
"unit-value"));
+
+ UnitTestGraphVariables.apply(variables, unitTest, stateMap);
+ UnitTestGraphVariables.clear(variables, stateMap);
+
+ assertNull(variables.getVariable("UNIT_VAR"));
+
assertNull(stateMap.get(DataSetConst.STATE_KEY_APPLIED_UNIT_TEST_VARIABLES));
+ }
+
+ @Test
+ void applyReplacesPreviousUnitTestVariables() {
+ Variables variables = new Variables();
+ Map<String, Object> stateMap = new HashMap<>();
+
+ PipelineUnitTest testA = new PipelineUnitTest();
+ testA.getVariableValues().add(new VariableValue("ONLY_A", "a"));
+ testA.getVariableValues().add(new VariableValue("SHARED", "from-a"));
+
+ PipelineUnitTest testB = new PipelineUnitTest();
+ testB.getVariableValues().add(new VariableValue("ONLY_B", "b"));
+ testB.getVariableValues().add(new VariableValue("SHARED", "from-b"));
+
+ UnitTestGraphVariables.apply(variables, testA, stateMap);
+ UnitTestGraphVariables.apply(variables, testB, stateMap);
+
+ assertNull(variables.getVariable("ONLY_A"));
+ assertEquals("b", variables.getVariable("ONLY_B"));
+ assertEquals("from-b", variables.getVariable("SHARED"));
+ }
+
+ @Test
+ void applySwitchingSameKeyUpdatesValue() {
+ // Mirrors two unit tests for one pipeline that both set UNIT_TEST_VAR
+ Variables variables = new Variables();
+ Map<String, Object> stateMap = new HashMap<>();
+
+ PipelineUnitTest test1 = new PipelineUnitTest();
+ test1.setName("test UNIT");
+ test1.getVariableValues().add(new VariableValue("UNIT_TEST_VAR",
"value1"));
+
+ PipelineUnitTest test2 = new PipelineUnitTest();
+ test2.setName("test UNIT 2");
+ test2.getVariableValues().add(new VariableValue("UNIT_TEST_VAR",
"value2"));
+
+ UnitTestGraphVariables.apply(variables, test1, stateMap);
+ assertEquals("value1", variables.getVariable("UNIT_TEST_VAR"));
+
+ UnitTestGraphVariables.apply(variables, test2, stateMap);
+ assertEquals("value2", variables.getVariable("UNIT_TEST_VAR"));
+
+ UnitTestGraphVariables.apply(variables, test1, stateMap);
+ assertEquals("value1", variables.getVariable("UNIT_TEST_VAR"));
+ }
+
+ @Test
+ void applyResolvesKeyAndValue() {
+ Variables variables = new Variables();
+ variables.setVariable("PREFIX", "env");
+ variables.setVariable("SUFFIX", "prod");
+ Map<String, Object> stateMap = new HashMap<>();
+
+ PipelineUnitTest unitTest = new PipelineUnitTest();
+ unitTest.getVariableValues().add(new VariableValue("${PREFIX}_MODE",
"${SUFFIX}"));
+
+ UnitTestGraphVariables.apply(variables, unitTest, stateMap);
+
+ assertEquals("prod", variables.getVariable("env_MODE"));
+ }
+
+ @Test
+ void applySkipsEmptyKeys() {
+ Variables variables = new Variables();
+ Map<String, Object> stateMap = new HashMap<>();
+ PipelineUnitTest unitTest = new PipelineUnitTest();
+ unitTest.getVariableValues().add(null);
+ unitTest.getVariableValues().add(new VariableValue("", "ignored"));
+ unitTest.getVariableValues().add(new VariableValue(null, "ignored-too"));
+ unitTest.getVariableValues().add(new VariableValue("OK", null));
+
+ UnitTestGraphVariables.apply(variables, unitTest, stateMap);
+
+ assertEquals("", variables.getVariable("OK"));
+ assertNull(variables.getVariable(""));
+ }
+
+ @Test
+ void applyNoopsSafelyWithNulls() {
+ UnitTestGraphVariables.apply(null, null, null);
+ UnitTestGraphVariables.clear(null, null);
+
+ Variables variables = new Variables();
+ variables.setVariable("KEEP", "yes");
+ UnitTestGraphVariables.apply(variables, new PipelineUnitTest(), new
HashMap<>());
+ assertEquals("yes", variables.getVariable("KEEP"));
+ }
+}
diff --git
a/plugins/misc/testing/src/test/java/org/apache/hop/testing/xp/HopGuiUnitTestCreateBeforeDialogTest.java
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/xp/HopGuiUnitTestCreateBeforeDialogTest.java
new file mode 100644
index 0000000000..0217a23fff
--- /dev/null
+++
b/plugins/misc/testing/src/test/java/org/apache/hop/testing/xp/HopGuiUnitTestCreateBeforeDialogTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.xp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests that do not need Hop GUI for the pure naming helper. Collision
checks against a real
+ * serializer are covered by integration / manual testing when metadata is
available.
+ */
+class HopGuiUnitTestCreateBeforeDialogTest {
+
+ @Test
+ void uniqueUnitTestNameUsesPipelineNameBase() throws Exception {
+ // Null metadata provider: no HopGui/SWT — base name only (headless CI
safe)
+ String name =
HopGuiUnitTestCreateBeforeDialog.uniqueUnitTestName("my-pipeline", null);
+ assertEquals("my-pipeline UNIT", name);
+ }
+
+ @Test
+ void uniqueUnitTestNameHandlesEmptyPipelineName() throws Exception {
+ String name = HopGuiUnitTestCreateBeforeDialog.uniqueUnitTestName(null,
null);
+ assertEquals("Pipeline UNIT", name);
+ assertTrue(name.endsWith(" UNIT"));
+ }
+}
diff --git
a/plugins/transforms/excel/src/test/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInputMetaTest.java
b/plugins/transforms/excel/src/test/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInputMetaTest.java
index 9d358e6773..123e31ae00 100644
---
a/plugins/transforms/excel/src/test/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInputMetaTest.java
+++
b/plugins/transforms/excel/src/test/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInputMetaTest.java
@@ -20,10 +20,23 @@ package org.apache.hop.pipeline.transforms.excelinput;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
import org.apache.hop.pipeline.transform.TransformSerializationTestUtil;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
class ExcelInputMetaTest {
+ @RegisterExtension
+ static RestoreHopEngineEnvironmentExtension env = new
RestoreHopEngineEnvironmentExtension();
+
+ @BeforeAll
+ static void setUpBeforeClass() throws HopException {
+ // Password fields require Encr.encoder (initialized by HopEnvironment)
+ HopEnvironment.init();
+ }
@Test
void testSerialization() throws Exception {
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataEditor.java
b/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataEditor.java
index 5a06c165da..ee70491b9a 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataEditor.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataEditor.java
@@ -105,6 +105,17 @@ public abstract class MetadataEditor<T extends
IHopMetadata> extends MetadataFil
ConstUi.LARGE_ICON_SIZE));
}
+ /**
+ * Mark this editor as creating a brand-new metadata object that has not
been persisted yet.
+ *
+ * <p>Create-before-dialog hooks may suggest a default name on the object.
That suggested name
+ * must not be treated as an existing persisted identity: changing it must
create a new object,
+ * not rename (and delete) another metadata entry that already uses the
suggested name.
+ */
+ public void markAsNew() {
+ this.originalName = null;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -279,10 +290,15 @@ public abstract class MetadataEditor<T extends
IHopMetadata> extends MetadataFil
IHopMetadataSerializer<T> serializer = manager.getSerializer();
if (StringUtils.isEmpty(originalName)) {
+ // New object (including create dialog with a suggested default name via
markAsNew())
isCreated = true;
+ if (serializer.exists(name)) {
+ throw new HopException(
+ BaseMessages.getString(PKG,
"MetadataEditor.Error.NameAlreadyExists", name));
+ }
}
- // If rename
+ // If rename of an already-persisted object
//
else if (!originalName.equals(name)) {
@@ -327,6 +343,10 @@ public abstract class MetadataEditor<T extends
IHopMetadata> extends MetadataFil
String objectKey =
manager.getManagedClass().getAnnotation(HopMetadata.class).key();
MetadataPerspective.getInstance()
.performGlobalReplaceIfSupported(objectKey, originalName, name);
+ }
+
+ // After a successful create or rename, the persisted identity is the
current name
+ if (isCreated || isRename) {
this.originalName = metadata.getName();
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataManager.java
b/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataManager.java
index d8218ac6bf..b7d421b116 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataManager.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataManager.java
@@ -459,6 +459,8 @@ public class MetadataManager<T extends IHopMetadata> {
element);
MetadataEditor<T> editor = this.createEditor(element);
+ // Suggested default names from create-before hooks are not a persisted
identity
+ editor.markAsNew();
// Always open this in a separate dialog so that we block until we have
a name for the new
// element.
@@ -467,15 +469,9 @@ public class MetadataManager<T extends IHopMetadata> {
MetadataEditorDialog dialog =
new MetadataEditorDialog(HopGui.getInstance().getShell(), editor);
+ // MetadataEditor.save() already fires HopGuiMetadataObjectCreated on
success
String name = dialog.open();
- if (name != null) {
- ExtensionPointHandler.callExtensionPoint(
- HopGui.getInstance().getLog(),
- variables,
- HopExtensionPoint.HopGuiMetadataObjectCreated.id,
- element);
- }
- return element;
+ return name != null ? element : null;
} catch (Exception e) {
new ErrorDialog(
HopGui.getInstance().getShell(), CONST_ERROR, "Error editing new
metadata element", e);
@@ -501,6 +497,8 @@ public class MetadataManager<T extends IHopMetadata> {
element);
MetadataEditor<T> editor = this.createEditor(element);
+ // Suggested default names from create-before hooks are not a persisted
identity
+ editor.markAsNew();
editor.setTitle(
BaseMessages.getString(
PKG,
@@ -512,12 +510,11 @@ public class MetadataManager<T extends IHopMetadata> {
//
MetadataPerspective perspective = HopGui.getMetadataPerspective();
if (perspective == null) {
+ // MetadataEditor.save() already fires HopGuiMetadataObjectCreated on
success
MetadataEditorDialog dialog = new
MetadataEditorDialog(hopGui.getActiveShell(), editor);
if (dialog.open() == null) {
return null;
}
- ExtensionPointHandler.callExtensionPoint(
- hopGui.getLog(), variables,
HopExtensionPoint.HopGuiMetadataObjectCreated.id, element);
return element;
}