mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-01 05:31:04 +08:00
Merge pull request #23170 from guerler/toolrequests.004
Remove legacy unqualified repeat synthesis from test-case resolution
This commit is contained in:
@@ -324,15 +324,16 @@ def test_case_state(
|
||||
|
||||
for test_input in inputs:
|
||||
input_name = test_input["name"]
|
||||
if input_name not in handled_inputs:
|
||||
if input_name not in handled_inputs and not _input_name_was_handled_by_legacy_fallback(
|
||||
input_name, handled_inputs, profile
|
||||
):
|
||||
unhandled_inputs.append(input_name)
|
||||
|
||||
tool_state = TestCaseToolState(state)
|
||||
if validate:
|
||||
tool_state.validate(tool_parameter_bundle, name=name)
|
||||
for input_name in unhandled_inputs:
|
||||
if not _input_name_was_handled_by_legacy_fallback(input_name, handled_inputs, profile):
|
||||
raise Exception(f"Invalid parameter name found {input_name}")
|
||||
raise Exception(f"Invalid parameter name found {input_name}")
|
||||
return TestCaseStateAndWarnings(tool_state, warnings, unhandled_inputs)
|
||||
|
||||
|
||||
@@ -431,7 +432,8 @@ def _merge_into_state(
|
||||
if input_name not in state_at_level:
|
||||
state_at_level[input_name] = repeat_state_array
|
||||
|
||||
repeat_instance_inputs = _repeat_inputs_to_array(state_path, tool_input.parameters, context.inputs)
|
||||
repeat_instance_inputs = _repeat_inputs_to_array(state_path, context.inputs)
|
||||
supplied_instances = len(repeat_instance_inputs)
|
||||
if tool_input.min is not None:
|
||||
while len(repeat_instance_inputs) < tool_input.min:
|
||||
repeat_instance_inputs.append([])
|
||||
@@ -440,14 +442,18 @@ def _merge_into_state(
|
||||
repeat_state_array.append({})
|
||||
|
||||
repeat_instance_prefix = f"{state_path}_{i}"
|
||||
handled_inputs.update(
|
||||
_merge_level_into_state(
|
||||
tool_input.parameters,
|
||||
context.for_inputs(repeat_instance_inputs[i]),
|
||||
repeat_state_array[i],
|
||||
repeat_instance_prefix,
|
||||
)
|
||||
instance_handled_inputs = _merge_level_into_state(
|
||||
tool_input.parameters,
|
||||
context.for_inputs(repeat_instance_inputs[i]),
|
||||
repeat_state_array[i],
|
||||
repeat_instance_prefix,
|
||||
)
|
||||
# Instances past the ones the test actually supplied exist only to satisfy min.
|
||||
# Nothing resolved into them, so their paths must not count as handling a raw
|
||||
# input - the legacy suffix match would otherwise read queries_0|input2 as
|
||||
# covering a bare input2 that was in fact dropped.
|
||||
if i < supplied_instances:
|
||||
handled_inputs.update(instance_handled_inputs)
|
||||
elif isinstance(tool_input, (SectionParameterModel,)):
|
||||
section_state = state_at_level.get(input_name, {})
|
||||
if input_name not in state_at_level:
|
||||
@@ -504,9 +510,7 @@ def _merge_into_state(
|
||||
return handled_inputs
|
||||
|
||||
|
||||
def _repeat_inputs_to_array(
|
||||
state_path: str, parameters: list[ToolParameterT], inputs: ToolSourceTestInputs
|
||||
) -> list[ToolSourceTestInputs]:
|
||||
def _repeat_inputs_to_array(state_path: str, inputs: ToolSourceTestInputs) -> list[ToolSourceTestInputs]:
|
||||
inputs_as_dict = _inputs_as_dict(inputs)
|
||||
repeat_instance_input_dicts = repeat_inputs_to_array(state_path, inputs_as_dict)
|
||||
if not repeat_instance_input_dicts and "|" in state_path:
|
||||
@@ -520,21 +524,7 @@ def _repeat_inputs_to_array(
|
||||
repeat_instance_input_dicts = repeat_inputs_to_array(candidate_path, inputs_as_dict)
|
||||
if repeat_instance_input_dicts:
|
||||
break
|
||||
repeat_instance_inputs = [list(instance_inputs.values()) for instance_inputs in repeat_instance_input_dicts]
|
||||
if repeat_instance_inputs:
|
||||
return repeat_instance_inputs
|
||||
|
||||
legacy_repeat_inputs: list[ToolSourceTestInputs] = []
|
||||
for parameter in parameters:
|
||||
parameter_name = parameter.name
|
||||
matching_inputs = [input for input in inputs if input["name"] == parameter_name]
|
||||
for i, input in enumerate(matching_inputs):
|
||||
while len(legacy_repeat_inputs) <= i:
|
||||
legacy_repeat_inputs.append([])
|
||||
synthetic_input = cast(ToolSourceTestInput, dict(input))
|
||||
synthetic_input["name"] = f"{state_path}_{i}|{parameter_name}"
|
||||
legacy_repeat_inputs[i].append(synthetic_input)
|
||||
return legacy_repeat_inputs
|
||||
return [list(instance_inputs.values()) for instance_inputs in repeat_instance_input_dicts]
|
||||
|
||||
|
||||
def _select_which_when(
|
||||
|
||||
@@ -80,10 +80,17 @@ def parse_tool_test_descriptions(
|
||||
tool_parameter_bundle = input_models_for_tool_source(tool_source)
|
||||
parameters = tool_parameter_bundle.parameters
|
||||
validated_test_case = case_state(raw_test_dict, parameters, profile, validate=validate_on_load)
|
||||
request_and_schema = TestRequestAndSchema(
|
||||
validated_test_case.tool_state,
|
||||
tool_parameter_bundle or ToolParameterBundleModel(parameters=parameters),
|
||||
)
|
||||
if validated_test_case.unhandled_inputs:
|
||||
# Inputs that map to no parameter (e.g. legacy unqualified repeats) can't be
|
||||
# represented in a modern request; fall back to the legacy API.
|
||||
validation_skipped_reason = (
|
||||
f"could not build request: unhandled inputs {validated_test_case.unhandled_inputs}"
|
||||
)
|
||||
else:
|
||||
request_and_schema = TestRequestAndSchema(
|
||||
validated_test_case.tool_state,
|
||||
tool_parameter_bundle or ToolParameterBundleModel(parameters=parameters),
|
||||
)
|
||||
except Exception as e:
|
||||
if validate_on_load:
|
||||
validation_exception = e
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<tool id="async_min_repeat_unqualified" name="async_min_repeat_unqualified" version="1.0.0" profile="22.01">
|
||||
<description>async regression: unqualified repeat param on a repeat with min set</description>
|
||||
<!--
|
||||
The multi_repeats anti-pattern (a param inside a <repeat> supplied at the top
|
||||
level of the test by its bare name), with one addition: the repeat sets min="1".
|
||||
|
||||
Sync (POST /api/tools, the default GALAXY_TEST_USE_LEGACY_TOOL_API=always): the
|
||||
legacy path maps the bare name into the repeat; job runs.
|
||||
|
||||
Async (GALAXY_TEST_USE_LEGACY_TOOL_API=if_needed): min="1" pads an empty repeat
|
||||
instance, whose visited-but-unresolved path `queries_0|input2` is then read as
|
||||
proof the bare `input2` was handled. unhandled_inputs comes back empty, so no
|
||||
fallback to the legacy API happens and the request is built and submitted as
|
||||
{"queries": [{}]} - the value is silently dropped and the job request fails:
|
||||
queries.0.input2 - Field required
|
||||
|
||||
Drop min="1" and the same tool degrades correctly: the input is reported
|
||||
unhandled, no request is built, and the test falls back to the legacy API.
|
||||
See async_repeat_unqualified_no_min.xml for that control case.
|
||||
-->
|
||||
<command><![CDATA[
|
||||
cat #for $q in $queries# '${q.input2}' #end for# > '$out_file1'
|
||||
]]></command>
|
||||
<inputs>
|
||||
<repeat name="queries" title="Query" min="1">
|
||||
<param name="input2" type="data" format="txt" label="Select" />
|
||||
</repeat>
|
||||
</inputs>
|
||||
<outputs>
|
||||
<data name="out_file1" format="txt" />
|
||||
</outputs>
|
||||
<tests>
|
||||
<test>
|
||||
<param name="input2" value="simple_line.txt" />
|
||||
<output name="out_file1" file="simple_line.txt" />
|
||||
</test>
|
||||
</tests>
|
||||
</tool>
|
||||
@@ -0,0 +1,32 @@
|
||||
<tool id="async_repeat_unqualified_no_min" name="async_repeat_unqualified_no_min" version="1.0.0" profile="22.01">
|
||||
<description>async control: unqualified repeat param, no min on the repeat</description>
|
||||
<!--
|
||||
Control for async_min_repeat_unqualified.xml - byte-for-byte the same tool
|
||||
except the repeat has no min attribute.
|
||||
|
||||
Async (GALAXY_TEST_USE_LEGACY_TOOL_API=if_needed): nothing pads an empty
|
||||
instance, so the bare `input2` is correctly reported in unhandled_inputs, no
|
||||
request is built, and the interactor falls back to the legacy API. Job runs.
|
||||
|
||||
This is the intended post-#23170 behaviour for the whole class of legacy
|
||||
unqualified-repeat tests. It passing while async_min_repeat_unqualified fails
|
||||
is what isolates `min` as the trigger.
|
||||
-->
|
||||
<command><![CDATA[
|
||||
cat #for $q in $queries# '${q.input2}' #end for# > '$out_file1'
|
||||
]]></command>
|
||||
<inputs>
|
||||
<repeat name="queries" title="Query">
|
||||
<param name="input2" type="data" format="txt" label="Select" />
|
||||
</repeat>
|
||||
</inputs>
|
||||
<outputs>
|
||||
<data name="out_file1" format="txt" />
|
||||
</outputs>
|
||||
<tests>
|
||||
<test>
|
||||
<param name="input2" value="simple_line.txt" />
|
||||
<output name="out_file1" file="simple_line.txt" />
|
||||
</test>
|
||||
</tests>
|
||||
</tool>
|
||||
@@ -125,6 +125,8 @@
|
||||
<tool file="async_nested_conditional_shared_discriminator.xml" />
|
||||
<tool file="async_nested_conditional_shared_discriminator_bare.xml" />
|
||||
<tool file="async_posix_regex_validator.xml" />
|
||||
<tool file="async_min_repeat_unqualified.xml" />
|
||||
<tool file="async_repeat_unqualified_no_min.xml" />
|
||||
<tool file="output_empty_work_dir.xml" />
|
||||
<tool file="output_filter_with_input.xml" />
|
||||
<tool file="output_filter_with_input_optional.xml" />
|
||||
|
||||
@@ -47,6 +47,8 @@ TOOLS_THAT_USE_UNQUALIFIED_PARAMETER_ACCESS = [
|
||||
"disambiguate_cond.xml",
|
||||
"multi_repeats.xml",
|
||||
"implicit_default_conds.xml",
|
||||
"async_min_repeat_unqualified.xml",
|
||||
"async_repeat_unqualified_no_min.xml",
|
||||
]
|
||||
|
||||
TOOLS_THAT_USE_SELECT_BY_VALUE = [
|
||||
@@ -420,19 +422,22 @@ def test_legacy_partial_conditional_paths_are_resolved_for_request_state():
|
||||
dict_verify_each(tool_state.input_state, expectations)
|
||||
|
||||
|
||||
def test_legacy_unqualified_repeat_inputs_are_expanded_for_request_state():
|
||||
def test_legacy_unqualified_repeat_inputs_are_not_expanded():
|
||||
# Unqualified repeat params (the multi_repeats anti-pattern) are no longer synthesized into
|
||||
# repeat instances; use explicit <repeat> tags. The bare param is rejected on validation.
|
||||
tool_source = tool_source_for("multi_repeats")
|
||||
test_cases = tool_source.parse_tests_to_dict()["tests"]
|
||||
with pytest.raises(Exception, match="Invalid parameter name found input2"):
|
||||
case_state_for(tool_source, test_cases[2])
|
||||
|
||||
test_case_state = case_state_for(tool_source, test_cases[2]).tool_state
|
||||
|
||||
expectations = [
|
||||
(["queries", 0, "input2", "path"], "simple_line.txt"),
|
||||
(["queries", 1, "input2", "path"], "simple_line.txt"),
|
||||
(["more_queries", 0, "more_queries_input", "path"], "simple_line.txt"),
|
||||
(["more_queries", 1, "more_queries_input", "path"], "simple_line.txt"),
|
||||
]
|
||||
dict_verify_each(test_case_state.input_state, expectations)
|
||||
def test_unrepresentable_test_case_falls_back_to_legacy_request():
|
||||
# A test whose inputs can't be represented in a modern request (unqualified repeats) yields no
|
||||
# request, so the interactor falls back to the legacy tool API rather than erroring.
|
||||
tests = list(parse_tool_test_descriptions(tool_source_for("multi_repeats")))
|
||||
description = tests[2].to_dict()
|
||||
assert description["error"] is False
|
||||
assert description["request"] is None
|
||||
|
||||
|
||||
def test_legacy_unqualified_repeat_inside_conditional_is_resolved():
|
||||
|
||||
Reference in New Issue
Block a user