mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-01 15:37:32 +08:00
f91012244b
The galaxy-tool-shed-schema package is basically pydantic models for the whole tool shed API. It is used by both the backend code for the new tool shed and the testing code that runs on both the old and new tool shed. The creation of galaxy-tool-util-models package is something I've wanted to do for a while - it should solve some TODOs I've left in the base. I think beyond enabling galaxy-tool-shed-schema package - the other nice thing is that galaxy-schema code can now rely on various tool stuff without requiring dependency on any of the parser or runtime code. This should also fix things for the new record types in #19377. I've introduced a type that can be used tools and is included schema for collection creation. The galaxy-tool-util-models packages can now be dependended on by galaxy-schema and galaxy-tool-util and these two packages do not need to depend on each other - either direction of that dependency would make me uncomfortable. xref https://github.com/galaxyproject/galaxy/actions/runs/13704893929/job/38327692654?pr=19377
324 lines
12 KiB
Python
324 lines
12 KiB
Python
import os
|
|
import re
|
|
import sys
|
|
from typing import (
|
|
Any,
|
|
List,
|
|
Optional,
|
|
Tuple,
|
|
)
|
|
|
|
import pytest
|
|
|
|
from galaxy.tool_util.model_factory import parse_tool
|
|
from galaxy.tool_util.parameters import (
|
|
DataCollectionRequest,
|
|
DataRequestHda,
|
|
encode_test,
|
|
input_models_for_tool_source,
|
|
)
|
|
from galaxy.tool_util.parameters.case import (
|
|
test_case_state as case_state,
|
|
TestCaseStateAndWarnings,
|
|
TestCaseStateValidationResult,
|
|
validate_test_cases_for_tool_source,
|
|
)
|
|
from galaxy.tool_util.parser.factory import get_tool_source
|
|
from galaxy.tool_util.parser.interface import (
|
|
ToolSource,
|
|
ToolSourceTest,
|
|
)
|
|
from galaxy.tool_util.unittest_utils import functional_test_tool_directory
|
|
from galaxy.tool_util.verify.parse import parse_tool_test_descriptions
|
|
from galaxy.tool_util_models.tool_source import (
|
|
JsonTestCollectionDefDict,
|
|
JsonTestDatasetDefDict,
|
|
)
|
|
from .util import dict_verify_each
|
|
|
|
# legacy tools allows specifying parameter and repeat parameters without
|
|
# qualification. This was problematic and could result in ambigious specifications.
|
|
TOOLS_THAT_USE_UNQUALIFIED_PARAMETER_ACCESS = [
|
|
"boolean_conditional.xml",
|
|
"simple_constructs.xml",
|
|
"section.xml",
|
|
"collection_paired_conditional_structured_like.xml",
|
|
"output_action_change_format.xml",
|
|
"top_level_data.xml",
|
|
"disambiguate_cond.xml",
|
|
"multi_repeats.xml",
|
|
"implicit_default_conds.xml",
|
|
"min_repeat.xml",
|
|
]
|
|
|
|
# tools that use truevalue/falsevalue in parameter setting, I think we're going to
|
|
# forbid this for a future tool profile version. Potential ambigouity could result.
|
|
TOOLS_THAT_USE_TRUE_FALSE_VALUE_BOOLEAN_SPECIFICATION = [
|
|
"inputs_as_json_profile.xml",
|
|
"inputs_as_json_with_paths.xml",
|
|
"inputs_as_json.xml",
|
|
]
|
|
|
|
TOOLS_THAT_USE_SELECT_BY_VALUE = [
|
|
"multi_select.xml",
|
|
]
|
|
|
|
# Figure out the problem and resolve.
|
|
TOOLS_THAT_ARE_OUTSTANDING_ISSUES = [
|
|
"gx_conditional_boolean_optional.xml",
|
|
"gx_conditional_boolean_discriminate_on_string_value.xml",
|
|
]
|
|
|
|
TEST_TOOL_THAT_DO_NOT_VALIDATE = (
|
|
TOOLS_THAT_USE_UNQUALIFIED_PARAMETER_ACCESS
|
|
+ TOOLS_THAT_USE_TRUE_FALSE_VALUE_BOOLEAN_SPECIFICATION
|
|
+ TOOLS_THAT_USE_SELECT_BY_VALUE
|
|
+ TOOLS_THAT_ARE_OUTSTANDING_ISSUES
|
|
+ [
|
|
# will never handle upload_dataset
|
|
"upload.xml",
|
|
]
|
|
)
|
|
|
|
MOCK_ID = "thisisafakeid"
|
|
|
|
|
|
if sys.version_info < (3, 8): # noqa: UP036
|
|
pytest.skip(reason="Pydantic tool parameter models require python3.8 or higher", allow_module_level=True)
|
|
|
|
|
|
def test_parameter_test_cases_validate():
|
|
validation_result = validate_test_cases_for("column_param")
|
|
assert len(validation_result[0].warnings) == 0
|
|
assert len(validation_result[1].warnings) == 0
|
|
assert len(validation_result[2].warnings) == 1
|
|
|
|
validation_result = validate_test_cases_for("column_param", use_latest_profile=True)
|
|
assert validation_result[2].validation_error
|
|
|
|
|
|
def test_legacy_features_fail_validation_with_24_2(tmp_path):
|
|
for filename in TOOLS_THAT_USE_UNQUALIFIED_PARAMETER_ACCESS + TOOLS_THAT_USE_TRUE_FALSE_VALUE_BOOLEAN_SPECIFICATION:
|
|
_assert_tool_test_parsing_only_fails_with_newer_profile(tmp_path, filename, index=None)
|
|
|
|
# column parameters need to be indexes
|
|
_assert_tool_test_parsing_only_fails_with_newer_profile(tmp_path, "column_param.xml", index=2)
|
|
|
|
# selection by value only
|
|
_assert_tool_test_parsing_only_fails_with_newer_profile(tmp_path, "multi_select.xml", index=1)
|
|
|
|
|
|
def _assert_tool_test_parsing_only_fails_with_newer_profile(tmp_path, filename: str, index: Optional[int] = 0):
|
|
test_tool_directory = functional_test_tool_directory()
|
|
original_path = os.path.join(test_tool_directory, filename)
|
|
new_path = tmp_path / filename
|
|
with open(original_path) as rf:
|
|
tool_contents = rf.read()
|
|
tool_contents = re.sub(r'profile="[\d\.]*"', r"", tool_contents)
|
|
new_profile_contents = tool_contents.replace("<tool ", '<tool profile="24.2" ', 1)
|
|
with open(new_path, "w") as wf:
|
|
wf.write(new_profile_contents)
|
|
test_cases = list(parse_tool_test_descriptions(get_tool_source(original_path)))
|
|
if index is not None:
|
|
assert test_cases[index].to_dict()["error"] is False
|
|
else:
|
|
# just make sure there is at least one failure...
|
|
assert not any(c.to_dict()["error"] is True for c in test_cases)
|
|
|
|
test_cases = list(parse_tool_test_descriptions(get_tool_source(new_path)))
|
|
if index is not None:
|
|
assert (
|
|
test_cases[index].to_dict()["error"] is True
|
|
), f"expected {filename} to have validation failure preventing loading of tools"
|
|
else:
|
|
assert any(c.to_dict()["error"] is True for c in test_cases)
|
|
|
|
|
|
def test_validate_framework_test_tools():
|
|
test_tool_directory = functional_test_tool_directory()
|
|
parameter_tool_directory = os.path.join(test_tool_directory, "parameters")
|
|
for test_directory in [test_tool_directory, parameter_tool_directory]:
|
|
for tool_name in os.listdir(test_directory):
|
|
if tool_name in TEST_TOOL_THAT_DO_NOT_VALIDATE:
|
|
continue
|
|
if tool_name.endswith("_conf.xml") or tool_name == "macros.xml":
|
|
# tool conf (toolbox) files or sample datatypes
|
|
continue
|
|
tool_path = os.path.join(test_directory, tool_name)
|
|
if not tool_path.endswith(".xml") or os.path.isdir(tool_path):
|
|
continue
|
|
|
|
try:
|
|
_validate_path(tool_path)
|
|
except Exception as e:
|
|
raise Exception(f"Failed to validate {tool_path}: {str(e)}")
|
|
|
|
|
|
def test_test_case_state_conversion():
|
|
tool_source = tool_source_for("collection_nested_test")
|
|
test_cases: List[ToolSourceTest] = tool_source.parse_tests_to_dict()["tests"]
|
|
state = case_state_for(tool_source, test_cases[0])
|
|
expectations: List[Tuple[List[Any], Optional[Any]]]
|
|
expectations = [
|
|
(["f1", "collection_type"], "list:paired"),
|
|
(["f1", "class"], "Collection"),
|
|
(["f1", "elements", 0, "class"], "Collection"),
|
|
(["f1", "elements", 0, "collection_type"], "paired"),
|
|
(["f1", "elements", 0, "elements", 0, "class"], "File"),
|
|
(["f1", "elements", 0, "elements", 0, "path"], "simple_line.txt"),
|
|
(["f1", "elements", 0, "elements", 0, "identifier"], "forward"),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
tool_source = tool_source_for("dbkey_filter_input")
|
|
test_cases = tool_source.parse_tests_to_dict()["tests"]
|
|
state = case_state_for(tool_source, test_cases[0])
|
|
expectations = [
|
|
(["inputs", "class"], "File"),
|
|
(["inputs", "dbkey"], "hg19"),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
tool_source = tool_source_for("discover_metadata_files")
|
|
test_cases = tool_source.parse_tests_to_dict()["tests"]
|
|
state = case_state_for(tool_source, test_cases[0])
|
|
expectations = [
|
|
(["input_bam", "class"], "File"),
|
|
(["input_bam", "filetype"], "bam"),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
tool_source = tool_source_for("remote_test_data_location")
|
|
test_cases = tool_source.parse_tests_to_dict()["tests"]
|
|
state = case_state_for(tool_source, test_cases[0])
|
|
expectations = [
|
|
(["input", "class"], "File"),
|
|
(
|
|
["input", "location"],
|
|
"https://raw.githubusercontent.com/galaxyproject/planemo/7be1bf5b3971a43eaa73f483125bfb8cabf1c440/tests/data/hello.txt",
|
|
),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
tool_source = tool_source_for("composite")
|
|
test_cases = tool_source.parse_tests_to_dict()["tests"]
|
|
state = case_state_for(tool_source, test_cases[0])
|
|
expectations = [
|
|
(["input", "class"], "File"),
|
|
(["input", "filetype"], "velvet"),
|
|
(["input", "composite_data", 0], "velveth_test1/Sequences"),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
tool_source = tool_source_for("parameters/gx_group_tag")
|
|
test_cases = tool_source.parse_tests_to_dict()["tests"]
|
|
state = case_state_for(tool_source, test_cases[0])
|
|
expectations = [
|
|
(["ref_parameter", "class"], "Collection"),
|
|
(["ref_parameter", "collection_type"], "paired"),
|
|
(["ref_parameter", "elements", 0, "identifier"], "forward"),
|
|
(["ref_parameter", "elements", 0, "tags", 0], "group:type:single"),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
index = 2
|
|
tool_source = tool_source_for("filter_param_value_ref_attribute")
|
|
test_cases = tool_source.parse_tests_to_dict()["tests"]
|
|
state = case_state_for(tool_source, test_cases[index])
|
|
expectations = [
|
|
(["data_mult", 0, "path"], "1.bed"),
|
|
(["data_mult", 0, "dbkey"], "hg19"),
|
|
(["data_mult", 1, "path"], "2.bed"),
|
|
(["data_mult", 0, "dbkey"], "hg19"),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
index = 1
|
|
tool_source = tool_source_for("expression_pick_larger_file")
|
|
test_cases = tool_source.parse_tests_to_dict()["tests"]
|
|
state = case_state_for(tool_source, test_cases[index])
|
|
expectations = [
|
|
(["input1", "path"], "simple_line_alternative.txt"),
|
|
(["input2"], None),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
index = 2
|
|
state = case_state_for(tool_source, test_cases[index])
|
|
expectations = [
|
|
(["input1"], None),
|
|
(["input2", "path"], "simple_line.txt"),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
index = 0
|
|
tool_source = tool_source_for("composite_shapefile")
|
|
test_cases = tool_source.parse_tests_to_dict()["tests"]
|
|
state = case_state_for(tool_source, test_cases[index])
|
|
expectations = [
|
|
(["input", "filetype"], "shp"),
|
|
(["input", "composite_data", 0], "shapefile/shapefile.shp"),
|
|
]
|
|
dict_verify_each(state.tool_state.input_state, expectations)
|
|
|
|
|
|
def test_convert_to_requests():
|
|
tools = [
|
|
"parameters/gx_drill_down_recurse_multiple",
|
|
"parameters/gx_conditional_select",
|
|
"expression_pick_larger_file",
|
|
"identifier_in_conditional",
|
|
"column_param_list",
|
|
"composite_shapefile",
|
|
]
|
|
for tool_path in tools:
|
|
tool_source = tool_source_for(tool_path)
|
|
parameters = input_models_for_tool_source(tool_source)
|
|
parsed_tool = parse_tool(tool_source)
|
|
profile = tool_source.parse_profile()
|
|
test_cases: List[ToolSourceTest] = tool_source.parse_tests_to_dict()["tests"]
|
|
|
|
def mock_adapt_datasets(input: JsonTestDatasetDefDict) -> DataRequestHda:
|
|
return DataRequestHda(src="hda", id=MOCK_ID)
|
|
|
|
def mock_adapt_collections(input: JsonTestCollectionDefDict) -> DataCollectionRequest:
|
|
return DataCollectionRequest(src="hdca", id=MOCK_ID)
|
|
|
|
for test_case in test_cases:
|
|
if test_case.get("expect_failure"):
|
|
continue
|
|
test_case_state_and_warnings = case_state(test_case, parsed_tool.inputs, profile)
|
|
test_case_state = test_case_state_and_warnings.tool_state
|
|
|
|
encode_test(test_case_state, parameters, mock_adapt_datasets, mock_adapt_collections)
|
|
|
|
|
|
def _validate_path(tool_path: str):
|
|
tool_source = get_tool_source(tool_path)
|
|
parsed_tool = parse_tool(tool_source)
|
|
profile = tool_source.parse_profile()
|
|
test_cases: List[ToolSourceTest] = tool_source.parse_tests_to_dict()["tests"]
|
|
for test_case in test_cases:
|
|
if test_case.get("expect_failure"):
|
|
continue
|
|
test_case_state_and_warnings = case_state(test_case, parsed_tool.inputs, profile)
|
|
tool_state = test_case_state_and_warnings.tool_state
|
|
assert tool_state.state_representation == "test_case_xml"
|
|
|
|
|
|
def validate_test_cases_for(tool_name: str, **kwd) -> List[TestCaseStateValidationResult]:
|
|
return validate_test_cases_for_tool_source(tool_source_for(tool_name), **kwd)
|
|
|
|
|
|
def case_state_for(tool_source: ToolSource, test_case: ToolSourceTest) -> TestCaseStateAndWarnings:
|
|
parsed_tool = parse_tool(tool_source)
|
|
profile = tool_source.parse_profile()
|
|
return case_state(test_case, parsed_tool.inputs, profile)
|
|
|
|
|
|
def tool_source_for(tool_name: str) -> ToolSource:
|
|
test_tool_directory = functional_test_tool_directory()
|
|
tool_path = os.path.join(test_tool_directory, f"{tool_name}.xml")
|
|
tool_source = get_tool_source(tool_path)
|
|
return tool_source
|