Skip to content

Commit

Permalink
Models for YAML test format.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Sep 23, 2024
1 parent d5f8feb commit 1ed1ae4
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 13 deletions.
76 changes: 75 additions & 1 deletion lib/galaxy/tool_util/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@
"""

from typing import (
Any,
Dict,
List,
Optional,
Union,
)

from pydantic import BaseModel
from pydantic import (
AnyUrl,
BaseModel,
ConfigDict,
RootModel,
)

from .parameters import (
input_models_for_tool_source,
Expand All @@ -25,6 +33,7 @@
from_tool_source,
ToolOutput,
)
from .verify.assertion_models import assertions


class ParsedTool(BaseModel):
Expand Down Expand Up @@ -73,3 +82,68 @@ def parse_tool(tool_source: ToolSource) -> ParsedTool:
xrefs=xrefs,
help=help,
)


class StrictModel(BaseModel):

model_config = ConfigDict(
extra="forbid",
)


class BaseTestOutputModel(StrictModel):
file: Optional[str] = None
path: Optional[str] = None
location: Optional[AnyUrl] = None
ftype: Optional[str] = None
sorted: Optional[bool] = None
compare: Optional[str] = None
checksum: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
asserts: Optional[assertions] = None
compare: Optional[str] = None # to do make more specific
delta: Optional[int] = None
delta_frac: Optional[float] = None
lines_diff: Optional[int] = None
decompress: Optional[bool] = None


class TestDataOutputAssertions(BaseTestOutputModel):
pass


class TestCollectionCollectionElementAssertions(StrictModel):
elements: Optional[Dict[str, "TestCollectionElementAssertion"]] = None
element_tests: Optional[Dict[str, "TestCollectionElementAssertion"]] = None


class TestCollectionDatasetElementAssertions(BaseTestOutputModel):
pass


TestCollectionElementAssertion = Union[
TestCollectionDatasetElementAssertions, TestCollectionCollectionElementAssertions
]
TestCollectionCollectionElementAssertions.model_rebuild()


class CollectionAttributes(StrictModel):
collection_type: Optional[str] = None


class TestCollectionOutputAssertions(StrictModel):
elements: Optional[Dict[str, TestCollectionElementAssertion]] = None
element_tests: Optional[Dict[str, "TestCollectionElementAssertion"]] = None
attributes: Optional[CollectionAttributes] = None


TestOutputAssertions = Union[TestCollectionOutputAssertions, TestDataOutputAssertions]


class TestJob(StrictModel):
doc: Optional[str]
job: Dict[str, Any]
outputs: Dict[str, TestOutputAssertions]


Tests = RootModel[List[TestJob]]
1 change: 1 addition & 0 deletions lib/galaxy/tool_util/verify/assertion_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
BeforeValidator,
ConfigDict,
Field,
model_validator,
RootModel,
StrictFloat,
StrictInt,
Expand Down
4 changes: 1 addition & 3 deletions lib/galaxy/tool_util/verify/asserts/size.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@

def assert_has_size(
output_bytes: OutputBytes,
value: Annotated[
OptionalXmlInt, AssertionParameter("Deprecated alias for `size`", xml_type="Bytes", deprecated=True)
] = None,
value: Annotated[OptionalXmlInt, AssertionParameter("Deprecated alias for `size`", xml_type="Bytes")] = None,
size: Annotated[
OptionalXmlInt,
AssertionParameter(
Expand Down
47 changes: 39 additions & 8 deletions lib/galaxy/tool_util/verify/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
BeforeValidator,
ConfigDict,
Field,
model_validator,
RootModel,
StrictFloat,
StrictInt,
Expand Down Expand Up @@ -113,9 +114,8 @@ def check_non_negative_if_int(v: typing.Any):
{{assertion.name}}_{{ parameter.name }}_description = '''{{ parameter.description }}'''
{% endfor %}
class {{assertion.name}}_model(AssertionModel):
r\"\"\"{{ assertion.docstring }}\"\"\"
that: Literal["{{assertion.name}}"] = "{{assertion.name}}"
class base_{{assertion.name}}_model(AssertionModel):
'''base model for {{assertion.name}} describing attributes.'''
{% for parameter in assertion.parameters %}
{% if not parameter.is_deprecated %}
{{ parameter.name }}: {{ parameter.type_str }} = Field(
Expand All @@ -124,21 +124,52 @@ class {{assertion.name}}_model(AssertionModel):
)
{% endif %}
{% endfor %}
{% if assertion.children in ["required", "allowed"] %}
children: typing.Optional["assertion_list"] = None
asserts: typing.Optional["assertion_list"] = None
{% if assertion.children == "required" %}
children: "assertion_list"
@model_validator(mode='before')
@classmethod
def validate_children(self, data: typing.Any):
if isinstance(data, dict) and 'children' not in data and 'asserts' not in data:
raise ValueError("At least one of 'children' or 'asserts' must be specified for this assertion type.")
return data
{% endif %}
{% if assertion.children == "allowed" %}
children: typing.Optional["assertion_list"] = None
{% endif %}
class {{assertion.name}}_model(base_{{assertion.name}}_model):
r\"\"\"{{ assertion.docstring }}\"\"\"
that: Literal["{{assertion.name}}"] = "{{assertion.name}}"
class {{assertion.name}}_model_nested(AssertionModel):
r\"\"\"Nested version of this assertion model.\"\"\"
{{assertion.name}}: base_{{assertion.name}}_model
{% endfor %}
any_assertion_model = Annotated[typing.Union[
any_assertion_model_flat = Annotated[typing.Union[
{% for assertion in assertions %}
{{assertion.name}}_model,
{% endfor %}
], Field(discriminator="that")]
assertion_list = RootModel[typing.List[any_assertion_model]]
any_assertion_model_nested = typing.Union[
{% for assertion in assertions %}
{{assertion.name}}_model_nested,
{% endfor %}
]
assertion_list = RootModel[typing.List[typing.Union[any_assertion_model_flat, any_assertion_model_nested]]]
class assertion_dict(AssertionModel):
{% for assertion in assertions %}
{{assertion.name}}: typing.Optional[base_{{assertion.name}}_model] = None
{% endfor %}
assertions = typing.Union[assertion_list, assertion_dict]
"""


