diff --git a/lib/galaxy/config/sample/tool_conf.xml.sample b/lib/galaxy/config/sample/tool_conf.xml.sample
index 218898880631..153b5ae314d4 100644
--- a/lib/galaxy/config/sample/tool_conf.xml.sample
+++ b/lib/galaxy/config/sample/tool_conf.xml.sample
@@ -38,6 +38,8 @@
+
+
diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py
index 917464e5b45d..8186b9611404 100644
--- a/lib/galaxy/tools/__init__.py
+++ b/lib/galaxy/tools/__init__.py
@@ -13,6 +13,7 @@
from collections.abc import MutableMapping
from pathlib import Path
from typing import (
+ Any,
cast,
Dict,
List,
@@ -3305,6 +3306,94 @@ def produce_outputs(self, trans, out_data, output_collections, incoming, history
)
+class CrossProductFlatCollectionTool(DatabaseOperationTool):
+ tool_type = "cross_product_flat"
+ require_terminal_states = False
+ require_dataset_ok = False
+
+ def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds):
+ input_a = incoming["input_a"]
+ input_b = incoming["input_b"]
+ join_identifier = incoming["join_identifier"]
+
+ output_a = {}
+ output_b = {}
+ all_copied_hdas = []
+
+ for input_a_dce in input_a.collection.elements:
+ element_identifier_a = input_a_dce.element_identifier
+ for input_b_dce in input_b.collection.elements:
+ element_identifier_b = input_b_dce.element_identifier
+ identifier = f"{element_identifier_a}{join_identifier}{element_identifier_b}"
+
+ hda_a_copy = input_a_dce.element_object.copy(copy_tags=input_a_dce.element_object.tags, flush=False)
+ hda_b_copy = input_b_dce.element_object.copy(copy_tags=input_b_dce.element_object.tags, flush=False)
+ all_copied_hdas.append(hda_a_copy)
+ all_copied_hdas.append(hda_b_copy)
+ output_a[identifier] = hda_a_copy
+ output_b[identifier] = hda_b_copy
+
+ self._add_datasets_to_history(history, all_copied_hdas)
+ output_collections.create_collection(
+ self.outputs["output_a"], "output_a", elements=output_a, propagate_hda_tags=False
+ )
+ output_collections.create_collection(
+ self.outputs["output_b"], "output_b", elements=output_b, propagate_hda_tags=False
+ )
+
+
+class CrossProductNestedCollectionTool(DatabaseOperationTool):
+ tool_type = "cross_product_nested"
+ require_terminal_states = False
+ require_dataset_ok = False
+
+ def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds):
+ input_a = incoming["input_a"]
+ input_b = incoming["input_b"]
+
+ output_a = {}
+ output_b = {}
+ all_copied_hdas = []
+
+ for input_a_dce in input_a.collection.elements:
+ element_identifier_a = input_a_dce.element_identifier
+
+ iter_elements_a = {}
+ iter_elements_b = {}
+
+ for input_b_dce in input_b.collection.elements:
+ element_identifier_b = input_b_dce.element_identifier
+
+ hda_a_copy = input_a_dce.element_object.copy(copy_tags=input_a_dce.element_object.tags, flush=False)
+ hda_b_copy = input_b_dce.element_object.copy(copy_tags=input_b_dce.element_object.tags, flush=False)
+ all_copied_hdas.append(hda_a_copy)
+ all_copied_hdas.append(hda_b_copy)
+ iter_elements_a[element_identifier_b] = hda_a_copy
+ iter_elements_b[element_identifier_b] = hda_b_copy
+
+ sub_collection_a: Dict[str, Any] = {}
+ sub_collection_a["src"] = "new_collection"
+ sub_collection_a["collection_type"] = "list"
+ sub_collection_a["elements"] = iter_elements_a
+
+ output_a[element_identifier_a] = sub_collection_a
+
+ sub_collection_b: Dict[str, Any] = {}
+ sub_collection_b["src"] = "new_collection"
+ sub_collection_b["collection_type"] = "list"
+ sub_collection_b["elements"] = iter_elements_b
+
+ output_b[element_identifier_a] = sub_collection_b
+
+ self._add_datasets_to_history(history, all_copied_hdas)
+ output_collections.create_collection(
+ self.outputs["output_a"], "output_a", elements=output_a, propagate_hda_tags=False
+ )
+ output_collections.create_collection(
+ self.outputs["output_b"], "output_b", elements=output_b, propagate_hda_tags=False
+ )
+
+
class BuildListCollectionTool(DatabaseOperationTool):
tool_type = "build_list"
require_terminal_states = False
diff --git a/lib/galaxy/tools/cross_product_flat.xml b/lib/galaxy/tools/cross_product_flat.xml
new file mode 100644
index 000000000000..891772c78a6c
--- /dev/null
+++ b/lib/galaxy/tools/cross_product_flat.xml
@@ -0,0 +1,89 @@
+
+
+
+
+ model_operation_macros.xml
+
+
+
+ operation_3436
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/galaxy/tools/cross_product_nested.xml b/lib/galaxy/tools/cross_product_nested.xml
new file mode 100644
index 000000000000..b4ba4d596de5
--- /dev/null
+++ b/lib/galaxy/tools/cross_product_nested.xml
@@ -0,0 +1,93 @@
+
+
+
+
+ model_operation_macros.xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/galaxy/tools/flatten_collection.xml b/lib/galaxy/tools/flatten_collection.xml
index 999d9b887bbd..67a96252dfeb 100644
--- a/lib/galaxy/tools/flatten_collection.xml
+++ b/lib/galaxy/tools/flatten_collection.xml
@@ -9,13 +9,12 @@
operation_2409
+
+ model_operation_macros.xml
+
-
-
-
-
-
+
@@ -35,14 +34,10 @@
-
-
-
+
-
-
-
+
diff --git a/lib/galaxy/tools/model_operation_macros.xml b/lib/galaxy/tools/model_operation_macros.xml
new file mode 100644
index 000000000000..a3f16c5398e3
--- /dev/null
+++ b/lib/galaxy/tools/model_operation_macros.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ This tool will create new history datasets copied from your input collections but your quota usage will not increase.
+
+
+ operation_3436
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/run_tests.sh b/run_tests.sh
index 7fbd26bc813c..2e04eab5eee9 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -82,6 +82,19 @@ Run a selenium test against a running server while watching client (fastest iter
. .venv/bin/activate # source the virtualenv so can skip run_tests.sh.
pytest lib/galaxy_test/selenium/test_workflow_editor.py::TestWorkflowEditor::test_data_input
+To run the tool tests for a specific framework test tool
+listed in test/functional/tools/sample_tool_conf.xml.
+
+ ./run_tests.sh -framework -id
+
+If you'd like to skip this script and run it with pytest
+directly a command like the following can be used. Note
+the framework tools run with conda installation on but 99%
+of the tools do not require this so this example includes
+disabling that.
+
+ GALAXY_TEST_TOOL_CONF="test/functional/tools/sample_tool_conf.xml" GALAXY_CONFIG_OVERRIDE_CONDA_AUTO_INIT=false pytest test/functional/test_toolbox_pytest.py -k -m tool
+
Note About Selenium Tests:
If using a local selenium driver such as a Chrome or Firefox based one
diff --git a/test/functional/tools/sample_tool_conf.xml b/test/functional/tools/sample_tool_conf.xml
index d63a21c73c4f..10a776989593 100644
--- a/test/functional/tools/sample_tool_conf.xml
+++ b/test/functional/tools/sample_tool_conf.xml
@@ -302,6 +302,8 @@
+
+