Skip to content

Commit c5bbfe1

Browse files
committed
Test case WIP...
1 parent d7b8ceb commit c5bbfe1

File tree

10 files changed

+243
-36
lines changed

10 files changed

+243
-36
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: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737
)
3838

3939
from galaxy.exceptions import RequestParameterInvalidException
40-
from galaxy.tool_util.parser.interface import DrillDownOptionsDict
40+
from galaxy.tool_util.parser.interface import (
41+
DrillDownOptionsDict,
42+
TestCollectionDict,
43+
)
4144
from ._types import (
4245
cast_as_type,
4346
is_optional,
@@ -58,7 +61,7 @@
5861
# + request_internal: This is a pydantic model to validate what Galaxy expects to find in the database,
5962
# in particular dataset and collection references should be decoded integers.
6063
StateRepresentationT = Literal[
61-
"request", "request_internal", "job_internal", "test_case", "workflow_step", "workflow_step_linked"
64+
"request", "request_internal", "job_internal", "test_case_xml", "workflow_step", "workflow_step_linked"
6265
]
6366

6467

@@ -311,9 +314,9 @@ def py_type_internal(self) -> Type:
311314
def py_type_test_case(self) -> Type:
312315
base_model: Type
313316
if self.multiple:
314-
base_model = MultiDataRequestInternal
317+
base_model = str
315318
else:
316-
base_model = DataTestCaseValue
319+
base_model = str
317320
return optional_if_needed(base_model, self.optional)
318321

319322
def pydantic_template(self, state_representation: StateRepresentationT) -> DynamicModelInformation:
@@ -325,7 +328,7 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
325328
)
326329
elif state_representation == "job_internal":
327330
return dynamic_model_information_from_py_type(self, self.py_type_internal)
328-
elif state_representation == "test_case":
331+
elif state_representation == "test_case_xml":
329332
return dynamic_model_information_from_py_type(self, self.py_type_test_case)
330333
elif state_representation == "workflow_step":
331334
return dynamic_model_information_from_py_type(self, type(None), requires_value=False)
@@ -369,6 +372,8 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
369372
return dynamic_model_information_from_py_type(self, type(None), requires_value=False)
370373
elif state_representation == "workflow_step_linked":
371374
return dynamic_model_information_from_py_type(self, ConnectedValue)
375+
elif state_representation == "test_case_xml":
376+
return dynamic_model_information_from_py_type(self, TestCollectionDict)
372377
else:
373378
raise NotImplementedError(
374379
f"Have not implemented data collection parameter models for state representation {state_representation}"
@@ -529,17 +534,23 @@ class SelectParameterModel(BaseGalaxyToolParameterModelDefinition):
529534
options: Optional[List[LabelValue]] = None
530535
multiple: bool
531536

532-
def py_type_if_required(self, allow_connections=False) -> Type:
537+
def py_type_if_required(self, allow_connections: bool = False, expect_list: bool = True) -> Type:
533538
if self.options is not None:
534539
literal_options: List[Type] = [cast_as_type(Literal[o.value]) for o in self.options]
535540
py_type = union_type(literal_options)
536541
else:
537542
py_type = StrictStr
538543
if self.multiple:
539544
if allow_connections:
540-
py_type = list_type(allow_connected_value(py_type))
545+
if expect_list:
546+
py_type = list_type(allow_connected_value(py_type))
547+
else:
548+
py_type = allow_connected_value(py_type)
541549
else:
542-
py_type = list_type(py_type)
550+
if expect_list:
551+
py_type = list_type(py_type)
552+
else:
553+
py_type = py_type
543554
elif allow_connections:
544555
py_type = allow_connected_value(py_type)
545556
return py_type
@@ -559,6 +570,10 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
559570
elif state_representation == "workflow_step_linked":
560571
py_type = self.py_type_if_required(allow_connections=True)
561572
return dynamic_model_information_from_py_type(self, optional_if_needed(py_type, self.optional))
573+
elif state_representation == "test_case_xml":
574+
# in a YAML test case representation this can be string, in XML we are still expecting a comma separated string
575+
py_type = self.py_type_if_required(allow_connections=False, expect_list=False)
576+
return dynamic_model_information_from_py_type(self, optional_if_needed(py_type, self.optional))
562577
else:
563578
return dynamic_model_information_from_py_type(self, self.py_type)
564579

@@ -1018,9 +1033,13 @@ class ToolParameterBundleModel(BaseModel):
10181033
input_models: List[ToolParameterT]
10191034

10201035

1021-
def parameters_by_name(tool_parameter_bundle: ToolParameterBundle) -> Dict[str, ToolParameterT]:
1036+
def parameters_by_name(inputs: Union[Iterable[ToolParameterModel], Iterable[ToolParameterT], ToolParameterBundle]) -> Dict[str, ToolParameterT]:
10221037
as_dict = {}
1023-
for input_model in simple_input_models(tool_parameter_bundle.input_models):
1038+
if hasattr(inputs, "input_models"):
1039+
inputs_list = simple_input_models(cast(ToolParameterBundle, inputs.input_models))
1040+
else:
1041+
inputs_list = cast(Union[Iterable[ToolParameterModel], Iterable[ToolParameterT]], inputs)
1042+
for input_model in inputs_list:
10241043
as_dict[input_model.name] = input_model
10251044
return as_dict
10261045

@@ -1058,7 +1077,7 @@ def create_job_internal_model(tool: ToolParameterBundle, name: str = "DynamicMod
10581077

10591078

10601079
def create_test_case_model(tool: ToolParameterBundle, name: str = "DynamicModelForTool") -> Type[BaseModel]:
1061-
return create_field_model(tool.input_models, name, "test_case")
1080+
return create_field_model(tool.input_models, name, "test_case_xml")
10621081

10631082

10641083
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):
@@ -545,6 +546,23 @@ def parse_input_sources(self) -> List[InputSource]:
545546
"""Return a list of InputSource objects."""
546547

547548

549+
TestCollectionAttributeDict = Dict[str, Any]
550+
CollectionType = str
551+
552+
553+
class TestCollectionDictElement(TypedDict):
554+
element_identifier: str
555+
element_definition: Union["TestCollectionDict", "ToolSourceTestInput"]
556+
557+
558+
class TestCollectionDict(TypedDict):
559+
model_class: Literal["TestCollectionDef"] = "TestCollectionDef"
560+
attributes: TestCollectionAttributeDict
561+
collection_type: CollectionType
562+
elements: List[TestCollectionDictElement]
563+
name: str
564+
565+
548566
class TestCollectionDef:
549567
__test__ = False # Prevent pytest from discovering this class (issue #12071)
550568

lib/galaxy/tool_util/parser/xml.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -654,15 +654,15 @@ def macro_paths(self):
654654
def source_path(self):
655655
return self._source_path
656656

657-
def parse_tests_to_dict(self) -> ToolSourceTests:
657+
def parse_tests_to_dict(self, for_json: bool = False) -> ToolSourceTests:
658658
tests_elem = self.root.find("tests")
659659
tests: List[ToolSourceTest] = []
660660
rval: ToolSourceTests = dict(tests=tests)
661661

662662
if tests_elem is not None:
663663
for i, test_elem in enumerate(tests_elem.findall("test")):
664664
profile = self.parse_profile()
665-
tests.append(_test_elem_to_dict(test_elem, i, profile))
665+
tests.append(_test_elem_to_dict(test_elem, i, profile, for_json=for_json))
666666

667667
return rval
668668

@@ -719,11 +719,11 @@ def parse_creator(self):
719719
return creators
720720

721721

722-
def _test_elem_to_dict(test_elem, i, profile=None) -> ToolSourceTest:
722+
def _test_elem_to_dict(test_elem, i, profile=None, for_json=False) -> ToolSourceTest:
723723
rval: ToolSourceTest = dict(
724724
outputs=__parse_output_elems(test_elem),
725725
output_collections=__parse_output_collection_elems(test_elem, profile=profile),
726-
inputs=__parse_input_elems(test_elem, i),
726+
inputs=__parse_input_elems(test_elem, i, for_json=for_json),
727727
expect_num_outputs=test_elem.get("expect_num_outputs"),
728728
command=__parse_assert_list_from_elem(test_elem.find("assert_command")),
729729
command_version=__parse_assert_list_from_elem(test_elem.find("assert_command_version")),
@@ -738,9 +738,9 @@ def _test_elem_to_dict(test_elem, i, profile=None) -> ToolSourceTest:
738738
return rval
739739

740740

741-
def __parse_input_elems(test_elem, i) -> ToolSourceTestInputs:
741+
def __parse_input_elems(test_elem, i, for_json=False) -> ToolSourceTestInputs:
742742
__expand_input_elems(test_elem)
743-
return __parse_inputs_elems(test_elem, i)
743+
return __parse_inputs_elems(test_elem, i, for_json=for_json)
744744

745745

746746
def __parse_output_elems(test_elem) -> ToolSourceTestOutputs:
@@ -986,15 +986,15 @@ def _copy_to_dict_if_present(elem, rval, attributes):
986986
return rval
987987

988988

989-
def __parse_inputs_elems(test_elem, i) -> ToolSourceTestInputs:
989+
def __parse_inputs_elems(test_elem, i, for_json=False) -> ToolSourceTestInputs:
990990
raw_inputs: ToolSourceTestInputs = []
991991
for param_elem in test_elem.findall("param"):
992-
raw_inputs.append(__parse_param_elem(param_elem, i))
992+
raw_inputs.append(__parse_param_elem(param_elem, i, for_json=for_json))
993993

994994
return raw_inputs
995995

996996

997-
def __parse_param_elem(param_elem, i=0) -> ToolSourceTestInput:
997+
def __parse_param_elem(param_elem, i=0, for_json=False) -> ToolSourceTestInput:
998998
attrib: ToolSourceTestInputAttributes = dict(param_elem.attrib)
999999
if "values" in attrib:
10001000
value = attrib["values"].split(",")
@@ -1032,7 +1032,8 @@ def __parse_param_elem(param_elem, i=0) -> ToolSourceTestInput:
10321032
elif child.tag == "edit_attributes":
10331033
attrib["edit_attributes"].append(child)
10341034
elif child.tag == "collection":
1035-
attrib["collection"] = TestCollectionDef.from_xml(child, __parse_param_elem)
1035+
collection = TestCollectionDef.from_xml(child, lambda elem: __parse_param_elem(elem, for_json=for_json))
1036+
attrib["collection"] = collection if not for_json else collection.to_dict()
10361037
if composite_data_name:
10371038
# Composite datasets need implicit renaming;
10381039
# 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)

0 commit comments

Comments
 (0)