import os import os.path import shutil import tempfile from math import isinf from typing import ( Optional, Sequence, Type, TypeVar, ) from galaxy.tool_util.parser.factory import get_tool_source from galaxy.tool_util.parser.output_objects import from_tool_source from galaxy.tool_util.unittest_utils import functional_test_tool_path from galaxy.tool_util_models.tool_outputs import ( ToolOutput, ToolOutputCollection, ToolOutputDataset, ) from galaxy.util import galaxy_directory from galaxy.util.resources import ( as_file, resource_path, ) from galaxy.util.unittest import TestCase TOOL_XML_1 = """ """ TOOL_WITH_TOKEN = r""" @NESTED_TOKEN@ """ TOOL_WTIH_TOKEN_FROM_MACRO_FILE = r""" macros.xml """ MACRO_CONTENTS = r""" cat """ TOOL_WITH_RECURSIVE_TOKEN = r""" @NESTED_TOKEN@ """ TOOL_YAML_1 = """ name: "Bowtie Mapper" class: GalaxyTool id: bowtie version: 1.0.2 description: "The Bowtie Mapper" xrefs: - type: bio.tools value: bwa command: "bowtie_wrapper.pl --map-the-stuff" interpreter: "perl" runtime_version: command: "bowtie --version" requirements: - type: package name: bwa version: 1.0.1 - type: resource cores_min: 1 - type: resource cuda_version_min: 10.2 - type: resource cuda_compute_capability: 6.1 - type: resource gpu_memory_min: 4042 - type: resource cuda_device_count_min: 1 - type: resource cuda_device_count_max: 2 - type: resource shm_size: 67108864 - type: resource timelimit: 60 containers: - type: docker identifier: "awesome/bowtie" credentials: - name: Apollo version: gmod.org/apollo secrets: - name: username label: Your Apollo username description: Username for Apollo inject_as_env: apollo_user optional: true - name: password label: Your Apollo password description: Password for Apollo inject_as_env: apollo_pass optional: true variables: - name: server label: Your Apollo server description: URL of your Apollo server inject_as_env: apollo_url optional: true outputs: out1: format: bam from_work_dir: out1.bam inputs: - name: input1 type: integer min: 7 max: 8 - name: moo label: cow type: repeat blocks: - name: nestinput type: data - name: nestsample type: text help: | This is HELP TEXT2!!! tests: - inputs: input1: 7 outputs: out1: moo.txt - inputs: input1: 8 outputs: out1: lines_diff: 4 compare: sim_size """ TOOL_EXPRESSION_XML_1 = """ Parse Int {return {'output': parseInt($job.input1)};} Parse an integer from text. """ TOOL_EXPRESSION_YAML_1 = """ class: GalaxyExpressionTool name: "parse_int" id: parse_int version: 1.0.2 expression: "{return {'output': parseInt($job.input1)};}" inputs: - name: input1 label: Text to parse type: text outputs: out1: type: integer from: "#output" """ def get_test_tool_source(source_file_name=None, source_contents=None, macro_contents=None, temp_directory=None): source_directory = temp_directory or tempfile.mkdtemp() macro_paths = [] if not os.path.isabs(source_file_name): path = os.path.join(source_directory, source_file_name) with open(path, "w") as out: out.write(source_contents) if macro_contents: macro_path = os.path.join(source_directory, "macros.xml") with open(macro_path, "w") as out: out.write(macro_contents) macro_paths = [macro_path] else: path = source_file_name tool_source = get_tool_source(path, macro_paths=macro_paths) if temp_directory is None: shutil.rmtree(source_directory) return tool_source class BaseLoaderTestCase(TestCase): source_file_name: Optional[str] = None source_contents: Optional[str] = None def setUp(self): self.temp_directory = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.temp_directory) @property def _tool_source(self): return self._get_tool_source() @property def _output_models(self) -> Sequence[ToolOutput]: return from_tool_source(self._tool_source) def _get_tool_source(self, source_file_name=None, source_contents=None, macro_contents=None): if source_file_name is None: source_file_name = self._get_source_file_name() if source_contents is None: source_contents = self.source_contents return get_test_tool_source( source_file_name, source_contents, macro_contents, self.temp_directory, ) def _get_source_file_name(self) -> str: assert self.source_file_name return self.source_file_name class TestXmlExpressionLoader(BaseLoaderTestCase): source_file_name = "expression.xml" source_contents = TOOL_EXPRESSION_XML_1 def test_expression(self): assert self._tool_source.parse_expression().strip() == "{return {'output': parseInt($job.input1)};}" def test_tool_type(self): assert self._tool_source.parse_tool_type() == "expression" class TestXmlLoader(BaseLoaderTestCase): source_file_name = "bwa.xml" source_contents = TOOL_XML_1 def test_tool_source_to_string(self): # Previously this threw an Exception - test for regression. str(self._tool_source) def test_version(self): assert self._tool_source.parse_version() == "1.0.1" def test_id(self): assert self._tool_source.parse_id() == "bwa" def test_module_and_type(self): assert self._tool_source.parse_tool_module() is None assert self._tool_source.parse_tool_type() is None def test_name(self): assert self._tool_source.parse_name() == "BWA Mapper" def test_display_interface(self): assert self._tool_source.parse_display_interface(False) def test_require_login(self): assert self._tool_source.parse_require_login(False) def test_parse_request_param_translation_elem(self): assert self._tool_source.parse_request_param_translation_elem() is None def test_command_parsing(self): assert self._tool_source.parse_command() == "bwa.py --arg1=42" assert self._tool_source.parse_interpreter() == "python" def test_descripting_parsing(self): assert self._tool_source.parse_description() == "The BWA Mapper" def test_version_command(self): assert self._tool_source.parse_version_command() == "bwa.py --version" assert self._tool_source.parse_version_command_interpreter() == "python" def test_parallelism(self): parallelism_info = self._tool_source.parse_parallelism() assert parallelism_info.method == "multi" assert parallelism_info.attributes["split_inputs"] == "input1" def test_hidden(self): assert self._tool_source.parse_hidden() def test_action(self): assert self._tool_source.parse_action_module() is None def test_requirements(self): requirements, containers, resource_requirements, _, credentials = self._tool_source.parse_requirements() assert requirements[0].type == "package" assert list(containers)[0].identifier == "mycool/bwa" assert resource_requirements[0].resource_type == "cores_min" assert resource_requirements[1].resource_type == "cuda_version_min" assert resource_requirements[2].resource_type == "cuda_compute_capability" assert resource_requirements[3].resource_type == "gpu_memory_min" assert resource_requirements[4].resource_type == "cuda_device_count_min" assert resource_requirements[5].resource_type == "cuda_device_count_max" assert resource_requirements[6].resource_type == "shm_size" assert resource_requirements[7].resource_type == "timelimit" assert not resource_requirements[0].runtime_required assert credentials[0].name == "Apollo" assert credentials[0].version == "gmod.org/apollo" assert len(credentials[0].secrets) == 2 assert len(credentials[0].variables) == 1 def test_outputs(self): outputs, output_collections = self._tool_source.parse_outputs(object()) assert len(outputs) == 1, outputs assert len(output_collections) == 0 def test_stdio(self): exit, regexes = self._tool_source.parse_stdio() assert len(exit) == 1 assert len(regexes) == 0 assert exit[0].range_start == 1 assert isinf(exit[0].range_end) def test_help(self): help_text = self._tool_source.parse_help().content assert help_text.strip() == "This is HELP TEXT1!!!" def test_tests(self): tests_dict = self._tool_source.parse_tests_to_dict() tests = tests_dict["tests"] assert len(tests) == 2 test_dict = tests[0] inputs = test_dict["inputs"] assert len(inputs) == 1, test_dict input1 = inputs[0] assert input1["name"] == "foo", input1 assert input1["value"] == "5" outputs = test_dict["outputs"] assert len(outputs) == 1 output1 = outputs[0] assert output1["name"] == "out1" assert output1["value"] == "moo.txt" attributes1 = output1["attributes"] assert attributes1["compare"] == "diff" assert attributes1["lines_diff"] == 0 test2 = tests[1] outputs = test2["outputs"] assert len(outputs) == 1 output2 = outputs[0] assert output2["name"] == "out1" assert output2["value"] is None attributes1 = output2["attributes"] assert attributes1["compare"] == "sim_size" assert attributes1["lines_diff"] == 4 def test_output_models(self): output_models = self._output_models assert len(output_models) == 1 output_model = output_models[0] assert output_model.name == "out1" assert not output_model.hidden assert output_model.label is None output_dataset_model = assert_output_model_of_type(output_model, ToolOutputDataset) assert output_dataset_model.metadata_source is None def test_xrefs(self): xrefs = self._tool_source.parse_xrefs() assert xrefs == [{"value": "bwa", "type": "bio.tools"}] def test_exit_code(self): tool_source = self._get_tool_source(source_contents=""" ls """) exit, regexes = tool_source.parse_stdio() assert len(exit) == 2, exit assert len(regexes) == 0, regexes tool_source = self._get_tool_source(source_contents=""" ls """) exit, regexes = tool_source.parse_stdio() assert len(exit) == 2, exit # error:, exception: various memory exception... assert len(regexes) > 2, regexes def test_sanitize_option(self): assert self._tool_source.parse_sanitize() is True def test_refresh_option(self): assert self._tool_source.parse_refresh() is False def test_nested_token(self): tool_source = self._get_tool_source(source_contents=TOOL_WITH_TOKEN) command = tool_source.parse_command() assert command assert "@" not in command def test_token_significant_whitespace(self): tool_source = self._get_tool_source( source_contents=TOOL_WTIH_TOKEN_FROM_MACRO_FILE, macro_contents=MACRO_CONTENTS ) command = tool_source.parse_command() assert command == "cat " assert "@" not in command def test_recursive_token(self): with self.assertRaisesRegex(Exception, "^Token '@NESTED_TOKEN@' cannot contain itself$"): self._get_tool_source(source_contents=TOOL_WITH_RECURSIVE_TOKEN) def test_creator(self): creators = self._tool_source.parse_creator() assert len(creators) == 2 creator1 = creators[0] assert creator1["class"] == "Person" assert creator1["identifier"] == "http://orcid.org/0000-0002-3079-6586" creator2 = creators[1] assert creator2["class"] == "Organization" assert creator2["name"] == "Galaxy IUC" class TestYamlLoader(BaseLoaderTestCase): source_file_name = "bwa.yml" source_contents = TOOL_YAML_1 def test_version(self): assert self._tool_source.parse_version() == "1.0.2" def test_id(self): assert self._tool_source.parse_id() == "bowtie" def test_module_and_type(self): # These just rely on defaults assert self._tool_source.parse_tool_module() is None assert self._tool_source.parse_tool_type() is None def test_name(self): assert self._tool_source.parse_name() == "Bowtie Mapper" def test_display_interface(self): assert not self._tool_source.parse_display_interface(False) assert self._tool_source.parse_display_interface(True) def test_require_login(self): assert not self._tool_source.parse_require_login(False) def test_parse_request_param_translation_elem(self): assert self._tool_source.parse_request_param_translation_elem() is None def test_command_parsing(self): assert self._tool_source.parse_command() == "bowtie_wrapper.pl --map-the-stuff" assert self._tool_source.parse_interpreter() == "perl" def test_parse_redirect_url_params_elem(self): assert self._tool_source.parse_redirect_url_params_elem() is None def test_descripting_parsing(self): assert self._tool_source.parse_description() == "The Bowtie Mapper" def test_version_command(self): assert self._tool_source.parse_version_command() == "bowtie --version" assert self._tool_source.parse_version_command_interpreter() is None def test_parallelism(self): assert self._tool_source.parse_parallelism() is None def test_hidden(self): assert not self._tool_source.parse_hidden() def test_action(self): assert self._tool_source.parse_action_module() is None def test_requirements(self): software_requirements, containers, resource_requirements, _, credentials = ( self._tool_source.parse_requirements() ) assert software_requirements.to_dict() == [{"name": "bwa", "type": "package", "version": "1.0.1", "specs": []}] assert len(containers) == 1 assert containers[0].to_dict() == { "identifier": "awesome/bowtie", "type": "docker", "resolve_dependencies": False, "shell": "/bin/sh", } assert len(resource_requirements) == 8 assert resource_requirements[0].to_dict() == {"resource_type": "cores_min", "value_or_expression": 1} assert resource_requirements[1].to_dict() == {"resource_type": "cuda_version_min", "value_or_expression": 10.2} assert resource_requirements[2].to_dict() == { "resource_type": "cuda_compute_capability", "value_or_expression": 6.1, } assert resource_requirements[3].to_dict() == {"resource_type": "gpu_memory_min", "value_or_expression": 4042} assert resource_requirements[4].to_dict() == { "resource_type": "cuda_device_count_min", "value_or_expression": 1, } assert resource_requirements[5].to_dict() == { "resource_type": "cuda_device_count_max", "value_or_expression": 2, } assert resource_requirements[6].to_dict() == { "resource_type": "shm_size", "value_or_expression": 67108864, } assert resource_requirements[7].to_dict() == { "resource_type": "timelimit", "value_or_expression": 60, } assert len(credentials) == 1 assert len(credentials[0].secrets) == 2 assert len(credentials[0].variables) == 1 def test_outputs(self): outputs, output_collections = self._tool_source.parse_outputs(object()) assert len(outputs) == 1 assert len(output_collections) == 0 def test_stdio(self): exit, regexes = self._tool_source.parse_stdio() assert len(exit) == 2 assert isinf(exit[0].range_start) assert exit[0].range_start == float("-inf") assert exit[1].range_start == 1 assert isinf(exit[1].range_end) def test_help(self): help_text = self._tool_source.parse_help().content assert help_text.strip() == "This is HELP TEXT2!!!" def test_inputs(self): input_pages = self._tool_source.parse_input_pages() assert input_pages.inputs_defined page_sources = input_pages.page_sources assert len(page_sources) == 1 page_source = page_sources[0] input_sources = page_source.parse_input_sources() assert len(input_sources) == 2 def test_tests(self): tests_dict = self._tool_source.parse_tests_to_dict() tests = tests_dict["tests"] assert len(tests) == 2 test_dict = tests[0] inputs = test_dict["inputs"] assert len(inputs) == 1 input1 = inputs[0] assert input1["name"] == "input1" assert input1["value"] == 7 outputs = test_dict["outputs"] assert len(outputs) == 1 output1 = outputs[0] assert output1["name"] == "out1" assert output1["value"] == "moo.txt" attributes1 = output1["attributes"] assert attributes1["compare"] == "diff" assert attributes1["lines_diff"] == 0 test2 = tests[1] outputs = test2["outputs"] assert len(outputs) == 1 output2 = outputs[0] assert output2["name"] == "out1" assert output2["value"] is None attributes1 = output2["attributes"] assert attributes1["compare"] == "sim_size" assert attributes1["lines_diff"] == 4 def test_xrefs(self): xrefs = self._tool_source.parse_xrefs() assert xrefs == [{"value": "bwa", "type": "bio.tools"}] def test_sanitize(self): assert self._tool_source.parse_sanitize() is True class TestDataSourceLoader(BaseLoaderTestCase): source_file_name = "ds.xml" source_contents = """ server data_source.py $output $__app__.config.output_size_limit go to yeastMine server $GALAXY_URL cow """ def test_sanitize_option(self): assert self._tool_source.parse_sanitize() is False def test_refresh_option(self): assert self._tool_source.parse_refresh() is True def test_tool_type(self): assert self._tool_source.parse_tool_type() == "data_source" def test_parse_request_param_translation_elem(self): assert self._tool_source.parse_request_param_translation_elem() is not None def test_redirect_url_params_elem(self): assert self._tool_source.parse_redirect_url_params_elem() is not None def test_parallelism(self): assert self._tool_source.parse_parallelism() is None def test_hidden(self): assert not self._tool_source.parse_hidden() class TestApplyRulesToolLoader(BaseLoaderTestCase): source_file_name = os.path.join(galaxy_directory(), "lib/galaxy/tools/apply_rules.xml") source_contents = None def test_tool_type(self): tool_module = self._tool_source.parse_tool_module() assert tool_module[0] == "galaxy.tools" assert tool_module[1] == "ApplyRulesTool" assert self._tool_source.parse_tool_type() == "apply_rules_to_collection" def test_outputs(self): outputs, output_collections = self._tool_source.parse_outputs(object()) assert len(outputs) == 1 assert len(output_collections) == 1 def test_output_models(self): output_models = self._output_models assert len(output_models) == 1 output_model = output_models[0] assert output_model.name == "output" assert not output_model.hidden assert output_model.label == "${input.name} (re-organized)" output_collection_model = assert_output_model_of_type(output_model, ToolOutputCollection) structure = output_collection_model.structure assert structure.collection_type_from_rules == "rules" class TestBuildListToolLoader(BaseLoaderTestCase): source_file_name = os.path.join(galaxy_directory(), "lib/galaxy/tools/build_list.xml") source_contents = None def test_tool_type(self): tool_module = self._tool_source.parse_tool_module() assert tool_module[0] == "galaxy.tools" assert tool_module[1] == "BuildListCollectionTool" class FunctionalTestToolTestCase(BaseLoaderTestCase): test_path: str source_contents: None def _get_source_file_name(self) -> str: return functional_test_tool_path(self.test_path) class TestExpressionTestToolLoader(FunctionalTestToolTestCase): test_path = "expression_null_handling_boolean.xml" def test_test(self): test_dicts = self._tool_source.parse_tests_to_dict()["tests"] assert len(test_dicts) == 3 test_dict_0 = test_dicts[0] assert "outputs" in test_dict_0, test_dict_0 outputs = test_dict_0["outputs"] output0 = outputs[0] assert "object" in output0["attributes"] assert output0["attributes"]["object"] is True test_dict_1 = test_dicts[1] assert "outputs" in test_dict_1, test_dict_1 outputs = test_dict_1["outputs"] output0 = outputs[0] assert "object" in output0["attributes"] assert output0["attributes"]["object"] is False test_dict_2 = test_dicts[2] assert "outputs" in test_dict_2, test_dict_2 outputs = test_dict_2["outputs"] output0 = outputs[0] assert "object" in output0["attributes"] assert output0["attributes"]["object"] is None def test_output_models(self): output_models = self._output_models assert len(output_models) == 1 output_model = output_models[0] assert output_model.name == "bool_out" assert not output_model.hidden assert output_model.label is None class TestDefaultDataTestToolLoader(FunctionalTestToolTestCase): test_path = "for_workflows/cat_default.xml" def test_input_parsing(self): input_pages = self._tool_source.parse_input_pages() assert input_pages.inputs_defined page_sources = input_pages.page_sources assert len(page_sources) == 1 page_source = page_sources[0] input_sources = page_source.parse_input_sources() assert len(input_sources) == 1 data_input = input_sources[0] default_dict = data_input.parse_default() assert default_dict assert default_dict["location"] == "https://raw.githubusercontent.com/galaxyproject/galaxy/dev/test-data/1.bed" class TestDefaultCollectionDataTestToolLoader(FunctionalTestToolTestCase): test_path = "collection_paired_default.xml" def test_input_parsing(self): input_pages = self._tool_source.parse_input_pages() assert input_pages.inputs_defined page_sources = input_pages.page_sources assert len(page_sources) == 1 page_source = page_sources[0] input_sources = page_source.parse_input_sources() assert len(input_sources) == 1 data_input = input_sources[0] default_dict = data_input.parse_default() assert default_dict assert default_dict["collection_type"] == "paired" elements = default_dict["elements"] assert len(elements) == 2 element0 = elements[0] assert element0["identifier"] == "forward" assert element0["location"] == "https://raw.githubusercontent.com/galaxyproject/galaxy/dev/test-data/1.bed" element1 = elements[1] assert element1["identifier"] == "reverse" assert element1["location"] == "https://raw.githubusercontent.com/galaxyproject/galaxy/dev/test-data/1.fasta" class TestDefaultNestedCollectionDataTestToolLoader(FunctionalTestToolTestCase): test_path = "collection_nested_default.xml" def test_input_parsing(self): input_pages = self._tool_source.parse_input_pages() assert input_pages.inputs_defined page_sources = input_pages.page_sources assert len(page_sources) == 1 page_source = page_sources[0] input_sources = page_source.parse_input_sources() assert len(input_sources) == 1 data_input = input_sources[0] default_dict = data_input.parse_default() assert default_dict assert default_dict["collection_type"] == "list:paired" elements = default_dict["elements"] assert len(elements) == 1 element0 = elements[0] assert element0["identifier"] == "i1" elements0 = element0["elements"] assert len(elements0) == 2 elements00 = elements0[0] assert elements00["identifier"] == "forward" elements01 = elements0[1] assert elements01["identifier"] == "reverse" class TestExpressionOutputDataToolLoader(FunctionalTestToolTestCase): test_path = "expression_pick_larger_file.xml" def test_output_parsing(self): outputs, _ = self._tool_source.parse_outputs(None) assert "larger_file" in outputs tool_output = outputs["larger_file"] assert tool_output.format == "data" assert tool_output.from_expression == "output" class TestSpecialToolLoader(BaseLoaderTestCase): source_file_name = os.path.join(galaxy_directory(), "lib/galaxy/tools/imp_exp/exp_history_to_archive.xml") source_contents = None def test_tool_type(self): tool_module = self._tool_source.parse_tool_module() # Probably we don't parse_tool_module any more? - # tool_type seems sufficient. assert tool_module[0] == "galaxy.tools" assert tool_module[1] == "ExportHistoryTool" assert self._tool_source.parse_tool_type() == "export_history" def test_version_command(self): assert self._tool_source.parse_version_command() is None assert self._tool_source.parse_version_command_interpreter() is None def test_action(self): action = self._tool_source.parse_action_module() assert action[0] == "galaxy.tools.actions.history_imp_exp" assert action[1] == "ExportHistoryToolAction" class TestCollection(FunctionalTestToolTestCase): test_path = "collection_two_paired.xml" def test_tests(self): tests_dict = self._tool_source.parse_tests_to_dict() tests = tests_dict["tests"] assert len(tests) == 2 assert len(tests[0]["inputs"]) == 3, tests[0] outputs, output_collections = self._tool_source.parse_outputs(None) assert len(output_collections) == 0 class TestCollectionOutputXml(FunctionalTestToolTestCase): test_path = "collection_creates_pair.xml" def test_tests(self): outputs, output_collections = self._tool_source.parse_outputs(None) assert len(output_collections) == 1 class TestCollectionOutputYaml(FunctionalTestToolTestCase): test_path = "collection_creates_pair_y.yml" def test_tests(self): outputs, output_collections = self._tool_source.parse_outputs(None) assert len(output_collections) == 1 class TestEnvironmentVariables(FunctionalTestToolTestCase): test_path = "environment_variables.xml" def test_tests(self): tests_dict = self._tool_source.parse_tests_to_dict() tests = tests_dict["tests"] assert len(tests) == 1 class TestExpectations(FunctionalTestToolTestCase): test_path = "detect_errors.xml" def test_tests(self): tests_dict = self._tool_source.parse_tests_to_dict() tests = tests_dict["tests"] assert len(tests) == 10 test_0 = tests[0] assert len(test_0["stderr"]) == 1 assert len(test_0["stdout"]) == 2 class TestExpectationsCommandVersion(FunctionalTestToolTestCase): test_path = "job_properties.xml" def test_tests(self): tests_dict = self._tool_source.parse_tests_to_dict() tests = tests_dict["tests"] assert len(tests) > 0 test_0 = tests[0] assert len(test_0["command_version"]) == 1 class TestQcStdio(FunctionalTestToolTestCase): test_path = "qc_stdout.xml" def test_tests(self): exit, regexes = self._tool_source.parse_stdio() assert len(exit) == 2 assert len(regexes) == 2 regex = regexes[0] assert regex.error_level == 1.1 class TestCollectionCatGroupTag(FunctionalTestToolTestCase): test_path = "collection_cat_group_tag.xml" def test_output_models(self): output_models = self._output_models assert len(output_models) == 1 output_model = output_models[0] assert output_model.name == "out_file1" assert not output_model.hidden assert output_model.label is None output_dataset_model = assert_output_model_of_type(output_model, ToolOutputDataset) assert output_dataset_model.metadata_source == "input1" def test_old_invalid_citation_dont_cause_failure_to_load(): with as_file(resource_path(__name__, "invalid_citation.xml")) as tool_path: tool_source = get_tool_source(tool_path) assert tool_source.parse_citations() == [] def test_invalid_citation_not_allowed_in_modern_tools(): with as_file(resource_path(__name__, "invalid_citation_24.2.xml")) as tool_path: tool_source = get_tool_source(tool_path) exc = None try: tool_source.parse_citations() except Exception as e: exc = e assert exc is not None class TestToolProvidedMetadata2(FunctionalTestToolTestCase): test_path = "tool_provided_metadata_2.xml" def test_output_models(self): output_models = self._output_models assert len(output_models) == 1 output_model = output_models[0] assert output_model.name == "sample" assert not output_model.hidden assert output_model.label is None output_dataset_model = assert_output_model_of_type(output_model, ToolOutputDataset) assert output_dataset_model.metadata_source is None discover_datasets = output_dataset_model.discover_datasets or [] assert len(discover_datasets) == 1 discover_datasets_0 = discover_datasets[0] assert discover_datasets_0.discover_via == "pattern" T = TypeVar("T") def assert_output_model_of_type(obj, clazz: Type[T]) -> T: assert isinstance(obj, clazz) return obj