from string import Template
import lxml.etree as ET
import pytest
from pydantic import ValidationError
from galaxy.tool_util.verify.codegen import galaxy_xsd_path
from galaxy.tool_util.verify.parse import assertion_xml_els_to_models
from galaxy.tool_util_models.assertions import assertion_list
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 = [
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
r"""""",
"""""",
r"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
]
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 = [
"""""",
"""""",
"""""",
# at least one child assertion is required here...
"""""",
"""""",
"""""",
"""""",
"""""",
# negative numbers not allowed
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
"""""",
# missing required arguments...
"""""",
"""""",
"""""",
"""""",
]
TOOL_TEMPLATE = Template("""
> '$output'
]]>
""")
def test_valid_json_models_validate():
assertion_list.model_validate(valid_assertions)
def test_invalid_json_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"
def test_valid_xml_models_validate_after_json_transform():
for assertion_xml in valid_xml_assertions:
assertion_xml_els_to_models([ET.fromstring(assertion_xml)])