Merge pull request #12528 from bernt-matthias/topic/n-col-assert

Various extensions and improvements for asserts
This commit is contained in:
Dannon
2022-01-25 08:42:05 -05:00
committed by GitHub
13 changed files with 2036 additions and 266 deletions
+37 -18
View File
@@ -90,28 +90,32 @@ def _build_tag(tag, hide_attributes):
tag_help = StringIO()
annotation_el = tag_el.find("{http://www.w3.org/2001/XMLSchema}annotation")
text = annotation_el.find("{http://www.w3.org/2001/XMLSchema}documentation").text
text = _replace_attribute_list(tag, text, attributes)
for line in text.splitlines():
if line.startswith("$attribute_list:"):
attributes_str, header_level = line.split(":")[1:3]
attribute_names = attributes_str.split(",")
header_level = int(header_level)
text = text.replace(line, _build_attributes_table(tag, attributes, attribute_names=attribute_names, header_level=header_level))
if line.startswith("$assertions"):
assertions_tag = xmlschema_doc.find("//{http://www.w3.org/2001/XMLSchema}complexType[@name='TestAssertions']")
assertion_tag = xmlschema_doc.find("//{http://www.w3.org/2001/XMLSchema}group[@name='TestAssertion']")
assertions_buffer = StringIO()
assertions_buffer.write(_doc_or_none(assertions_tag))
assertions_buffer.write("\n\n")
assertions_buffer.write("Child Element/Assertion | Details \n")
assertions_buffer.write("--- | ---\n")
elements = assertion_tag.findall("{http://www.w3.org/2001/XMLSchema}choice/{http://www.w3.org/2001/XMLSchema}element")
for element in elements:
doc = _doc_or_none(element)
if doc is None:
doc = _doc_or_none(_type_el(element))
assert doc is not None, "Documentation for %s is empty" % element.attrib["name"]
doc = doc.strip()
assertions_buffer.write("``{}`` | {}\n".format(element.attrib["name"], doc))
assertion_groups = assertions_tag.xpath("xs:choice/xs:group", namespaces={'xs': 'http://www.w3.org/2001/XMLSchema'})
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:
doc = _doc_or_none(element)
if doc is None:
doc = _doc_or_none(_type_el(element))
assert doc is not None, "Documentation for %s is empty" % element.attrib["name"]
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")
text = text.replace(line, assertions_buffer.getvalue())
tag_help.write(text)
best_practices = _get_bp_link(annotation_el)
@@ -125,6 +129,20 @@ element [here](%s).""" % best_practices)
return tag_help.getvalue()
def _replace_attribute_list(tag, text, attributes):
for line in text.splitlines():
if not line.startswith("$attribute_list:"):
continue
attributes_str, header_level = line.split(":")[1:3]
if attributes_str == "":
attribute_names = None
else:
attribute_names = attributes_str.split(",")
header_level = int(header_level)
text = text.replace(line, _build_attributes_table(tag, attributes, attribute_names=attribute_names, header_level=header_level))
return text
def _get_bp_link(annotation_el):
anchor = annotation_el.attrib.get("{http://galaxyproject.org/xml/1.0}best_practices", None)
link = None
@@ -190,8 +208,9 @@ def _find_tag_el(tag):
def _type_el(tag):
element_type = tag.attrib["type"]
type_el = xmlschema_doc.find("//{http://www.w3.org/2001/XMLSchema}complexType/[@name='%s']" % element_type) or \
xmlschema_doc.find("//{http://www.w3.org/2001/XMLSchema}simpleType/[@name='%s']" % element_type)
type_el = xmlschema_doc.find("//{http://www.w3.org/2001/XMLSchema}complexType/[@name='%s']" % element_type)
if type_el is None:
type_el = xmlschema_doc.find("//{http://www.w3.org/2001/XMLSchema}simpleType/[@name='%s']" % element_type)
return type_el
+50 -3
View File
@@ -1,5 +1,8 @@
"""This module contains a linting functions for tool tests."""
from inspect import Parameter, signature
from ._util import is_datasource
from ..verify import asserts
# Misspelled so as not be picked up by nosetests.
@@ -35,9 +38,14 @@ def lint_tsts(tool_xml, lint_ctx):
break
test_assert = ("assert_stdout", "assert_stderr", "assert_command")
for ta in test_assert:
if len(test.findall(ta)) > 0:
has_test = True
break
assertions = test.findall(ta)
if len(assertions) == 0:
continue
if len(assertions) > 1:
lint_ctx.error("Test {test_idx}: More than one {ta} found. Only the first is considered.")
has_test = True
_check_asserts(test_idx, assertions, lint_ctx)
_check_asserts(test_idx, test.findall(".//assert_contents"), lint_ctx)
# really simple test that test parameters are also present in the inputs
for param in test.findall("param"):
@@ -94,6 +102,45 @@ def lint_tsts(tool_xml, lint_ctx):
lint_ctx.warn("No valid test(s) found.", line=tests_line, xpath=tests_path)
def _check_asserts(test_idx, assertions, lint_ctx):
"""
assertions is a list of assert_contents, assert_stdout, assert_stderr, assert_command
in practice only for the first case the list may be longer than one
"""
for assertion in assertions:
for i, a in enumerate(assertion.iter()):
if i == 0: # skip root note itself
continue
assert_function_name = "assert_" + a.tag
if assert_function_name not in asserts.assertion_functions:
lint_ctx.error(f"Test {test_idx}: unknown assertion {a.tag}")
continue
assert_function_sig = signature(asserts.assertion_functions[assert_function_name])
# check type of the attributes (int, float ...)
for attrib in a.attrib:
if attrib not in assert_function_sig.parameters:
lint_ctx.error(f"Test {test_idx}: unknown attribute {attrib} for {a.tag}")
continue
if assert_function_sig.parameters[attrib].annotation is not Parameter.empty:
try:
assert_function_sig.parameters[attrib].annotation(a.attrib[attrib])
except ValueError:
lint_ctx.error(f"Test {test_idx}: attribute {attrib} for {a.tag} needs to be {assert_function_sig.parameters[attrib].annotation.__name__} got {a.attrib[attrib]}")
# check missing required attributes
for p in assert_function_sig.parameters:
if p in ["output", "output_bytes", "verify_assertions_function", "children"]:
continue
if assert_function_sig.parameters[p].default is Parameter.empty and p not in a.attrib:
lint_ctx.error(f"Test {test_idx}: missing attribute {p} for {a.tag}")
# has_n_lines, has_n_columns, and has_size need to specify n/value, min, or max
if a.tag in ["has_n_lines", "has_n_columns"]:
if "n" not in a.attrib and "min" not in a.attrib and "max" not in a.attrib:
lint_ctx.error(f"Test {test_idx}: {a.tag} needs to specify 'n', 'min', or 'max'")
if a.tag == "has_size":
if "value" not in a.attrib and "min" not in a.attrib and "max" not in a.attrib:
lint_ctx.error(f"Test {test_idx}: {a.tag} needs to specify 'n', 'min', or 'max'")
def _collect_output_names(tool_xml):
output_data_names = []
output_collection_names = []
@@ -1,6 +1,6 @@
import logging
import sys
from inspect import getfullargspec
from inspect import getfullargspec, getmembers
from galaxy.util import unicodify
@@ -12,16 +12,19 @@ assertion_module_names = ['text', 'tabular', 'xml', 'hdf5', 'archive', 'size']
# 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_modules = []
assertion_functions = {}
for assertion_module_name in assertion_module_names:
full_assertion_module_name = f"galaxy.tool_util.verify.asserts.{assertion_module_name}"
try:
# Dynamically import module
__import__(full_assertion_module_name)
assertion_module = sys.modules[full_assertion_module_name]
assertion_modules.append(assertion_module)
except Exception:
log.exception('Failed to load assertion module: %s', assertion_module_name)
continue
for member, value in getmembers(assertion_module):
if member.startswith("assert_"):
assertion_functions[member] = value
def verify_assertions(data, assertion_description_list):
@@ -33,14 +36,11 @@ def verify_assertions(data, assertion_description_list):
def verify_assertion(data, assertion_description):
tag = assertion_description["tag"]
assert_function_name = f"assert_{tag}"
assert_function = None
for assertion_module in assertion_modules:
if hasattr(assertion_module, assert_function_name):
assert_function = getattr(assertion_module, assert_function_name)
assert_function_name = "assert_" + tag
assert_function = assertion_functions.get(assert_function_name)
if assert_function is None:
errmsg = f"Unable to find test function associated with XML tag '{tag}'. Check your tool file syntax."
errmsg = f"Unable to find test function associated with XML tag {tag}. Check your tool file syntax."
raise AssertionError(errmsg)
assert_function_args = getfullargspec(assert_function).args
@@ -0,0 +1,58 @@
from math import inf
from galaxy.util import asbool
from galaxy.util.bytesize import parse_bytesize
def _assert_number(count, n, delta, min, max, negate, n_text, min_max_text):
"""
helper function for assering that count is in
- [n-delta:n+delta]
- [min:max]
raising an assertion error using n_text and min_max_text (resp)
substituting {n}, {delta}, {min}, and {max}
(and keeping potentially present {text} and {output})
n, delta, min, max can be suffixed by (K|M|G|T|P|E)i?
"""
negate = asbool(negate)
expected = "Expected" if not negate else "Did not expect"
if n is not None:
n_bytes = parse_bytesize(n)
delta_bytes = parse_bytesize(delta)
assert (not negate) == (abs(count - n_bytes) <= delta_bytes), n_text.format(expected=expected, n=n, delta=delta, text="{text}", output="{output}") + f" found {count}"
if min is not None or max is not None:
if min is None:
min = -inf # also replacing min/max for output
min_bytes = -inf
else:
min_bytes = parse_bytesize(min)
if max is None:
max = inf
max_bytes = inf
else:
max_bytes = parse_bytesize(max)
assert (not negate) == (min_bytes <= count <= max_bytes), min_max_text.format(expected=expected, min=min, max=max, text="{text}", output="{output}") + f" found {count}"
def _assert_presence_number(output, text, n, delta, min, max, negate, check_presence_foo, count_foo, presence_text, n_text, min_max_text):
"""
helper function to assert that
- text is present in output using check_presence_foo
this is done only if n, min, and max are None
- text appears a certain number of times, where the count is determined with count foo
raising an assertion error using presence_text, n_text or min_max_text (resp)
substituting {n}, {delta}, {min}, {max}, {text}, and {output}
n, delta, min, max can be suffixed by (K|M|G|T|P|E)i?
"""
negate = asbool(negate)
expected = "Expected" if not negate else "Did not expect"
if n is None and min is None and max is None:
assert (not negate) == check_presence_foo(output, text), presence_text.format(expected=expected, output=output, text=text)
try:
_assert_number(count_foo(output, text), n, delta, min, max, negate, n_text, min_max_text)
except AssertionError as e:
raise AssertionError(str(e).format(output=output, text=text))
+77 -24
View File
@@ -1,37 +1,90 @@
import io
import re
import tarfile
import tempfile
import zipfile
def _extract_from_tar(tar_temp, path):
for fn in tar_temp.getnames():
if re.match(path, fn):
# Will only match on first hit, probably fine for now
return tar_temp.extractfile(fn)
from galaxy.util import asbool
from ._util import _assert_presence_number
def _extract_from_zip(zip_temp, path):
for fn in zip_temp.namelist():
if re.match(path, fn):
# Will only match on first hit, probably fine for now
return zip_temp.open(fn)
def _extract_from_tar(bytes, fn):
with io.BytesIO(bytes) as temp:
with tarfile.open(fileobj=temp, mode='r') as tar_temp:
ti = tar_temp.getmember(fn)
# zip treats directories like empty files.
# so make this consistent for tar
if ti.isdir():
return ""
with tar_temp.extractfile(fn) as member_fh:
return member_fh.read()
def assert_has_archive_member(output_bytes, path, verify_assertions_function, children):
def _list_from_tar(bytes, path):
lst = list()
with io.BytesIO(bytes) as temp:
with tarfile.open(fileobj=temp, mode='r') as tar_temp:
for fn in tar_temp.getnames():
if not re.match(path, fn):
continue
lst.append(fn)
return sorted(lst)
def _extract_from_zip(bytes, fn):
with io.BytesIO(bytes) as temp:
with zipfile.ZipFile(temp, mode='r') as zip_temp:
with zip_temp.open(fn) as member_fh:
return member_fh.read()
def _list_from_zip(bytes, path):
lst = list()
with io.BytesIO(bytes) as temp:
with zipfile.ZipFile(temp, mode='r') as zip_temp:
for fn in zip_temp.namelist():
if not re.match(path, fn):
continue
lst.append(fn)
return sorted(lst)
def assert_has_archive_member(output_bytes, path, verify_assertions_function, children, all="false", n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False):
""" 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."""
all = asbool(all)
extract_foo = None
# from python 3.9 is_tarfile supports file like objects then we do not need
# the tempfile detour but can use io.BytesIO(output_bytes)
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(output_bytes)
tmp.flush()
if zipfile.is_zipfile(tmp.name):
extract_foo = _extract_from_zip
list_foo = _list_from_zip
elif tarfile.is_tarfile(tmp.name):
extract_foo = _extract_from_tar
list_foo = _list_from_tar
assert extract_foo is not None, f"Expected path '{path}' to be an archive"
output_temp = io.BytesIO(output_bytes)
try: # tar / tar.gz
temp = tarfile.open(fileobj=output_temp, mode='r')
contents = _extract_from_tar(temp, path)
except tarfile.TarError: # zip
temp = zipfile.ZipFile(output_temp, mode='r')
contents = _extract_from_zip(temp, path)
finally:
verify_assertions_function(contents.read(), children)
contents.close()
temp.close()
output_temp.close()
# get list of matching file names in archive and check against n, delta,
# min, max (slightly abusing the output and text as well as the function
# parameters)
fns = list_foo(output_bytes, path)
_assert_presence_number(None, path, n, delta, min, max, negate,
lambda o, t: len(fns) > 0,
lambda o, t: len(fns),
"{expected} path '{text}' in archive",
"{expected} {n}+-{delta} matches for path '{text}' in archive",
"{expected} that the number of matches for path '{text}' in archive is in [{min}:{max}]")
# check sub-assertions on members matching path
for fn in fns:
contents = extract_foo(output_bytes, fn)
try:
verify_assertions_function(contents, children)
except AssertionError as e:
raise AssertionError(f"Archive member '{path}': {str(e)}")
if not all:
break
+2 -1
View File
@@ -20,9 +20,10 @@ def assert_has_h5_attribute(output_bytes, key, value):
output_temp = io.BytesIO(output_bytes)
local_attrs = h5py.File(output_temp, 'r').attrs
assert key in local_attrs and str(local_attrs[key]) == value, (
f"Not a HDF5 file or H5 attributes do not match:\n\t{local_attrs.items()}\n\n\t({key} : {value})")
f"Not a HDF5 file or H5 attributes do not match:\n\t{list(local_attrs.items())}\n\n\t({key} : {value})")
# TODO the function actually queries groups. so the function and argument name are misleading
def assert_has_h5_keys(output_bytes, keys):
""" Asserts the specified HDF5 output has the given keys."""
_assert_h5py()
+11 -3
View File
@@ -1,4 +1,12 @@
def assert_has_size(output_bytes, value, delta=0):
"""Asserts the specified output has a size of the specified value"""
from ._util import _assert_number
def assert_has_size(output_bytes, value: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False):
"""
Asserts the specified output has a size of the specified value,
allowing for absolute (delta) and relative (delta_frac) difference.
"""
output_size = len(output_bytes)
assert abs(output_size - int(value)) <= int(delta), f"Expected file size was {value}, actual file size was {output_size} (difference of {delta} accepted)"
_assert_number(output_size, value, delta, min, max, negate,
"{expected} file size of {n}+-{delta}",
"{expected} file size to be in [{min}:{max}]")
+19 -9
View File
@@ -1,19 +1,29 @@
import re
from ._util import _assert_number
def get_first_line(output):
match = re.search("^(.*)$", output, flags=re.MULTILINE)
def get_first_line(output, comment):
"""
get the first non-comment and non-empty line
"""
if comment != "":
match = re.search(f"^([^{comment}].*)$", output, flags=re.MULTILINE)
else:
match = re.search("^(.+)$", output, flags=re.MULTILINE)
if match is None:
return None
return ""
else:
return match.group(1)
def assert_has_n_columns(output, n, sep='\t'):
def assert_has_n_columns(output, n: int = None, delta: int = 0, min: int = None, max: int = None, sep='\t', comment="", negate: bool = False):
""" Asserts the tabular output contains n columns. The optional
sep argument specifies the column seperator used to determine the
number of columns."""
n = int(n)
first_line = get_first_line(output)
assert first_line is not None, "Was expecting output with %d columns, but output was empty." % n
assert len(first_line.split(sep)) == n, "Output does not have %d columns." % n
number of columns. The optional comment argument specifies
comment characters"""
first_line = get_first_line(output, comment)
n_columns = len(first_line.split(sep))
_assert_number(n_columns, n, delta, min, max, negate,
"{expected} {n}+-{delta} columns in output",
"{expected} the number of columns in output to be in [{min}:{max}]")
+47 -28
View File
@@ -1,54 +1,73 @@
import re
from ._util import _assert_number, _assert_presence_number
def assert_has_text(output, text, n=None):
def assert_has_text(output, text, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False):
""" Asserts specified output contains the substring specified by
the argument text. The exact number of occurrences can be
optionally specified by the argument n."""
optionally specified by the argument n"""
assert output is not None, "Checking has_text assertion on empty output (None)"
if n is None:
assert output.find(text) >= 0, f"Output file did not contain expected text '{text}' (output '{output}')"
else:
matches = re.findall(re.escape(text), output)
assert len(matches) == int(n), f"Expected {n} matches for '{text}' in output file (output '{output}'); found {len(matches)}"
_assert_presence_number(output, text, n, delta, min, max, negate,
lambda o, t: o.find(t) >= 0,
lambda o, t: len(re.findall(re.escape(t), o)),
"{expected} text '{text}' in output ('{output}')",
"{expected} {n}+-{delta} occurences of '{text}' in output ('{output}')",
"{expected} that the number of occurences of '{text}' in output is in [{min}:{max}] ('{output}')")
def assert_not_has_text(output, text):
""" Asserts specified output does not contain the substring
specified by the argument text."""
specified by the argument text"""
assert output is not None, "Checking not_has_text assertion on empty output (None)"
assert output.find(text) < 0, f"Output file contains unexpected text '{text}'"
def assert_has_line(output, line, n=None):
def assert_has_line(output, line, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False):
""" Asserts the specified output contains the line specified by the
argument line. The exact number of occurrences can be optionally
specified by the argument n."""
specified by the argument n"""
assert output is not None, "Checking has_line assertion on empty output (None)"
if n is None:
match = re.search(f"^{re.escape(line)}$", output, flags=re.MULTILINE)
assert match is not None, f"No line of output file was '{line}' (output was '{output}') "
else:
matches = re.findall(f"^{re.escape(line)}$", output, flags=re.MULTILINE)
assert len(matches) == int(n), f"Expected {n} lines matching '{line}' in output file (output was '{output}'); found {len(matches)}"
_assert_presence_number(output, line, n, delta, min, max, negate,
lambda o, l: re.search(f"^{re.escape(l)}$", o, flags=re.MULTILINE) is not None,
lambda o, l: len(re.findall(f"^{re.escape(l)}$", o, flags=re.MULTILINE)),
"{expected} line '{text}' in output ('{output}')",
"{expected} {n}+-{delta} lines '{text}' in output ('{output}')",
"{expected} that the number of lines '{text}' in output is in [{min}:{max}] ('{output}')")
def assert_has_n_lines(output, n):
"""Asserts the specified output contains ``n`` lines."""
def assert_has_n_lines(output, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False):
"""Asserts the specified output contains ``n`` lines allowing
for a difference in the number of lines (delta)
or relative differebce in the number of lines"""
assert output is not None, "Checking has_n_lines assertion on empty output (None)"
n_lines_found = len(output.splitlines())
assert n_lines_found == int(n), f"Expected {n} lines in output, found {n_lines_found} lines"
count = len(output.splitlines())
_assert_number(count, n, delta, min, max, negate,
"{expected} {n}+-{delta} lines in the output",
"{expected} the number of line to be in [{min}:{max}]")
def assert_has_text_matching(output, expression):
def assert_has_text_matching(output, expression, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False):
""" Asserts the specified output contains text matching the
regular expression specified by the argument expression."""
match = re.search(expression, output)
assert match is not None, f"No text matching expression '{expression}' was found in output file."
regular expression specified by the argument expression.
If n is given the assertion checks for exacly n (nonoverlapping)
occurences.
"""
_assert_presence_number(output, expression, n, delta, min, max, negate,
lambda o, e: re.search(e, o) is not None,
lambda o, e: len(re.findall(e, o)),
"{expected} text matching expression '{text}' in output ('{output}')",
"{expected} {n}+-{delta} (non-overlapping) matches for '{text}' in output ('{output}')",
"{expected} that the number of (non-overlapping) matches for '{text}' in output is in [{min}:{max}] ('{output}')")
def assert_has_line_matching(output, expression):
def assert_has_line_matching(output, expression, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False):
""" Asserts the specified output contains a line matching the
regular expression specified by the argument expression."""
match = re.search(f"^{expression}$", output, flags=re.MULTILINE)
assert match is not None, f"No line matching expression '{expression}' was found in output file."
regular expression specified by the argument expression. If n is given
the assertion checks for exactly n occurences."""
_assert_presence_number(output, expression, n, delta, min, max, negate,
lambda o, e: re.search(f"^{e}$", o, flags=re.MULTILINE) is not None,
lambda o, e: len(re.findall(f"^{e}$", o, flags=re.MULTILINE)),
"{expected} line matching expression '{text}' in output ('{output}')",
"{expected} {n}+-{delta} lines matching for '{text}' in output ('{output}')",
"{expected} that the number of lines matching for '{text}' in output is in [{min}:{max}] ('{output}')")
+63 -49
View File
@@ -1,91 +1,105 @@
import re
from lxml.etree import XMLSyntaxError
from galaxy.tool_util.verify import asserts
from galaxy.util import (
asbool,
parse_xml_string,
unicodify,
)
# Helper functions used to work with XML output.
def to_xml(output):
return parse_xml_string(output)
def xml_find_text(output, path):
xml = to_xml(output)
text = xml.findtext(path)
return text
def xml_find(output, path):
xml = to_xml(output)
return xml.find(path)
def assert_is_valid_xml(output):
""" Simple assertion that just verifies the specified output
is valid XML."""
try:
to_xml(output)
except Exception as e:
# TODO: Narrow caught exception to just parsing failure
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, path):
def assert_has_element_with_path(output, path, negate: bool = False):
""" 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;
http://effbot.org/zone/element-xpath.htm for more information."""
if xml_find(output, path) is None:
errmsg = f"Expected to find XML element matching expression {path}, not such match was found."
raise AssertionError(errmsg)
https://lxml.de/xpathxslt.html for more information."""
assert_xml_element(output, path, negate=negate)
def assert_has_n_elements_with_path(output, path, n):
def assert_has_n_elements_with_path(output, path, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False):
""" Asserts the specified output has exactly n elements matching the
path specified."""
xml = to_xml(output)
n = int(n)
num_elements = len(xml.findall(path))
if num_elements != n:
errmsg = "Expected to find %d elements with path %s, but %d were found." % (n, path, num_elements)
raise AssertionError(errmsg)
assert_xml_element(output, path, n=n, delta=delta, min=min, max=max, negate=negate)
def assert_element_text_matches(output, path, expression):
def assert_element_text_matches(output, path, expression, negate: bool = False):
""" Asserts the text of the first element matching the specified
path matches the specified regular expression."""
text = xml_find_text(output, path)
if re.match(expression, text) is None:
errmsg = f"Expected element with path '{path}' to contain text matching '{expression}', instead text '{text}' was found."
raise AssertionError(errmsg)
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, path, text):
def assert_element_text_is(output, path, text, negate: bool = False):
""" Asserts the text of the first element matching the specified
path matches exactly the specified text. """
assert_element_text_matches(output, path, re.escape(text))
assert_element_text_matches(output, path, re.escape(text) + "$", negate=negate)
def assert_attribute_matches(output, path, attribute, expression):
def assert_attribute_matches(output, path, attribute, expression, negate: bool = False):
""" Asserts the specified attribute of the first element matching
the specified path matches the specified regular expression."""
xml = xml_find(output, path)
attribute_value = xml.attrib[attribute]
if re.match(expression, attribute_value) is None:
errmsg = f"Expected attribute '{attribute}' on element with path '{path}' to match '{expression}', instead attribute value was '{attribute_value}'."
raise AssertionError(errmsg)
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, path, attribute, text):
def assert_attribute_is(output, path, attribute, text, negate: bool = False):
""" Asserts the specified attribute of the first element matching
the specified path matches exactly the specified text."""
assert_attribute_matches(output, path, attribute, re.escape(text))
assert_attribute_matches(output, path, attribute, re.escape(text) + "$", negate=negate)
def assert_element_text(output, path, verify_assertions_function, children):
def assert_element_text(output, path, verify_assertions_function, children, negate: bool = False):
""" Recursively checks the specified assertions against the text of
the first element matching the specified path."""
text = xml_find_text(output, path)
verify_assertions_function(text, children)
assert_xml_element(output, path, verify_assertions_function, children, negate=negate)
def assert_xml_element(output, path, verify_assertions_function=None, children=None, attribute=None, all=False, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False):
"""
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.
"""
children = children or []
all = asbool(all)
# assert that path is in output (the specified number of times)
xml = parse_xml_string(output)
asserts._util._assert_presence_number(xml, path, n, delta, min, max, negate,
lambda x, p: x.find(p) is not None,
lambda x, p: len(x.findall(p)),
"{expected} path '{text}' in xml",
"{expected} {n}+-{delta} occurrences of path '{text}' in xml",
"{expected} that the number of occurences of path '{text}' in xml is in [{min}:{max}]")
# check sub-assertions
if len(children) == 0 or verify_assertions_function is None:
return
for occ in xml.findall(path):
if attribute is None or attribute == "":
content = occ.text
else:
content = occ.attrib[attribute]
try:
verify_assertions_function(content, children)
except AssertionError as e:
if attribute is not None and attribute != "":
raise AssertionError(f"Attribute '{attribute}' on element with path '{path}': {str(e)}")
else:
raise AssertionError(f"Text of element with path '{path}': {str(e)}")
if not all:
break
+596 -120
View File
@@ -1778,7 +1778,6 @@ provides a demonstration of using this tag.
</output>
</test>
```
]]></xs:documentation>
</xs:annotation>
<xs:attribute name="name" type="xs:string" use="required">
@@ -2016,128 +2015,561 @@ module.
]]>
</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:group ref="TestAssertion" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<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="TestAssertionsH5" minOccurs="0" maxOccurs="unbounded"/>
</xs:choice>
</xs:complexType>
<xs:group name="TestAssertion">
<xs:group name="TestAssertionsGeneral">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[General asserions]]></xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="has_text" type="xs:anyType">
<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``.]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="not_has_text" type="xs:anyType">
<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" />``).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="has_text_matching" type="xs:anyType">
<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" />`` ).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="has_line" type="xs:anyType">
<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``.]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="has_n_lines" type="xs:anyType">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Asserts that an output contains ``n`` lines, e.g. ``<has_n_lines n="3" />``.]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="has_line_matching" type="xs:anyType">
<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" />``).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="has_n_columns" type="xs:anyType">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Asserts tabular output contains the specified number (``n``) of columns (e.g. ``<has_n_columns n="3" />``).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="is_valid_xml" type="xs:anyType">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Asserts the output is a valid XML file (e.g. ``<is_valid_xml />``).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="has_element_with_path" type="xs:anyType">
<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. ``<has_element_with_path path="BlastOutput_param/Parameters/Parameters_matrix" />``).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="has_n_elements_with_path" type="xs:anyType">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Asserts the XML output contains the specified number (``n``) of elements (or tags) with the specified XPath-like ``path`` (e.g. ``<has_n_elements_with_path n="9" path="BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num" />``).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="element_text_is" type="xs:anyType">
<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. ``<element_text_is path="BlastOutput_program" text="blastp" />``).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="element_text_matches">
<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. ``<element_text_matches path="BlastOutput_version" expression="BLASTP\s+2\.2.*" />``).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="attribute_is" type="xs:anyType">
<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. ``<attribute_is path="outerElement/innerElement1" attribute="foo" text="bar" />`` ).]]>
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="attribute_matches" type="xs:anyType">
<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. ``<attribute_matches path="outerElement/innerElement2" attribute="foo2" expression="bar\d+" />``).]]></xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="element_text" type="xs:anyType">
<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. ``<element_text path="BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def"><not_has_text text="EDK72998.1" /></element_text>``).]]></xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="has_h5_keys" type="AssertHasH5Keys">
</xs:element>
<xs:element name="has_h5_attribute" type="AssertHasH5Attribute">
</xs:element>
<xs:element name="has_archive_member" type="AssertHasArchiveMember">
</xs:element>
<xs:element name="has_size" type="AssertHasSize">
</xs:element>
<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="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 specific size (in bytes), e.g. ``<has_size value="10000" delta="100" />``.]]></xs:documentation>
<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="value" type="xs:string">
<xs:attribute name="value" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Desired size of the outpyt (in bytes)</xs:documentation>
<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="delta" type="xs:integer" use="optional">
<xs:attribute name="delta" type="Bytes" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">Maximum allowed size difference (default is 0).</xs:documentation>
<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: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="TestAssertionsXml" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsH5" 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="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. ``<has_h5_keys keys="bins,chroms,indexes,pixels,chroms/lengths" />``).]]></xs:documentation>
<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>
@@ -2147,7 +2579,16 @@ module.
</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. ``<has_attr key="nchroms" value="15" />``).]]></xs:documentation>
<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>
@@ -2160,19 +2601,46 @@ module.
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasArchiveMember">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[This tag allows the developer to recursively specify additional assertions as child elements about just the member contained in the archive specified by the regular expression ``path`` (e.g. ``<has_archive_member path=".*/my-file.txt"><not_has_text text="EDK72998.1" /></has_archive_member>``). Valid archive formats include ``.zip``, ``.tar``, and ``.tar.gz``.]]></xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:group ref="TestAssertion" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="path" type="xs:string">
<xs:attributeGroup name="AssertAttributePath">
<xs:attribute name="path" type="xs:string" use="required">
<xs:annotation>
<xs:documentation xml:lang="en">The regular expression specifying the archive member.</xs:documentation>
<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:complexType>
</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
@@ -6287,8 +6755,8 @@ and ``bibtex`` are the only supported options.</xs:documentation>
<xs:annotation>
<xs:documentation xml:lang="en">Type of comparison to use when comparing
test generated output files to expected output files. Currently valid value are
``diff`` (the default), ``re_match``, ``sim_size``, ``re_match_multiline``,
and ``contains``.</xs:documentation>
``diff`` (the default), ``re_match``, ``re_match_multiline``,
and ``contains``. In addtion there is ``sim_size`` which is discouraged in fafour of a ``has_size`` assertion.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:enumeration value="diff"/>
@@ -6313,6 +6781,14 @@ and ``contains``.</xs:documentation>
<xs:enumeration value="no"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Bytes">
<xs:annotation>
<xs:documentation xml:lang="en">Number of bytes alowing for suffix (k|K|M|G|P|E)i? </xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="[1-9][0-9]*([kKMGTPE]i?)?"></xs:pattern>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="EdamTopics">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
+55 -2
View File
@@ -549,6 +549,44 @@ TESTS_EXPECT_FAILURE_OUTPUT = """
</tool>
"""
ASSERTS = """
<tool>
<outputs>
<data name="out_archive"/>
<data name="out_tabular"/>
</outputs>
<tests>
<test>
<assert_stdout>
<has_text text="blah" n="1"/>
<has_line line="blah"/>
</assert_stdout>
<assert_stderr>
<invalid/>
</assert_stderr>
<assert_command>
<has_text invalid_attrib="blah"/>
</assert_command>
<output name="out_archive">
<assert_contents>
<has_size value="500k" delta="1O"/>
<has_archive_member path=".*/my-file.txt">
<not_has_text invalid_attrib_also_checked_in_nested_asserts="Blah" text="EDK72998.1" />
</has_archive_member>
</assert_contents>
</output>
<output name="out_tabular">
<assert_contents>
<has_size/>
<has_n_columns/>
<has_n_lines/>
</assert_contents>
</output>
</test>
</tests>
</tool>
"""
# tool xml for xml_order linter
XML_ORDER = """
<tool>
@@ -873,6 +911,20 @@ TESTS = [
'1 outputs found.' in x.info_messages
and len(x.info_messages) == 1 and len(x.valid_messages) == 0 and len(x.warn_messages) == 0 and len(x.error_messages) == 0
),
(
ASSERTS, tests.lint_tsts,
lambda x:
'Test 1: unknown assertion invalid' in x.error_messages
and 'Test 1: unknown attribute invalid_attrib for has_text' in x.error_messages
and 'Test 1: missing attribute text for has_text' in x.error_messages
and 'Test 1: attribute value for has_size needs to be int got 500k' in x.error_messages
and 'Test 1: attribute delta for has_size needs to be int got 1O' in x.error_messages
and 'Test 1: unknown attribute invalid_attrib_also_checked_in_nested_asserts for not_has_text' in x.error_messages
and "Test 1: has_size needs to specify 'n', 'min', or 'max'" in x.error_messages
and "Test 1: has_n_columns needs to specify 'n', 'min', or 'max'" in x.error_messages
and "Test 1: has_n_lines needs to specify 'n', 'min', or 'max'" in x.error_messages
and len(x.warn_messages) == 0 and len(x.error_messages) == 9
),
(
REPEATS, inputs.lint_repeats,
lambda x:
@@ -954,7 +1006,7 @@ TESTS = [
'Unknown tag [wrong_tag] encountered, this may result in a warning in the future.' in x.info_messages
and 'Best practice violation [stdio] elements should come before [command]' in x.warn_messages
and len(x.info_messages) == 1 and len(x.valid_messages) == 0 and len(x.warn_messages) == 1 and len(x.error_messages) == 0
)
),
]
TEST_IDS = [
@@ -997,6 +1049,7 @@ TEST_IDS = [
'outputs: format="input"',
'outputs collection static elements with format_source',
'outputs discover datatsets with tool provided metadata',
'outputs: asserts',
'repeats',
'stdio: default for default profile',
'stdio: default for non-legacy profile',
@@ -1007,7 +1060,7 @@ TEST_IDS = [
'tests: without expectations',
'tests: param and output names',
'tests: expecting failure with outputs',
'xml_order'
'xml_order',
]
File diff suppressed because it is too large Load Diff