diff --git a/lib/galaxy/tool_util/models.py b/lib/galaxy/tool_util/models.py
index deafef1a5243..4f1ea35670c6 100644
--- a/lib/galaxy/tool_util/models.py
+++ b/lib/galaxy/tool_util/models.py
@@ -5,11 +5,23 @@
"""
from typing import (
+ Any,
+ Dict,
List,
Optional,
+ Union,
)
-from pydantic import BaseModel
+from pydantic import (
+ AnyUrl,
+ BaseModel,
+ ConfigDict,
+ RootModel,
+)
+from typing_extensions import (
+ NotRequired,
+ TypedDict,
+)
from .parameters import (
input_models_for_tool_source,
@@ -18,6 +30,7 @@
from .parser.interface import (
Citation,
HelpContent,
+ OutputCompareType,
ToolSource,
XrefDict,
)
@@ -25,6 +38,7 @@
from_tool_source,
ToolOutput,
)
+from .verify.assertion_models import assertions
class ParsedTool(BaseModel):
@@ -73,3 +87,85 @@ 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
+ sort: Optional[bool] = None
+ compare: Optional[OutputCompareType] = None
+ checksum: Optional[str] = None
+ metadata: Optional[Dict[str, Any]] = None
+ asserts: Optional[assertions] = None
+ 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
+
+
+TestOutputLiteral = Union[bool, int, float, str]
+
+TestOutputAssertions = Union[TestCollectionOutputAssertions, TestDataOutputAssertions, TestOutputLiteral]
+
+JobDict = Dict[str, Any]
+
+
+class TestJob(StrictModel):
+ doc: Optional[str]
+ job: JobDict
+ outputs: Dict[str, TestOutputAssertions]
+
+
+Tests = RootModel[List[TestJob]]
+
+# TODO: typed dict versions of all thee above for verify code - make this Dict[str, Any] here more
+# specific.
+OutputChecks = Union[TestOutputLiteral, Dict[str, Any]]
+OutputsDict = Dict[str, OutputChecks]
+
+
+class TestJobDict(TypedDict):
+ doc: NotRequired[str]
+ job: NotRequired[JobDict]
+ outputs: OutputsDict
+
+
+TestDicts = List[TestJobDict]
diff --git a/lib/galaxy/tool_util/parser/interface.py b/lib/galaxy/tool_util/parser/interface.py
index 50ec9ed30d17..c137955dbeb1 100644
--- a/lib/galaxy/tool_util/parser/interface.py
+++ b/lib/galaxy/tool_util/parser/interface.py
@@ -5,6 +5,7 @@
ABCMeta,
abstractmethod,
)
+from enum import Enum
from os.path import join
from typing import (
Any,
@@ -49,9 +50,18 @@ class AssertionDict(TypedDict):
XmlInt = Union[str, int]
+class OutputCompareType(str, Enum):
+ diff = "diff"
+ re_match = "re_match"
+ sim_size = "sim_size"
+ re_match_multiline = "re_match_multiline"
+ contains = "contains"
+ image_diff = "image_diff"
+
+
class ToolSourceTestOutputAttributes(TypedDict):
object: NotRequired[Optional[Any]]
- compare: str
+ compare: OutputCompareType
lines_diff: int
delta: int
delta_frac: Optional[float]
diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py
index c61b178e64c7..a89754553f84 100644
--- a/lib/galaxy/tool_util/parser/xml.py
+++ b/lib/galaxy/tool_util/parser/xml.py
@@ -43,6 +43,7 @@
DynamicOptions,
HelpContent,
InputSource,
+ OutputCompareType,
PageSource,
PagesSource,
RequiredFiles,
@@ -834,7 +835,7 @@ def __parse_test_attributes(
value_object = json.loads(attrib.pop("value_json"))
# Method of comparison
- compare: str = attrib.pop("compare", "diff").lower()
+ compare: OutputCompareType = cast(OutputCompareType, attrib.pop("compare", "diff").lower())
# Number of lines to allow to vary in logs (for dates, etc)
lines_diff: int = int(attrib.pop("lines_diff", "0"))
# Allow a file size to vary if sim_size compare
diff --git a/lib/galaxy/tool_util/validate_test_format.py b/lib/galaxy/tool_util/validate_test_format.py
new file mode 100644
index 000000000000..fd9e055b3789
--- /dev/null
+++ b/lib/galaxy/tool_util/validate_test_format.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+
+import argparse
+import sys
+
+import yaml
+
+from galaxy.tool_util.models import Tests
+
+DESCRIPTION = """
+A small utility to verify the Planemo test format.
+
+This script doesn't use semantic information about tools or workflows so only
+the structure of the file is checked and things like inputs matching up is not
+included.
+"""
+
+
+def validate_test_file(test_file: str) -> None:
+ with open(test_file) as f:
+ json = yaml.safe_load(f)
+ Tests.model_validate(json)
+
+
+def arg_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=DESCRIPTION)
+ parser.add_argument("test_file")
+ return parser
+
+
+def main(argv=None) -> None:
+ if argv is None:
+ argv = sys.argv[1:]
+
+ args = arg_parser().parse_args(argv)
+ validate_test_file(args.test_file)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/lib/galaxy/tool_util/verify/__init__.py b/lib/galaxy/tool_util/verify/__init__.py
index fdc4c2044428..6ddfefeb69f8 100644
--- a/lib/galaxy/tool_util/verify/__init__.py
+++ b/lib/galaxy/tool_util/verify/__init__.py
@@ -597,6 +597,8 @@ def files_image_diff(file1: str, file2: str, attributes: Optional[Dict[str, Any]
# TODO: After tool-util with this included is published, fefactor planemo.test._check_output
# to use this function. There is already a comment there about breaking fewer abstractions.
# https://github.com/galaxyproject/planemo/blob/master/planemo/test/_check_output.py
+# TODO: Also migrate the logic for checking non-dictionaries out of Planemo - this function now
+# does that check also.
def verify_file_path_against_dict(
get_filename: GetFilenameT,
get_location: GetLocationT,
@@ -621,30 +623,38 @@ def verify_file_contents_against_dict(
test_properties,
test_data_target_dir: Optional[str] = None,
) -> None:
- # Support Galaxy-like file location (using "file") or CWL-like ("path" or "location").
- expected_file = test_properties.get("file", None)
- if expected_file is None:
- expected_file = test_properties.get("path", None)
- if expected_file is None:
- location = test_properties.get("location")
- if location:
- if location.startswith(("http://", "https://")):
- assert get_location
- expected_file = get_location(location)
- else:
- expected_file = location.split("file://", 1)[-1]
-
- if "asserts" in test_properties:
- test_properties["assert_list"] = to_test_assert_list(test_properties["asserts"])
- verify(
- item_label,
- output_content,
- attributes=test_properties,
- filename=expected_file,
- get_filename=get_filename,
- keep_outputs_dir=test_data_target_dir,
- verify_extra_files=None,
- )
+ expected_file: Optional[str] = None
+ if isinstance(test_properties, dict):
+ # Support Galaxy-like file location (using "file") or CWL-like ("path" or "location").
+ expected_file = test_properties.get("file", None)
+ if expected_file is None:
+ expected_file = test_properties.get("path", None)
+ if expected_file is None:
+ location = test_properties.get("location")
+ if location:
+ if location.startswith(("http://", "https://")):
+ assert get_location
+ expected_file = get_location(location)
+ else:
+ expected_file = location.split("file://", 1)[-1]
+
+ if "asserts" in test_properties:
+ test_properties["assert_list"] = to_test_assert_list(test_properties["asserts"])
+ verify(
+ item_label,
+ output_content,
+ attributes=test_properties,
+ filename=expected_file,
+ get_filename=get_filename,
+ keep_outputs_dir=test_data_target_dir,
+ verify_extra_files=None,
+ )
+ else:
+ output_value = json.loads(output_content.decode("utf-8"))
+ if test_properties != output_value:
+ template = "Output [%s] value [%s] does not match expected value [%s]."
+ message = template % (item_label, output_value, test_properties)
+ raise AssertionError(message)
__all__ = [
diff --git a/lib/galaxy/tool_util/verify/assertion_models.py b/lib/galaxy/tool_util/verify/assertion_models.py
index 5f21e488e52b..142c476ffdb6 100644
--- a/lib/galaxy/tool_util/verify/assertion_models.py
+++ b/lib/galaxy/tool_util/verify/assertion_models.py
@@ -8,6 +8,7 @@
BeforeValidator,
ConfigDict,
Field,
+ model_validator,
RootModel,
StrictFloat,
StrictInt,
@@ -80,12 +81,8 @@ def check_non_negative_if_int(v: typing.Any):
has_line_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_line_model(AssertionModel):
- r"""Asserts the specified output contains the line specified by the
- argument line. The exact number of occurrences can be optionally
- specified by the argument n"""
-
- that: Literal["has_line"] = "has_line"
+class base_has_line_model(AssertionModel):
+ """base model for has_line describing attributes."""
line: str = Field(
...,
@@ -132,6 +129,20 @@ class has_line_model(AssertionModel):
)
+class has_line_model(base_has_line_model):
+ r"""Asserts the specified output contains the line specified by the
+ argument line. The exact number of occurrences can be optionally
+ specified by the argument n"""
+
+ that: Literal["has_line"] = "has_line"
+
+
+class has_line_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_line: base_has_line_model
+
+
has_line_matching_expression_description = """The regular expressions to attempt match in the output."""
has_line_matching_n_description = """Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``"""
@@ -147,12 +158,8 @@ class has_line_model(AssertionModel):
has_line_matching_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_line_matching_model(AssertionModel):
- r"""Asserts the specified output contains a line matching the
- regular expression specified by the argument expression. If n is given
- the assertion checks for exactly n occurences."""
-
- that: Literal["has_line_matching"] = "has_line_matching"
+class base_has_line_matching_model(AssertionModel):
+ """base model for has_line_matching describing attributes."""
expression: str = Field(
...,
@@ -199,6 +206,20 @@ class has_line_matching_model(AssertionModel):
)
+class has_line_matching_model(base_has_line_matching_model):
+ r"""Asserts the specified output contains a line matching the
+ regular expression specified by the argument expression. If n is given
+ the assertion checks for exactly n occurences."""
+
+ that: Literal["has_line_matching"] = "has_line_matching"
+
+
+class has_line_matching_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_line_matching: base_has_line_matching_model
+
+
has_n_lines_n_description = """Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``"""
has_n_lines_delta_description = (
@@ -212,12 +233,8 @@ class has_line_matching_model(AssertionModel):
has_n_lines_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_n_lines_model(AssertionModel):
- r"""Asserts the specified output contains ``n`` lines allowing
- for a difference in the number of lines (delta)
- or relative differebce in the number of lines"""
-
- that: Literal["has_n_lines"] = "has_n_lines"
+class base_has_n_lines_model(AssertionModel):
+ """base model for has_n_lines describing attributes."""
n: Annotated[
typing.Optional[typing.Union[str, int]],
@@ -259,6 +276,20 @@ class has_n_lines_model(AssertionModel):
)
+class has_n_lines_model(base_has_n_lines_model):
+ r"""Asserts the specified output contains ``n`` lines allowing
+ for a difference in the number of lines (delta)
+ or relative differebce in the number of lines"""
+
+ that: Literal["has_n_lines"] = "has_n_lines"
+
+
+class has_n_lines_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_n_lines: base_has_n_lines_model
+
+
has_text_text_description = """The text to search for in the output."""
has_text_n_description = """Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``"""
@@ -274,12 +305,8 @@ class has_n_lines_model(AssertionModel):
has_text_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_text_model(AssertionModel):
- r"""Asserts specified output contains the substring specified by
- the argument text. The exact number of occurrences can be
- optionally specified by the argument n"""
-
- that: Literal["has_text"] = "has_text"
+class base_has_text_model(AssertionModel):
+ """base model for has_text describing attributes."""
text: str = Field(
...,
@@ -326,6 +353,20 @@ class has_text_model(AssertionModel):
)
+class has_text_model(base_has_text_model):
+ r"""Asserts specified output contains the substring specified by
+ the argument text. The exact number of occurrences can be
+ optionally specified by the argument n"""
+
+ that: Literal["has_text"] = "has_text"
+
+
+class has_text_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_text: base_has_text_model
+
+
has_text_matching_expression_description = """The regular expressions to attempt match in the output."""
has_text_matching_n_description = """Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``"""
@@ -341,13 +382,8 @@ class has_text_model(AssertionModel):
has_text_matching_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_text_matching_model(AssertionModel):
- r"""Asserts the specified output contains text matching the
- regular expression specified by the argument expression.
- If n is given the assertion checks for exacly n (nonoverlapping)
- occurences."""
-
- that: Literal["has_text_matching"] = "has_text_matching"
+class base_has_text_matching_model(AssertionModel):
+ """base model for has_text_matching describing attributes."""
expression: str = Field(
...,
@@ -394,14 +430,26 @@ class has_text_matching_model(AssertionModel):
)
-not_has_text_text_description = """The text to search for in the output."""
+class has_text_matching_model(base_has_text_matching_model):
+ r"""Asserts the specified output contains text matching the
+ regular expression specified by the argument expression.
+ If n is given the assertion checks for exacly n (nonoverlapping)
+ occurences."""
+
+ that: Literal["has_text_matching"] = "has_text_matching"
-class not_has_text_model(AssertionModel):
- r"""Asserts specified output does not contain the substring
- specified by the argument text"""
+class has_text_matching_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_text_matching: base_has_text_matching_model
+
+
+not_has_text_text_description = """The text to search for in the output."""
- that: Literal["not_has_text"] = "not_has_text"
+
+class base_not_has_text_model(AssertionModel):
+ """base model for not_has_text describing attributes."""
text: str = Field(
...,
@@ -409,6 +457,19 @@ class not_has_text_model(AssertionModel):
)
+class not_has_text_model(base_not_has_text_model):
+ r"""Asserts specified output does not contain the substring
+ specified by the argument text"""
+
+ that: Literal["not_has_text"] = "not_has_text"
+
+
+class not_has_text_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ not_has_text: base_not_has_text_model
+
+
has_n_columns_n_description = """Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``"""
has_n_columns_delta_description = (
@@ -428,19 +489,8 @@ class not_has_text_model(AssertionModel):
has_n_columns_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_n_columns_model(AssertionModel):
- r"""Asserts tabular output contains the specified
- number (``n``) of columns.
-
- For instance, ````. The assertion tests only the first line.
- Number of columns can optionally also be specified with ``delta``. Alternatively the
- range of expected occurences can be specified by ``min`` and/or ``max``.
-
- Optionally a column separator (``sep``, default is `` ``) `and comment character(s)
- can be specified (``comment``, default is empty string). The first non-comment
- line is used for determining the number of columns."""
-
- that: Literal["has_n_columns"] = "has_n_columns"
+class base_has_n_columns_model(AssertionModel):
+ """base model for has_n_columns describing attributes."""
n: Annotated[
typing.Optional[typing.Union[str, int]],
@@ -492,30 +542,38 @@ class has_n_columns_model(AssertionModel):
)
-attribute_is_path_description = """The Python xpath-like expression to find the target element."""
+class has_n_columns_model(base_has_n_columns_model):
+ r"""Asserts tabular output contains the specified
+ number (``n``) of columns.
-attribute_is_attribute_description = """The XML attribute name to test against from the target XML element."""
+ For instance, ````. The assertion tests only the first line.
+ Number of columns can optionally also be specified with ``delta``. Alternatively the
+ range of expected occurences can be specified by ``min`` and/or ``max``.
-attribute_is_text_description = """The expected attribute value to test against on the target XML element"""
+ Optionally a column separator (``sep``, default is `` ``) `and comment character(s)
+ can be specified (``comment``, default is empty string). The first non-comment
+ line is used for determining the number of columns."""
-attribute_is_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
+ that: Literal["has_n_columns"] = "has_n_columns"
-class attribute_is_model(AssertionModel):
- r"""Asserts the XML ``attribute`` for the element (or tag) with the specified
- XPath-like ``path`` is the specified ``text``.
+class has_n_columns_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
- For example:
+ has_n_columns: base_has_n_columns_model
- ```xml
-
- ```
- The assertion implicitly also asserts that an element matching ``path`` exists.
- With ``negate`` the result of the assertion (on the equality) can be inverted (the
- implicit assertion on the existence of the path is not affected)."""
+attribute_is_path_description = """The Python xpath-like expression to find the target element."""
+
+attribute_is_attribute_description = """The XML attribute name to test against from the target XML element."""
+
+attribute_is_text_description = """The expected attribute value to test against on the target XML element"""
+
+attribute_is_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
- that: Literal["attribute_is"] = "attribute_is"
+
+class base_attribute_is_model(AssertionModel):
+ """base model for attribute_is describing attributes."""
path: str = Field(
...,
@@ -538,6 +596,29 @@ class attribute_is_model(AssertionModel):
)
+class attribute_is_model(base_attribute_is_model):
+ r"""Asserts the XML ``attribute`` for the element (or tag) with the specified
+ XPath-like ``path`` is the specified ``text``.
+
+ For example:
+
+ ```xml
+
+ ```
+
+ The assertion implicitly also asserts that an element matching ``path`` exists.
+ With ``negate`` the result of the assertion (on the equality) can be inverted (the
+ implicit assertion on the existence of the path is not affected)."""
+
+ that: Literal["attribute_is"] = "attribute_is"
+
+
+class attribute_is_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ attribute_is: base_attribute_is_model
+
+
attribute_matches_path_description = """The Python xpath-like expression to find the target element."""
attribute_matches_attribute_description = """The XML attribute name to test against from the target XML element."""
@@ -549,21 +630,8 @@ class attribute_is_model(AssertionModel):
attribute_matches_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class attribute_matches_model(AssertionModel):
- r"""Asserts the XML ``attribute`` for the element (or tag) with the specified
- XPath-like ``path`` matches the regular expression specified by ``expression``.
-
- For example:
-
- ```xml
-
- ```
-
- The assertion implicitly also asserts that an element matching ``path`` exists.
- With ``negate`` the result of the assertion (on the matching) can be inverted (the
- implicit assertion on the existence of the path is not affected)."""
-
- that: Literal["attribute_matches"] = "attribute_matches"
+class base_attribute_matches_model(AssertionModel):
+ """base model for attribute_matches describing attributes."""
path: str = Field(
...,
@@ -586,12 +654,59 @@ class attribute_matches_model(AssertionModel):
)
+class attribute_matches_model(base_attribute_matches_model):
+ r"""Asserts the XML ``attribute`` for the element (or tag) with the specified
+ XPath-like ``path`` matches the regular expression specified by ``expression``.
+
+ For example:
+
+ ```xml
+
+ ```
+
+ The assertion implicitly also asserts that an element matching ``path`` exists.
+ With ``negate`` the result of the assertion (on the matching) can be inverted (the
+ implicit assertion on the existence of the path is not affected)."""
+
+ that: Literal["attribute_matches"] = "attribute_matches"
+
+
+class attribute_matches_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ attribute_matches: base_attribute_matches_model
+
+
element_text_path_description = """The Python xpath-like expression to find the target element."""
element_text_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class element_text_model(AssertionModel):
+class base_element_text_model(AssertionModel):
+ """base model for element_text describing attributes."""
+
+ path: str = Field(
+ ...,
+ description=element_text_path_description,
+ )
+
+ negate: typing.Union[bool, str] = Field(
+ False,
+ description=element_text_negate_description,
+ )
+
+ children: typing.Optional["assertion_list"] = None
+ asserts: typing.Optional["assertion_list"] = None
+
+ @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
+
+
+class element_text_model(base_element_text_model):
r"""This tag allows the developer to recurisively specify additional assertions as
child elements about just the text contained in the element specified by the
XPath-like ``path``, e.g.
@@ -609,17 +724,11 @@ class element_text_model(AssertionModel):
that: Literal["element_text"] = "element_text"
- path: str = Field(
- ...,
- description=element_text_path_description,
- )
- negate: typing.Union[bool, str] = Field(
- False,
- description=element_text_negate_description,
- )
+class element_text_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
- children: "assertion_list"
+ element_text: base_element_text_model
element_text_is_path_description = """The Python xpath-like expression to find the target element."""
@@ -631,7 +740,26 @@ class element_text_model(AssertionModel):
element_text_is_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class element_text_is_model(AssertionModel):
+class base_element_text_is_model(AssertionModel):
+ """base model for element_text_is describing attributes."""
+
+ path: str = Field(
+ ...,
+ description=element_text_is_path_description,
+ )
+
+ text: str = Field(
+ ...,
+ description=element_text_is_text_description,
+ )
+
+ negate: typing.Union[bool, str] = Field(
+ False,
+ description=element_text_is_negate_description,
+ )
+
+
+class element_text_is_model(base_element_text_is_model):
r"""Asserts the text of the XML element with the specified XPath-like ``path`` is
the specified ``text``.
@@ -647,20 +775,11 @@ class element_text_is_model(AssertionModel):
that: Literal["element_text_is"] = "element_text_is"
- path: str = Field(
- ...,
- description=element_text_is_path_description,
- )
- text: str = Field(
- ...,
- description=element_text_is_text_description,
- )
+class element_text_is_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
- negate: typing.Union[bool, str] = Field(
- False,
- description=element_text_is_negate_description,
- )
+ element_text_is: base_element_text_is_model
element_text_matches_path_description = """The Python xpath-like expression to find the target element."""
@@ -672,21 +791,8 @@ class element_text_is_model(AssertionModel):
)
-class element_text_matches_model(AssertionModel):
- r"""Asserts the text of the XML element with the specified XPath-like ``path``
- matches the regular expression defined by ``expression``.
-
- For example:
-
- ```xml
-
- ```
-
- The assertion implicitly also asserts that an element matching ``path`` exists.
- With ``negate`` the result of the assertion (on the matching) can be inverted (the
- implicit assertion on the existence of the path is not affected)."""
-
- that: Literal["element_text_matches"] = "element_text_matches"
+class base_element_text_matches_model(AssertionModel):
+ """base model for element_text_matches describing attributes."""
path: str = Field(
...,
@@ -704,24 +810,38 @@ class element_text_matches_model(AssertionModel):
)
-has_element_with_path_path_description = """The Python xpath-like expression to find the target element."""
+class element_text_matches_model(base_element_text_matches_model):
+ r"""Asserts the text of the XML element with the specified XPath-like ``path``
+ matches the regular expression defined by ``expression``.
-has_element_with_path_negate_description = (
- """A boolean that can be set to true to negate the outcome of the assertion."""
-)
-
-
-class has_element_with_path_model(AssertionModel):
- r"""Asserts the XML output contains at least one element (or tag) with the specified
- XPath-like ``path``, e.g.
+ For example:
```xml
-
+
```
- With ``negate`` the result of the assertion can be inverted."""
+ The assertion implicitly also asserts that an element matching ``path`` exists.
+ With ``negate`` the result of the assertion (on the matching) can be inverted (the
+ implicit assertion on the existence of the path is not affected)."""
- that: Literal["has_element_with_path"] = "has_element_with_path"
+ that: Literal["element_text_matches"] = "element_text_matches"
+
+
+class element_text_matches_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ element_text_matches: base_element_text_matches_model
+
+
+has_element_with_path_path_description = """The Python xpath-like expression to find the target element."""
+
+has_element_with_path_negate_description = (
+ """A boolean that can be set to true to negate the outcome of the assertion."""
+)
+
+
+class base_has_element_with_path_model(AssertionModel):
+ """base model for has_element_with_path describing attributes."""
path: str = Field(
...,
@@ -734,6 +854,25 @@ class has_element_with_path_model(AssertionModel):
)
+class has_element_with_path_model(base_has_element_with_path_model):
+ r"""Asserts the XML output contains at least one element (or tag) with the specified
+ XPath-like ``path``, e.g.
+
+ ```xml
+
+ ```
+
+ With ``negate`` the result of the assertion can be inverted."""
+
+ that: Literal["has_element_with_path"] = "has_element_with_path"
+
+
+class has_element_with_path_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_element_with_path: base_has_element_with_path_model
+
+
has_n_elements_with_path_path_description = """The Python xpath-like expression to find the target element."""
has_n_elements_with_path_n_description = """Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``"""
@@ -755,21 +894,8 @@ class has_element_with_path_model(AssertionModel):
)
-class has_n_elements_with_path_model(AssertionModel):
- r"""Asserts the XML output contains the specified number (``n``, optionally with ``delta``) of elements (or
- tags) with the specified XPath-like ``path``.
-
- For example:
-
- ```xml
-
- ```
-
- Alternatively to ``n`` and ``delta`` also the ``min`` and ``max`` attributes
- can be used to specify the range of the expected number of occurences.
- With ``negate`` the result of the assertion can be inverted."""
-
- that: Literal["has_n_elements_with_path"] = "has_n_elements_with_path"
+class base_has_n_elements_with_path_model(AssertionModel):
+ """base model for has_n_elements_with_path describing attributes."""
path: str = Field(
...,
@@ -816,12 +942,45 @@ class has_n_elements_with_path_model(AssertionModel):
)
-class is_valid_xml_model(AssertionModel):
+class has_n_elements_with_path_model(base_has_n_elements_with_path_model):
+ r"""Asserts the XML output contains the specified number (``n``, optionally with ``delta``) of elements (or
+ tags) with the specified XPath-like ``path``.
+
+ For example:
+
+ ```xml
+
+ ```
+
+ Alternatively to ``n`` and ``delta`` also the ``min`` and ``max`` attributes
+ can be used to specify the range of the expected number of occurences.
+ With ``negate`` the result of the assertion can be inverted."""
+
+ that: Literal["has_n_elements_with_path"] = "has_n_elements_with_path"
+
+
+class has_n_elements_with_path_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_n_elements_with_path: base_has_n_elements_with_path_model
+
+
+class base_is_valid_xml_model(AssertionModel):
+ """base model for is_valid_xml describing attributes."""
+
+
+class is_valid_xml_model(base_is_valid_xml_model):
r"""Asserts the output is a valid XML file (e.g. ````)."""
that: Literal["is_valid_xml"] = "is_valid_xml"
+class is_valid_xml_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ is_valid_xml: base_is_valid_xml_model
+
+
xml_element_path_description = """The Python xpath-like expression to find the target element."""
xml_element_attribute_description = """The XML attribute name to test against from the target XML element."""
@@ -843,40 +1002,8 @@ class is_valid_xml_model(AssertionModel):
xml_element_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class xml_element_model(AssertionModel):
- r"""Assert if the XML file contains element(s) or tag(s) with the specified
- [XPath-like ``path``](https://lxml.de/xpathxslt.html). If ``n`` and ``delta``
- or ``min`` and ``max`` are given also the number of occurences is checked.
-
- ```xml
-
-
-
-
-
- ```
-
- With ``negate="true"`` the outcome of the assertions wrt the precence and number
- of ``path`` can be negated. If there are any sub assertions then check them against
-
- - the content of the attribute ``attribute``
- - the element's text if no attribute is given
-
- ```xml
-
-
-
-
-
- ```
-
- Sub-assertions are not subject to the ``negate`` attribute of ``xml_element``.
- If ``all`` is ``true`` then the sub assertions are checked for all occurences.
-
- Note that all other XML assertions can be expressed by this assertion (Galaxy
- also implements the other assertions by calling this one)."""
-
- that: Literal["xml_element"] = "xml_element"
+class base_xml_element_model(AssertionModel):
+ """base model for xml_element describing attributes."""
path: str = Field(
...,
@@ -933,21 +1060,58 @@ class xml_element_model(AssertionModel):
)
children: typing.Optional["assertion_list"] = None
+ asserts: typing.Optional["assertion_list"] = None
-has_json_property_with_text_property_description = """The property name to search the JSON document for."""
+class xml_element_model(base_xml_element_model):
+ r"""Assert if the XML file contains element(s) or tag(s) with the specified
+ [XPath-like ``path``](https://lxml.de/xpathxslt.html). If ``n`` and ``delta``
+ or ``min`` and ``max`` are given also the number of occurences is checked.
-has_json_property_with_text_text_description = """The expected text value of the target JSON attribute."""
+ ```xml
+
+
+
+
+
+ ```
+ With ``negate="true"`` the outcome of the assertions wrt the precence and number
+ of ``path`` can be negated. If there are any sub assertions then check them against
-class has_json_property_with_text_model(AssertionModel):
- r"""Asserts the JSON document contains a property or key with the specified text (i.e. string) value.
+ - the content of the attribute ``attribute``
+ - the element's text if no attribute is given
```xml
-
- ```"""
+
+
+
+
+
+ ```
+
+ Sub-assertions are not subject to the ``negate`` attribute of ``xml_element``.
+ If ``all`` is ``true`` then the sub assertions are checked for all occurences.
+
+ Note that all other XML assertions can be expressed by this assertion (Galaxy
+ also implements the other assertions by calling this one)."""
+
+ that: Literal["xml_element"] = "xml_element"
+
+
+class xml_element_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ xml_element: base_xml_element_model
- that: Literal["has_json_property_with_text"] = "has_json_property_with_text"
+
+has_json_property_with_text_property_description = """The property name to search the JSON document for."""
+
+has_json_property_with_text_text_description = """The expected text value of the target JSON attribute."""
+
+
+class base_has_json_property_with_text_model(AssertionModel):
+ """base model for has_json_property_with_text describing attributes."""
property: str = Field(
...,
@@ -960,6 +1124,22 @@ class has_json_property_with_text_model(AssertionModel):
)
+class has_json_property_with_text_model(base_has_json_property_with_text_model):
+ r"""Asserts the JSON document contains a property or key with the specified text (i.e. string) value.
+
+ ```xml
+
+ ```"""
+
+ that: Literal["has_json_property_with_text"] = "has_json_property_with_text"
+
+
+class has_json_property_with_text_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_json_property_with_text: base_has_json_property_with_text_model
+
+
has_json_property_with_value_property_description = """The property name to search the JSON document for."""
has_json_property_with_value_value_description = (
@@ -967,14 +1147,8 @@ class has_json_property_with_text_model(AssertionModel):
)
-class has_json_property_with_value_model(AssertionModel):
- r"""Asserts the JSON document contains a property or key with the specified JSON value.
-
- ```xml
-
- ```"""
-
- that: Literal["has_json_property_with_value"] = "has_json_property_with_value"
+class base_has_json_property_with_value_model(AssertionModel):
+ """base model for has_json_property_with_value describing attributes."""
property: str = Field(
...,
@@ -987,19 +1161,29 @@ class has_json_property_with_value_model(AssertionModel):
)
-has_h5_attribute_key_description = """HDF5 attribute to check value of."""
+class has_json_property_with_value_model(base_has_json_property_with_value_model):
+ r"""Asserts the JSON document contains a property or key with the specified JSON value.
-has_h5_attribute_value_description = """Expected value of HDF5 attribute to check."""
+ ```xml
+
+ ```"""
+ that: Literal["has_json_property_with_value"] = "has_json_property_with_value"
-class has_h5_attribute_model(AssertionModel):
- r"""Asserts HDF5 output contains the specified ``value`` for an attribute (``key``), e.g.
- ```xml
-
- ```"""
+class has_json_property_with_value_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_json_property_with_value: base_has_json_property_with_value_model
- that: Literal["has_h5_attribute"] = "has_h5_attribute"
+
+has_h5_attribute_key_description = """HDF5 attribute to check value of."""
+
+has_h5_attribute_value_description = """Expected value of HDF5 attribute to check."""
+
+
+class base_has_h5_attribute_model(AssertionModel):
+ """base model for has_h5_attribute describing attributes."""
key: str = Field(
...,
@@ -1012,13 +1196,27 @@ class has_h5_attribute_model(AssertionModel):
)
-has_h5_keys_keys_description = """HDF5 attributes to check value of as a comma-separated string."""
+class has_h5_attribute_model(base_has_h5_attribute_model):
+ r"""Asserts HDF5 output contains the specified ``value`` for an attribute (``key``), e.g.
+
+ ```xml
+
+ ```"""
+
+ that: Literal["has_h5_attribute"] = "has_h5_attribute"
-class has_h5_keys_model(AssertionModel):
- r"""Asserts the specified HDF5 output has the given keys."""
+class has_h5_attribute_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_h5_attribute: base_has_h5_attribute_model
- that: Literal["has_h5_keys"] = "has_h5_keys"
+
+has_h5_keys_keys_description = """HDF5 attributes to check value of as a comma-separated string."""
+
+
+class base_has_h5_keys_model(AssertionModel):
+ """base model for has_h5_keys describing attributes."""
keys: str = Field(
...,
@@ -1026,6 +1224,18 @@ class has_h5_keys_model(AssertionModel):
)
+class has_h5_keys_model(base_has_h5_keys_model):
+ r"""Asserts the specified HDF5 output has the given keys."""
+
+ that: Literal["has_h5_keys"] = "has_h5_keys"
+
+
+class has_h5_keys_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_h5_keys: base_has_h5_keys_model
+
+
has_archive_member_path_description = """The regular expression specifying the archive member."""
has_archive_member_all_description = (
@@ -1045,53 +1255,8 @@ class has_h5_keys_model(AssertionModel):
has_archive_member_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_archive_member_model(AssertionModel):
- r"""This tag allows to check if ``path`` is contained in a compressed file.
-
- The path is a regular expression that is matched against the full paths of the objects in
- the compressed file (remember that "matching" means it is checked if a prefix of
- the full path of an archive member is described by the regular expression).
- Valid archive formats include ``.zip``, ``.tar``, and ``.tar.gz``. Note that
- depending on the archive creation method:
-
- - full paths of the members may be prefixed with ``./``
- - directories may be treated as empty files
-
- ```xml
-
- ```
-
- With ``n`` and ``delta`` (or ``min`` and ``max``) assertions on the number of
- archive members matching ``path`` can be expressed. The following could be used,
- e.g., to assert an archive containing n±1 elements out of which at least
- 4 need to have a ``txt`` extension.
-
- ```xml
-
-
- ```
-
- In addition the tag can contain additional assertions as child elements about
- the first member in the archive matching the regular expression ``path``. For
- instance
-
- ```xml
-
-
-
- ```
-
- If the ``all`` attribute is set to ``true`` then all archive members are subject
- to the assertions. Note that, archive members matching the ``path`` are sorted
- alphabetically.
-
- The ``negate`` attribute of the ``has_archive_member`` assertion only affects
- the asserts on the presence and number of matching archive members, but not any
- sub-assertions (which can offer the ``negate`` attribute on their own). The
- check if the file is an archive at all, which is also done by the function, is
- not affected."""
-
- that: Literal["has_archive_member"] = "has_archive_member"
+class base_has_archive_member_model(AssertionModel):
+ """base model for has_archive_member describing attributes."""
path: str = Field(
...,
@@ -1143,6 +1308,62 @@ class has_archive_member_model(AssertionModel):
)
children: typing.Optional["assertion_list"] = None
+ asserts: typing.Optional["assertion_list"] = None
+
+
+class has_archive_member_model(base_has_archive_member_model):
+ r"""This tag allows to check if ``path`` is contained in a compressed file.
+
+ The path is a regular expression that is matched against the full paths of the objects in
+ the compressed file (remember that "matching" means it is checked if a prefix of
+ the full path of an archive member is described by the regular expression).
+ Valid archive formats include ``.zip``, ``.tar``, and ``.tar.gz``. Note that
+ depending on the archive creation method:
+
+ - full paths of the members may be prefixed with ``./``
+ - directories may be treated as empty files
+
+ ```xml
+
+ ```
+
+ With ``n`` and ``delta`` (or ``min`` and ``max``) assertions on the number of
+ archive members matching ``path`` can be expressed. The following could be used,
+ e.g., to assert an archive containing n±1 elements out of which at least
+ 4 need to have a ``txt`` extension.
+
+ ```xml
+
+
+ ```
+
+ In addition the tag can contain additional assertions as child elements about
+ the first member in the archive matching the regular expression ``path``. For
+ instance
+
+ ```xml
+
+
+
+ ```
+
+ If the ``all`` attribute is set to ``true`` then all archive members are subject
+ to the assertions. Note that, archive members matching the ``path`` are sorted
+ alphabetically.
+
+ The ``negate`` attribute of the ``has_archive_member`` assertion only affects
+ the asserts on the presence and number of matching archive members, but not any
+ sub-assertions (which can offer the ``negate`` attribute on their own). The
+ check if the file is an archive at all, which is also done by the function, is
+ not affected."""
+
+ that: Literal["has_archive_member"] = "has_archive_member"
+
+
+class has_archive_member_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_archive_member: base_has_archive_member_model
has_size_value_description = """Deprecated alias for `size`"""
@@ -1160,13 +1381,17 @@ class has_archive_member_model(AssertionModel):
has_size_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_size_model(AssertionModel):
- r"""Asserts the specified output has a size of the specified value
-
- Attributes size and value or synonyms though value is considered deprecated.
- The size optionally allows for absolute (``delta``) difference."""
+class base_has_size_model(AssertionModel):
+ """base model for has_size describing attributes."""
- that: Literal["has_size"] = "has_size"
+ value: Annotated[
+ typing.Optional[typing.Union[str, int]],
+ BeforeValidator(check_bytes),
+ BeforeValidator(check_non_negative_if_int),
+ ] = Field(
+ None,
+ description=has_size_value_description,
+ )
size: Annotated[
typing.Optional[typing.Union[str, int]],
@@ -1208,6 +1433,21 @@ class has_size_model(AssertionModel):
)
+class has_size_model(base_has_size_model):
+ r"""Asserts the specified output has a size of the specified value
+
+ Attributes size and value or synonyms though value is considered deprecated.
+ The size optionally allows for absolute (``delta``) difference."""
+
+ that: Literal["has_size"] = "has_size"
+
+
+class has_size_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_size: base_has_size_model
+
+
has_image_center_of_mass_center_of_mass_description = """The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma)."""
has_image_center_of_mass_channel_description = """Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel)."""
@@ -1217,14 +1457,8 @@ class has_size_model(AssertionModel):
)
-class has_image_center_of_mass_model(AssertionModel):
- r"""Asserts the specified output is an image and has the specified center of mass.
-
- Asserts the output is an image and has a specific center of mass,
- or has an Euclidean distance of ``eps`` or less to that point (e.g.,
- ````)."""
-
- that: Literal["has_image_center_of_mass"] = "has_image_center_of_mass"
+class base_has_image_center_of_mass_model(AssertionModel):
+ """base model for has_image_center_of_mass describing attributes."""
center_of_mass: Annotated[str, BeforeValidator(check_center_of_mass)] = Field(
...,
@@ -1242,6 +1476,22 @@ class has_image_center_of_mass_model(AssertionModel):
)
+class has_image_center_of_mass_model(base_has_image_center_of_mass_model):
+ r"""Asserts the specified output is an image and has the specified center of mass.
+
+ Asserts the output is an image and has a specific center of mass,
+ or has an Euclidean distance of ``eps`` or less to that point (e.g.,
+ ````)."""
+
+ that: Literal["has_image_center_of_mass"] = "has_image_center_of_mass"
+
+
+class has_image_center_of_mass_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_image_center_of_mass: base_has_image_center_of_mass_model
+
+
has_image_channels_channels_description = """Expected number of channels of the image."""
has_image_channels_delta_description = """Maximum allowed difference of the number of channels (default is 0). The observed number of channels has to be in the range ``value +- delta``."""
@@ -1253,14 +1503,8 @@ class has_image_center_of_mass_model(AssertionModel):
has_image_channels_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_image_channels_model(AssertionModel):
- r"""Asserts the output is an image and has a specific number of channels.
-
- The number of channels is plus/minus ``delta`` (e.g., ````).
-
- Alternatively the range of the expected number of channels can be specified by ``min`` and/or ``max``."""
-
- that: Literal["has_image_channels"] = "has_image_channels"
+class base_has_image_channels_model(AssertionModel):
+ """base model for has_image_channels describing attributes."""
channels: Annotated[typing.Optional[StrictInt], BeforeValidator(check_non_negative_if_set)] = Field(
None,
@@ -1288,6 +1532,22 @@ class has_image_channels_model(AssertionModel):
)
+class has_image_channels_model(base_has_image_channels_model):
+ r"""Asserts the output is an image and has a specific number of channels.
+
+ The number of channels is plus/minus ``delta`` (e.g., ````).
+
+ Alternatively the range of the expected number of channels can be specified by ``min`` and/or ``max``."""
+
+ that: Literal["has_image_channels"] = "has_image_channels"
+
+
+class has_image_channels_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_image_channels: base_has_image_channels_model
+
+
has_image_height_height_description = """Expected height of the image (in pixels)."""
has_image_height_delta_description = """Maximum allowed difference of the image height (in pixels, default is 0). The observed height has to be in the range ``value +- delta``."""
@@ -1299,13 +1559,8 @@ class has_image_channels_model(AssertionModel):
has_image_height_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_image_height_model(AssertionModel):
- r"""Asserts the output is an image and has a specific height (in pixels).
-
- The height is plus/minus ``delta`` (e.g., ````).
- Alternatively the range of the expected height can be specified by ``min`` and/or ``max``."""
-
- that: Literal["has_image_height"] = "has_image_height"
+class base_has_image_height_model(AssertionModel):
+ """base model for has_image_height describing attributes."""
height: Annotated[typing.Optional[StrictInt], BeforeValidator(check_non_negative_if_set)] = Field(
None,
@@ -1333,6 +1588,21 @@ class has_image_height_model(AssertionModel):
)
+class has_image_height_model(base_has_image_height_model):
+ r"""Asserts the output is an image and has a specific height (in pixels).
+
+ The height is plus/minus ``delta`` (e.g., ````).
+ Alternatively the range of the expected height can be specified by ``min`` and/or ``max``."""
+
+ that: Literal["has_image_height"] = "has_image_height"
+
+
+class has_image_height_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_image_height: base_has_image_height_model
+
+
has_image_mean_intensity_channel_description = """Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel)."""
has_image_mean_intensity_mean_intensity_description = """The required mean value of the image intensities."""
@@ -1344,13 +1614,8 @@ class has_image_height_model(AssertionModel):
has_image_mean_intensity_max_description = """An upper bound of the required mean value of the image intensities."""
-class has_image_mean_intensity_model(AssertionModel):
- r"""Asserts the output is an image and has a specific mean intensity value.
-
- The mean intensity value is plus/minus ``eps`` (e.g., ````).
- Alternatively the range of the expected mean intensity value can be specified by ``min`` and/or ``max``."""
-
- that: Literal["has_image_mean_intensity"] = "has_image_mean_intensity"
+class base_has_image_mean_intensity_model(AssertionModel):
+ """base model for has_image_mean_intensity describing attributes."""
channel: typing.Optional[StrictInt] = Field(
None,
@@ -1378,6 +1643,21 @@ class has_image_mean_intensity_model(AssertionModel):
)
+class has_image_mean_intensity_model(base_has_image_mean_intensity_model):
+ r"""Asserts the output is an image and has a specific mean intensity value.
+
+ The mean intensity value is plus/minus ``eps`` (e.g., ````).
+ Alternatively the range of the expected mean intensity value can be specified by ``min`` and/or ``max``."""
+
+ that: Literal["has_image_mean_intensity"] = "has_image_mean_intensity"
+
+
+class has_image_mean_intensity_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_image_mean_intensity: base_has_image_mean_intensity_model
+
+
has_image_mean_object_size_channel_description = """Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel)."""
has_image_mean_object_size_labels_description = """List of labels, separated by a comma. Labels *not* on this list will be excluded from consideration. Cannot be used in combination with ``exclude_labels``."""
@@ -1397,14 +1677,8 @@ class has_image_mean_intensity_model(AssertionModel):
)
-class has_image_mean_object_size_model(AssertionModel):
- r"""Asserts the output is an image with labeled objects which have the specified mean size (number of pixels),
-
- The mean size is plus/minus ``eps`` (e.g., ````).
-
- The labels must be unique."""
-
- that: Literal["has_image_mean_object_size"] = "has_image_mean_object_size"
+class base_has_image_mean_object_size_model(AssertionModel):
+ """base model for has_image_mean_object_size describing attributes."""
channel: typing.Optional[StrictInt] = Field(
None,
@@ -1448,6 +1722,22 @@ class has_image_mean_object_size_model(AssertionModel):
)
+class has_image_mean_object_size_model(base_has_image_mean_object_size_model):
+ r"""Asserts the output is an image with labeled objects which have the specified mean size (number of pixels),
+
+ The mean size is plus/minus ``eps`` (e.g., ````).
+
+ The labels must be unique."""
+
+ that: Literal["has_image_mean_object_size"] = "has_image_mean_object_size"
+
+
+class has_image_mean_object_size_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_image_mean_object_size: base_has_image_mean_object_size_model
+
+
has_image_n_labels_channel_description = """Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel)."""
has_image_n_labels_labels_description = """List of labels, separated by a comma. Labels *not* on this list will be excluded from consideration. Cannot be used in combination with ``exclude_labels``."""
@@ -1465,15 +1755,8 @@ class has_image_mean_object_size_model(AssertionModel):
has_image_n_labels_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_image_n_labels_model(AssertionModel):
- r"""Asserts the output is an image and has the specified labels.
-
- Labels can be a number of labels or unique values (e.g.,
- ````).
-
- The primary usage of this assertion is to verify the number of objects in images with uniquely labeled objects."""
-
- that: Literal["has_image_n_labels"] = "has_image_n_labels"
+class base_has_image_n_labels_model(AssertionModel):
+ """base model for has_image_n_labels describing attributes."""
channel: typing.Optional[StrictInt] = Field(
None,
@@ -1516,6 +1799,23 @@ class has_image_n_labels_model(AssertionModel):
)
+class has_image_n_labels_model(base_has_image_n_labels_model):
+ r"""Asserts the output is an image and has the specified labels.
+
+ Labels can be a number of labels or unique values (e.g.,
+ ````).
+
+ The primary usage of this assertion is to verify the number of objects in images with uniquely labeled objects."""
+
+ that: Literal["has_image_n_labels"] = "has_image_n_labels"
+
+
+class has_image_n_labels_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_image_n_labels: base_has_image_n_labels_model
+
+
has_image_width_width_description = """Expected width of the image (in pixels)."""
has_image_width_delta_description = """Maximum allowed difference of the image width (in pixels, default is 0). The observed width has to be in the range ``value +- delta``."""
@@ -1527,13 +1827,8 @@ class has_image_n_labels_model(AssertionModel):
has_image_width_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
-class has_image_width_model(AssertionModel):
- r"""Asserts the output is an image and has a specific width (in pixels).
-
- The width is plus/minus ``delta`` (e.g., ````).
- Alternatively the range of the expected width can be specified by ``min`` and/or ``max``."""
-
- that: Literal["has_image_width"] = "has_image_width"
+class base_has_image_width_model(AssertionModel):
+ """base model for has_image_width describing attributes."""
width: Annotated[typing.Optional[StrictInt], BeforeValidator(check_non_negative_if_set)] = Field(
None,
@@ -1561,7 +1856,22 @@ class has_image_width_model(AssertionModel):
)
-any_assertion_model = Annotated[
+class has_image_width_model(base_has_image_width_model):
+ r"""Asserts the output is an image and has a specific width (in pixels).
+
+ The width is plus/minus ``delta`` (e.g., ````).
+ Alternatively the range of the expected width can be specified by ``min`` and/or ``max``."""
+
+ that: Literal["has_image_width"] = "has_image_width"
+
+
+class has_image_width_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_image_width: base_has_image_width_model
+
+
+any_assertion_model_flat = Annotated[
typing.Union[
has_line_model,
has_line_matching_model,
@@ -1596,4 +1906,100 @@ class has_image_width_model(AssertionModel):
Field(discriminator="that"),
]
-assertion_list = RootModel[typing.List[any_assertion_model]]
+any_assertion_model_nested = typing.Union[
+ has_line_model_nested,
+ has_line_matching_model_nested,
+ has_n_lines_model_nested,
+ has_text_model_nested,
+ has_text_matching_model_nested,
+ not_has_text_model_nested,
+ has_n_columns_model_nested,
+ attribute_is_model_nested,
+ attribute_matches_model_nested,
+ element_text_model_nested,
+ element_text_is_model_nested,
+ element_text_matches_model_nested,
+ has_element_with_path_model_nested,
+ has_n_elements_with_path_model_nested,
+ is_valid_xml_model_nested,
+ xml_element_model_nested,
+ has_json_property_with_text_model_nested,
+ has_json_property_with_value_model_nested,
+ has_h5_attribute_model_nested,
+ has_h5_keys_model_nested,
+ has_archive_member_model_nested,
+ has_size_model_nested,
+ has_image_center_of_mass_model_nested,
+ has_image_channels_model_nested,
+ has_image_height_model_nested,
+ has_image_mean_intensity_model_nested,
+ has_image_mean_object_size_model_nested,
+ has_image_n_labels_model_nested,
+ has_image_width_model_nested,
+]
+
+assertion_list = RootModel[typing.List[typing.Union[any_assertion_model_flat, any_assertion_model_nested]]]
+
+
+class assertion_dict(AssertionModel):
+
+ has_line: typing.Optional[base_has_line_model] = None
+
+ has_line_matching: typing.Optional[base_has_line_matching_model] = None
+
+ has_n_lines: typing.Optional[base_has_n_lines_model] = None
+
+ has_text: typing.Optional[base_has_text_model] = None
+
+ has_text_matching: typing.Optional[base_has_text_matching_model] = None
+
+ not_has_text: typing.Optional[base_not_has_text_model] = None
+
+ has_n_columns: typing.Optional[base_has_n_columns_model] = None
+
+ attribute_is: typing.Optional[base_attribute_is_model] = None
+
+ attribute_matches: typing.Optional[base_attribute_matches_model] = None
+
+ element_text: typing.Optional[base_element_text_model] = None
+
+ element_text_is: typing.Optional[base_element_text_is_model] = None
+
+ element_text_matches: typing.Optional[base_element_text_matches_model] = None
+
+ has_element_with_path: typing.Optional[base_has_element_with_path_model] = None
+
+ has_n_elements_with_path: typing.Optional[base_has_n_elements_with_path_model] = None
+
+ is_valid_xml: typing.Optional[base_is_valid_xml_model] = None
+
+ xml_element: typing.Optional[base_xml_element_model] = None
+
+ has_json_property_with_text: typing.Optional[base_has_json_property_with_text_model] = None
+
+ has_json_property_with_value: typing.Optional[base_has_json_property_with_value_model] = None
+
+ has_h5_attribute: typing.Optional[base_has_h5_attribute_model] = None
+
+ has_h5_keys: typing.Optional[base_has_h5_keys_model] = None
+
+ has_archive_member: typing.Optional[base_has_archive_member_model] = None
+
+ has_size: typing.Optional[base_has_size_model] = None
+
+ has_image_center_of_mass: typing.Optional[base_has_image_center_of_mass_model] = None
+
+ has_image_channels: typing.Optional[base_has_image_channels_model] = None
+
+ has_image_height: typing.Optional[base_has_image_height_model] = None
+
+ has_image_mean_intensity: typing.Optional[base_has_image_mean_intensity_model] = None
+
+ has_image_mean_object_size: typing.Optional[base_has_image_mean_object_size_model] = None
+
+ has_image_n_labels: typing.Optional[base_has_image_n_labels_model] = None
+
+ has_image_width: typing.Optional[base_has_image_width_model] = None
+
+
+assertions = typing.Union[assertion_list, assertion_dict]
diff --git a/lib/galaxy/tool_util/verify/asserts/size.py b/lib/galaxy/tool_util/verify/asserts/size.py
index e4b3e8a6ef1f..0e3eebe06f86 100644
--- a/lib/galaxy/tool_util/verify/asserts/size.py
+++ b/lib/galaxy/tool_util/verify/asserts/size.py
@@ -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(
diff --git a/lib/galaxy/tool_util/verify/codegen.py b/lib/galaxy/tool_util/verify/codegen.py
index 7219d40b6dcb..2e93d29ccbf0 100644
--- a/lib/galaxy/tool_util/verify/codegen.py
+++ b/lib/galaxy/tool_util/verify/codegen.py
@@ -55,6 +55,7 @@
BeforeValidator,
ConfigDict,
Field,
+ model_validator,
RootModel,
StrictFloat,
StrictInt,
@@ -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(
@@ -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]
"""
diff --git a/lib/galaxy/workflow/scheduling_manager.py b/lib/galaxy/workflow/scheduling_manager.py
index 3868e24c13a9..8d31130bad21 100644
--- a/lib/galaxy/workflow/scheduling_manager.py
+++ b/lib/galaxy/workflow/scheduling_manager.py
@@ -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()
diff --git a/lib/galaxy_test/api/test_workflows.py b/lib/galaxy_test/api/test_workflows.py
index d7a77b04bb22..b6a5ec037bea 100644
--- a/lib/galaxy_test/api/test_workflows.py
+++ b/lib/galaxy_test/api/test_workflows.py
@@ -5150,56 +5150,6 @@ def test_run_with_numeric_input_connection(self, history_id):
assert int(str_43) == 43
assert abs(float(str_4point14) - 4.14) < 0.0001
- @skip_without_tool("param_value_from_file")
- def test_expression_tool_map_over(self, history_id):
- self._run_jobs(
- """
-class: GalaxyWorkflow
-inputs:
- text_input1: collection
-steps:
-- label: param_out
- tool_id: param_value_from_file
- in:
- input1: text_input1
-- label: consume_expression_parameter
- tool_id: validation_default
- in:
- input1: param_out/text_param
- outputs:
- out_file1:
- rename: "replaced_param_collection"
-test_data:
- text_input1:
- collection_type: list
- elements:
- - identifier: A
- content: A
- - identifier: B
- content: B
-""",
- history_id=history_id,
- )
- history_contents = self._get(f"histories/{history_id}/contents").json()
- collection = [
- c
- for c in history_contents
- if c["history_content_type"] == "dataset_collection" and c["name"] == "replaced_param_collection"
- ][0]
- collection_details = self._get(collection["url"]).json()
- assert collection_details["element_count"] == 2
- elements = collection_details["elements"]
- assert elements[0]["element_identifier"] == "A"
- assert elements[1]["element_identifier"] == "B"
- element_a_content = self.dataset_populator.get_history_dataset_content(
- history_id, dataset=elements[0]["object"]
- )
- element_b_content = self.dataset_populator.get_history_dataset_content(
- history_id, dataset=elements[1]["object"]
- )
- assert element_a_content.strip() == "A"
- assert element_b_content.strip() == "B"
-
@skip_without_tool("create_input_collection")
def test_workflow_optional_input_text_parameter_reevaluation(self):
with self.dataset_populator.test_history() as history_id:
diff --git a/lib/galaxy_test/base/populators.py b/lib/galaxy_test/base/populators.py
index 5c25baf2bd89..98dc9cb5215f 100644
--- a/lib/galaxy_test/base/populators.py
+++ b/lib/galaxy_test/base/populators.py
@@ -3151,7 +3151,12 @@ def read_test_data(test_dict):
elif is_dict and "type" in value:
input_type = value.pop("type")
if input_type == "File":
- content = open_test_data(value)
+ if "value" in value:
+ content = open_test_data(value)
+ elif "content" in value:
+ content = value["content"]
+ else:
+ raise ValueError(f"Invalid test_data def {test_data}")
new_dataset_kwds = {"content": content}
if "name" in value:
new_dataset_kwds["name"] = value["name"]
diff --git a/lib/galaxy_test/workflow/flatten_collection.gxwf-tests.yml b/lib/galaxy_test/workflow/flatten_collection.gxwf-tests.yml
index 1a2b5c65b596..bfd0a6a02435 100644
--- a/lib/galaxy_test/workflow/flatten_collection.gxwf-tests.yml
+++ b/lib/galaxy_test/workflow/flatten_collection.gxwf-tests.yml
@@ -3,6 +3,7 @@
job: {}
outputs:
out:
+ attributes: {collection_type: 'list'}
elements:
'oe1-ie1':
asserts:
diff --git a/lib/galaxy_test/workflow/map_over_expression.gxwf-tests.yml b/lib/galaxy_test/workflow/map_over_expression.gxwf-tests.yml
new file mode 100644
index 000000000000..0357ca6c53ca
--- /dev/null
+++ b/lib/galaxy_test/workflow/map_over_expression.gxwf-tests.yml
@@ -0,0 +1,22 @@
+- doc: |
+ Test to verify text parameter can be connected to data column param
+ job:
+ text_input1:
+ collection_type: list
+ elements:
+ - identifier: A
+ content: A
+ - identifier: B
+ content: B
+ outputs:
+ out1:
+ attributes: { collection_type: list }
+ elements:
+ A:
+ asserts:
+ - that: has_line
+ line: A
+ B:
+ asserts:
+ - that: has_line
+ line: B
diff --git a/lib/galaxy_test/workflow/map_over_expression.gxwf.yml b/lib/galaxy_test/workflow/map_over_expression.gxwf.yml
new file mode 100644
index 000000000000..c361b2232022
--- /dev/null
+++ b/lib/galaxy_test/workflow/map_over_expression.gxwf.yml
@@ -0,0 +1,18 @@
+class: GalaxyWorkflow
+inputs:
+ text_input1: collection
+outputs:
+ out1:
+ outputSource: consume_expression_parameter/out_file1
+steps:
+ param_out:
+ tool_id: param_value_from_file
+ in:
+ input1: text_input1
+ consume_expression_parameter:
+ tool_id: validation_default
+ in:
+ input1: param_out/text_param
+ outputs:
+ out_file1:
+ rename: "replaced_param_collection"
diff --git a/lib/galaxy_test/workflow/output_parameter.gxwf-tests.yml b/lib/galaxy_test/workflow/output_parameter.gxwf-tests.yml
new file mode 100644
index 000000000000..d422d9e3d836
--- /dev/null
+++ b/lib/galaxy_test/workflow/output_parameter.gxwf-tests.yml
@@ -0,0 +1,8 @@
+- doc: |
+ Test to verify exact output parameter verification works propery.
+ job:
+ text_int:
+ type: File
+ content: "43"
+ outputs:
+ out_int: 43
diff --git a/lib/galaxy_test/workflow/output_parameter.gxwf.yml b/lib/galaxy_test/workflow/output_parameter.gxwf.yml
new file mode 100644
index 000000000000..4157f894f492
--- /dev/null
+++ b/lib/galaxy_test/workflow/output_parameter.gxwf.yml
@@ -0,0 +1,13 @@
+class: GalaxyWorkflow
+inputs:
+ text_int: data
+outputs:
+ out_int:
+ outputSource: param_out/integer_param
+steps:
+ param_out:
+ tool_id: param_value_from_file
+ state:
+ param_type: integer
+ in:
+ input1: text_int
diff --git a/lib/galaxy_test/workflow/tests.py b/lib/galaxy_test/workflow/tests.py
index f0702fb8240b..a850490740b6 100644
--- a/lib/galaxy_test/workflow/tests.py
+++ b/lib/galaxy_test/workflow/tests.py
@@ -8,6 +8,12 @@
import yaml
from gxformat2.yaml import ordered_load
+from galaxy.tool_util.models import (
+ OutputChecks,
+ OutputsDict,
+ TestDicts,
+ TestJobDict,
+)
from galaxy.tool_util.parser.interface import TestCollectionOutputDef
from galaxy.tool_util.verify import verify_file_contents_against_dict
from galaxy.tool_util.verify.interactor import (
@@ -52,7 +58,7 @@ def setUp(self):
self.dataset_collection_populator = DatasetCollectionPopulator(self.galaxy_interactor)
@pytest.mark.workflow
- def test_workflow(self, workflow_path: Path, test_job):
+ def test_workflow(self, workflow_path: Path, test_job: TestJobDict):
with workflow_path.open() as f:
yaml_content = ordered_load(f)
with self.dataset_populator.test_history() as history_id:
@@ -63,30 +69,32 @@ def test_workflow(self, workflow_path: Path, test_job):
)
self._verify(run_summary, test_job["outputs"])
- def _verify(self, run_summary: RunJobsSummary, output_definitions):
+ def _verify(self, run_summary: RunJobsSummary, output_definitions: OutputsDict):
for output_name, output_definition in output_definitions.items():
self._verify_output(run_summary, output_name, output_definition)
- def _verify_output(self, run_summary: RunJobsSummary, output_name, test_properties):
- is_collection_test = "elements" in test_properties
+ def _verify_output(self, run_summary: RunJobsSummary, output_name, test_properties: OutputChecks):
+ is_collection_test = isinstance(test_properties, dict) and "elements" in test_properties
item_label = f"Output named {output_name}"
def get_filename(name):
return tempfile.NamedTemporaryFile(prefix=f"gx_workflow_framework_test_file_{output_name}", delete=False)
- def verify_dataset(dataset: dict, test_properties: dict):
+ def verify_dataset(dataset: dict, test_properties: OutputChecks):
output_content = self.dataset_populator.get_history_dataset_content(
run_summary.history_id, dataset=dataset, type="bytes"
)
verify_file_contents_against_dict(get_filename, _get_location, item_label, output_content, test_properties)
- metadata = get_metadata_to_test(test_properties)
- if metadata:
- dataset_details = self.dataset_populator.get_history_dataset_details(
- run_summary.history_id, content_id=dataset["id"]
- )
- compare_expected_metadata_to_api_response(metadata, dataset_details)
+ if isinstance(test_properties, dict):
+ metadata = get_metadata_to_test(test_properties)
+ if metadata:
+ dataset_details = self.dataset_populator.get_history_dataset_details(
+ run_summary.history_id, content_id=dataset["id"]
+ )
+ compare_expected_metadata_to_api_response(metadata, dataset_details)
if is_collection_test:
+ assert isinstance(test_properties, dict)
test_properties["name"] = output_name
# setup preferred name "elements" in accordance with work in https://github.com/galaxyproject/planemo/pull/1417
test_properties["element_tests"] = test_properties["elements"]
@@ -105,14 +113,15 @@ def verify_dataset_element(element, test_properties, element_outfile):
verify_collection(output_def, output_collection, verify_dataset_element)
else:
- test_properties["name"] = output_name
+ if isinstance(test_properties, dict):
+ test_properties["name"] = output_name
invocation_details = self.workflow_populator.get_invocation(run_summary.invocation_id, step_details=True)
assert output_name in invocation_details["outputs"]
test_output = invocation_details["outputs"][output_name]
verify_dataset(test_output, test_properties)
-def _test_jobs(workflow_path: Path) -> list:
+def _test_jobs(workflow_path: Path) -> TestDicts:
test_path = _workflow_test_path(workflow_path)
with test_path.open() as f:
jobs = yaml.safe_load(f)
diff --git a/packages/tool_util/setup.cfg b/packages/tool_util/setup.cfg
index 6fd52ba67a7f..eabca1a6e1b9 100644
--- a/packages/tool_util/setup.cfg
+++ b/packages/tool_util/setup.cfg
@@ -51,6 +51,7 @@ console_scripts =
galaxy-tool-test = galaxy.tool_util.verify.script:main
galaxy-tool-test-case-validation = galaxy.tool_util.parameters.scripts.validate_test_cases:main
galaxy-tool-upgrade-advisor = galaxy.tool_util.upgrade.script:main
+ validate-test-format = galaxy.tool_util.validate_test_format:main
mulled-build = galaxy.tool_util.deps.mulled.mulled_build:main
mulled-build-channel = galaxy.tool_util.deps.mulled.mulled_build_channel:main
mulled-build-files = galaxy.tool_util.deps.mulled.mulled_build_files:main
diff --git a/test/functional/tools/sample_tool_conf.xml b/test/functional/tools/sample_tool_conf.xml
index 8e22ee39eace..ebc4fb3cbd59 100644
--- a/test/functional/tools/sample_tool_conf.xml
+++ b/test/functional/tools/sample_tool_conf.xml
@@ -139,6 +139,7 @@
+
diff --git a/test/unit/tool_util/test_test_format_model.py b/test/unit/tool_util/test_test_format_model.py
new file mode 100644
index 000000000000..986f5c12ba87
--- /dev/null
+++ b/test/unit/tool_util/test_test_format_model.py
@@ -0,0 +1,39 @@
+import os
+from pathlib import Path
+from typing import List
+
+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")
+IWC_WORKFLOWS_USING_UNVERIFIED_SYNTAX: List[str] = []
+
+
+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")
+
+ for test_file in test_files:
+ print(test_file)
+ skip_file = False
+ for unverified in IWC_WORKFLOWS_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)