Skip to content

Commit 3028bb6

Browse files
authored
Merge pull request #21495 from bernt-matthias/test-output_collections_min_max
Tool testing: add min/max attributes to test output collections
2 parents 7b9d336 + b857a21 commit 3028bb6

7 files changed

Lines changed: 149 additions & 21 deletions

File tree

lib/galaxy/tool_util/linters/tests.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -426,9 +426,14 @@ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
426426
discover_datasets = corresponding_output.find(".//discover_datasets")
427427
if discover_datasets is None:
428428
continue
429-
if "count" not in output.attrib and output.find("./discovered_dataset") is None:
429+
if (
430+
"count" not in output.attrib
431+
and "min" not in output.attrib
432+
and "max" not in output.attrib
433+
and output.find("./discovered_dataset") is None
434+
):
430435
lint_ctx.error(
431-
f"Test {test_idx}: test output '{name}' must have a 'count' attribute and/or 'discovered_dataset' children",
436+
f"Test {test_idx}: test output '{name}' must have a 'count/min/max' attribute and/or 'discovered_dataset' children",
432437
linter=cls.name(),
433438
node=output,
434439
)
@@ -456,12 +461,16 @@ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
456461
continue
457462
# - test/collection to outputs/output_collection
458463
corresponding_output = output_data_or_collection[name]
459-
discover_datasets = corresponding_output.find(".//discover_datasets")
460-
if discover_datasets is None:
464+
if corresponding_output.find(".//discover_datasets") is None:
461465
continue
462-
if "count" not in output.attrib and output.find("./element") is None:
466+
if (
467+
"count" not in output.attrib
468+
and "min" not in output.attrib
469+
and "max" not in output.attrib
470+
and output.find("./element") is None
471+
):
463472
lint_ctx.error(
464-
f"Test {test_idx}: test collection '{name}' must have a 'count' attribute or 'element' children",
473+
f"Test {test_idx}: test collection '{name}' must have a 'count/min/max' attribute or 'element' children",
465474
linter=cls.name(),
466475
node=output,
467476
)
@@ -491,10 +500,10 @@ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
491500
continue
492501
if corresponding_output.get("type", "") in ["list:list", "list:paired"]:
493502
nested_elements = output.find("./element/element")
494-
element_with_count = output.find("./element[@count]")
495-
if nested_elements is None and element_with_count is None:
503+
elements_with_count = output.xpath("./element[@count or @min or @max]")
504+
if nested_elements is None and not elements_with_count:
496505
lint_ctx.error(
497-
f"Test {test_idx}: test collection '{name}' must contain nested 'element' tags and/or element children with a 'count' attribute",
506+
f"Test {test_idx}: test collection '{name}' must contain nested 'element' tags and/or element children with a 'count/min/max' attribute",
498507
linter=cls.name(),
499508
node=output,
500509
)

