diff --git a/lib/galaxy/config/sample/tool_conf.xml.sample b/lib/galaxy/config/sample/tool_conf.xml.sample
index 841f054b93d0..faf441a119cc 100644
--- a/lib/galaxy/config/sample/tool_conf.xml.sample
+++ b/lib/galaxy/config/sample/tool_conf.xml.sample
@@ -32,6 +32,7 @@
+
diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py
index 9967c08fe667..b56d0f517b62 100644
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -4723,8 +4723,10 @@ def set_skipped(self, object_store_populator: "ObjectStorePopulator") -> None:
self.state = self.states.OK
self.blurb = "skipped"
self.visible = False
+ null = json.dumps(None)
with open(self.dataset.get_file_name(), "w") as out:
- out.write(json.dumps(None))
+ out.write(null)
+ self.peek = null
self.set_total_size()
def get_file_name(self, sync_cache: bool = True) -> str:
diff --git a/lib/galaxy/tool_util/models.py b/lib/galaxy/tool_util/models.py
index eb76e1d711ef..3f9946e30404 100644
--- a/lib/galaxy/tool_util/models.py
+++ b/lib/galaxy/tool_util/models.py
@@ -156,6 +156,7 @@ class TestCollectionOutputAssertions(StrictModel):
class_: Optional[Literal["Collection"]] = Field("Collection", alias="class")
elements: Optional[Dict[str, TestCollectionElementAssertion]] = None
element_tests: Optional[Dict[str, "TestCollectionElementAssertion"]] = None
+ element_count: Optional[int] = None
attributes: Optional[CollectionAttributes] = None
collection_type: CollectionType = None
diff --git a/lib/galaxy/tool_util/parser/interface.py b/lib/galaxy/tool_util/parser/interface.py
index a52e39e0200b..9b4bac4e0459 100644
--- a/lib/galaxy/tool_util/parser/interface.py
+++ b/lib/galaxy/tool_util/parser/interface.py
@@ -868,10 +868,13 @@ def matches(ie_list: List, rel_path: str):
class TestCollectionOutputDef:
__test__ = False # Prevent pytest from discovering this class (issue #12071)
- def __init__(self, name, attrib, element_tests):
+ def __init__(self, name, attrib, element_tests, element_count: Optional[int] = None):
self.name = name
self.collection_type = attrib.get("type", None)
- count = attrib.get("count", None)
+ if element_count is not None:
+ count = element_count
+ else:
+ count = attrib.get("count")
self.count = int(count) if count is not None else None
self.attrib = attrib
self.element_tests = element_tests
@@ -881,7 +884,8 @@ def from_dict(as_dict):
return TestCollectionOutputDef(
name=as_dict["name"],
attrib=as_dict.get("attributes", {}),
- element_tests=as_dict["element_tests"],
+ element_tests=as_dict.get("element_tests"),
+ element_count=as_dict.get("element_count"),
)
@staticmethod
@@ -898,7 +902,7 @@ def from_yaml_test_format(as_dict):
return TestCollectionOutputDef.from_dict(as_dict)
def to_dict(self):
- return dict(name=self.name, attributes=self.attrib, element_tests=self.element_tests)
+ return dict(name=self.name, attributes=self.attrib, element_tests=self.element_tests, element_count=self.count)
class DrillDownOptionsDict(TypedDict):
diff --git a/lib/galaxy/tool_util/verify/interactor.py b/lib/galaxy/tool_util/verify/interactor.py
index 9e1dd5bf87d4..0185b6aff5b8 100644
--- a/lib/galaxy/tool_util/verify/interactor.py
+++ b/lib/galaxy/tool_util/verify/interactor.py
@@ -1132,7 +1132,7 @@ def verify_collection(output_collection_def, data_collection, verify_dataset):
raise AssertionError(message)
expected_element_count = output_collection_def.count
- if expected_element_count:
+ if expected_element_count is not None:
actual_element_count = len(data_collection["elements"])
if expected_element_count != actual_element_count:
message = f"Output collection '{name}': expected to have {expected_element_count} elements, but it had {actual_element_count}."
@@ -1185,7 +1185,8 @@ def verify_elements(element_objects, element_tests):
message = f"Output collection '{name}': identifier '{identifier}' found out of order, expected order of {expected_sort_order} for the tool generated collection elements {eo_ids}"
raise AssertionError(message)
- verify_elements(data_collection["elements"], output_collection_def.element_tests)
+ if output_collection_def.element_tests:
+ verify_elements(data_collection["elements"], output_collection_def.element_tests)
def _verify_composite_datatype_file_content(
diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py
index c085ac512699..a7b6c96bc1f3 100644
--- a/lib/galaxy/tools/__init__.py
+++ b/lib/galaxy/tools/__init__.py
@@ -3798,7 +3798,15 @@ class FilterNullTool(FilterDatasetsTool):
def element_is_valid(element: model.DatasetCollectionElement):
element_object = element.element_object
assert isinstance(element_object, model.DatasetInstance)
- return element_object.extension == "expression.json" and element_object.blurb == "skipped"
+ if element_object.extension == "expression.json":
+ if element_object.peek == "null":
+ # shortcut
+ return False
+ else:
+ with open(element_object.get_file_name()) as fh:
+ if fh.read(5) == "null":
+ return False
+ return True
class FlattenTool(DatabaseOperationTool):
diff --git a/lib/galaxy/tools/filter_null.xml b/lib/galaxy/tools/filter_null.xml
new file mode 100644
index 000000000000..06fa4745ee94
--- /dev/null
+++ b/lib/galaxy/tools/filter_null.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+ operation_3695
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/galaxy_test/workflow/filter_null.gxwf-tests.yml b/lib/galaxy_test/workflow/filter_null.gxwf-tests.yml
new file mode 100644
index 000000000000..7572b8c5c623
--- /dev/null
+++ b/lib/galaxy_test/workflow/filter_null.gxwf-tests.yml
@@ -0,0 +1,32 @@
+- doc: |
+ Test to verify filter null tool keeps non-null datasets.
+ job:
+ input_collection:
+ collection_type: list
+ elements:
+ - identifier: first
+ content: "abc"
+ when:
+ value: true
+ type: raw
+ outputs:
+ out:
+ class: Collection
+ collection_type: list
+ element_count: 1
+- doc: |
+ Test to verify filter null tool discards null datasets.
+ job:
+ input_collection:
+ collection_type: list
+ elements:
+ - identifier: first
+ content: "abc"
+ when:
+ value: false
+ type: raw
+ outputs:
+ out:
+ class: Collection
+ collection_type: list
+ element_count: 0
diff --git a/lib/galaxy_test/workflow/filter_null.gxwf.yml b/lib/galaxy_test/workflow/filter_null.gxwf.yml
new file mode 100644
index 000000000000..b2e35edc36c0
--- /dev/null
+++ b/lib/galaxy_test/workflow/filter_null.gxwf.yml
@@ -0,0 +1,22 @@
+class: GalaxyWorkflow
+inputs:
+ input_collection:
+ type: data_collection
+ when:
+ type: boolean
+outputs:
+ out:
+ outputSource: filter_null/output
+steps:
+ cat:
+ tool_id: cat
+ in:
+ input1:
+ source: input_collection
+ when:
+ source: when
+ when: $(inputs.when)
+ filter_null:
+ tool_id: '__FILTER_NULL__'
+ in:
+ input: cat/out_file1
diff --git a/lib/galaxy_test/workflow/test_framework_workflows.py b/lib/galaxy_test/workflow/test_framework_workflows.py
index d879a1f310ae..a358433025ca 100644
--- a/lib/galaxy_test/workflow/test_framework_workflows.py
+++ b/lib/galaxy_test/workflow/test_framework_workflows.py
@@ -74,7 +74,9 @@ def _verify(self, run_summary: RunJobsSummary, output_definitions: OutputsDict):
self._verify_output(run_summary, output_name, output_definition)
def _verify_output(self, run_summary: RunJobsSummary, output_name, test_properties: OutputChecks):
- is_collection_test = isinstance(test_properties, dict) and "elements" in test_properties
+ is_collection_test = isinstance(test_properties, dict) and (
+ "elements" in test_properties or test_properties.get("class") == "Collection"
+ )
item_label = f"Output named {output_name}"
def get_filename(name):
diff --git a/test/functional/tools/sample_tool_conf.xml b/test/functional/tools/sample_tool_conf.xml
index ebc4fb3cbd59..bb9d7568f600 100644
--- a/test/functional/tools/sample_tool_conf.xml
+++ b/test/functional/tools/sample_tool_conf.xml
@@ -303,6 +303,7 @@
+