Skip to content

Commit 25b91ad

Browse files
committed
Test case WIP...
1 parent 25a18b7 commit 25b91ad

File tree

11 files changed

+241
-35
lines changed

11 files changed

+241
-35
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from dataclasses import dataclass
2+
from typing import (
3+
Any,
4+
Dict,
5+
List,
6+
)
7+
8+
from galaxy.tool_util.parser.interface import ToolSourceTest
9+
from galaxy.util import asbool
10+
from .models import (
11+
BooleanParameterModel,
12+
DataCollectionParameterModel,
13+
DataParameterModel,
14+
FloatParameterModel,
15+
IntegerParameterModel,
16+
parameters_by_name,
17+
ToolParameterBundle,
18+
ToolParameterT,
19+
)
20+
from .state import TestCaseToolState
21+
22+
23+
@dataclass
24+
class TestCaseStateAndWarnings:
25+
tool_state: TestCaseToolState
26+
warnings: List[str]
27+
28+
29+
def legacy_from_string(parameter: ToolParameterT, value: str, warnings: List[str], profile: str) -> Any:
30+
"""Convert string values in XML test cases into typed variants.
31+
32+
This should only be used when parsing XML test cases into a TestCaseToolState object.
33+
We have to maintain backward compatibility on these for older Galaxy tool profile versions.
34+
"""
35+
is_string = isinstance(value, str)
36+
result_value: Any = value
37+
if is_string and isinstance(parameter, (IntegerParameterModel,)):
38+
warnings.append(
39+
f"Implicitly converted {parameter.name} to an integer from a string value, please use 'value_json' to define this test input parameter value instead."
40+
)
41+
result_value = int(value)
42+
elif is_string and isinstance(parameter, (FloatParameterModel,)):
43+
warnings.append(
44+
f"Implicitly converted {parameter.name} to a floating point number from a string value, please use 'value_json' to define this test input parameter value instead."
45+
)
46+
result_value = float(value)
47+
elif is_string and isinstance(parameter, (BooleanParameterModel,)):
48+
warnings.append(
49+
f"Implicitly converted {parameter.name} to a boolean from a string value, please use 'value_json' to define this test input parameter value instead."
50+
)
51+
result_value = asbool(value)
52+
return result_value
53+
54+
55+
def test_case_state(
56+
test_dict: ToolSourceTest, tool_parameter_bundle: ToolParameterBundle, profile: str
57+
) -> TestCaseStateAndWarnings:
58+
warnings: List[str] = []
59+
inputs = test_dict["inputs"]
60+
state = {}
61+
by_name = parameters_by_name(tool_parameter_bundle)
62+
for input in inputs:
63+
input_name = input["name"]
64+
if input_name not in by_name:
65+
raise Exception(f"Cannot find tool parameter for {input_name}")
66+
tool_parameter_model = by_name[input_name]
67+
if isinstance(tool_parameter_model, (DataCollectionParameterModel,)):
68+
input_value = input.get("attributes", {}).get("collection")
69+
else:
70+
input_value = input["value"]
71+
input_value = legacy_from_string(tool_parameter_model, input_value, warnings, profile)
72+
73+
state[input_name] = input_value
74+
75+
tool_state = TestCaseToolState(state)
76+
tool_state.validate(tool_parameter_bundle)
77+
return TestCaseStateAndWarnings(tool_state, warnings)

lib/galaxy/tool_util/parameters/models.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
)
3838