Expand Down
1 change: 0 additions & 1 deletion lib/galaxy/workflow/scheduling_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ def __schedule(self, workflow_scheduler_id, workflow_scheduler):
def __attempt_schedule(self, invocation_id, workflow_scheduler):
with self.app.model.context() as session:
workflow_invocation = session.get(model.WorkflowInvocation, invocation_id)

try:
if workflow_invocation.state == workflow_invocation.states.CANCELLING:
workflow_invocation.cancel_invocation_steps()
Expand Down
1 change: 1 addition & 0 deletions lib/galaxy_test/workflow/flatten_collection.gxwf-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
job: {}
outputs:
out:
attributes: {collection_type: 'list'}
elements:
'oe1-ie1':
asserts:
Expand Down
1 change: 1 addition & 0 deletions test/functional/tools/sample_tool_conf.xml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
<tool file="column_param_configfile.xml" />
<tool file="column_param_list.xml" />
<tool file="column_multi_param.xml" />
<tool file="select_optional_legacy.xml" />
<tool file="select_optional.xml" />
<tool file="select_dynamic.xml" />
<tool file="hidden_param.xml" />
Expand Down
42 changes: 42 additions & 0 deletions test/unit/tool_util/test_test_format_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import os
from pathlib import Path

import yaml

from galaxy.tool_util.models import Tests
from galaxy.util import galaxy_directory
from galaxy.util.unittest_utils import skip_unless_environ

TEST_WORKFLOW_DIRECTORY = os.path.join(galaxy_directory(), "lib", "galaxy_test", "workflow")
USING_UNVERIFIED_SYNTAX = [
"Assembly-Hifi-Trio-phasing-VGP5-tests.yml",
"Assembly-Hifi-HiC-phasing-VGP4-tests.yml",
"Assembly-Hifi-only-VGP3-tests.yml",
]


def test_validate_workflow_tests():
path = Path(TEST_WORKFLOW_DIRECTORY)
test_files = path.glob("*.gxwf-tests.yml")
for test_file in test_files:
with open(test_file) as f:
json = yaml.safe_load(f)
Tests.model_validate(json)


@skip_unless_environ("GALAXY_TEST_IWC_DIRECTORY")
def test_iwc_directory():
path = Path(os.environ["GALAXY_TEST_IWC_DIRECTORY"])
test_files = path.glob("workflows/**/*-test*.yml")
print(f"test_files:: {test_files}")
for test_file in test_files:
print(test_file)
skip_file = False
for unverified in USING_UNVERIFIED_SYNTAX:
if str(test_file).endswith(unverified):
skip_file = True
if skip_file:
continue
with open(test_file) as f:
json = yaml.safe_load(f)
Tests.model_validate(json)

0 comments on commit 1ed1ae4

Please sign in to comment.