mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-29 17:42:12 +08:00
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:
co-authored by
Claude Opus 4.6
parent
7493fed862
commit
30b167c7ed
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user