3939
from galaxy.exceptions import RequestParameterInvalidException
40+
from galaxy.tool_util.parser.interface import TestCollectionDict
4041
from ._types import (
4142
cast_as_type,
4243
is_optional,
@@ -57,7 +58,7 @@
5758
# + request_internal: This is a pydantic model to validate what Galaxy expects to find in the database,
5859
# in particular dataset and collection references should be decoded integers.
5960
StateRepresentationT = Literal[
60-
"request", "request_internal", "job_internal", "test_case", "workflow_step", "workflow_step_linked"
61+
"request", "request_internal", "job_internal", "test_case_xml", "workflow_step", "workflow_step_linked"
6162
]
6263

6364

@@ -310,9 +311,9 @@ def py_type_internal(self) -> Type:
310311
def py_type_test_case(self) -> Type:
311312
base_model: Type
312313
if self.multiple:
313-
base_model = MultiDataRequestInternal
314+
base_model = str
314315
else:
315-
base_model = DataTestCaseValue
316+
base_model = str
316317
return optional_if_needed(base_model, self.optional)
317318

318319
def pydantic_template(self, state_representation: StateRepresentationT) -> DynamicModelInformation:
@@ -324,7 +325,7 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
324325
)
325326
elif state_representation == "job_internal":
326327
return dynamic_model_information_from_py_type(self, self.py_type_internal)
327-
elif state_representation == "test_case":
328+
elif state_representation == "test_case_xml":
328329
return dynamic_model_information_from_py_type(self, self.py_type_test_case)
329330
elif state_representation == "workflow_step":
330331
return dynamic_model_information_from_py_type(self, type(None), requires_value=False)
@@ -368,6 +369,8 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
368369
return dynamic_model_information_from_py_type(self, type(None), requires_value=False)
369370
elif state_representation == "workflow_step_linked":
370371
return dynamic_model_information_from_py_type(self, ConnectedValue)
372+
elif state_representation == "test_case_xml":
373+
return dynamic_model_information_from_py_type(self, TestCollectionDict)
371374
else:
372375
raise NotImplementedError(
373376
f"Have not implemented data collection parameter models for state representation {state_representation}"
@@ -528,17 +531,23 @@ class SelectParameterModel(BaseGalaxyToolParameterModelDefinition):
528531
options: Optional[List[LabelValue]] = None
529532
multiple: bool
530533

531-
def py_type_if_required(self, allow_connections=False) -> Type:
534+
def py_type_if_required(self, allow_connections: bool = False, expect_list: bool = True) -> Type:
532535
if self.options is not None:
533536
literal_options: List[Type] = [cast_as_type(Literal[o.value]) for o in self.options]
534537
py_type = union_type(literal_options)
535538
else:
536539
py_type = StrictStr
537540
if self.multiple:
538541
if allow_connections:
539-
py_type = list_type(allow_connected_value(py_type))
542+
if expect_list:
543+
py_type = list_type(allow_connected_value(py_type))
544+
else:
545+
py_type = allow_connected_value(py_type)
540546
else:
541-
py_type = list_type(py_type)
547+
if expect_list:
548+
py_type = list_type(py_type)
549+
else:
550+
py_type = py_type
542551
elif allow_connections:
543552
py_type = allow_connected_value(py_type)
544553
return py_type
@@ -558,6 +567,10 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
558567
elif state_representation == "workflow_step_linked":
559568
py_type = self.py_type_if_required(allow_connections=True)
560569
return dynamic_model_information_from_py_type(self, optional_if_needed(py_type, self.optional))
570+
elif state_representation == "test_case_xml":
571+
# in a YAML test case representation this can be string, in XML we are still expecting a comma separated string
572+
py_type = self.py_type_if_required(allow_connections=False, expect_list=False)
573+
return dynamic_model_information_from_py_type(self, optional_if_needed(py_type, self.optional))
561574
else:
562575
return dynamic_model_information_from_py_type(self, self.py_type)
563576

@@ -963,9 +976,13 @@ class ToolParameterBundleModel(BaseModel):
963976
input_models: List[ToolParameterT]
964977

965978

966-
def parameters_by_name(tool_parameter_bundle: ToolParameterBundle) -> Dict[str, ToolParameterT]:
979+
def parameters_by_name(inputs: Union[Iterable[ToolParameterModel], Iterable[ToolParameterT], ToolParameterBundle]) -> Dict[str, ToolParameterT]:
967980
as_dict = {}
968-
for input_model in simple_input_models(tool_parameter_bundle.input_models):
981+
if hasattr(inputs, "input_models"):
982+
inputs_list = simple_input_models(cast(ToolParameterBundle, inputs.input_models))
983+
else:
984+
inputs_list = cast(Union[Iterable[ToolParameterModel], Iterable[ToolParameterT]], inputs)
985+
for input_model in inputs_list:
969986
as_dict[input_model.name] = input_model
970987
return as_dict
971988

@@ -1003,7 +1020,7 @@ def create_job_internal_model(tool: ToolParameterBundle, name: str = "DynamicMod
10031020

10041021

10051022
def create_test_case_model(tool: ToolParameterBundle, name: str = "DynamicModelForTool") -> Type[BaseModel]:
1006-
return create_field_model(tool.input_models, name, "test_case")
1023+
return create_field_model(tool.input_models, name, "test_case_xml")
10071024

10081025

10091026
def create_workflow_step_model(tool: ToolParameterBundle, name: str = "DynamicModelForTool") -> Type[BaseModel]:

lib/galaxy/tool_util/parameters/state.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
create_job_internal_model,
1818
create_request_internal_model,
1919
create_request_model,
20+
create_test_case_model,
2021
create_workflow_step_linked_model,
2122
create_workflow_step_model,
2223
StateRepresentationT,
@@ -91,12 +92,12 @@ def _parameter_model_for(cls, input_models: ToolParameterBundle) -> Type[BaseMod
9192

9293

9394
class TestCaseToolState(ToolState):
94-
state_representation: Literal["test_case"] = "test_case"
95+
state_representation: Literal["test_case_xml"] = "test_case_xml"
9596

9697
@classmethod
9798
def _parameter_model_for(cls, input_models: ToolParameterBundle) -> Type[BaseModel]:
9899
# implement a test case model...
99-
return create_request_internal_model(input_models)
100+
return create_test_case_model(input_models)
100101

101102

102103
class WorkflowStepToolState(ToolState):

lib/galaxy/tool_util/parser/interface.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import packaging.version
2020
from pydantic import BaseModel
2121
from typing_extensions import (
22+
Literal,
2223
NotRequired,
2324
TypedDict,
2425
)
@@ -376,7 +377,7 @@ def paths_and_modtimes(self):
376377
paths_and_modtimes[self.source_path] = os.path.getmtime(self.source_path)
377378
return paths_and_modtimes
378379

379-
def parse_tests_to_dict(self) -> ToolSourceTests:
380+
def parse_tests_to_dict(self, for_json: bool = False) -> ToolSourceTests:
380381
return {"tests": []}
381382

382383
def __str__(self):
@@ -525,6 +526,23 @@ def parse_input_sources(self) -> List[InputSource]:
525526
"""Return a list of InputSource objects."""
526527

527528

529+
TestCollectionAttributeDict = Dict[str, Any]
530+
CollectionType = str
531+
532+
533+
class TestCollectionDictElement(TypedDict):
534+
element_identifier: str
535+
element_definition: Union["TestCollectionDict", "ToolSourceTestInput"]
536+
537+
538+
class TestCollectionDict(TypedDict):
539+
model_class: Literal["TestCollectionDef"] = "TestCollectionDef"
540+
attributes: TestCollectionAttributeDict
541+
collection_type: CollectionType
542+
elements: List[TestCollectionDictElement]
543+
name: str
544+
545+
528546
class TestCollectionDef:
529547
__test__ = False # Prevent pytest from discovering this class (issue #12071)
530548

lib/galaxy/tool_util/parser/xml.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -650,15 +650,15 @@ def macro_paths(self):
650650
def source_path(self):
651651
return self._source_path
652652

653-
def parse_tests_to_dict(self) -> ToolSourceTests:
653+
def parse_tests_to_dict(self, for_json: bool = False) -> ToolSourceTests:
654654
tests_elem = self.root.find("tests")
655655
tests: List[ToolSourceTest] = []
656656
rval: ToolSourceTests = dict(tests=tests)
657657

658658
if tests_elem is not None:
659659
for i, test_elem in enumerate(tests_elem.findall("test")):
660660
profile = self.parse_profile()
661-
tests.append(_test_elem_to_dict(test_elem, i, profile))
661+
tests.append(_test_elem_to_dict(test_elem, i, profile, for_json=for_json))
662662

663663
return rval
664664

@@ -715,11 +715,11 @@ def parse_creator(self):
715715
return creators
716716

717717

718-
def _test_elem_to_dict(test_elem, i, profile=None) -> ToolSourceTest:
718+
def _test_elem_to_dict(test_elem, i, profile=None, for_json=False) -> ToolSourceTest:
719719
rval: ToolSourceTest = dict(
720720
outputs=__parse_output_elems(test_elem),
721721
output_collections=__parse_output_collection_elems(test_elem, profile=profile),
722-
inputs=__parse_input_elems(test_elem, i),
722+
inputs=__parse_input_elems(test_elem, i, for_json=for_json),
723723
expect_num_outputs=test_elem.get("expect_num_outputs"),
724724
command=__parse_assert_list_from_elem(test_elem.find("assert_command")),
725725
command_version=__parse_assert_list_from_elem(test_elem.find("assert_command_version")),
@@ -734,9 +734,9 @@ def _test_elem_to_dict(test_elem, i, profile=None) -> ToolSourceTest:
734734
return rval
735735

736736

737-
def __parse_input_elems(test_elem, i) -> ToolSourceTestInputs:
737+
def __parse_input_elems(test_elem, i, for_json=False) -> ToolSourceTestInputs:
738738
__expand_input_elems(test_elem)
739-
return __parse_inputs_elems(test_elem, i)
739+
return __parse_inputs_elems(test_elem, i, for_json=for_json)
740740

741741

742742
def __parse_output_elems(test_elem) -> ToolSourceTestOutputs:
@@ -982,15 +982,15 @@ def _copy_to_dict_if_present(elem, rval, attributes):
982982
return rval
983983

984984

985-
def __parse_inputs_elems(test_elem, i) -> ToolSourceTestInputs:
985+
def __parse_inputs_elems(test_elem, i, for_json=False) -> ToolSourceTestInputs:
986986
raw_inputs: ToolSourceTestInputs = []
987987
for param_elem in test_elem.findall("param"):
988-
raw_inputs.append(__parse_param_elem(param_elem, i))
988+
raw_inputs.append(__parse_param_elem(param_elem, i, for_json=for_json))
989989

990990
return raw_inputs
991991

992992

993-
def __parse_param_elem(param_elem, i=0) -> ToolSourceTestInput:
993+
def __parse_param_elem(param_elem, i=0, for_json=False) -> ToolSourceTestInput:
994994
attrib: ToolSourceTestInputAttributes = dict(param_elem.attrib)
995995
if "values" in attrib:
996996
value = attrib["values"].split(",")
@@ -1028,7 +1028,8 @@ def __parse_param_elem(param_elem, i=0) -> ToolSourceTestInput:
10281028
elif child.tag == "edit_attributes":
10291029
attrib["edit_attributes"].append(child)
10301030
elif child.tag == "collection":
1031-
attrib["collection"] = TestCollectionDef.from_xml(child, __parse_param_elem)
1031+
collection = TestCollectionDef.from_xml(child, lambda elem: __parse_param_elem(elem, for_json=for_json))
1032+
attrib["collection"] = collection if not for_json else collection.to_dict()
10321033
if composite_data_name:
10331034
# Composite datasets need implicit renaming;
10341035
# inserted at front of list so explicit declarations

lib/galaxy/tool_util/parser/yaml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def _parse_output_collection(self, tool, name, output_dict):
189189
)
190190
return output_collection
191191

192-
def parse_tests_to_dict(self) -> ToolSourceTests:
192+
def parse_tests_to_dict(self, for_json: bool = False) -> ToolSourceTests:
193193
tests: List[ToolSourceTest] = []
194194
rval: ToolSourceTests = dict(tests=tests)
195195

lib/galaxy/tool_util/unittest_utils/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,9 @@ def get_content(filename: Optional[str]) -> bytes:
3131
return get_content
3232

3333

34+
def functional_test_tool_directory() -> str:
35+
return os.path.join(galaxy_directory(), "test/functional/tools")
36+
37+
3438
def functional_test_tool_path(test_path: str) -> str:
35-
return os.path.join(galaxy_directory(), "test/functional/tools", test_path)
39+
return os.path.join(functional_test_tool_directory(), test_path)

lib/galaxy/tool_util/verify/_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Any,
55
Dict,
66
List,
7+
Optional,
78
Tuple,
89
)
910

0 commit comments

Comments
 (0)