lib/galaxy/tool_util/parser/interface.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ class ToolSourceTestOutputAttributes(TypedDict):
8989
metric: str
9090
pin_labels: Optional[Any]
9191
count: Optional[int]
92+
min: Optional[int]
93+
max: Optional[int]
9294
metadata: Dict[str, Any]
9395
md5: Optional[str]
9496
checksum: Optional[str]
@@ -865,6 +867,10 @@ def __init__(self, name, attrib, element_tests, element_count: Optional[int] = N
865867
else:
866868
count = attrib.get("count")
867869
self.count = int(count) if count is not None else None
870+
min = attrib.get("min")
871+
self.min = int(min) if min is not None else None
872+
max = attrib.get("max")
873+
self.max = int(max) if max is not None else None
868874
self.attrib = attrib
869875
self.element_tests = element_tests
870876

lib/galaxy/tool_util/parser/xml.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,17 @@ def __parse_test_attributes(
922922
count = int(attrib.pop("count"))
923923
except KeyError:
924924
pass
925+
min: Optional[int] = None
926+
try:
927+
min = int(attrib.pop("min"))
928+
except KeyError:
929+
pass
930+
max: Optional[int] = None
931+
try:
932+
max = int(attrib.pop("max"))
933+
except KeyError:
934+
pass
935+
has_count_assertions = count is not None or min is not None or max is not None
925936
extra_files: List[Dict[str, Any]] = []
926937
ftype: Optional[str] = None
927938
if "ftype" in attrib:
@@ -949,7 +960,7 @@ def __parse_test_attributes(
949960
has_checksum = md5sum or checksum
950961
has_nested_tests = extra_files or element_tests or primary_datasets
951962
has_object = value_object is not VALUE_OBJECT_UNSET
952-
if not (assert_list or file or metadata or has_checksum or has_nested_tests or has_object):
963+
if not (assert_list or file or metadata or has_checksum or has_nested_tests or has_object or has_count_assertions):
953964
raise Exception(
954965
"Test output defines nothing to check (e.g. must have a 'file' check against, assertions to check, metadata or checksum tests, etc...)"
955966
)
@@ -966,6 +977,8 @@ def __parse_test_attributes(
966977
pin_labels=pin_labels,
967978
location=location,
968979
count=count,
980+
min=min,
981+
max=max,
969982
metadata=metadata,
970983
md5=md5sum,
971984
checksum=checksum,

lib/galaxy/tool_util/verify/interactor.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ def verify_output(self, history_id, jobs, output_data, output_testdef, tool_id,
332332
attributes = output_testdef.attributes
333333
name = output_testdef.name
334334
expected_count = attributes.get("count")
335+
min_count = attributes.get("min")
336+
max_count = attributes.get("max")
335337
hid = self.__output_id(output_data)
336338
# TODO: Twill version verifies dataset is 'ok' in here.
337339
try:
@@ -357,6 +359,14 @@ def verify_output(self, history_id, jobs, output_data, output_testdef, tool_id,
357359
raise AssertionError(
358360
f"Output '{name}': expected to have '{expected_count}' datasets, but it had '{found_datasets}'"
359361
)
362+
if min_count is not None and min_count > found_datasets:
363+
raise AssertionError(
364+
f"Output '{name}': expected to have at least '{min_count}' datasets, but it had '{found_datasets}'"
365+
)
366+
if max_count is not None and max_count < found_datasets:
367+
raise AssertionError(
368+
f"Output '{name}': expected to have at most '{max_count}' datasets, but it had '{found_datasets}'"
369+
)
360370
for designation, (primary_outfile, primary_attributes) in primary_datasets.items():
361371
primary_output = None
362372
for output in outputs:
@@ -1335,12 +1345,16 @@ def verify_collection(output_collection_def, data_collection, verify_dataset):
13351345
message = f"Output collection '{name}': expected to be of type [{expected_collection_type}], was of type [{collection_type}]."
13361346
raise AssertionError(message)
13371347

1338-
expected_element_count = output_collection_def.count
1339-
if expected_element_count is not None:
1340-
actual_element_count = len(data_collection["elements"])
1341-
if expected_element_count != actual_element_count:
1342-
message = f"Output collection '{name}': expected to have {expected_element_count} elements, but it had {actual_element_count}."
1343-
raise AssertionError(message)
1348+
actual_element_count = len(data_collection["elements"])
1349+
if output_collection_def.count and output_collection_def.count != actual_element_count:
1350+
message = f"Output collection '{name}': expected to have {output_collection_def.count} elements, but it had {actual_element_count}."
1351+
raise AssertionError(message)
1352+
if output_collection_def.min and output_collection_def.min > actual_element_count:
1353+
message = f"Output collection '{name}': expected to have at least {output_collection_def.min} elements, but it had {actual_element_count}."
1354+
raise AssertionError(message)
1355+
if output_collection_def.max and output_collection_def.max < actual_element_count:
1356+
message = f"Output collection '{name}': expected to have at most {output_collection_def.max} elements, but it had {actual_element_count}."
1357+
raise AssertionError(message)
13441358

13451359
def get_element(elements, id):
13461360
for element in elements:
@@ -1356,7 +1370,6 @@ def verify_elements(element_objects, element_tests):
13561370
element_outfile, element_attrib = None, element_test
13571371
else:
13581372
element_outfile, element_attrib = element_test
1359-
expected_count = element_attrib.get("count")
13601373
if "expected_sort_order" in element_attrib:
13611374
expected_sort_order[element_attrib["expected_sort_order"]] = element_identifier
13621375

@@ -1373,10 +1386,21 @@ def verify_elements(element_objects, element_tests):
13731386
elements = element["object"]["elements"]
13741387
element_count = len(elements)
13751388
verify_elements(elements, element_attrib.get("elements", {}))
1389+
expected_count = element_attrib.get("count")
13761390
if expected_count is not None and expected_count != element_count:
13771391
raise AssertionError(
13781392
f"Element '{element_identifier}': expected to have {expected_count} elements, but it had {element_count}"
13791393
)
1394+
max = element_attrib.get("max")
1395+
if max is not None and max < element_count:
1396+
raise AssertionError(
1397+
f"Element '{element_identifier}': expected to have at most {max} elements, but it had {element_count}"
1398+
)
1399+
min = element_attrib.get("min")
1400+
if min is not None and min > element_count:
1401+
raise AssertionError(
1402+
f"Element '{element_identifier}': expected to have at least {min} elements, but it had {element_count}"
1403+
)
13801404

13811405
if len(expected_sort_order) > 0:
13821406
generated_sort_order = [_["element_identifier"] for _ in element_objects]

lib/galaxy/tool_util/xsd/galaxy.xsd

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1996,6 +1996,16 @@ This is available in Galaxy since release 17.05 and was introduced in [pull requ
19961996
<xs:documentation xml:lang="en">Number or datasets for this output. Should be used for outputs with ``discover_datasets``</xs:documentation>
19971997
</xs:annotation>
19981998
</xs:attribute>
1999+
<xs:attribute name="min" type="xs:integer" gxdocs:added="26.0">
2000+
<xs:annotation>
2001+
<xs:documentation xml:lang="en">Minimum number or datasets for this output. Should be used for outputs with ``discover_datasets``</xs:documentation>
2002+
</xs:annotation>
2003+
</xs:attribute>
2004+
<xs:attribute name="max" type="xs:integer" gxdocs:added="26.0">
2005+
<xs:annotation>
2006+
<xs:documentation xml:lang="en">Maximum number or datasets for this output. Should be used for outputs with ``discover_datasets``</xs:documentation>
2007+
</xs:annotation>
2008+
</xs:attribute>
19992009
<xs:attribute name="location" type="xs:anyURI" gxdocs:added="23.1">
20002010
<xs:annotation>
20012011
<xs:documentation xml:lang="en">URL that points to a remote output file that will downloaded and used for output comparison.
@@ -2348,6 +2358,16 @@ This value is the same as the value of the ``name`` attribute of the
23482358
<xs:documentation xml:lang="en">Number of elements in output collection.</xs:documentation>
23492359
</xs:annotation>
23502360
</xs:attribute>
2361+
<xs:attribute name="min" type="xs:integer" gxdocs:added="26.0">
2362+
<xs:annotation>
2363+
<xs:documentation xml:lang="en">Minimum number of elements in output collection.</xs:documentation>
2364+
</xs:annotation>
2365+
</xs:attribute>
2366+
<xs:attribute name="max" type="xs:integer" gxdocs:added="26.0">
2367+
<xs:annotation>
2368+
<xs:documentation xml:lang="en">Maximum number of elements in output collection.</xs:documentation>
2369+
</xs:annotation>
2370+
</xs:attribute>
23512371
</xs:complexType>
23522372
<xs:complexType name="TestAssertions">
23532373
<xs:annotation>

test/functional/tools/expect_num_outputs.xml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ true
157157
</discovered_dataset>
158158
</output>
159159
</test>
160+
<test expect_num_outputs="1">
161+
<param name="produce_discovered_dataset_invisible" value="true" />
162+
<output name="discovered_dataset_invisible" min="3" max="3"/>
163+
</test>
164+
<test expect_num_outputs="1" expect_test_failure="true">
165+
<param name="produce_discovered_dataset_invisible" value="true" />
166+
<output name="discovered_dataset_invisible" min="4"/>
167+
</test>
168+
<test expect_num_outputs="1" expect_test_failure="true">
169+
<param name="produce_discovered_dataset_invisible" value="true" />
170+
<output name="discovered_dataset_invisible" max="2"/>
171+
</test>
160172

161173
<!-- discovered datasets invisible -->
162174
<test expect_num_outputs="1">
@@ -322,5 +334,49 @@ true
322334
</element>
323335
</output_collection>
324336
</test>
337+
338+
<!-- successful test of min/max of output_collection and elements -->
339+
<test expect_num_outputs="1">
340+
<param name="produce_paired_list" value="true" />
341+
<output_collection name="paired_list" type="list:paired" min="2" max="2">
342+
<element name="p1" min="2" max="2">
343+
<element name="forward" min="1" max="1"/>
344+
<element name="reverse" min="1" max="1"/>
345+
</element>
346+
<element name="p2" min="2" max="2">
347+
<element name="forward" min="1" max="1"/>
348+
<element name="reverse" min="1" max="1"/>
349+
</element>
350+
</output_collection>
351+
</test>
352+
353+
<!-- unsuccessful test of min of output_collection -->
354+
<test expect_num_outputs="1" expect_test_failure="true">
355+
<param name="produce_paired_list" value="true" />
356+
<output_collection name="paired_list" type="list:paired" min="3"/>
357+
</test>
358+
<!-- unsuccessful test of max of output_collection -->
359+
<test expect_num_outputs="1" expect_test_failure="true">
360+
<param name="produce_paired_list" value="true" />
361+
<output_collection name="paired_list" type="list:paired" max="1"/>
362+
</test>
363+
364+
<!-- unsuccessful test of min of element -->
365+
<test expect_num_outputs="1" expect_test_failure="true">
366+
<param name="produce_paired_list" value="true" />
367+
<output_collection name="paired_list" type="list:paired" min="2" max="2">
368+
<element name="p1" min="3"/>
369+
<element name="p2" min="3"/>
370+
</output_collection>
371+
</test>
372+
<!-- unsuccessful test of min of element -->
373+
<test expect_num_outputs="1" expect_test_failure="true">
374+
<param name="produce_paired_list" value="true" />
375+
<output_collection name="paired_list" type="list:paired" min="2" max="2">
376+
<element name="p1" max="1"/>
377+
<element name="p2" max="1"/>
378+
</output_collection>
379+
</test>
380+
325381
</tests>
326382
</tool>

test/unit/tool_util/test_tool_linters.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,19 +2098,19 @@ def test_tests_discover_outputs(lint_ctx):
20982098
tool_source = get_xml_tool_source(TESTS_DISCOVER_OUTPUTS)
20992099
run_lint_module(lint_ctx, tests, tool_source)
21002100
assert (
2101-
"Test 3: test output 'data_name' must have a 'count' attribute and/or 'discovered_dataset' children"
2101+
"Test 3: test output 'data_name' must have a 'count/min/max' attribute and/or 'discovered_dataset' children"
21022102
in lint_ctx.error_messages
21032103
)
21042104
assert (
2105-
"Test 3: test collection 'collection_name' must have a 'count' attribute or 'element' children"
2105+
"Test 3: test collection 'collection_name' must have a 'count/min/max' attribute or 'element' children"
21062106
in lint_ctx.error_messages
21072107
)
21082108
assert (
2109-
"Test 3: test collection 'collection_name' must contain nested 'element' tags and/or element children with a 'count' attribute"
2109+
"Test 3: test collection 'collection_name' must contain nested 'element' tags and/or element children with a 'count/min/max' attribute"
21102110
in lint_ctx.error_messages
21112111
)
21122112
assert (
2113-
"Test 5: test collection 'collection_name' must contain nested 'element' tags and/or element children with a 'count' attribute"
2113+
"Test 5: test collection 'collection_name' must contain nested 'element' tags and/or element children with a 'count/min/max' attribute"
21142114
in lint_ctx.error_messages
21152115
)
21162116
assert len(lint_ctx.error_messages) == 4

0 commit comments

Comments
 (0)