Modeling from assertions.

This commit is contained in:
John Chilton
2024-09-04 23:00:40 -04:00
parent d7f2776d7b
commit b70d83d8b2
17 changed files with 3187 additions and 1180 deletions
+11 -19
View File
@@ -83,28 +83,20 @@ def _build_tag(tag, hide_attributes):
assertions_buffer.write(_doc_or_none(assertions_tag))
assertions_buffer.write("\n\n")
assertion_groups = assertions_tag.xpath(
"xs:choice/xs:group", namespaces={"xs": "http://www.w3.org/2001/XMLSchema"}
assertion_tag = xmlschema_doc.find("//{http://www.w3.org/2001/XMLSchema}group[@name='TestAssertion']")
elements = assertion_tag.findall(
"{http://www.w3.org/2001/XMLSchema}choice/{http://www.w3.org/2001/XMLSchema}element"
)
for group in assertion_groups:
ref = group.attrib["ref"]
assertion_tag = xmlschema_doc.find("//{http://www.w3.org/2001/XMLSchema}group[@name='" + ref + "']")
doc = _doc_or_none(assertion_tag)
assertions_buffer.write(f"### {doc}\n\n")
elements = assertion_tag.findall(
"{http://www.w3.org/2001/XMLSchema}choice/{http://www.w3.org/2001/XMLSchema}element"
)
for element in elements:
for element in elements:
doc = _doc_or_none(element)
if doc is None:
doc = _doc_or_none(element)
if doc is None:
doc = _doc_or_none(_type_el(element))
assert doc is not None, f"Documentation for {element.attrib['name']} is empty"
doc = doc.strip()
assert doc is not None, f"Documentation for {element.attrib['name']} is empty"
doc = doc.strip()
element_el = _find_tag_el(element)
element_attributes = _find_attributes(element_el)
doc = _replace_attribute_list(element_el, doc, element_attributes)
assertions_buffer.write(f"#### ``{element.attrib['name']}``:\n\n{doc}\n\n")
element_attributes = _find_attributes(element)
doc = _replace_attribute_list(element, doc, element_attributes)
assertions_buffer.write(f"#### ``{element.attrib['name']}``:\n\n{doc}\n\n")
text = text.replace(line, assertions_buffer.getvalue())
tag_help.write(text)
if best_practices := _get_bp_link(annotation_el):
File diff suppressed because it is too large Load Diff
@@ -5,6 +5,11 @@ from inspect import (
getmembers,
)
from tempfile import NamedTemporaryFile
from typing import (
Callable,
Dict,
Tuple,
)
from galaxy.util import unicodify
from galaxy.util.compression_utils import get_fileobj
@@ -13,11 +18,8 @@ log = logging.getLogger(__name__)
assertion_module_names = ["text", "tabular", "xml", "json", "hdf5", "archive", "size", "image"]
# Code for loading modules containing assertion checking functions, to
# create a new module of assertion functions, create the needed python
# source file "test/base/asserts/<MODULE_NAME>.py" and add
# <MODULE_NAME> to the list of assertion module names defined above.
assertion_functions = {}
assertion_module_and_functions: Dict[str, Tuple[str, Callable]] = {}
for assertion_module_name in assertion_module_names:
full_assertion_module_name = f"galaxy.tool_util.verify.asserts.{assertion_module_name}"
try:
@@ -29,10 +31,17 @@ for assertion_module_name in assertion_module_names:
continue
for member, value in getmembers(assertion_module):
if member.startswith("assert_"):
assertion_functions[member] = value
assertion_module_and_functions[member] = (f"{full_assertion_module_name}.{member}", value)
def verify_assertions(data: bytes, assertion_description_list, decompress: bool = False):
# Code for loading modules containing assertion checking functions, to
# create a new module of assertion functions, create the needed python
# source file "test/base/asserts/<MODULE_NAME>.py" and add
# <MODULE_NAME> to the list of assertion module names defined above.
assertion_functions: Dict[str, Callable] = {k: v[1] for (k, v) in assertion_module_and_functions.items()}
def verify_assertions(data: bytes, assertion_description_list: list, decompress: bool = False):
"""This function takes a list of assertions and a string to check
these assertions against."""
if decompress:
@@ -0,0 +1,102 @@
from typing import (
Any,
List,
Optional,
Union,
)
from typing_extensions import (
Annotated,
Protocol,
)
class AssertionParameter:
doc: str
xml_type: Optional[str]
json_type: Optional[str]
deprecated: bool
validators: List[str]
def __init__(
self,
doc: Optional[str],
xml_type: Optional[str] = None,
json_type: Optional[str] = None,
deprecated: bool = False,
validators: Optional[List[str]] = None,
):
self.doc = doc or ""
self.xml_type = xml_type
self.json_type = json_type
self.deprecated = deprecated
self.validators = validators or []
XmlInt = Union[int, str]
XmlFloat = Union[float, str]
XmlBool = Union[bool, str]
XmlRegex = str
OptionalXmlInt = Optional[XmlInt]
OptionalXmlFloat = Optional[XmlFloat]
OptionalXmlBool = Optional[XmlBool]
Output = Annotated[str, "The target output of a tool or workflow read as a UTF-8 string"]
OutputBytes = Annotated[bytes, "The target output of a tool or workflow read as raw Python 'bytes'"]
class VerifyAssertionsFunction(Protocol):
def __call__(self, data: bytes, assertion_description_list: list, decompress: bool = False):
"""Callback for recursirve functions."""
ChildAssertions = Annotated[Any, "Parsed child assertions"]
Negate = Annotated[
XmlBool,
AssertionParameter(
"A boolean that can be set to true to negate the outcome of the assertion.", xml_type="PermissiveBoolean"
),
]
NEGATE_DEFAULT = False
N = Annotated[
Optional[XmlInt], AssertionParameter("Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", xml_type="Bytes")
]
Delta = Annotated[
XmlInt,
AssertionParameter(
"Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", xml_type="Bytes"
),
]
Min = Annotated[
Optional[XmlInt],
AssertionParameter("Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", xml_type="Bytes"),
]
Max = Annotated[
Optional[XmlInt],
AssertionParameter("Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", xml_type="Bytes"),
]
__all__ = (
"Annotated",
"AssertionParameter",
"ChildAssertions",
"Delta",
"Max",
"Min",
"Negate",
"N",
"NEGATE_DEFAULT",
"OptionalXmlBool",
"OptionalXmlFloat",
"OptionalXmlInt",
"Output",
"OutputBytes",
"VerifyAssertionsFunction",
"XmlBool",
"XmlFloat",
"XmlInt",
"XmlRegex",
)
+78 -17
View File
@@ -3,12 +3,22 @@ import re
import tarfile
import tempfile
import zipfile
from typing import (
Optional,
Union,
)
from galaxy.util import asbool
from ._types import (
Annotated,
AssertionParameter,
ChildAssertions,
Delta,
Max,
Min,
N,
Negate,
NEGATE_DEFAULT,
OutputBytes,
VerifyAssertionsFunction,
XmlBool,
)
from ._util import _assert_presence_number
@@ -55,21 +65,72 @@ def _list_from_zip(output_bytes, path):
return sorted(lst)
Path = Annotated[str, AssertionParameter("The regular expression specifying the archive member.")]
All = Annotated[
XmlBool,
AssertionParameter(
"Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first",
xml_type="PermissiveBoolean",
),
]
def assert_has_archive_member(
output_bytes: bytes,
path: str,
verify_assertions_function,
children,
all: Union[bool, str] = False,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output_bytes: OutputBytes,
path: Path,
verify_assertions_function: VerifyAssertionsFunction,
children: ChildAssertions = None,
all: All = False,
n: N = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Recursively checks the specified children assertions against the text of
the first element matching the specified path found within the archive.
Currently supported formats: .zip, .tar, .tar.gz."""
"""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
<has_archive_member path="./path/to/my-file.txt"/>
```
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&plusmn;1 elements out of which at least
4 need to have a ``txt`` extension.
```xml
<has_archive_member path=".*" n="10" delta="1"/>
<has_archive_member path=".*\\.txt" min="4"/>
```
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
<has_archive_member path=".*/my-file.txt">
<not_has_text text="EDK72998.1"/>
</has_archive_member>
```
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."""
all = asbool(all)
extract_foo = None
# from python 3.9 is_tarfile supports file like objects then we do not need
+18 -4
View File
@@ -5,17 +5,31 @@ try:
except ImportError:
h5py = None
from ._types import (
Annotated,
AssertionParameter,
OutputBytes,
)
IMPORT_MISSING_MESSAGE = "h5 assertion requires unavailable optional dependency h5py"
Key = Annotated[str, AssertionParameter("HDF5 attribute to check value of.")]
Value = Annotated[str, AssertionParameter("Expected value of HDF5 attribute to check.")]
Keys = Annotated[str, AssertionParameter("HDF5 attributes to check value of as a comma-separated string.")]
def _assert_h5py():
if h5py is None:
raise Exception(IMPORT_MISSING_MESSAGE)
def assert_has_h5_attribute(output_bytes: bytes, key: str, value: str) -> None:
"""Asserts the specified HDF5 output has a given key-value pair as HDF5
attribute"""
def assert_has_h5_attribute(output_bytes: OutputBytes, key: Key, value: Value) -> None:
"""Asserts HDF5 output contains the specified ``value`` for an attribute (``key``), e.g.
```xml
<has_h5_attribute key="nchroms" value="15" />
```
"""
_assert_h5py()
output_temp = io.BytesIO(output_bytes)
local_attrs = h5py.File(output_temp, "r").attrs
@@ -25,7 +39,7 @@ def assert_has_h5_attribute(output_bytes: bytes, key: str, value: str) -> None:
# TODO the function actually queries groups. so the function and argument name are misleading
def assert_has_h5_keys(output_bytes: bytes, keys: str) -> None:
def assert_has_h5_keys(output_bytes: OutputBytes, keys: Keys) -> None:
"""Asserts the specified HDF5 output has the given keys."""
_assert_h5py()
h5_keys = sorted([k.strip() for k in keys.strip().split(",")])
+341 -63
View File
@@ -8,6 +8,17 @@ from typing import (
Union,
)
from ._types import (
Annotated,
AssertionParameter,
Negate,
NEGATE_DEFAULT,
OptionalXmlFloat,
OptionalXmlInt,
OutputBytes,
XmlFloat,
XmlInt,
)
from ._util import _assert_number
try:
@@ -27,6 +38,255 @@ if TYPE_CHECKING:
import numpy.typing
JSON_STRICT_NUMBER = "typing.Union[StrictInt, StrictFloat]"
JSON_OPTIONAL_STRICT_NUMBER = f"typing.Optional[{JSON_STRICT_NUMBER}]"
Width = Annotated[
OptionalXmlInt,
AssertionParameter(
"Expected width of the image (in pixels).",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
Height = Annotated[
OptionalXmlInt,
AssertionParameter(
"Expected height of the image (in pixels).",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
Channels = Annotated[
OptionalXmlInt,
AssertionParameter(
"Expected number of channels of the image.",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
WidthDelta = Annotated[
XmlInt,
AssertionParameter(
"Maximum allowed difference of the image width (in pixels, default is 0). The observed width has to be in the range ``value +- delta``.",
json_type="StrictInt",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
WidthMin = Annotated[
OptionalXmlInt,
AssertionParameter(
"Minimum allowed width of the image (in pixels).",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
WidthMax = Annotated[
OptionalXmlInt,
AssertionParameter(
"Maximum allowed width of the image (in pixels).",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
HeightDelta = Annotated[
XmlInt,
AssertionParameter(
"Maximum allowed difference of the image height (in pixels, default is 0). The observed height has to be in the range ``value +- delta``.",
json_type="StrictInt",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
HeightMin = Annotated[
OptionalXmlInt,
AssertionParameter(
"Minimum allowed height of the image (in pixels).",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
HeightMax = Annotated[
OptionalXmlInt,
AssertionParameter(
"Maximum allowed height of the image (in pixels).",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
ChannelsDelta = Annotated[
XmlInt,
AssertionParameter(
"Maximum allowed difference of the number of channels (default is 0). The observed number of channels has to be in the range ``value +- delta``.",
json_type="StrictInt",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
ChannelsMin = Annotated[
OptionalXmlInt,
AssertionParameter(
"Minimum allowed number of channels.",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
ChannelsMax = Annotated[
OptionalXmlInt,
AssertionParameter(
"Maximum allowed number of channels.",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
MeanIntensity = Annotated[
OptionalXmlFloat,
AssertionParameter("The required mean value of the image intensities.", json_type=JSON_OPTIONAL_STRICT_NUMBER),
]
MeanIntensityEps = Annotated[
XmlFloat,
AssertionParameter(
"The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean value of the image intensities has to be in the range ``value +- eps``.",
json_type=JSON_STRICT_NUMBER,
validators=["check_non_negative_if_set"],
),
]
MeanIntensityMin = Annotated[
OptionalXmlFloat,
AssertionParameter(
"A lower bound of the required mean value of the image intensities.", json_type=JSON_OPTIONAL_STRICT_NUMBER
),
]
MeanIntensityMax = Annotated[
OptionalXmlFloat,
AssertionParameter(
"An upper bound of the required mean value of the image intensities.", json_type=JSON_OPTIONAL_STRICT_NUMBER
),
]
NumLabels = Annotated[
OptionalXmlInt,
AssertionParameter(
"Expected number of labels.",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
NumLabelsDelta = Annotated[
XmlInt,
AssertionParameter(
"Maximum allowed difference of the number of labels (default is 0). The observed number of labels has to be in the range ``value +- delta``.",
json_type="StrictInt",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
NumLabelsMin = Annotated[
OptionalXmlInt,
AssertionParameter(
"Minimum allowed number of labels.",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
NumLabelsMax = Annotated[
OptionalXmlInt,
AssertionParameter(
"Maximum allowed number of labels.",
json_type="typing.Optional[StrictInt]",
xml_type="xs:nonNegativeInteger",
validators=["check_non_negative_if_set"],
),
]
Channel = Annotated[
OptionalXmlInt,
AssertionParameter(
"Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel).",
json_type="typing.Optional[StrictInt]",
),
]
CenterOfMass = Annotated[
str,
AssertionParameter(
"The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma).",
validators=["check_center_of_mass"],
),
]
CenterOfMassEps = Annotated[
XmlFloat,
AssertionParameter(
"The maximum allowed Euclidean distance to the required center of mass (defaults to ``0.01``).",
json_type=JSON_STRICT_NUMBER,
validators=["check_non_negative_if_set"],
),
]
Labels = Annotated[
Optional[Union[str, List[int]]],
AssertionParameter(
"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``.",
xml_type="xs:string",
json_type="typing.Optional[typing.List[int]]",
),
]
ExcludeLabels = Annotated[
Optional[Union[str, List[int]]],
AssertionParameter(
"List of labels to be excluded from consideration, separated by a comma. The primary usage of this attribute is to exclude the background of a label image. Cannot be used in combination with ``labels``.",
xml_type="xs:string",
json_type="typing.Optional[typing.List[int]]",
),
]
MeanObjectSize = Annotated[
OptionalXmlFloat,
AssertionParameter(
"The required mean size of the uniquely labeled objects.",
xml_type="xs:float",
json_type=JSON_OPTIONAL_STRICT_NUMBER,
validators=["check_non_negative_if_set"],
),
]
MeanObjectSizeEps = Annotated[
XmlFloat,
AssertionParameter(
"The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean size of the uniquely labeled objects has to be in the range ``value +- eps``.",
xml_type="xs:float",
json_type=JSON_STRICT_NUMBER,
validators=["check_non_negative_if_set"],
),
]
MeanObjectSizeMin = Annotated[
OptionalXmlFloat,
AssertionParameter(
"A lower bound of the required mean size of the uniquely labeled objects.",
xml_type="xs:float",
json_type=JSON_OPTIONAL_STRICT_NUMBER,
validators=["check_non_negative_if_set"],
),
]
MeanObjectSizeMax = Annotated[
OptionalXmlFloat,
AssertionParameter(
"An upper bound of the required mean size of the uniquely labeled objects.",
xml_type="xs:float",
json_type=JSON_OPTIONAL_STRICT_NUMBER,
validators=["check_non_negative_if_set"],
),
]
def _assert_float(
actual: float,
label: str,
@@ -52,15 +312,17 @@ def _assert_float(
def assert_has_image_width(
output_bytes: bytes,
width: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output_bytes: OutputBytes,
width: Width = None,
delta: WidthDelta = 0,
min: WidthMin = None,
max: WidthMax = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""
Asserts the specified output is an image and has a width of the specified value.
"""Asserts the output is an image and has a specific width (in pixels).
The width is plus/minus ``delta`` (e.g., ``<has_image_width width="512" delta="2" />``).
Alternatively the range of the expected width can be specified by ``min`` and/or ``max``.
"""
im_arr = _get_image(output_bytes)
_assert_number(
@@ -77,14 +339,16 @@ def assert_has_image_width(
def assert_has_image_height(
output_bytes: bytes,
height: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
height: Height = None,
delta: HeightDelta = 0,
min: HeightMin = None,
max: HeightMax = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""
Asserts the specified output is an image and has a height of the specified value.
"""Asserts the output is an image and has a specific height (in pixels).
The height is plus/minus ``delta`` (e.g., ``<has_image_height height="512" delta="2" />``).
Alternatively the range of the expected height can be specified by ``min`` and/or ``max``.
"""
im_arr = _get_image(output_bytes)
_assert_number(
@@ -101,14 +365,17 @@ def assert_has_image_height(
def assert_has_image_channels(
output_bytes: bytes,
channels: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
channels: Channels = None,
delta: ChannelsDelta = 0,
min: ChannelsMin = None,
max: ChannelsMax = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""
Asserts the specified output is an image and has the specified number of channels.
"""Asserts the output is an image and has a specific number of channels.
The number of channels is plus/minus ``delta`` (e.g., ``<has_image_channels channels="3" />``).
Alternatively the range of the expected number of channels can be specified by ``min`` and/or ``max``.
"""
im_arr = _get_image(output_bytes)
n_channels = 1 if im_arr.ndim < 3 else im_arr.shape[2] # we assume here that the image is a 2-D image
@@ -165,15 +432,17 @@ def _get_image(
def assert_has_image_mean_intensity(
output_bytes: bytes,
channel: Optional[Union[int, str]] = None,
mean_intensity: Optional[Union[float, str]] = None,
eps: Union[float, str] = 0.01,
min: Optional[Union[float, str]] = None,
max: Optional[Union[float, str]] = None,
output_bytes: OutputBytes,
channel: Channel = None,
mean_intensity: MeanIntensity = None,
eps: MeanIntensityEps = 0.01,
min: MeanIntensityMin = None,
max: MeanIntensityMax = None,
) -> None:
"""
Asserts the specified output is an image and has the specified mean intensity value.
"""Asserts the output is an image and has a specific mean intensity value.
The mean intensity value is plus/minus ``eps`` (e.g., ``<has_image_mean_intensity mean_intensity="0.83" />``).
Alternatively the range of the expected mean intensity value can be specified by ``min`` and/or ``max``.
"""
im_arr = _get_image(output_bytes, channel)
_assert_float(
@@ -187,22 +456,24 @@ def assert_has_image_mean_intensity(
def assert_has_image_center_of_mass(
output_bytes: bytes,
center_of_mass: Union[Tuple[float, float], str],
channel: Optional[Union[int, str]] = None,
eps: Union[float, str] = 0.01,
output_bytes: OutputBytes,
center_of_mass: CenterOfMass,
channel: Channel = None,
eps: CenterOfMassEps = 0.01,
) -> None:
"""
Asserts the specified output is an image and has the specified center of mass.
"""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.,
``<has_image_center_of_mass center_of_mass="511.07, 223.34" />``).
"""
im_arr = _get_image(output_bytes, channel)
if isinstance(center_of_mass, str):
center_of_mass_parts = [c.strip() for c in center_of_mass.split(",")]
assert len(center_of_mass_parts) == 2
center_of_mass = (float(center_of_mass_parts[0]), float(center_of_mass_parts[1]))
assert len(center_of_mass) == 2, "center_of_mass must have two components"
center_of_mass_parts = [c.strip() for c in center_of_mass.split(",")]
assert len(center_of_mass_parts) == 2
center_of_mass_tuple = (float(center_of_mass_parts[0]), float(center_of_mass_parts[1]))
assert len(center_of_mass_tuple) == 2, "center_of_mass must have two components"
actual_center_of_mass = _compute_center_of_mass(im_arr)
distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual_center_of_mass))
distance = numpy.linalg.norm(numpy.subtract(center_of_mass_tuple, actual_center_of_mass))
assert distance <= float(
eps
), f"Wrong center of mass: {actual_center_of_mass} (expected {center_of_mass}, distance: {distance}, eps: {eps})"
@@ -251,18 +522,22 @@ def _get_image_labels(
def assert_has_image_n_labels(
output_bytes: bytes,
channel: Optional[Union[int, str]] = None,
labels: Optional[Union[str, List[int]]] = None,
exclude_labels: Optional[Union[str, List[int]]] = None,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output_bytes: OutputBytes,
channel: Channel = None,
labels: Labels = None,
exclude_labels: ExcludeLabels = None,
n: NumLabels = None,
delta: NumLabelsDelta = 0,
min: NumLabelsMin = None,
max: NumLabelsMax = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""
Asserts the specified output is an image and has the specified number of unique values (e.g., uniquely labeled objects).
"""Asserts the output is an image and has the specified labels.
Labels can be a number of labels or unique values (e.g.,
``<has_image_n_labels n="187" exclude_labels="0" />``).
The primary usage of this assertion is to verify the number of objects in images with uniquely labeled objects.
"""
present_labels = _get_image_labels(output_bytes, channel, labels, exclude_labels)[1]
_assert_number(
@@ -278,17 +553,20 @@ def assert_has_image_n_labels(
def assert_has_image_mean_object_size(
output_bytes: bytes,
channel: Optional[Union[int, str]] = None,
labels: Optional[Union[str, List[int]]] = None,
exclude_labels: Optional[Union[str, List[int]]] = None,
mean_object_size: Optional[Union[float, str]] = None,
eps: Union[float, str] = 0.01,
min: Optional[Union[float, str]] = None,
max: Optional[Union[float, str]] = None,
output_bytes: OutputBytes,
channel: Channel = None,
labels: Labels = None,
exclude_labels: ExcludeLabels = None,
mean_object_size: MeanObjectSize = None,
eps: MeanObjectSizeEps = 0.01,
min: MeanObjectSizeMin = None,
max: MeanObjectSizeMax = None,
) -> None:
"""
Asserts the specified output is an image with labeled objects which have the specified mean size (number of pixels).
"""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., ``<has_image_mean_object_size mean_object_size="111.87" exclude_labels="0" />``).
The labels must be unique.
"""
im_arr, present_labels = _get_image_labels(output_bytes, channel, labels, exclude_labels)
actual_mean_object_size = sum((im_arr == label).sum() for label in present_labels) / len(present_labels)
+34 -12
View File
@@ -5,8 +5,20 @@ from typing import (
cast,
)
from ._types import (
Annotated,
AssertionParameter,
Output,
)
PropertyVisitor = Callable[[str, Any], Any]
Property = Annotated[str, AssertionParameter("The property name to search the JSON document for.")]
Text = Annotated[str, AssertionParameter("The expected text value of the target JSON attribute.")]
Value = Annotated[
str, AssertionParameter("The expected JSON value of the target JSON attribute (as a JSON encoded string).")
]
def any_in_tree(f: PropertyVisitor, json_tree: Any):
if isinstance(json_tree, list):
@@ -25,13 +37,18 @@ def any_in_tree(f: PropertyVisitor, json_tree: Any):
def assert_has_json_property_with_value(
output,
property: str,
value: str,
output: Output,
property: Property,
value: Value,
):
"""Assert JSON tree contains the specified property with specified JSON-ified value."""
output_json = assert_json_and_load(output)
expected_value = assert_json_and_load(value)
"""Asserts the JSON document contains a property or key with the specified JSON value.
```xml
<has_json_property_with_value property="skipped_columns" value="[1, 3, 5]" />
```
"""
output_json = _assert_json_and_load(output)
expected_value = _assert_json_and_load(value)
def is_property(key, value):
return key == property and value == expected_value
@@ -40,12 +57,17 @@ def assert_has_json_property_with_value(
def assert_has_json_property_with_text(
output,
property: str,
text: str,
output: Output,
property: Property,
text: Text,
):
"""Assert JSON tree contains the specified property with specified JSON-ified value."""
output_json = assert_json_and_load(output)
"""Asserts the JSON document contains a property or key with the specified text (i.e. string) value.
```xml
<has_json_property_with_text property="color" text="red" />
```
"""
output_json = _assert_json_and_load(output)
def is_property(key, value):
return key == property and value == text
@@ -53,7 +75,7 @@ def assert_has_json_property_with_text(
assert any_in_tree(is_property, output_json), f"Failed to find property [{property}] with text [{text}]"
def assert_json_and_load(json_str: str):
def _assert_json_and_load(json_str: str):
try:
return json.loads(json_str)
except Exception:
+28 -15
View File
@@ -1,24 +1,37 @@
from typing import (
Optional,
Union,
from ._types import (
Annotated,
AssertionParameter,
Delta,
Max,
Min,
Negate,
NEGATE_DEFAULT,
OptionalXmlInt,
OutputBytes,
)
from ._util import _assert_number
def assert_has_size(
output_bytes: bytes,
value: Optional[Union[int, str]] = None,
size: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output_bytes: OutputBytes,
value: Annotated[
OptionalXmlInt, AssertionParameter("Deprecated alias for `size`", xml_type="Bytes", deprecated=True)
] = None,
size: Annotated[
OptionalXmlInt,
AssertionParameter(
"Desired size of the output (in bytes), can be suffixed by ``(k|M|G|T|P|E)i?``", xml_type="Bytes"
),
] = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""
Asserts the specified output has a size of the specified value
(size and value or synonyms),
allowing for absolute (delta) and relative (delta_frac) difference.
"""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.
"""
output_size = len(output_bytes)
if size is None:
+38 -16
View File
@@ -1,11 +1,26 @@
import re
from typing import (
Optional,
Union,
)
from ._types import (
Annotated,
AssertionParameter,
Delta,
Max,
Min,
N,
Negate,
NEGATE_DEFAULT,
Output,
)
from ._util import _assert_number
Sep = Annotated[str, AssertionParameter("Separator defining columns, default: tab")]
Comment = Annotated[
str,
AssertionParameter(
"Comment character(s) used to skip comment lines (which should not be used for counting columns)"
),
]
def get_first_line(output: str, comment: str) -> str:
"""
@@ -22,19 +37,26 @@ def get_first_line(output: str, comment: str) -> str:
def assert_has_n_columns(
output: str,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
sep: str = "\t",
comment: str = "",
negate: Union[bool, str] = False,
output: Output,
n: N = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
sep: Sep = "\t",
comment: Comment = "",
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Asserts the tabular output contains n columns. The optional
sep argument specifies the column seperator used to determine the
number of columns. The optional comment argument specifies
comment characters"""
"""Asserts tabular output contains the specified
number (``n``) of columns.
For instance, ``<has_n_columns n="3"/>``. 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 ``\t``) `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.
"""
first_line = get_first_line(output, comment)
n_columns = len(first_line.split(sep))
_assert_number(
+51 -39
View File
@@ -1,23 +1,35 @@
import re
from typing import (
Optional,
Union,
)
from typing_extensions import Annotated
from ._types import (
AssertionParameter,
Delta,
Max,
Min,
N,
Negate,
NEGATE_DEFAULT,
Output,
)
from ._util import (
_assert_number,
_assert_presence_number,
)
Text = Annotated[str, AssertionParameter("The text to search for in the output.")]
Line = Annotated[str, AssertionParameter("The full line of text to search for in the output.")]
Expression = Annotated[str, AssertionParameter("The regular expressions to attempt match in the output.")]
def assert_has_text(
output: str,
text: str,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output: Output,
text: Text,
n: N = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Asserts specified output contains the substring specified by
the argument text. The exact number of occurrences can be
@@ -39,7 +51,7 @@ def assert_has_text(
)
def assert_not_has_text(output: str, text: str) -> None:
def assert_not_has_text(output: Output, text: Text) -> None:
"""Asserts specified output does not contain the substring
specified by the argument text"""
assert output is not None, "Checking not_has_text assertion on empty output (None)"
@@ -47,13 +59,13 @@ def assert_not_has_text(output: str, text: str) -> None:
def assert_has_line(
output: str,
line: str,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output: Output,
line: Line,
n: N = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Asserts the specified output contains the line specified by the
argument line. The exact number of occurrences can be optionally
@@ -76,12 +88,12 @@ def assert_has_line(
def assert_has_n_lines(
output: str,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output: Output,
n: N = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Asserts the specified output contains ``n`` lines allowing
for a difference in the number of lines (delta)
@@ -101,13 +113,13 @@ def assert_has_n_lines(
def assert_has_text_matching(
output: str,
expression: str,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output: Output,
expression: Expression,
n: N = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Asserts the specified output contains text matching the
regular expression specified by the argument expression.
@@ -131,13 +143,13 @@ def assert_has_text_matching(
def assert_has_line_matching(
output: str,
expression: str,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output: Output,
expression: Expression,
n: N = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Asserts the specified output contains a line matching the
regular expression specified by the argument expression. If n is given
+206 -55
View File
@@ -1,8 +1,5 @@
import re
from typing import (
Optional,
Union,
)
from typing import Optional
from lxml.etree import XMLSyntaxError
@@ -12,95 +9,247 @@ from galaxy.util import (
parse_xml_string,
unicodify,
)
from ._types import (
Annotated,
AssertionParameter,
ChildAssertions,
Delta,
Max,
Min,
N,
Negate,
NEGATE_DEFAULT,
Output,
VerifyAssertionsFunction,
XmlBool,
XmlRegex,
)
Path = Annotated[str, AssertionParameter("The Python xpath-like expression to find the target element.")]
ElementExpression = Annotated[
XmlRegex,
AssertionParameter("The regular expressions to apply against the target element.", validators=["check_regex"]),
]
AttributeExpression = Annotated[
XmlRegex,
AssertionParameter(
"The regular expressions to apply against the named attribute on the target XML element.",
validators=["check_regex"],
),
]
Attribute = Annotated[str, AssertionParameter("The XML attribute name to test against from the target XML element.")]
OptionalAttribute = Annotated[
Optional[str], AssertionParameter("The XML attribute name to test against from the target XML element.")
]
ElementText = Annotated[
str, AssertionParameter("The expected element text (body of the XML tag) to test against on the target XML element")
]
AttributeText = Annotated[
str, AssertionParameter("The expected attribute value to test against on the target XML element")
]
All = Annotated[
XmlBool,
AssertionParameter(
"Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first ",
xml_type="PermissiveBoolean",
),
]
def assert_is_valid_xml(output: str) -> None:
"""Simple assertion that just verifies the specified output
is valid XML."""
def assert_is_valid_xml(output: Output) -> None:
"""Asserts the output is a valid XML file (e.g. ``<is_valid_xml />``)."""
try:
parse_xml_string(output)
except XMLSyntaxError as e:
raise AssertionError(f"Expected valid XML, but could not parse output. {unicodify(e)}")
def assert_has_element_with_path(output: str, path: str, negate: Union[bool, str] = False) -> None:
"""Asserts the specified output has at least one XML element with a
path matching the specified path argument. Valid paths are the
simplified subsets of XPath implemented by lxml.etree;
https://lxml.de/xpathxslt.html for more information."""
def assert_has_element_with_path(output: Output, path: Path, negate: Negate = NEGATE_DEFAULT) -> None:
"""Asserts the XML output contains at least one element (or tag) with the specified
XPath-like ``path``, e.g.
```xml
<has_element_with_path path="BlastOutput_param/Parameters/Parameters_matrix" />
```
With ``negate`` the result of the assertion can be inverted."""
assert_xml_element(output, path, negate=negate)
def assert_has_n_elements_with_path(
output: str,
path: str,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output: Output,
path: Path,
n: N = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Asserts the specified output has exactly n elements matching the
path specified."""
"""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
<has_n_elements_with_path n="9" path="BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num" />
```
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.
"""
assert_xml_element(output, path, n=n, delta=delta, min=min, max=max, negate=negate)
def assert_element_text_matches(output: str, path: str, expression: str, negate: Union[bool, str] = False) -> None:
"""Asserts the text of the first element matching the specified
path matches the specified regular expression."""
def assert_element_text_matches(
output: Output, path: Path, expression: ElementExpression, negate: Negate = NEGATE_DEFAULT
) -> None:
"""Asserts the text of the XML element with the specified XPath-like ``path``
matches the regular expression defined by ``expression``.
For example:
```xml
<element_text_matches path="BlastOutput_version" expression="BLASTP\\s+2\\.2.*"/>
```
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).
"""
sub = {"tag": "has_text_matching", "attributes": {"expression": expression, "negate": negate}}
assert_xml_element(output, path, asserts.verify_assertions, [sub])
def assert_element_text_is(output: str, path: str, text: str, negate: Union[bool, str] = False) -> None:
"""Asserts the text of the first element matching the specified
path matches exactly the specified text."""
def assert_element_text_is(output: Output, path: Path, text: ElementText, negate: Negate = NEGATE_DEFAULT) -> None:
"""Asserts the text of the XML element with the specified XPath-like ``path`` is
the specified ``text``.
For example:
```xml
<element_text_is path="BlastOutput_program" text="blastp" />
```
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).
"""
assert_element_text_matches(output, path, re.escape(text) + "$", negate=negate)
def assert_attribute_matches(
output: str, path: str, attribute, expression: str, negate: Union[bool, str] = False
output: Output,
path: Path,
attribute: Attribute,
expression: AttributeExpression,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Asserts the specified attribute of the first element matching
the specified path matches the specified regular expression."""
"""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
<attribute_matches path="outerElement/innerElement2" attribute="foo2" expression="bar\\d+" />
```
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).
"""
sub = {"tag": "has_text_matching", "attributes": {"expression": expression, "negate": negate}}
assert_xml_element(output, path, asserts.verify_assertions, [sub], attribute=attribute)
def assert_attribute_is(output: str, path: str, attribute: str, text, negate: Union[bool, str] = False) -> None:
"""Asserts the specified attribute of the first element matching
the specified path matches exactly the specified text."""
def assert_attribute_is(
output: Output, path: Path, attribute: Attribute, text: AttributeText, negate: Negate = NEGATE_DEFAULT
) -> None:
"""Asserts the XML ``attribute`` for the element (or tag) with the specified
XPath-like ``path`` is the specified ``text``.
For example:
```xml
<attribute_is path="outerElement/innerElement1" attribute="foo" text="bar" />
```
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).
"""
assert_attribute_matches(output, path, attribute, re.escape(text) + "$", negate=negate)
def assert_element_text(
output: str, path: str, verify_assertions_function, children, negate: Union[bool, str] = False
output: Output,
path: Path,
verify_assertions_function: VerifyAssertionsFunction,
children: ChildAssertions,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""Recursively checks the specified assertions against the text of
the first element matching the specified path."""
"""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.
```xml
<element_text path="BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def">
<not_has_text text="EDK72998.1" />
</element_text>
```
The assertion implicitly also asserts that an element matching ``path`` exists.
With ``negate`` the result of the implicit assertions can be inverted.
The sub-assertions, which have their own ``negate`` attribute, are not affected
by ``negate``.
"""
assert_xml_element(output, path, verify_assertions_function, children, negate=negate)
def assert_xml_element(
output: str,
path: str,
verify_assertions_function=None,
children=None,
attribute: Optional[str] = None,
all: Union[bool, str] = False,
n: Optional[Union[int, str]] = None,
delta: Union[int, str] = 0,
min: Optional[Union[int, str]] = None,
max: Optional[Union[int, str]] = None,
negate: Union[bool, str] = False,
output: Output,
path: Path,
verify_assertions_function: Optional[VerifyAssertionsFunction] = None,
children: ChildAssertions = None,
attribute: OptionalAttribute = None,
all: All = False,
n: N = None,
delta: Delta = 0,
min: Min = None,
max: Max = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
"""
Check if path occurs in the xml. If n and delta or min and max are given
also the number of occurences is checked.
If there are any sub assertions then check them against
- the element's text if attribute is None
- the content of the attribute
If all is True then the sub assertions are checked for all occurences.
"""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
<assert_contents>
<xml_element path="./elem"/>
<xml_element path="./elem/more[2]"/>
<xml_element path=".//more" n="3" delta="1"/>
</assert_contents>
```
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
<assert_contents>
<xml_element path="./elem/more[2]" attribute="name">
<has_text_matching expression="foo$"/>
</xml_element>
</assert_contents>
```
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).
"""
children = children or []
all = asbool(all)
@@ -131,7 +280,9 @@ def assert_xml_element(
else:
content = occ.attrib[attribute] # type: ignore[assignment] # https://github.com/lxml/lxml-stubs/pull/99
try:
verify_assertions_function(content, children)
if content is None:
raise AssertionError("Failed to find expected XML content")
verify_assertions_function(content.encode("utf-8"), children)
except AssertionError as e:
if attribute is not None and attribute != "":
raise AssertionError(f"Attribute '{attribute}' on element with path '{path}': {str(e)}")
+420
View File
@@ -0,0 +1,420 @@
#!/usr/bin/env python
# how to use this function...
# PYTHONPATH=lib python lib/galaxy/tool_util/verify/codegen.py
from __future__ import annotations
import argparse
import inspect
import os
from shutil import move
from typing import (
cast,
List,
Optional,
Union,
)
import lxml.etree as ET
from jinja2 import Environment
from typing_extensions import (
Annotated,
get_args,
get_origin,
Literal,
)
from galaxy.tool_util.verify.asserts import assertion_module_and_functions
from galaxy.tool_util.verify.asserts._types import AssertionParameter as AssertionParameterAnnotation
from galaxy.util.commands import shell
models_path = os.path.join(os.path.dirname(__file__), "assertion_models.py")
galaxy_xsd_path = os.path.join(os.path.dirname(__file__), "..", "xsd", "galaxy.xsd")
Children = Literal["allowed", "required", "forbidden"]
DESCRIPTION = """This script synchronizes dynamic code aritfacts against models in Galaxy.
Right now this just synchronizes Galaxy's XSD file against documentation in Galaxy's assertion modules but
in the future it will also build Pydantic models for these functions.
"""
assert_models_template = """
'''This module is auto-generated, please do not modify.'''
import typing
import re
from typing_extensions import (
Annotated,
Literal,
)
from pydantic import (
BaseModel,
BeforeValidator,
ConfigDict,
Field,
RootModel,
StrictFloat,
StrictInt,
)
BYTES_PATTERN = re.compile(r"^(0|[1-9][0-9]*)([kKMGTPE]i?)?$")
class AssertionModel(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
def check_bytes(v: typing.Any) -> typing.Any:
if isinstance(v, str):
assert BYTES_PATTERN.match(v), "Not valid bytes string"
return v
def check_center_of_mass(v: typing.Any):
assert isinstance(v, str)
split_parts = v.split(",")
assert len(split_parts) == 2
for part in split_parts:
assert float(part.strip())
return v
def check_regex(v: typing.Any):
assert isinstance(v, str)
try:
re.compile(typing.cast(str, v))
except re.error:
raise AssertionError(f"Invalid regular expression {v}")
return v
def check_non_negative_if_set(v: typing.Any):
if v is not None:
try:
assert v >= 0
except TypeError:
raise AssertionError(f"Invalid type found {v}")
return v
def check_non_negative_if_int(v: typing.Any):
if v is not None and isinstance(v, int):
assert typing.cast(int, v) >= 0
return v
{% for assertion in assertions %}
{% for parameter in assertion.parameters %}
{{assertion.name}}_{{ parameter.name }}_description = '''{{ parameter.description }}'''
{% endfor %}
class {{assertion.name}}_model(AssertionModel):
r\"\"\"{{ assertion.docstring }}\"\"\"
that: Literal["{{assertion.name}}"] = "{{assertion.name}}"
{% for parameter in assertion.parameters %}
{% if not parameter.is_deprecated %}
{{ parameter.name }}: {{ parameter.type_str }} = Field(
{{ parameter.field_default_str }},
description={{ assertion.name }}_{{ parameter.name }}_description,
)
{% endif %}
{% endfor %}
{% if assertion.children == "required" %}
children: "assertion_list"
{% endif %}
{% if assertion.children == "allowed" %}
children: typing.Optional["assertion_list"] = None
{% endif %}
{% endfor %}
any_assertion_model = Annotated[typing.Union[
{% for assertion in assertions %}
{{assertion.name}}_model,
{% endfor %}
], Field(discriminator="that")]
assertion_list = RootModel[typing.List[any_assertion_model]]
"""
def get_default_args(func):
signature = inspect.signature(func)
return {k: v.default for k, v in signature.parameters.items()}
def main():
assertions = []
for function_name, (module_and_function, assertion_function) in assertion_module_and_functions.items():
if not function_name.startswith("assert_"):
continue
name = function_name[len("assert_") :]
docstring = inspect.cleandoc(assertion_function.__doc__ or "")
annotations = assertion_function.__annotations__
default_args = get_default_args(assertion_function)
parameters = []
children: Children = "forbidden"
for parameter_name, parameter_type in annotations.items():
if parameter_name == "return":
continue
elif parameter_name == "children":
if default_args.get("children", inspect._empty) is not inspect._empty:
children = "allowed"
else:
children = "required"
continue
elif parameter_name in ["output", "output_bytes", "verify_assertions_function"]:
continue
default_value = default_args.get(parameter_name)
parameters.append(AssertionParameter(parameter_name, parameter_type, default_value))
assertion = Assertion(name, docstring, parameters, children, module_and_function)
assertions.append(assertion)
rewrite_galaxy_xsd(assertions)
write_assertion_models(assertions)
def _expand_template(template_str: str, assertions) -> str:
template = Environment().from_string(template_str)
return template.render(assertions=assertions)
def write_assertion_models(assertions):
models_file_contents = _expand_template(assert_models_template, assertions)
with open(models_path, "w") as f:
f.write(models_file_contents)
shell(["isort", models_path])
shell(["black", models_path])
def get_annotation(text: str, nsmap):
annotation = ET.Element("{http://www.w3.org/2001/XMLSchema}annotation")
documentation = ET.Element("{http://www.w3.org/2001/XMLSchema}documentation")
documentation.text = ET.CDATA(text)
documentation.attrib["{http://www.w3.org/XML/1998/namespace}lang"] = "en"
annotation.append(documentation)
return annotation
def parameter_xsd_element(assertion_parameter, nsmap):
el = ET.Element("{http://www.w3.org/2001/XMLSchema}attribute", nsmap=nsmap)
el.attrib["name"] = assertion_parameter.name
el.attrib["type"] = assertion_parameter.xml_type_str
el.attrib["use"] = "required" if not assertion_parameter.has_default_value else "optional"
annotation = get_annotation(assertion_parameter.description, nsmap)
el.append(annotation)
return el
def xsd_element(assertion, nsmap):
el = ET.Element("{http://www.w3.org/2001/XMLSchema}element", nsmap=nsmap)
el.attrib["name"] = assertion.name
annotation = get_annotation(assertion.docstring + "\n\n$attribute_list::5", nsmap)
complexType = ET.Element("{http://www.w3.org/2001/XMLSchema}complexType", nsmap=nsmap)
if assertion.children != "forbidden":
sequence = ET.Element("{http://www.w3.org/2001/XMLSchema}sequence", nsmap=nsmap)
group = ET.Element("{http://www.w3.org/2001/XMLSchema}group", nsmap=nsmap)
group.attrib["ref"] = "TestAssertion"
group.attrib["minOccurs"] = "0" if assertion.children == "allowed" else "1"
group.attrib["maxOccurs"] = "unbounded"
sequence.append(group)
complexType.append(sequence)
for parameter in assertion.parameters:
complexType.append(parameter_xsd_element(parameter, nsmap))
el.append(annotation)
el.append(complexType)
return el
def xsd_elements(assertions, nsmap):
elements = []
for assertion in assertions:
elements.append(ET.Comment(f" XSD doc for element auto-generated from {assertion.module_and_function}"))
elements.append(xsd_element(assertion, nsmap))
return elements
def rewrite_galaxy_xsd(assertions):
with open(galaxy_xsd_path, "rb") as f:
contents = f.read()
parser = ET.XMLParser(strip_cdata=False)
root = ET.fromstring(contents, parser=parser)
raw_element = root.find(
".//{http://www.w3.org/2001/XMLSchema}group[@name='TestAssertion']/{http://www.w3.org/2001/XMLSchema}choice"
)
assert raw_element
element = cast(ET._Element, raw_element)
for el in element.iterchildren():
element.remove(el)
element.append(ET.Comment("The following block and all children are auto-generated - please do not modify."))
for xsd_element in xsd_elements(assertions, root.nsmap):
xsd_element.tail = "\n "
element.append(xsd_element)
xml_new = ET.tostring(root).decode("utf-8")
with open(galaxy_xsd_path, "w") as f:
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
f.write(xml_new)
f.write("\n")
ret_code = shell(["xmllint", "--format", "--output", "galaxy-tmp.xsd", galaxy_xsd_path])
assert ret_code == 0
move("galaxy-tmp.xsd", galaxy_xsd_path)
class AssertionParameter:
def __init__(self, name: str, type: str, default_value):
self.name = name
self.type = type
self.default_value = default_value
@property
def description(self) -> str:
type = self.type
if hasattr(type, "__metadata__"):
return type.__metadata__[0].doc
else:
return ""
@property
def type_str(self) -> str:
raw_type_str = as_type_str(self.type)
validators = self.validators[:]
if self.xml_type_str == "Bytes":
validators.append("check_bytes")
validators.append("check_non_negative_if_int")
if len(validators) > 0:
validation_str = ",".join([f"BeforeValidator({v})" for v in validators])
return f"Annotated[{raw_type_str}, {validation_str}]"
return raw_type_str
@property
def xml_type_str(self) -> str:
return as_xml_type(self.type)
@property
def field_default_str(self) -> str:
if not self.has_default_value:
return "..."
elif isinstance(self.default_value, str):
return f'''"{self.default_value}"'''
else:
return str(self.default_value)
@property
def has_default_value(self) -> bool:
return self.default_value is not inspect._empty
@property
def is_deprecated(self) -> bool:
assertion_parameter = self._get_type_annotation()
if assertion_parameter is not None:
return assertion_parameter.deprecated
return False
@property
def validators(self) -> List[str]:
assertion_parameter = self._get_type_annotation()
if assertion_parameter is not None:
return assertion_parameter.validators
return []
def _get_type_annotation(self) -> Optional[AssertionParameterAnnotation]:
target_type = self.type
if get_origin(target_type) is Annotated:
args = get_args(target_type)
if len(args) > 1:
return cast(AssertionParameterAnnotation, args[1])
return None
def _is_none_type(target_type):
return target_type is type(None)
def _non_optional_types(union_type):
return [t for t in get_args(union_type) if not _is_none_type(t)]
def as_xml_type(target_type) -> str:
if get_origin(target_type) is Annotated:
args = get_args(target_type)
if len(args) > 1:
assertion_parameter = args[1]
if assertion_parameter.xml_type:
return assertion_parameter.xml_type
return as_xml_type(args[0])
elif get_origin(target_type) is Union:
types = _non_optional_types(target_type)
if len(types) == 2:
non_str_types = [t for t in types if t is not str]
if len(non_str_types) == 1 and non_str_types[0] is bool:
return "xs:boolean"
elif len(non_str_types) == 1 and non_str_types[0] is int:
return "xs:integer"
elif len(non_str_types) == 1 and non_str_types[0] is float:
return "xs:float"
return "xs:string"
def as_type_str(target_type):
if get_origin(target_type) is Annotated:
args = get_args(target_type)
if len(args) > 1:
if args[1].json_type:
return args[1].json_type
return as_type_str(args[0])
elif get_origin(target_type) is Union:
is_optional = any(_is_none_type(t) for t in get_args(target_type))
types_as_str = ", ".join(map(as_type_str, _non_optional_types(target_type)))
union_type = f"typing.Union[{types_as_str}]"
if is_optional:
return f"typing.Optional[{union_type}]"
else:
return union_type
elif target_type is str:
return "str"
elif target_type is int:
return "int"
elif target_type is float:
return "float"
elif target_type is bool:
return "bool"
else:
return str(target_type)
class Assertion:
def __init__(
self,
name: str,
docstring: str,
parameters: List[AssertionParameter],
children: Children,
module_and_function: str,
):
self.name = name
self.parameters = parameters
self.docstring = docstring
self.children = children
self.module_and_function = module_and_function
def arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=DESCRIPTION)
return parser
if __name__ == "__main__":
main()
+3 -933
View File
@@ -2163,946 +2163,16 @@ module.
]]></xs:documentation>
</xs:annotation>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:group ref="TestAssertionsGeneral" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsText" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsTabular" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsArchive" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsXml" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsJson" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsH5" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsImage" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertion"/>
</xs:choice>
</xs:complexType>
<xs:group name="TestAssertionsGeneral">
<xs:group name="TestAssertion">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[General assertions]]></xs:documentation>
<xs:documentation xml:lang="en">An individual test assertion definition.</xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="has_size" type="AssertHasSize"/>
</xs:choice>
</xs:group>
<xs:group name="TestAssertionsText">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Assertions for text data]]></xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="has_text" type="AssertHasText"/>
<xs:element name="not_has_text" type="AssertNotHasText"/>
<xs:element name="has_text_matching" type="AssertHasTextMatching"/>
<xs:element name="has_line" type="AssertHasLine"/>
<xs:element name="has_line_matching" type="AssertHasLineMatching"/>
<xs:element name="has_n_lines" type="AssertHasNLines"/>
</xs:choice>
</xs:group>
<xs:group name="TestAssertionsTabular">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Assertions for tabular data]]></xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="has_n_columns" type="AssertHasNColumns"/>
</xs:choice>
</xs:group>
<xs:group name="TestAssertionsArchive">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Assertions for archives (zip/tar)]]></xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="has_archive_member" type="AssertHasArchiveMember"/>
</xs:choice>
</xs:group>
<xs:group name="TestAssertionsXml">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Assertions for XML data]]></xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="is_valid_xml" type="AssertIsValidXML"/>
<xs:element name="xml_element" type="AssertXMLElement"/>
<xs:element name="has_element_with_path" type="AssertHasElementWithPath"/>
<xs:element name="has_n_elements_with_path" type="AssertHasNElementsWithPath"/>
<xs:element name="element_text_matches" type="AssertElementTextMatches"/>
<xs:element name="element_text_is" type="AssertElementTextIs"/>
<xs:element name="attribute_matches" type="AssertAttributeMatches"/>
<xs:element name="attribute_is" type="AssertAttributeIs"/>
<xs:element name="element_text" type="AssertElementText"/>
</xs:choice>
</xs:group>
<xs:group name="TestAssertionsJson">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Assertions for JSON data]]></xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="has_json_property_with_value" type="AssertHasJsonPropertyWithValue"/>
<xs:element name="has_json_property_with_text" type="AssertHasJsonPropertyWithText"/>
</xs:choice>
</xs:group>
<xs:group name="TestAssertionsImage">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Assertions for image data]]></xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="has_image_width" type="AssertHasImageWidth"/>
<xs:element name="has_image_height" type="AssertHasImageHeight"/>
<xs:element name="has_image_channels" type="AssertHasImageChannels"/>
<xs:element name="has_image_mean_intensity" type="AssertHasImageMeanIntensity"/>
<xs:element name="has_image_center_of_mass" type="AssertHasImageCenterOfMass"/>
<xs:element name="has_image_n_labels" type="AssertHasImageNLabels"/>
<xs:element name="has_image_mean_object_size" type="AssertHasImageMeanObjectSize"/>
</xs:choice>
</xs:group>
<xs:group name="TestAssertionsH5">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Assertions for H5 data]]></xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="has_h5_keys" type="AssertHasH5Keys"/>
<xs:element name="has_h5_attribute" type="AssertHasH5Attribute"/>
</xs:choice>
</xs:group>
<xs:complexType name="AssertHasSize">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the output has a specific size (in bytes) of ``value`` plus minus
``delta``, e.g. ``<has_size value="10000" delta="100" />``.
Alternatively the range of the expected size can be specified by ``min`` and/or
``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="size" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Desired size of the output (in bytes), can be suffixed by ``(k|M|G|T|P|E)i?``</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="value" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">An outdated alias for `size`</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="delta" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed size difference (default is 0). The observed size has to be in the range ``value +- delta``. Can be suffixed by ``(k|M|G|T|P|E)i?``</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="min" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Minimum expected size, can be suffixed by ``(k|M|G|T|P|E)i?``</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum expected size, can be suffixed by ``(k|M|G|T|P|E)i?``</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasText">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the specified ``text`` appears in the output (e.g. ``<has_text
text="chr7">``). If the ``text`` is expected to occur a particular number of
times, this value can be specified using ``n``. Optionally also with a certain
``delta``. Alternatively the range of expected occurences can be specified by
``min`` and/or ``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="text" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">Text to check for</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeN"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertNotHasText">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the specified ``text`` does not appear in the output (e.g. ``<not_has_text text="chr8" />``).
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="text" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">Text to check for</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasTextMatching">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts text matching the specified regular expression (``expression``) appears
in the output (e.g. ``<has_text_matching expression="1274\d+53" />`` ). If the
regular expression is expected to match a particular number of times, this value
can be specified using ``n``. Note only non-overlapping occurences are counted.
Optionally also with a certain ``delta``. Alternatively the range of expected
occurences can be specified by ``min`` and/or ``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="expression" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">Regular expression to check for</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeN"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasLine">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts a line matching the specified string (``line``) appears in the output
(e.g. ``<has_line line="A full example line." />``). If the ``line`` is expected
to occur a particular number of times, this value can be specified using ``n``.
Optionally also with a certain ``delta``. Alternatively the range of expected
occurences can be specified by ``min`` and/or ``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="line" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">The line to check for</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeN"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasNLines">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts that an output contains ``n`` lines, allowing for a difference of
``delta`` (default is 0), e.g. ``<has_n_lines n="3" delta="1"/>``.
Alternatively the range of expected occurences can be specified by ``min``
and/or ``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributeN"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasLineMatching">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts a line matching the specified regular expression (``expression``)
appears in the output (e.g. ``<has_line_matching expression=".*\s+127489808\s+127494553" />``).
If a particular number of matching lines is expected, this value can be
specified using ``n``. Optionally also with ``delta``. Alternatively the range
of expected occurences can be specified by ``min`` and/or ``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="expression" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">Regular expression to check for</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeN"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasNColumns">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts tabular output (actually only the first line) contains the specified
number (``n``) of columns (e.g. ``<has_n_columns n="3"/>``) optionally also with
``delta``. Alternatively the range of expected occurences can be specified by
``min`` and/or ``max``. Optionally a column separator (``sep``, default is
``\t``) `and comment character(s) can be specified (``comment``, default is
empty string), then the first non-comment line is used for determining the
number of columns.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributeN"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
<xs:attribute name="sep" type="xs:string" default="&#9;">
<xs:annotation>
<xs:documentation xml:lang="en">Separator defining columns, default: tab</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="comment" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">Comment character(s) used to skip comment lines (which should not be used for counting columns)</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasArchiveMember">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
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
<has_archive_member path="./path/to/my-file.txt"/>
```
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&plusmn;1 elements out of which at least
4 need to have a ``txt`` extension.
```xml
<has_archive_member path=".*" n="10" delta="1"/>
<has_archive_member path=".*\.txt" min="4"/>
```
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
<has_archive_member path=".*/my-file.txt">
<not_has_text text="EDK72998.1"/>
</has_archive_member>
```
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.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:group ref="TestAssertionsGeneral" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsText" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsTabular" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsJson" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsXml" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsH5" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsImage" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="path" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">The regular expression specifying the archive member.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="all" type="PermissiveBoolean" default="false">
<xs:annotation>
<xs:documentation xml:lang="en">Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first </xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeN"/>
</xs:complexType>
<xs:complexType name="AssertIsValidXML">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the output is a valid XML file (e.g. ``<is_valid_xml />``).
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
</xs:complexType>
<xs:complexType name="AssertXMLElement">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
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
<assert_contents>
<xml_element path="./elem"/>
<xml_element path="./elem/more[2]"/>
<xml_element path=".//more" n="3" delta="1"/>
</assert_contents>
```
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
<assert_contents>
<xml_element path="./elem/more[2]" attribute="name">
<has_text_matching expression="foo$"/>
</xml_element>
</assert_contents>
```
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).
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributePath"/>
<xs:attribute name="all" type="PermissiveBoolean" default="false">
<xs:annotation>
<xs:documentation xml:lang="en">Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="attribute" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">The name of the attribute to apply sub-assertion on. If not given then the element text is used</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeN"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasElementWithPath">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the XML output contains at least one element (or tag) with the specified
XPath-like ``path``, e.g.
```xml
<has_element_with_path path="BlastOutput_param/Parameters/Parameters_matrix" />
```
With ``negate`` the result of the assertion can be inverted.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributePath"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasNElementsWithPath">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the XML output contains the specified number (``n``, optionally with ``delta``) of elements (or
tags) with the specified XPath-like ``path``, e.g.
```xml
<has_n_elements_with_path n="9" path="BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num" />
```
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.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributePath"/>
<xs:attributeGroup ref="AssertAttributeN"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertElementTextMatches">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the text of the XML element with the specified XPath-like ``path``
matches the regular expression defined by ``expression``, e.g.
```xml
<element_text_matches path="BlastOutput_version" expression="BLASTP\s+2\.2.*"/>
```
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).
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributePath"/>
<xs:attribute name="expression" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">The regular expression to use.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertElementTextIs">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the text of the XML element with the specified XPath-like ``path`` is
the specified ``text``, e.g.
```xml
<element_text_is path="BlastOutput_program" text="blastp" />
```
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_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributePath"/>
<xs:attribute name="text" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">Text to check for.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertAttributeMatches">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the XML ``attribute`` for the element (or tag) with the specified
XPath-like ``path`` matches the regular expression specified by ``expression``, e.g.
```xml
<attribute_matches path="outerElement/innerElement2" attribute="foo2" expression="bar\d+" />
```
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).
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributePath"/>
<xs:attribute name="expression" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">The regular expression to use.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertAttributeIs">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the XML ``attribute`` for the element (or tag) with the specified
XPath-like ``path`` is the specified ``text``, e.g.
```xml
<attribute_is path="outerElement/innerElement1" attribute="foo" text="bar" />
```
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_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributePath"/>
<xs:attribute name="text" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">Text to check for.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertElementText">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
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.
```xml
<element_text path="BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def">
<not_has_text text="EDK72998.1" />
</element_text>
```
The assertion implicitly also asserts that an element matching ``path`` exists.
With ``negate`` the result of the implicit assertions can be inverted.
The sub-assertions, which have their own ``negate`` attribute, are not affected
by ``negate``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:group ref="TestAssertionsText" minOccurs="1" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attributeGroup ref="AssertAttributePath"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasImageWidth">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the output is an image and has a specific width (in pixels),
plus/minus ``delta`` (e.g., ``<has_image_width width="512" delta="2" />``).
Alternatively the range of the expected width can be specified by ``min`` and/or ``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="width" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Expected width of the image (in pixels).`</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="delta" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed difference of the image width (in pixels, default is 0). The observed width has to be in the range ``value +- delta``.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="min" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Minimum allowed width of the image (in pixels).</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed width of the image (in pixels).</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasImageHeight">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the output is an image and has a specific height (in pixels),
plus/minus ``delta`` (e.g., ``<has_image_height height="512" delta="2" />``).
Alternatively the range of the expected height can be specified by ``min`` and/or ``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="height" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Expected height of the image (in pixels).`</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="delta" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed difference of the image height (in pixels, default is 0). The observed height has to be in the range ``value +- delta``.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="min" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Minimum allowed height of the image (in pixels).</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed height of the image (in pixels).</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasImageChannels">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the output is an image and has a specific number of channels,
plus/minus ``delta`` (e.g., ``<has_image_channels channels="3" />``).
Alternatively the range of the expected number of channels can be specified by ``min`` and/or ``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="channels" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Expected number of channels of the image.`</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="delta" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed difference of the number of channels (default is 0). The observed number of channels has to be in the range ``value +- delta``.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="min" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Minimum allowed number of channels.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed number of channels.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasImageMeanIntensity">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the output is an image and has a specific mean intensity value,
plus/minus ``eps`` (e.g., ``<has_image_mean_intensity mean_intensity="0.83" />``).
Alternatively the range of the expected mean intensity value can be specified by ``min`` and/or ``max``.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="mean_intensity" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">The required mean value of the image intensities.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="eps" type="xs:float" use="optional" default="0.01">
<xs:annotation>
<xs:documentation xml:lang="en">The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean value of the image intensities has to be in the range ``value +- eps``.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="min" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">A lower bound of the required mean value of the image intensities.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">An upper bound of the required mean value of the image intensities.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="channel" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel).</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasImageCenterOfMass">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
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.,
``<has_image_center_of_mass center_of_mass="511.07, 223.34" />``).
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="center_of_mass" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma).</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="eps" type="xs:float" use="optional" default="0.01">
<xs:annotation>
<xs:documentation xml:lang="en">The maximum allowed Euclidean distance to the required center of mass (defaults to ``0.01``).</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="channel" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel).</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasImageNLabels">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the output is an image and has the specified number of labels, or unique values (e.g.,
``<has_image_n_labels n="187" exclude_labels="0" />``).
The primary usage of this assertion is to verify the number of objects in images with uniquely labeled objects.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="n" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Expected number of labels.`</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="delta" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed difference of the number of labels (default is 0). The observed number of labels has to be in the range ``value +- delta``.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="min" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Minimum allowed number of labels.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed number of labels.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="channel" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel). Must be used with multi-channel imags.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="labels" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">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``.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="exclude_labels" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">List of labels to be excluded from consideration, separated by a comma. The primary usage of this attribute is to exclude the background of a label image. Cannot be used in combination with ``labels``.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertHasImageMeanObjectSize">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the output is an image with labeled objects which have the specified mean size (number of pixels),
plus/minus ``eps`` (e.g., ``<has_image_mean_object_size mean_object_size="111.87" exclude_labels="0" />``).
The labels must be unique.
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="mean_object_size" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">The required mean size of the uniquely labeled objects.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="eps" type="xs:float" use="optional" default="0.01">
<xs:annotation>
<xs:documentation xml:lang="en">The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean size of the uniquely labeled objects has to be in the range ``value +- eps``.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="min" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">A lower bound of the required mean size of the uniquely labeled objects.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">An upper bound of the required mean size of the uniquely labeled objects.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="channel" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel). Must be used with multi-channel imags.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="labels" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">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``.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="exclude_labels" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">List of labels to be excluded from consideration, separated by a comma. The primary usage of this attribute is to exclude the background of a label image. Cannot be used in combination with ``labels``.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasJsonPropertyWithValue">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the JSON document contains a property or key with the specified JSON value.
```xml
<has_json_property_with_value property="skipped_columns" value="[1, 3, 5]" />
```
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributeProperty"/>
<xs:attribute name="value" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">JSON-ified value to search for. This will be converted from an XML string to JSON with Python's json.loads function.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasJsonPropertyWithText">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the JSON document contains a property or key with the specified text (i.e. string) value.
```xml
<has_json_property_with_text property="color" text="red" />
```
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attributeGroup ref="AssertAttributeProperty"/>
<xs:attribute name="text" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">Text value to search for.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasH5Keys">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts HDF5 output has a set of attributes (``keys``), specified as a
comma-separated list, e.g.
```xml
<has_h5_keys keys="bins,chroms,indexes,pixels,chroms/lengths" />
```
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="keys" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">Comma-separated list of HDF5 attributes to check for.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasH5Attribute">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts HDF5 output contains the specified ``value`` for an attribute (``key``), e.g.
```xml
<has_h5_attribute key="nchroms" value="15" />
```
$attribute_list::5
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="key" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">HDF5 attribute to check value of.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="value" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">Expected value of HDF5 attribute to check.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:attributeGroup name="AssertAttributePath">
<xs:attribute name="path" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="AssertAttributeProperty">
<xs:attribute name="property" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">JSON property to search the target for.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="AssertAttributeNegate">
<xs:attribute name="negate" type="PermissiveBoolean" default="false">
<xs:annotation>
<xs:documentation xml:lang="en">Negate the outcome of the assertion.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="AssertAttributeN">
<xs:attribute name="n" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="delta" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="min" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:complexType name="Inputs">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Consists of all elements that define the
+4
View File
@@ -217,6 +217,10 @@ allow-dict-calls-with-keyword-arguments = true
combine-as-imports = true
relative-imports-order = "closest-to-furthest"
[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true
[tool.ruff.lint.per-file-ignores]
# Don't check pyupgrade rules on tool scripts, which may use different Python versions
"test/functional/tools/*" = ["UP"]
@@ -0,0 +1,237 @@
import sys
from string import Template
import pytest
from pydantic import ValidationError
from galaxy.tool_util.verify.assertion_models import assertion_list
from galaxy.tool_util.verify.codegen import galaxy_xsd_path
from galaxy.util.commands import shell
from galaxy.util.unittest_utils import skip_unless_executable
valid_assertions = [
{"that": "has_size", "size": "5G"},
{"that": "has_size", "size": 1},
{"that": "has_size", "size": "5Mi"},
{"that": "has_text", "text": "JBrowseDefaultMainPage"},
{"that": "has_line", "line": "'>Wildtype Staphylococcus aureus strain WT.'"},
{"that": "has_n_columns", "n": 2},
{"that": "is_valid_xml"},
{"that": "has_element_with_path", "path": "//el"},
{"that": "has_n_elements_with_path", "path": "//el", "n": 4},
{"that": "element_text_matches", "expression": "foob[a]r", "path": "//el"},
{"that": "element_text_is", "text": "foobar", "path": "//el"},
{
"that": "xml_element",
"path": "./elem/more[2]",
"children": [{"that": "has_text_matching", "expression": "foo$"}],
},
{"that": "xml_element", "path": "./elem/more[2]"},
{"that": "element_text", "path": "./elem/more[2]", "children": [{"that": "has_text", "text": "foo"}]},
{"that": "has_json_property_with_value", "property": "foobar", "value": "'6'"},
{"that": "has_json_property_with_text", "property": "foobar", "text": "cowdog"},
{"that": "has_archive_member", "path": ".*/my-file.txt"},
{"that": "has_archive_member", "path": ".*/my-file.txt", "children": [{"that": "has_text", "text": "1235abc"}]},
{"that": "has_image_width", "width": 560},
{"that": "has_image_width", "width": 560, "delta": 490},
{"that": "has_image_height", "height": 560, "delta": 490},
{"that": "has_image_height", "min": 45, "max": 90},
{"that": "has_image_channels", "channels": 3},
{"that": "has_image_channels", "channels": 3, "delta": 1},
{"that": "has_image_channels", "min": 1, "max": 4},
{"that": "has_image_channels", "min": 1, "max": 4, "negate": True},
{"that": "has_image_mean_intensity", "mean_intensity": 3.4},
{"that": "has_image_mean_intensity", "mean_intensity": 3.4, "eps": 0.2},
{"that": "has_image_mean_intensity", "mean_intensity": 3.4, "eps": 0.2, "channel": 1},
{"that": "has_image_mean_intensity", "min": 0.4, "max": 0.6, "channel": 1},
{"that": "has_image_center_of_mass", "center_of_mass": "511.07, 223.34"},
{"that": "has_image_center_of_mass", "center_of_mass": "511.07, 223.34", "channel": 1},
{"that": "has_image_center_of_mass", "center_of_mass": "511.07, 223.34", "channel": 1, "eps": 0.2},
{"that": "has_image_n_labels", "n": 85},
{"that": "has_image_n_labels", "labels": [1, 3, 4]},
{"that": "has_image_n_labels", "n": 9, "exclude_labels": [1, 3, 4]},
{"that": "has_image_n_labels", "n": 9, "exclude_labels": [1, 3, 4], "negate": True},
{"that": "has_image_mean_object_size", "mean_object_size": 9, "exclude_labels": [1, 3, 4]},
{"that": "has_image_mean_object_size", "mean_object_size": 9, "labels": [1, 3, 4]},
{"that": "has_image_mean_object_size", "mean_object_size": 9, "channel": 1, "eps": 0.2},
{"that": "has_json_property_with_value", "property": "skipped_columns", "value": "[1, 3, 5]"},
{"that": "has_json_property_with_text", "property": "color", "text": "red"},
{"that": "has_n_columns", "n": 30},
{"that": "has_n_columns", "n": 30, "delta": 4},
{"that": "has_n_columns", "n": 30, "delta": 4, "sep": " ", "comment": "####"},
]
valid_xml_assertions = [
"""<has_size value="5G" />""",
"""<has_size size="5G" />""",
"""<has_size size="5G" />""",
"""<has_text text="JBrowseDefaultMainPage" />""",
"""<has_line line="'>Wildtype Staphylococcus aureus strain WT.'" />""",
"""<xml_element path="./elem/more[2]" attribute="name"><has_text_matching expression="foo$"/></xml_element>""",
"""<xml_element path="./elem/more[2]" attribute="name"></xml_element>""",
"""<element_text path="./elem/more[2]"><has_text text="foo" /></element_text>""",
"""<has_element_with_path path="BlastOutput_param/Parameters/Parameters_matrix" />""",
r"""<element_text_matches path="BlastOutput_version" expression="BLASTP\s+2\.2.*"/>""",
"""<element_text_is path="BlastOutput_program" text="blastp" />""",
r"""<attribute_matches path="outerElement/innerElement2" attribute="foo2" expression="bar\d+" />""",
"""<element_text path="BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def"><not_has_text text="EDK72998.1" /></element_text>""",
"""<has_archive_member path=".*/my-file.txt"><not_has_text text="EDK72998.1"/></has_archive_member>""",
"""<has_archive_member path=".*/my-file.txt"></has_archive_member>""",
"""<has_image_n_labels n="85" />""",
"""<has_image_center_of_mass center_of_mass="511.07, 223.34" />""",
"""<has_image_center_of_mass center_of_mass="511.07, 223.34" eps=".2" />""",
"""<has_image_mean_intensity mean_intensity="0.83" />""",
"""<has_image_height height="512" delta="2" />""",
"""<has_image_width width="512" delta="2" />""",
"""<has_image_channels channels="3" />""",
"""<has_image_channels channels="3" delta="1" />""",
"""<has_image_channels min="3" max="5" />""",
"""<has_image_channels min="3" max="5" negate="true" />""",
"""<has_json_property_with_value property="skipped_columns" value="[1, 3, 5]" />""",
"""<has_json_property_with_text property="color" text="red" />""",
"""<has_n_columns n="30" />""",
"""<has_n_columns n="30" delta="4" />""",
"""<has_n_columns n="30" delta="4" sep=" " comment="###" />""",
]
invalid_assertions = [
{"that": "has_size", "size": "5Gigabytes"},
# negative sizes not allowed
{"that": "has_size", "size": -1},
{"that": "has_n_columns", "n": [2]},
{"that": "has_n_columns", "n": -2},
{"that": "is_valid_xml_foo"},
{"that": "has_element_with_path", "path": 45},
{"that": "has_n_elements_with_path", "n": 4},
{"that": "has_n_elements_with_path", "n": -4},
{"that": "element_text_matches", "expression": 12, "path": "//el"},
# unclosed regex group
{"that": "element_text_matches", "expression": "[12", "path": "//el"},
{"that": "xml_element", "path": "./elem/more[2]", "children": [{"that": "foobar"}]},
{
"that": "xml_element",
"path": "./elem/more[2]",
"children": [{"that": "has_text_matching", "line": "invalidprop"}],
},
# must specify children for element_text
{"that": "element_text", "path": "./elem/more[2]"},
{"that": "has_json_property_with_value", "property": 42, "value": "cowdog"},
{"that": "has_json_property_with_text", "property": "foobar", "text": 6},
{"that": "has_archive_member", "path": ".*/my-file.txt", "extra": "param"},
{"that": "has_archive_member", "path": ".*/my-file.txt", "children": [{"that": "invalid"}]},
{"that": "has_image_width", "width": "560"},
{"that": "has_image_width", "width": -560},
{"that": "has_image_width", "width": 560, "delta": "wrong"},
{"that": "has_image_height", "height": -560},
{"that": "has_image_center_of_mass", "center_of_mass": "511.07, 223.34, foobar"},
{"that": "has_image_center_of_mass", "center_of_mass": "511.07"},
{"that": "has_image_center_of_mass", "center_of_mass": "511.07, cow"},
# negative mean object sizes are not allowed
{"that": "has_image_n_labels", "n": -85},
{"that": "has_image_n_labels", "n": 85, "delta": -3},
# negative mean object sizes are not allowed
{"that": "has_image_mean_object_size", "mean_object_size": -9, "exclude_labels": [1, 3, 4]},
{"that": "has_image_mean_object_size", "mean_object_size": -9.0, "exclude_labels": [1, 3, 4]},
{"that": "has_image_mean_object_size", "mean_object_size": 9, "exclude_labels": [1, 3, 4], "eps": -0.2},
# looks a little odd in JSON but value is JSON loaded so must be a string
{"that": "has_json_property_with_value", "property": "skipped_columns", "value": [1, 3, 5]},
# missing property
{"that": "has_json_property_with_value", "property": "skipped_columns"},
{"that": "has_json_property_with_text", "property": "color"},
{"that": "has_n_columns", "n": 30, "delta": "wrongtype"},
{"that": "has_n_columns", "n": 30, "delta": -2},
{"that": "has_n_columns", "n": 30, "delta": 4, "sep": " ", "comment": "####", "extra": "param"},
]
invalid_xml_assertions = [
"""<has_n_columns n="cow" />""",
"""<has_text line="JBrowseDefaultMainPage" />""",
"""<has_line text="JBrowseDefaultMainPage" />""",
# at least one child assertion is required here...
"""<element_text path="./elem/more[2]"></element_text>""",
"""<has_archive_member path=".*/my-file.txt"><not_has_text line="EDK72998.1"/></has_archive_member>""",
"""<has_archive_member path=".*/my-file.txt"><foobar /></has_archive_member>""",
"""<has_image_center_of_mass center_of_mass="511.07, 223.34" extra="param" />""",
"""<has_image_center_of_mass center_of_mass="511.07, 223.34" eps="foobar" />""",
# negative numbers not allowed
"""<has_image_n_labels n="-85" />""",
"""<has_image_n_labels n="85" delta="-3" />""",
"""<has_image_mean_intensity mean_intensity="not_a_number" />""",
"""<has_image_mean_intensity mean_intensity=".83" extra="param" />""",
"""<has_image_height height="512" delta="notanumber" />""",
"""<has_image_width width="512" delta="2" extra="param" />""",
"""<has_image_height height="512" delta="-2" />""",
"""<has_image_width width="512" delta="-2" />""",
"""<has_image_height height="-512" />""",
"""<has_image_width width="-512" />""",
"""<has_image_channels channels="3" exclude="wrong" />""",
"""<has_image_channels channels="3" delta="not_a_number" />""",
"""<has_image_channels min="3" max="5" foo="bar" />""",
"""<has_image_channels min="3" max="5" negate="NOTABOOLEAN" />""",
# missing required arguments...
"""<has_json_property_with_value property="skipped_columns" />""",
"""<has_json_property_with_text property="color" />""",
"""<has_n_columns n="30" delta="wrong_type" />""",
"""<has_n_columns n="30" delta="4" sep=" " comment="###" extra="param"/>""",
]
TOOL_TEMPLATE = Template(
"""
<tool id="gx_test" name="gx_test" version="1.0.0">
<command><![CDATA[
echo '$parameter' >> '$output'
]]></command>
<inputs>
<param name="parameter" value="1" type="integer" />
</inputs>
<outputs>
<data name="output" format="txt" />
</outputs>
<tests>
<test>
<param name="parameter" value_json="12456" />
<output name="output">
<assert_contents>
$assertion_xml
</assert_contents>
</output>
</test>
</tests>
</tool>
"""
)
if sys.version_info < (3, 8): # noqa: UP036
pytest.skip(reason="Pydantic assertion models require python3.8 or higher", allow_module_level=True)
def test_valid_models_validate():
assertion_list.model_validate(valid_assertions)
def test_invalid_models_do_not_validate():
for invalid_assertion in invalid_assertions:
with pytest.raises(ValidationError):
assertion_list.model_validate([invalid_assertion])
@skip_unless_executable("xmllint")
def test_valid_xsd(tmp_path):
for assertion_xml in valid_xml_assertions:
tool_xml = TOOL_TEMPLATE.safe_substitute(assertion_xml=assertion_xml)
tool_path = tmp_path / "tool.xml"
tool_path.write_text(tool_xml)
ret = shell(["xmllint", "--nowarning", "--noout", "--schema", galaxy_xsd_path, str(tool_path)])
assert ret == 0, f"{assertion_xml} failed to validate"
@skip_unless_executable("xmllint")
def test_invalid_xsd(tmp_path):
for assertion_xml in invalid_xml_assertions:
tool_xml = TOOL_TEMPLATE.safe_substitute(assertion_xml=assertion_xml)
tool_path = tmp_path / "tool.xml"
tool_path.write_text(tool_xml)
ret = shell(["xmllint", "--nowarning", "--noout", "--schema", galaxy_xsd_path, str(tool_path)])
assert ret != 0, f"{assertion_xml} validated when error expected"
@@ -1324,6 +1324,7 @@ if h5py is not None:
def run_assertions(assertion_xml: str, data, decompress=False) -> Tuple:
assertion = parse_xml_string(assertion_xml)
assertion_description = __parse_assert_list_from_elem(assertion)
assert assertion_description
try:
asserts.verify_assertions(data, assertion_description, decompress=decompress)
except AssertionError as e: