Comma-separated collection type specs, deeper E2E tests.

- Add gx_data_collection_list_paired_or_list_list and
  gx_data_collection_list_or_list_paired tool definitions + specs
- Add job_runtime valid/invalid specs for comma-separated type unions
- Add E2E test for list:list:paired, record:paired dynamic models
- Tighten nested collection assertions to use dynamic model types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John Chilton
2026-02-12 15:58:17 -05:00
co-authored by Claude Opus 4.6
parent 7493fed862
commit 30b167c7ed
5 changed files with 389 additions and 5 deletions
@@ -0,0 +1,18 @@
class: GalaxyTool
id: gx_data_collection_list_or_list_paired
name: gx_data_collection_list_or_list_paired
version: "1.0.0"
profile: "25.0"
container: busybox
shell_command: |
echo "Collection type: $(inputs.parameter.collection_type)" > output.txt
inputs:
- name: parameter
type: data_collection
collection_type: "list,list:paired"
label: Input list or list:paired collection
outputs:
- name: output
type: data
format: txt
from_work_dir: output.txt
@@ -0,0 +1,18 @@
class: GalaxyTool
id: gx_data_collection_list_paired_or_list_list
name: gx_data_collection_list_paired_or_list_list
version: "1.0.0"
profile: "25.0"
container: busybox
shell_command: |
echo "Collection type: $(inputs.parameter.collection_type)" > output.txt
inputs:
- name: parameter
type: data_collection
collection_type: "list:paired,list:list"
label: Input list:paired or list:list collection
outputs:
- name: output
type: data
format: txt
from_work_dir: output.txt
@@ -304,6 +304,8 @@
<tool file="parameters/gx_data_collection_list_list.yml" />
<tool file="parameters/gx_data_collection_list_list_paired.yml" />
<tool file="parameters/gx_data_collection_list_or_record.yml" />
<tool file="parameters/gx_data_collection_list_paired_or_list_list.yml" />
<tool file="parameters/gx_data_collection_list_or_list_paired.yml" />
<tool file="parameters/gx_data_collection_paired_or_pou.yml" />
<section id="filter" name="For Tours">
@@ -3709,6 +3709,240 @@ gx_data_collection_paired_or_pou:
format: "txt"
size: 100
gx_data_collection_list_paired_or_list_list:
request_valid:
- parameter: {src: hdca, id: abcdabcd}
request_invalid:
- parameter: {src: hdca, id: 7}
- parameter: null
- {}
request_internal_valid:
- parameter: {src: hdca, id: 5}
request_internal_invalid:
- parameter: {src: hdca, id: abcdabcd}
- parameter: null
- {}
job_internal_valid:
- parameter: {src: hdca, id: 5}
job_internal_invalid:
- parameter: {src: hdca, id: abcdabcd}
- {}
job_runtime_valid:
# list:paired is accepted
- parameter:
class: Collection
name: test_lp
collection_type: "list:paired"
tags: []
elements:
- class: Collection
name: p1
collection_type: paired
tags: []
elements:
forward:
class: File
element_identifier: forward
basename: "f.txt"
location: "step_input://0"
path: "/tmp/f.txt"
nameroot: "f"
nameext: ".txt"
format: "txt"
size: 100
reverse:
class: File
element_identifier: reverse
basename: "r.txt"
location: "step_input://1"
path: "/tmp/r.txt"
nameroot: "r"
nameext: ".txt"
format: "txt"
size: 100
# list:list is also accepted
- parameter:
class: Collection
name: test_ll
collection_type: "list:list"
tags: []
elements:
- class: Collection
name: inner1
collection_type: list
tags: []
elements:
- class: File
element_identifier: f1
basename: "f1.txt"
location: "step_input://0"
path: "/tmp/f1.txt"
nameroot: "f1"
nameext: ".txt"
format: "txt"
size: 100
job_runtime_invalid:
- parameter: {src: hdca, id: 5}
- {}
# Simple paired -- not in union (union is list:paired, list:list)
- parameter:
class: Collection
name: wrong_type
collection_type: paired
tags: []
elements:
forward:
class: File
element_identifier: forward
basename: "f.txt"
location: "step_input://0"
path: "/tmp/f.txt"
nameroot: "f"
nameext: ".txt"
format: "txt"
size: 100
reverse:
class: File
element_identifier: reverse
basename: "r.txt"
location: "step_input://1"
path: "/tmp/r.txt"
nameroot: "r"
nameext: ".txt"
format: "txt"
size: 100
# Simple list -- not in union (union is list:paired, list:list, not plain list)
- parameter:
class: Collection
name: wrong_type_simple_list
collection_type: list
tags: []
elements:
- class: File
element_identifier: f1
basename: "f1.txt"
location: "step_input://0"
path: "/tmp/f1.txt"
nameroot: "f1"
nameext: ".txt"
format: "txt"
size: 100
gx_data_collection_list_or_list_paired:
request_valid:
- parameter: {src: hdca, id: abcdabcd}
request_invalid:
- parameter: {src: hdca, id: 7}
- parameter: null
- {}
request_internal_valid:
- parameter: {src: hdca, id: 5}
request_internal_invalid:
- parameter: {src: hdca, id: abcdabcd}
- parameter: null
- {}
job_internal_valid:
- parameter: {src: hdca, id: 5}
job_internal_invalid:
- parameter: {src: hdca, id: abcdabcd}
- {}
job_runtime_valid:
# Simple list is accepted
- parameter:
class: Collection
name: test_list
collection_type: list
tags: []
elements:
- class: File
element_identifier: f1
basename: "f1.txt"
location: "step_input://0"
path: "/tmp/f1.txt"
nameroot: "f1"
nameext: ".txt"
format: "txt"
size: 100
# list:paired is also accepted
- parameter:
class: Collection
name: test_lp
collection_type: "list:paired"
tags: []
elements:
- class: Collection
name: p1
collection_type: paired
tags: []
elements:
forward:
class: File
element_identifier: forward
basename: "f.txt"
location: "step_input://0"
path: "/tmp/f.txt"
nameroot: "f"
nameext: ".txt"
format: "txt"
size: 100
reverse:
class: File
element_identifier: reverse
basename: "r.txt"
location: "step_input://1"
path: "/tmp/r.txt"
nameroot: "r"
nameext: ".txt"
format: "txt"
size: 100
job_runtime_invalid:
- parameter: {src: hdca, id: 5}
- {}
# Paired -- not in union (union is list, list:paired)
- parameter:
class: Collection
name: wrong_type
collection_type: paired
tags: []
elements:
forward:
class: File
element_identifier: forward
basename: "f.txt"
location: "step_input://0"
path: "/tmp/f.txt"
nameroot: "f"
nameext: ".txt"
format: "txt"
size: 100
reverse:
class: File
element_identifier: reverse
basename: "r.txt"
location: "step_input://1"
path: "/tmp/r.txt"
nameroot: "r"
nameext: ".txt"
format: "txt"
size: 100
# Record -- not in union
- parameter:
class: Collection
name: wrong_type_record
collection_type: record
tags: []
elements:
field1:
class: File
element_identifier: field1
basename: "f1.txt"
location: "step_input://0"
path: "/tmp/f1.txt"
nameroot: "f1"
nameext: ".txt"
format: "txt"
size: 100
cwl_int:
request_valid:
- parameter: 5
+117 -5
View File
@@ -123,14 +123,13 @@ def test_collection_to_runtime_nested(mock_adapt_dataset):
compute_environment=None
)
assert isinstance(runtime, DataCollectionInternalJsonBase)
expected_model = build_collection_model_for_type("list:paired")
assert isinstance(runtime, expected_model)
assert type(runtime).__name__ == "DynamicCollection_list_paired"
assert runtime.collection_type == "list:paired"
assert len(runtime.elements) == 1
inner = runtime.elements[0]
# It should be a paired runtime because that's what we put in
# With the updated nested model, Pydantic should match the inner dict to DataCollectionPairedRuntime
assert isinstance(inner, DataCollectionPairedRuntime)
assert inner.elements.forward.class_ == "File"
@@ -320,3 +319,116 @@ def test_dynamic_model_caching():
m1 = build_collection_model_for_type("list:paired")
m2 = build_collection_model_for_type("list:paired")
assert m1 is m2
# --- Step 2: E2E list:list:paired (gap #1) ---
def test_collection_to_runtime_deeply_nested(mock_adapt_dataset):
"""End-to-end: list:list:paired through collection_to_runtime pipeline."""
paired_data = ["paired", [("forward", "hda1"), ("reverse", "hda2")]]
list_paired_data = ["list:paired", [("p1", paired_data)]]
collection = create_model_collection("list:list:paired", [("batch1", list_paired_data)])
runtime = collection_to_runtime(
collection,
name="test_deeply_nested",
tags=[],
adapt_dataset=mock_adapt_dataset,
compute_environment=None,
)
expected_model = build_collection_model_for_type("list:list:paired")
assert isinstance(runtime, expected_model)
assert runtime.collection_type == "list:list:paired"
assert len(runtime.elements) == 1
middle = runtime.elements[0]
expected_middle = build_collection_model_for_type("list:paired")
assert isinstance(middle, expected_middle)
assert middle.collection_type == "list:paired"
assert len(middle.elements) == 1
inner = middle.elements[0]
assert isinstance(inner, DataCollectionPairedRuntime)
assert inner.elements.forward.class_ == "File"
# --- Step 3: record:* nested dynamic models (gap #2) ---
def test_dynamic_model_record_paired_accepts_correct():
"""record:paired dynamic model: dict of paired collections."""
model = build_collection_model_for_type("record:paired")
assert model is not None
result = model.model_validate({
"class": "Collection", "name": "rec",
"collection_type": "record:paired", "tags": [],
"elements": {
"sample_a": {
"class": "Collection", "name": "sample_a",
"collection_type": "paired", "tags": [],
"elements": {
"forward": {**_FILE_ELEMENT, "element_identifier": "forward"},
"reverse": {**_FILE_ELEMENT, "element_identifier": "reverse",
"basename": "r.txt", "location": "step_input://1",
"path": "/tmp/r.txt", "nameroot": "r"},
},
},
},
})
assert result.collection_type == "record:paired"
assert isinstance(result.elements["sample_a"], DataCollectionPairedRuntime)
def test_dynamic_model_record_paired_rejects_wrong_inner():
"""record:paired rejects inner collection with wrong type (list instead of paired)."""
model = build_collection_model_for_type("record:paired")
with pytest.raises(ValidationError):
model.model_validate({
"class": "Collection", "name": "bad_rec",
"collection_type": "record:paired", "tags": [],
"elements": {
"sample_a": {
"class": "Collection", "name": "sample_a",
"collection_type": "list", "tags": [],
"elements": [_FILE_ELEMENT],
},
},
})
# --- Step 4: Unknown nested segments return None (gap #5) ---
def test_build_collection_model_returns_none_for_unknown_nested():
assert build_collection_model_for_type("list:unknown_type") is None
assert build_collection_model_for_type("record:unknown") is None
assert build_collection_model_for_type("list:list:unknown") is None
# --- Step 5: Empty dict elements on record-like dynamic model (gap #6) ---
def test_dynamic_model_record_paired_empty_elements():
"""Empty dict elements on a record-like dynamic model should validate."""
model = build_collection_model_for_type("record:paired")
result = model.model_validate({
"class": "Collection", "name": "empty_rec",
"collection_type": "record:paired", "tags": [],
"elements": {},
})
assert result.elements == {}
# --- Step 6: JSON schema for deeply nested models (gap #7) ---
def test_dynamic_model_json_schema_deeply_nested():
"""JSON Schema for list:list:paired has 3 levels, no anyOf at any level."""
model = build_collection_model_for_type("list:list:paired")
schema = model.model_json_schema()
# Outer elements: array of list:paired models
elements_schema = schema["properties"]["elements"]
schema_str = str(elements_schema)
assert "anyOf" not in schema_str
# Should have definitions for multiple distinct models (at least the inner dynamic + leaf)
defs = schema.get("$defs", {})
assert len(defs) >= 2
assert any("list_paired" in k for k in defs.keys())