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 @@ +