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