Files
galaxy/test/unit/tool_util/test_runtime.py
T
John ChiltonandClaude Opus 4.6 aa67790998 Fail fast on unknown collection_type instead of silent fallbacks.
- 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>
2026-02-12 15:58:17 -05:00

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"