mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-11-03 12:53:35 +08:00
- collection_runtime_discriminator: raise ValueError for unknown types - _runtime_model_for_collection_type: remove nested-type fallback to generic models - py_type_internal_json: raise on unrecognized declared collection_type - _validate_collection_runtime_dict: raise instead of trying list/generic models - Add tests for discriminator routing, unknown type rejection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
276 lines
10 KiB
Python
276 lines
10 KiB
Python
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from galaxy.tool_util_models.parameters import (
|
|
build_collection_model_for_type,
|
|
collection_runtime_discriminator,
|
|
DataCollectionListRuntime,
|
|
DataCollectionPairedRuntime,
|
|
DataCollectionRecordRuntime,
|
|
)
|
|
|
|
_FILE_ELEMENT = {
|
|
"class": "File",
|
|
"element_identifier": "f1",
|
|
"basename": "f1.txt",
|
|
"location": "step_input://0",
|
|
"path": "/tmp/f1.txt",
|
|
"nameroot": "f1",
|
|
"nameext": ".txt",
|
|
"format": "txt",
|
|
"size": 100,
|
|
}
|
|
|
|
|
|
def test_build_collection_model_returns_static_for_leaf():
|
|
assert build_collection_model_for_type("list") is DataCollectionListRuntime
|
|
assert build_collection_model_for_type("paired") is DataCollectionPairedRuntime
|
|
assert build_collection_model_for_type("record") is DataCollectionRecordRuntime
|
|
|
|
|
|
def test_build_collection_model_returns_none_for_unknown():
|
|
assert build_collection_model_for_type("unknown_type") is None
|
|
|
|
|
|
def test_dynamic_model_accepts_correct_inner_type():
|
|
model = build_collection_model_for_type("list:paired")
|
|
assert model is not None
|
|
result = model.model_validate(
|
|
{
|
|
"class": "Collection",
|
|
"name": "good",
|
|
"collection_type": "list:paired",
|
|
"tags": [],
|
|
"elements": [
|
|
{
|
|
"class": "Collection",
|
|
"name": "p1",
|
|
"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 == "list:paired"
|
|
assert isinstance(result.elements[0], DataCollectionPairedRuntime)
|
|
|
|
|
|
def test_dynamic_model_rejects_wrong_inner_type():
|
|
model = build_collection_model_for_type("list:paired")
|
|
assert model is not None
|
|
with pytest.raises(ValidationError):
|
|
model.model_validate(
|
|
{
|
|
"class": "Collection",
|
|
"name": "bad",
|
|
"collection_type": "list:paired",
|
|
"tags": [],
|
|
"elements": [
|
|
{
|
|
"class": "Collection",
|
|
"name": "l1",
|
|
"collection_type": "list",
|
|
"tags": [],
|
|
"elements": [_FILE_ELEMENT],
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
|
|
def test_dynamic_model_rejects_wrong_collection_type_literal():
|
|
model = build_collection_model_for_type("list:paired")
|
|
assert model is not None
|
|
with pytest.raises(ValidationError):
|
|
model.model_validate(
|
|
{
|
|
"class": "Collection",
|
|
"name": "bad",
|
|
"collection_type": "list:list",
|
|
"tags": [],
|
|
"elements": [],
|
|
}
|
|
)
|
|
|
|
|
|
def test_dynamic_model_rejects_depth_mismatch():
|
|
model = build_collection_model_for_type("list:list:paired")
|
|
assert model is not None
|
|
# Inner is "paired" (depth 1) but should be "list:paired" (depth 2)
|
|
with pytest.raises(ValidationError):
|
|
model.model_validate(
|
|
{
|
|
"class": "Collection",
|
|
"name": "bad",
|
|
"collection_type": "list:list:paired",
|
|
"tags": [],
|
|
"elements": [
|
|
{
|
|
"class": "Collection",
|
|
"name": "p1",
|
|
"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",
|
|
},
|
|
},
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
|
|
def test_dynamic_model_json_schema_precise():
|
|
"""JSON Schema for list:paired shows elements as array of paired, not Union."""
|
|
model = build_collection_model_for_type("list:paired")
|
|
assert model is not None
|
|
schema = model.model_json_schema()
|
|
# elements should reference paired model, NOT anyOf with all types
|
|
elements_schema = schema["properties"]["elements"]
|
|
schema_str = str(elements_schema)
|
|
assert "anyOf" not in schema_str
|
|
|
|
|
|
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
|
|
|
|
|
|
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")
|
|
assert model is not None
|
|
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],
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
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")
|
|
assert model is not None
|
|
result = model.model_validate(
|
|
{
|
|
"class": "Collection",
|
|
"name": "empty_rec",
|
|
"collection_type": "record:paired",
|
|
"tags": [],
|
|
"elements": {},
|
|
}
|
|
)
|
|
assert result.elements == {}
|
|
|
|
|
|
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")
|
|
assert model is not None
|
|
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())
|
|
|
|
|
|
def test_collection_runtime_discriminator_known_types():
|
|
"""Known leaf and nested types route correctly."""
|
|
assert collection_runtime_discriminator({"collection_type": "list"}) == "list"
|
|
assert collection_runtime_discriminator({"collection_type": "paired"}) == "paired"
|
|
assert collection_runtime_discriminator({"collection_type": "record"}) == "record"
|
|
assert collection_runtime_discriminator({"collection_type": "paired_or_unpaired"}) == "paired_or_unpaired"
|
|
assert collection_runtime_discriminator({"collection_type": "sample_sheet"}) == "sample_sheet"
|
|
assert collection_runtime_discriminator({"collection_type": "list:paired"}) == "nested_list"
|
|
assert collection_runtime_discriminator({"collection_type": "record:paired"}) == "nested_record"
|
|
|
|
|
|
def test_collection_runtime_discriminator_rejects_unknown():
|
|
"""Unknown collection_type raises ValueError instead of silently defaulting."""
|
|
with pytest.raises(ValueError, match="Unknown collection_type"):
|
|
collection_runtime_discriminator({"collection_type": "banana"})
|
|
|
|
|
|
def test_collection_runtime_discriminator_missing_routes_to_list():
|
|
"""Missing/empty collection_type routes to list for Pydantic schema rejection."""
|
|
assert collection_runtime_discriminator({"collection_type": ""}) == "list"
|
|
assert collection_runtime_discriminator({}) == "list"
|