From 494722a792f2c361ed2f1ac30fc6950324d4675b Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 23 Feb 2026 22:41:28 -0500 Subject: [PATCH] Add sample_sheet workflow runtime framework tests 7 new framework workflow tests for sample_sheet collection semantics: - SAMPLE_SHEET_MAPPING (cat workflow, map over data) - SAMPLE_SHEET_MATCHES_LIST (cat_collection, canMatch reduction) - SAMPLE_SHEET_PAIRED_MAPPING_OVER_PAIRED (paired_or_unpaired workflow) - SAMPLE_SHEET_PAIRED_MATCHES_LIST_PAIRED (new list_paired workflow) - SAMPLE_SHEET_PAIRED_MAPPING_OVER_PAIRED_OR_UNPAIRED - SAMPLE_SHEET_PAIRED_OR_UNPAIRED_MATCHES_LIST_PAIRED_OR_UNPAIRED - SAMPLE_SHEET_MATCHES_SAMPLE_SHEET (new cat_sample_sheet workflow using __SAMPLE_SHEET_TO_TABULAR__ tool) Make rows optional in galactic_job_json() replacement_collection() (value.get("rows") instead of value["rows"]). Test data still provides rows: {} per element since server-side SampleSheetDatasetCollectionType requires it. Add 3 unit tests for sample_sheet collection creation in test_cwl_util.py. Add framework_test references to collection_semantics.yml for 7 examples. Co-Authored-By: Claude Opus 4.6 --- .../types/collection_semantics.yml | 14 +++ lib/galaxy/tool_util/cwl/util.py | 2 +- .../collection_semantics_cat.gxwf-tests.yml | 31 ++++++ ...on_semantics_cat_collection.gxwf-tests.yml | 25 +++++ ..._semantics_cat_sample_sheet.gxwf-tests.yml | 24 ++++ ...ection_semantics_cat_sample_sheet.gxwf.yml | 13 +++ ...ction_semantics_list_paired.gxwf-tests.yml | 27 +++++ .../collection_semantics_list_paired.gxwf.yml | 13 +++ ...ics_list_paired_or_unpaired.gxwf-tests.yml | 28 +++++ ...emantics_paired_or_unpaired.gxwf-tests.yml | 50 +++++++++ test/unit/tool_util/test_cwl_util.py | 104 ++++++++++++++++++ 11 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 lib/galaxy_test/workflow/collection_semantics_cat_sample_sheet.gxwf-tests.yml create mode 100644 lib/galaxy_test/workflow/collection_semantics_cat_sample_sheet.gxwf.yml create mode 100644 lib/galaxy_test/workflow/collection_semantics_list_paired.gxwf-tests.yml create mode 100644 lib/galaxy_test/workflow/collection_semantics_list_paired.gxwf.yml diff --git a/lib/galaxy/model/dataset_collections/types/collection_semantics.yml b/lib/galaxy/model/dataset_collections/types/collection_semantics.yml index 10e28ba627b..b4bfbff4e9e 100644 --- a/lib/galaxy/model/dataset_collections/types/collection_semantics.yml +++ b/lib/galaxy/model/dataset_collections/types/collection_semantics.yml @@ -1012,6 +1012,8 @@ invocation: {inputs: {i: {type: dataset, ref: d_n}}} output: o tests: + workflow_runtime: + framework_test: "collection_semantics_cat_4" workflow_editor: "accepts sample_sheet data -> data connection (maps like list)" - example: @@ -1031,6 +1033,8 @@ produces: o: {type: dataset} tests: + workflow_runtime: + framework_test: "collection_semantics_cat_collection_1" workflow_editor: "accepts sample_sheet -> list connection (canMatch)" - doc: | @@ -1063,6 +1067,8 @@ invocation: {inputs: {i: {type: collection, ref: "C\\_PAIRED"}}} output: o tests: + workflow_runtime: + framework_test: "collection_semantics_paired_or_unpaired_5" workflow_editor: "accepts sample_sheet:paired -> paired connection (maps over like list:paired)" - example: @@ -1082,6 +1088,8 @@ produces: o: {type: dataset} tests: + workflow_runtime: + framework_test: "collection_semantics_list_paired_0" workflow_editor: "accepts sample_sheet:paired -> list:paired connection (canMatch)" - doc: | @@ -1142,6 +1150,8 @@ inputs: i: {type: map_over, collection: C_AS_MIXED} tests: + workflow_runtime: + framework_test: "collection_semantics_paired_or_unpaired_6" workflow_editor: "accepts sample_sheet:paired -> paired_or_unpaired connection" - example: @@ -1161,6 +1171,8 @@ produces: o: {type: dataset} tests: + workflow_runtime: + framework_test: "collection_semantics_list_paired_or_unpaired_1" workflow_editor: "accepts sample_sheet:paired_or_unpaired -> list:paired_or_unpaired connection (canMatch)" - doc: | @@ -1223,4 +1235,6 @@ produces: o: {type: dataset} tests: + workflow_runtime: + framework_test: "collection_semantics_cat_sample_sheet_0" workflow_editor: "accepts sample_sheet -> sample_sheet connection" diff --git a/lib/galaxy/tool_util/cwl/util.py b/lib/galaxy/tool_util/cwl/util.py index 2a455085aa0..fde6669af57 100644 --- a/lib/galaxy/tool_util/cwl/util.py +++ b/lib/galaxy/tool_util/cwl/util.py @@ -365,7 +365,7 @@ def galactic_job_json( elements = to_elements(value, collection_type) kwds = {} if collection_type.startswith("sample_sheet"): - kwds["rows"] = value["rows"] + kwds["rows"] = value.get("rows") if "name" in value: kwds["name"] = value["name"] collection = collection_create_func(elements, collection_type, **kwds) diff --git a/lib/galaxy_test/workflow/collection_semantics_cat.gxwf-tests.yml b/lib/galaxy_test/workflow/collection_semantics_cat.gxwf-tests.yml index 8139f677173..8b682a6ab98 100644 --- a/lib/galaxy_test/workflow/collection_semantics_cat.gxwf-tests.yml +++ b/lib/galaxy_test/workflow/collection_semantics_cat.gxwf-tests.yml @@ -108,3 +108,34 @@ asserts: - that: has_text text: "reverse content" + +- doc: | + SAMPLE_SHEET_MAPPING: A sample_sheet mapped over a data input produces + a sample_sheet output collection. + job: + input1: + class: Collection + collection_type: sample_sheet + rows: + el1: [] + el2: [] + elements: + - identifier: el1 + class: File + contents: "element 1" + - identifier: el2 + class: File + contents: "element 2" + outputs: + wf_output: + class: Collection + collection_type: sample_sheet + elements: + el1: + asserts: + - that: has_text + text: "element 1" + el2: + asserts: + - that: has_text + text: "element 2" diff --git a/lib/galaxy_test/workflow/collection_semantics_cat_collection.gxwf-tests.yml b/lib/galaxy_test/workflow/collection_semantics_cat_collection.gxwf-tests.yml index 086cbb77fa0..36b84a84ceb 100644 --- a/lib/galaxy_test/workflow/collection_semantics_cat_collection.gxwf-tests.yml +++ b/lib/galaxy_test/workflow/collection_semantics_cat_collection.gxwf-tests.yml @@ -19,3 +19,28 @@ text: "element 1" - that: has_text text: "element 2" + +- doc: | + SAMPLE_SHEET_MATCHES_LIST: A sample_sheet collection consumed by a list + collection input via canMatch (reduction). + job: + input1: + class: Collection + collection_type: sample_sheet + rows: + el1: [] + el2: [] + elements: + - identifier: el1 + class: File + contents: "element 1" + - identifier: el2 + class: File + contents: "element 2" + outputs: + wf_output: + asserts: + - that: has_text + text: "element 1" + - that: has_text + text: "element 2" diff --git a/lib/galaxy_test/workflow/collection_semantics_cat_sample_sheet.gxwf-tests.yml b/lib/galaxy_test/workflow/collection_semantics_cat_sample_sheet.gxwf-tests.yml new file mode 100644 index 00000000000..dc70eb23d0d --- /dev/null +++ b/lib/galaxy_test/workflow/collection_semantics_cat_sample_sheet.gxwf-tests.yml @@ -0,0 +1,24 @@ +- doc: | + SAMPLE_SHEET_MATCHES_SAMPLE_SHEET: A sample_sheet consumed by a sample_sheet + collection input (identity match, reduction). + job: + input1: + class: Collection + collection_type: sample_sheet + rows: + el1: [] + el2: [] + elements: + - identifier: el1 + class: File + contents: "element 1" + - identifier: el2 + class: File + contents: "element 2" + outputs: + wf_output: + asserts: + - that: has_text + text: "el1" + - that: has_text + text: "el2" diff --git a/lib/galaxy_test/workflow/collection_semantics_cat_sample_sheet.gxwf.yml b/lib/galaxy_test/workflow/collection_semantics_cat_sample_sheet.gxwf.yml new file mode 100644 index 00000000000..033361052f2 --- /dev/null +++ b/lib/galaxy_test/workflow/collection_semantics_cat_sample_sheet.gxwf.yml @@ -0,0 +1,13 @@ +class: GalaxyWorkflow +inputs: + input1: + type: collection + collection_type: sample_sheet +outputs: + wf_output: + outputSource: tool_step/output +steps: + tool_step: + tool_id: __SAMPLE_SHEET_TO_TABULAR__ + in: + input: input1 diff --git a/lib/galaxy_test/workflow/collection_semantics_list_paired.gxwf-tests.yml b/lib/galaxy_test/workflow/collection_semantics_list_paired.gxwf-tests.yml new file mode 100644 index 00000000000..13aca3bc2f7 --- /dev/null +++ b/lib/galaxy_test/workflow/collection_semantics_list_paired.gxwf-tests.yml @@ -0,0 +1,27 @@ +- doc: | + SAMPLE_SHEET_PAIRED_MATCHES_LIST_PAIRED: A sample_sheet:paired consumed by a + list:paired collection input via canMatch (reduction). + job: + input1: + class: Collection + collection_type: sample_sheet:paired + rows: + el1: [] + elements: + - identifier: el1 + class: Collection + type: paired + elements: + - identifier: forward + class: File + contents: "forward content" + - identifier: reverse + class: File + contents: "reverse content" + outputs: + wf_output: + asserts: + - that: has_text + text: "identifier is el1" + - that: has_text + text: "collection_type" diff --git a/lib/galaxy_test/workflow/collection_semantics_list_paired.gxwf.yml b/lib/galaxy_test/workflow/collection_semantics_list_paired.gxwf.yml new file mode 100644 index 00000000000..d1671ce66e2 --- /dev/null +++ b/lib/galaxy_test/workflow/collection_semantics_list_paired.gxwf.yml @@ -0,0 +1,13 @@ +class: GalaxyWorkflow +inputs: + input1: + type: collection + collection_type: list:paired +outputs: + wf_output: + outputSource: tool_step/out1 +steps: + tool_step: + tool_id: collection_paired_or_list_paired_input + in: + f1: input1 diff --git a/lib/galaxy_test/workflow/collection_semantics_list_paired_or_unpaired.gxwf-tests.yml b/lib/galaxy_test/workflow/collection_semantics_list_paired_or_unpaired.gxwf-tests.yml index 4f760a7639f..9a31554ecac 100644 --- a/lib/galaxy_test/workflow/collection_semantics_list_paired_or_unpaired.gxwf-tests.yml +++ b/lib/galaxy_test/workflow/collection_semantics_list_paired_or_unpaired.gxwf-tests.yml @@ -23,3 +23,31 @@ text: "forward content" - that: has_text text: "reverse content" + +- doc: | + SAMPLE_SHEET_PAIRED_OR_UNPAIRED_MATCHES_LIST_PAIRED_OR_UNPAIRED: A + sample_sheet:paired_or_unpaired consumed by list:paired_or_unpaired via canMatch. + job: + input1: + class: Collection + collection_type: sample_sheet:paired_or_unpaired + rows: + el1: [] + elements: + - identifier: el1 + class: Collection + type: paired_or_unpaired + elements: + - identifier: forward + class: File + contents: "forward content" + - identifier: reverse + class: File + contents: "reverse content" + outputs: + wf_output: + asserts: + - that: has_text + text: "forward content" + - that: has_text + text: "reverse content" diff --git a/lib/galaxy_test/workflow/collection_semantics_paired_or_unpaired.gxwf-tests.yml b/lib/galaxy_test/workflow/collection_semantics_paired_or_unpaired.gxwf-tests.yml index 8db03048fac..cb3a59908ff 100644 --- a/lib/galaxy_test/workflow/collection_semantics_paired_or_unpaired.gxwf-tests.yml +++ b/lib/galaxy_test/workflow/collection_semantics_paired_or_unpaired.gxwf-tests.yml @@ -114,3 +114,53 @@ wf_output: class: Collection collection_type: list + +- doc: | + SAMPLE_SHEET_PAIRED_MAPPING_OVER_PAIRED: A sample_sheet:paired mapped over + a paired_or_unpaired tool input produces a sample_sheet output. + job: + input1: + class: Collection + collection_type: sample_sheet:paired + rows: + el1: [] + elements: + - identifier: el1 + class: Collection + type: paired + elements: + - identifier: forward + class: File + contents: "forward content" + - identifier: reverse + class: File + contents: "reverse content" + outputs: + wf_output: + class: Collection + collection_type: sample_sheet + +- doc: | + SAMPLE_SHEET_PAIRED_MAPPING_OVER_PAIRED_OR_UNPAIRED: A sample_sheet:paired + mapped over paired_or_unpaired (equiv to sample_sheet:paired_or_unpaired mapping). + job: + input1: + class: Collection + collection_type: sample_sheet:paired + rows: + el1: [] + elements: + - identifier: el1 + class: Collection + type: paired + elements: + - identifier: forward + class: File + contents: "forward content" + - identifier: reverse + class: File + contents: "reverse content" + outputs: + wf_output: + class: Collection + collection_type: sample_sheet diff --git a/test/unit/tool_util/test_cwl_util.py b/test/unit/tool_util/test_cwl_util.py index 57881e15981..84c513c76bb 100644 --- a/test/unit/tool_util/test_cwl_util.py +++ b/test/unit/tool_util/test_cwl_util.py @@ -151,3 +151,107 @@ def test_galactic_job_json_collection_element_filetype(): for target in captured_targets: assert isinstance(target, FileLiteralTarget) assert target.properties.get("filetype") == "fastqsanger" + + +def test_galactic_job_json_sample_sheet_collection_without_rows(): + """sample_sheet collection works without rows metadata.""" + created_collections = [] + + def collection_create_func(element_identifiers, collection_type, rows=None, name=None): + created_collections.append( + {"element_identifiers": element_identifiers, "collection_type": collection_type, "rows": rows, "name": name} + ) + return {"id": f"collection_{collection_type}"} + + job = { + "input1": { + "class": "Collection", + "collection_type": "sample_sheet", + "elements": [ + {"identifier": "el1", "class": "File", "contents": "element 1"}, + {"identifier": "el2", "class": "File", "contents": "element 2"}, + ], + } + } + result_job, datasets = galactic_job_json( + job, ".", _mock_upload_func, collection_create_func, tool_or_workflow="workflow" + ) + assert len(created_collections) == 1 + coll = created_collections[0] + assert coll["collection_type"] == "sample_sheet" + assert coll["rows"] is None + assert len(coll["element_identifiers"]) == 2 + assert coll["element_identifiers"][0]["name"] == "el1" + assert coll["element_identifiers"][1]["name"] == "el2" + + +def test_galactic_job_json_sample_sheet_collection_with_rows(): + """sample_sheet collection passes rows metadata through.""" + created_collections = [] + + def collection_create_func(element_identifiers, collection_type, rows=None, name=None): + created_collections.append( + {"element_identifiers": element_identifiers, "collection_type": collection_type, "rows": rows, "name": name} + ) + return {"id": f"collection_{collection_type}"} + + job = { + "input1": { + "class": "Collection", + "collection_type": "sample_sheet", + "elements": [ + {"identifier": "el1", "class": "File", "contents": "element 1"}, + {"identifier": "el2", "class": "File", "contents": "element 2"}, + ], + "rows": {"el1": {"condition": "treatment"}, "el2": {"condition": "control"}}, + } + } + result_job, datasets = galactic_job_json( + job, ".", _mock_upload_func, collection_create_func, tool_or_workflow="workflow" + ) + assert len(created_collections) == 1 + coll = created_collections[0] + assert coll["collection_type"] == "sample_sheet" + assert coll["rows"] == {"el1": {"condition": "treatment"}, "el2": {"condition": "control"}} + + +def test_galactic_job_json_sample_sheet_paired_collection(): + """sample_sheet:paired creates nested collection with paired sub-elements.""" + created_collections = [] + + def collection_create_func(element_identifiers, collection_type, rows=None, name=None): + created_collections.append( + {"element_identifiers": element_identifiers, "collection_type": collection_type, "rows": rows, "name": name} + ) + return {"id": f"collection_{collection_type}"} + + job = { + "input1": { + "class": "Collection", + "collection_type": "sample_sheet:paired", + "elements": [ + { + "identifier": "sample1", + "class": "Collection", + "type": "paired", + "elements": [ + {"identifier": "forward", "class": "File", "contents": "fwd reads"}, + {"identifier": "reverse", "class": "File", "contents": "rev reads"}, + ], + } + ], + } + } + result_job, datasets = galactic_job_json( + job, ".", _mock_upload_func, collection_create_func, tool_or_workflow="workflow" + ) + assert len(created_collections) == 1 + coll = created_collections[0] + assert coll["collection_type"] == "sample_sheet:paired" + assert coll["rows"] is None + assert len(coll["element_identifiers"]) == 1 + el = coll["element_identifiers"][0] + assert el["name"] == "sample1" + assert el["src"] == "new_collection" + assert el["collection_type"] == "paired" + assert len(el["element_identifiers"]) == 2