diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py
index 9ab4a30f65b6..8e098236a110 100644
--- a/lib/galaxy/tool_util/parser/xml.py
+++ b/lib/galaxy/tool_util/parser/xml.py
@@ -376,6 +376,10 @@ def _get_option_value(self, key, default):
def _command_el(self):
return self.root.find("command")
+ @property
+ def _outputs_el(self):
+ return self.root.find("outputs")
+
def _get_attribute_as_bool(self, attribute, default, elem=None):
if elem is None:
elem = self.root
@@ -411,7 +415,7 @@ def parse_input_pages(self) -> "XmlPagesSource":
def parse_provided_metadata_style(self):
style = None
- out_elem = self.root.find("outputs")
+ out_elem = self._outputs_el
if out_elem is not None and "provided_metadata_style" in out_elem.attrib:
style = out_elem.attrib["provided_metadata_style"]
diff --git a/lib/galaxy/tool_util/upgrade/__init__.py b/lib/galaxy/tool_util/upgrade/__init__.py
new file mode 100644
index 000000000000..2ff1aa6f0861
--- /dev/null
+++ b/lib/galaxy/tool_util/upgrade/__init__.py
@@ -0,0 +1,270 @@
+# Documented profile changes not covered.
+# - Allow invalid values. @mvdbeek any clue what to say here. The PR is here:
+# - https://github.com/galaxyproject/galaxy/pull/6264
+#
+# I don't want to declare these TODO but somethings could be more percise.
+# - We could inspect the XML source and tell when tools are using galaxy.json and improve
+# the messaging on 17.09 for that. We can't be absolutely sure these would or would not be
+# problems but we can be more confident they might be.
+# - Ditto for $HOME and the 18.01 migration.
+# - We could try to walk parameters and give more specific advice on structured_like qualification.
+from json import loads
+from typing import (
+ cast,
+ Dict,
+ List,
+ Optional,
+ Type,
+)
+
+from typing_extensions import (
+ Literal,
+ NotRequired,
+ TypedDict,
+)
+
+from galaxy.tool_util.parser.factory import get_tool_source
+from galaxy.tool_util.parser.xml import XmlToolSource
+from galaxy.util import Element
+from galaxy.util.resources import resource_string
+
+TOOL_TOO_NEW = "Target tool has a profile that is too new, consider upgrading this script for the latest advice."
+TARGET_TOO_NEW = (
+ "Target upgrade version is too new for this script, consider upgrading this script for the latest advice."
+)
+
+
+class AdviceCode(TypedDict):
+ name: str
+ level: Literal["must_fix", "consider", "ready", "info"]
+ message: str
+ niche: NotRequired[bool]
+ url: NotRequired[str]
+
+
+upgrade_codes_json = resource_string(__package__, "upgrade_codes.json")
+upgrade_codes_by_name: Dict[str, AdviceCode] = {}
+
+for name, upgrade_object in loads(upgrade_codes_json).items():
+ upgrade_object["name"] = name
+ upgrade_codes_by_name[name] = cast(AdviceCode, upgrade_object)
+
+
+class AdviceCollection:
+ _advice: List[AdviceCode]
+
+ def __init__(self):
+ self._advice = []
+
+ def add(self, code: str):
+ self._advice.append(upgrade_codes_by_name[code])
+
+ def to_list(self) -> List[AdviceCode]:
+ return self._advice
+
+
+class ProfileMigration:
+ """A class offering advice on upgrading a Galaxy tool between two profile versions."""
+
+ from_version: str
+ to_version: str
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ return None
+
+
+class ProfileMigration16_04(ProfileMigration):
+ from_version = "16.01"
+ to_version = "16.04"
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ tool_source = _xml_tool_source(xml_file)
+ interpreter = tool_source.parse_interpreter()
+ if interpreter:
+ advice_collection.add("16_04_fix_interpreter")
+ else:
+ advice_collection.add("16_04_ready_interpreter")
+ advice_collection.add("16_04_consider_implicit_extra_file_collection")
+
+ if _has_matching_xpath(tool_source, ".//data[@format = 'input']"):
+ advice_collection.add("16_04_fix_output_format")
+
+ if not _has_matching_xpath(tool_source, ".//stdio") and not _has_matching_xpath(
+ tool_source, ".//command[@detect_errors]"
+ ):
+ advice_collection.add("16_04_exit_code")
+
+
+class ProfileMigration17_09(ProfileMigration):
+ from_version = "16.04"
+ to_version = "17.09"
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ tool_source = _xml_tool_source(xml_file)
+
+ outputs_el = tool_source._outputs_el
+ if outputs_el is not None and outputs_el.get("`provided_metadata_style`", None) is not None:
+ advice_collection.add("17_09_consider_provided_metadata_style")
+
+
+class ProfileMigration18_01(ProfileMigration):
+ from_version = "17.09"
+ to_version = "18.01"
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ tool_source = _xml_tool_source(xml_file)
+ command_el = tool_source._command_el
+ if command_el is not None and command_el.get("use_shared_home", None) is None:
+ advice_collection.add("18_01_consider_home_directory")
+
+ if _has_matching_xpath(tool_source, ".//outputs/collection[@structured_like]"):
+ advice_collection.add("18_01_consider_structured_like")
+
+
+class ProfileMigration18_09(ProfileMigration):
+ from_version = "18.01"
+ to_version = "18.09"
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ tool_source = _xml_tool_source(xml_file)
+ tool_type = tool_source.parse_tool_type()
+ if tool_type == "manage_data":
+ advice_collection.add("18_09_consider_python_environment")
+
+
+class ProfileMigration20_05(ProfileMigration):
+ from_version = "18.09"
+ to_version = "20.05"
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ tool_source = _xml_tool_source(xml_file)
+
+ if _has_matching_xpath(tool_source, ".//configfiles/inputs"):
+ advice_collection.add("20_05_consider_inputs_as_json_changes")
+
+
+class ProfileMigration20_09(ProfileMigration):
+ from_version = "20.05"
+ to_version = "20.09"
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ tool_source = _xml_tool_source(xml_file)
+
+ tests = tool_source.parse_tests_to_dict()
+ for test in tests["tests"]:
+ output_collections = test.get("output_collections")
+ if not output_collections:
+ continue
+
+ for output_collection in output_collections:
+ if output_collection.get("element_tests"):
+ advice_collection.add("20_09_consider_output_collection_order")
+
+ command_el = tool_source._command_el
+ if command_el is not None:
+ strict = command_el.get("strict", None)
+ if strict is None:
+ advice_collection.add("20_09_consider_set_e")
+
+
+class ProfileMigration21_09(ProfileMigration):
+ from_version = "20.09"
+ to_version = "21.09"
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ tool_source = _xml_tool_source(xml_file)
+ for el in _find_all(tool_source, ".//data[@from_work_dir]"):
+ from_work_dir = el.get("from_work_dir") or ""
+ if from_work_dir != from_work_dir.strip():
+ advice_collection.add("")
+
+ tool_type = tool_source.parse_tool_type()
+ if tool_type == "data_source":
+ advice_collection.add("21_09_consider_python_environment")
+
+
+class ProfileMigration23_0(ProfileMigration):
+ from_version = "21.09"
+ to_version = "23.0"
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ tool_source = _xml_tool_source(xml_file)
+ for text_param in _find_all(tool_source, ".//input[@type='text']"):
+ optional_tag_set = text_param.get("optional", None) is not None
+ if not optional_tag_set:
+ advice_collection.add("23_0_consider_optional_text")
+
+
+class ProfileMigration24_0(ProfileMigration):
+ from_version = "23.0"
+ to_version = "24.0"
+
+ @classmethod
+ def advise(cls, advice_collection: AdviceCollection, xml_file: str) -> None:
+ tool_source = _xml_tool_source(xml_file)
+ tool_type = tool_source.parse_tool_type()
+ if tool_type == "data_source_async":
+ advice_collection.add("24_0_consider_python_environment")
+ if tool_type in ["data_source_async", "data_source"]:
+ advice_collection.add("24_0_request_cleaning")
+
+
+profile_migrations: List[Type[ProfileMigration]] = [
+ ProfileMigration16_04,
+ ProfileMigration17_09,
+ ProfileMigration18_01,
+ ProfileMigration18_09,
+ ProfileMigration20_05,
+ ProfileMigration20_09,
+ ProfileMigration21_09,
+ ProfileMigration23_0,
+ ProfileMigration24_0,
+]
+
+latest_supported_version = "24.0"
+
+
+def advise_on_upgrade(xml_file: str, to_version: Optional[str] = None) -> List[AdviceCode]:
+ to_version = to_version or latest_supported_version
+ tool_source = _xml_tool_source(xml_file)
+ initial_version = tool_source.parse_profile()
+ if initial_version > latest_supported_version:
+ raise Exception(TOOL_TOO_NEW)
+ elif to_version > latest_supported_version:
+ raise Exception(TARGET_TOO_NEW)
+ advice_collection = AdviceCollection()
+
+ for migration in profile_migrations:
+ if migration.to_version < initial_version:
+ # tool started past here... just skip this advice
+ continue
+ if migration.to_version > to_version:
+ # we're not advising on upgrading past this point
+ break
+ migration.advise(advice_collection, xml_file)
+
+ return advice_collection.to_list()
+
+
+def _xml_tool_source(xml_file: str) -> XmlToolSource:
+ tool_source = get_tool_source(xml_file)
+ if not isinstance(tool_source, XmlToolSource):
+ raise Exception("Can only provide upgrade advice for XML tools.")
+ return cast(XmlToolSource, tool_source)
+
+
+def _has_matching_xpath(tool_source: XmlToolSource, xpath: str) -> bool:
+ return tool_source.xml_tree.find(xpath) is not None
+
+
+def _find_all(tool_source: XmlToolSource, xpath: str) -> List[Element]:
+ return cast(List[Element], tool_source.xml_tree.findall(".//data[@from_work_dir]") or [])
diff --git a/lib/galaxy/tool_util/upgrade/script.py b/lib/galaxy/tool_util/upgrade/script.py
new file mode 100755
index 000000000000..2ec4b2020ab3
--- /dev/null
+++ b/lib/galaxy/tool_util/upgrade/script.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+
+import argparse
+import sys
+from json import dumps
+from textwrap import wrap
+from typing import List
+
+from galaxy.tool_util.upgrade import (
+ AdviceCode,
+ advise_on_upgrade,
+ latest_supported_version,
+)
+
+LEVEL_TO_STRING = {
+ "must_fix": "â",
+ "ready": "â
",
+ "consider": "đ¤",
+ "info": "âšī¸",
+}
+DESCRIPTION = f"""
+A small utility to check for potential problems and provide advice when upgrading a tool's
+profile version. This version of the script can provide advice for upgrading tools through
+{latest_supported_version}.
+"""
+
+
+def arg_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=DESCRIPTION)
+ parser.add_argument("xml_file")
+ parser.add_argument(
+ "-p",
+ "--profile-version",
+ dest="profile_version",
+ default=latest_supported_version,
+ help="Provide upgrade advice up to this Galaxy tool profile version.",
+ )
+ parser.add_argument(
+ "-j",
+ "--json",
+ default=False,
+ action="store_true",
+ help="Output aadvice as JSON.",
+ )
+ parser.add_argument(
+ "-n",
+ "--niche",
+ default=False,
+ action="store_true",
+ help="Include advice about niche features that may not be relevant for most tools - including the use of 'galaxy.json' and writing global state in the $HOME directory.",
+ )
+ return parser
+
+
+def _print_advice(advice: AdviceCode):
+ message = "\n".join(wrap(advice["message"], initial_indent="", subsequent_indent=" "))
+ level = advice["level"]
+ level_str = LEVEL_TO_STRING[level]
+ url = advice.get("url")
+ print(f"- {level_str}{message}\n")
+ if url:
+ print(f" More information at {url}")
+
+
+def _print_advice_list(advice_list: List[AdviceCode]):
+ for advice in advice_list:
+ _print_advice(advice)
+
+
+def advise(xml_file: str, version: str, json: bool, niche: bool):
+ advice_list = advise_on_upgrade(xml_file, version)
+ if not niche:
+ advice_list = [a for a in advice_list if not a.get("niche", False)]
+ if json:
+ print(dumps(advice_list))
+ else:
+ _print_advice_list(advice_list)
+
+
+def main(argv=None) -> None:
+ if argv is None:
+ argv = sys.argv[1:]
+
+ args = arg_parser().parse_args(argv)
+ advise(args.xml_file, args.profile_version, args.json, args.niche)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/lib/galaxy/tool_util/upgrade/upgrade_codes.json b/lib/galaxy/tool_util/upgrade/upgrade_codes.json
new file mode 100644
index 000000000000..3116a1d73305
--- /dev/null
+++ b/lib/galaxy/tool_util/upgrade/upgrade_codes.json
@@ -0,0 +1,89 @@
+{
+ "16_04_fix_interpreter": {
+ "level": "must_fix",
+ "message": "This tool uses an interpreter on the command block, this was disabled with 16.04. The command line needs to be rewritten to call the language runtime with a full path to the target script using `$tool_directory` to refer to the path to the tool and its script.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/1688"
+ },
+ "16_04_ready_interpreter": {
+ "level": "ready",
+ "message": "This tool follows best practice and does not specify an interpreter on the command block.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/1688"
+ },
+ "16_04_consider_implicit_extra_file_collection": {
+ "level": "consider",
+ "message": "Starting is with profile 16.04 tools, Galaxy no longer attempts to just find tool outputs keyed on the output ID in the working directory. Tool outputs need to be explicitly declared and dynamic outputs need to be specified in a 'galaxy.json' file or with a 'discover_datasets' block.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/1688",
+ "niche": true
+ },
+ "16_04_fix_output_format": {
+ "level": "must_fix",
+ "message": "Starting with 16.04 tools, having format='input' on a tool output is disabled. The behavior was not well defined for these outputs. Please add 'format_source=\"a_specific_input_name\" for a specific input to inherit the format from.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/1688"
+ },
+ "16_04_exit_code": {
+ "level": "consider",
+ "message": "Starting with 16.04 tools the exit code of the command executed will be used to detect errors by default. This tool previously would have discovered errors by checking if any content is written to standard error. Add '' to your tool to restore the legacy behavior or restructure your command block to rely on the exit code.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/1688"
+ },
+ "18_01_consider_structured_like": {
+ "level": "consider",
+ "message": "Starting with 18.01 tools, the 'structured_like` attribute must reference inputs in a fully qualified manner - using '|' to describe parent conditionals for instance.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/6162"
+ },
+ "20_05_consider_inputs_as_json_changes": {
+ "level": "consider",
+ "message": "Starting with 20.05, the format of data in 'inputs' config files changed slightly. Unselected optional `select` and `data_column` parameters get json null values instead of the string 'None' and multiple `select` and `data_column` parameters are lists (instead of comma separated strings).",
+ "url": "https://github.com/galaxyproject/galaxy/pull/9776/files"
+ },
+ "20_09_consider_output_collection_order": {
+ "level": "consider",
+ "message": "Starting in profile 20.09 tools, the order elements defined in tool test became relevant in order to verify collections are properly sorted. This may cause tool tests to fail after the upgrade, rearrange the elements defined in output collections if this occurs.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/10434"
+ },
+ "20_09_consider_set_e": {
+ "level": "consider",
+ "message": "Starting with profile 20.09 tools, tool scripts are executed with the 'set -e' instruction. The 'set -e' option instructs the shell to immediately exit if any command has a non-zero exit status. If your command uses multiple sub-commands and you'd like to allow them to execute with non-zero exit codes add 'strict=\"false\"' to the command tag to restore the tool's legacy behavior.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/9962"
+ },
+ "18_01_consider_home_directory": {
+ "level": "consider",
+ "message": "Starting with profile 18.01 tools, each job is given its own home directory. Most tools should not depend on global state in a home directory, if this is required though set 'use_shared_home=\"true\"' on the command tag of the tool.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/5193",
+ "niche": true
+ },
+ "18_09_consider_python_environment": {
+ "level": "consider",
+ "message": "Starting with profile 18.09 tools, data managers run without Galaxy's virtual environment. Be sure your requirements reflect all the data manager's dependencies.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/6466"
+ },
+ "17_09_consider_provided_metadata_style": {
+ "level": "consider",
+ "message": "Starting with 17.09 tools, the format of 'galaxy.json' (a rarely used file that can be used to dynamically collect datasets or metadata about datasets produced by the tool) changed - the original behavior can be restored by adding 'provided_metadata_style=\"legacy\"' to the tool's outputs tag.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/4437",
+ "niche": true
+ },
+ "21_09_fix_from_work_dir_whitespace": {
+ "level": "must_fix",
+ "message": "Starting with 21.09 tools, from_work_dir output file names are quoted so white space needs to be stripped out of attribute.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/12536"
+ },
+ "21_09_consider_python_environment": {
+ "level": "consider",
+ "message": "Starting with 21.09 data source tools, Galaxy's virtual environment is no longer included in the tool's runtime environment. Tools that require it, should include the galaxy-util package in their requirements.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/12515"
+ },
+ "23_0_consider_optional_text": {
+ "level": "consider",
+ "message": "Text parameters that are inferred to be optional (i.e the `optional` tag is not set, but the tool parameter accepts an empty string) are set to `None` for templating in Cheetah. Previous to this version tools would receive the empty string `\"\"` as the templated value.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/15491/files"
+ },
+ "24_0_consider_python_environment": {
+ "level": "consider",
+ "message": "Starting with 24.0 async data source tools, Galaxy's virtual environment is no longer included in the tool's runtime environment. Tools that require it, should include the galaxy-util package in their requirements.",
+ "url": "https://github.com/galaxyproject/galaxy/pull/17422"
+ },
+ "24_0_request_cleaning": {
+ "level": "consider",
+ "message": "Starting with 24.0 data source tools, Galaxy requires explicit `request_param_translation` for each parameter sent to the tool. If this tools depends on unspecified parameters - new xml elements will need to be added for these parameters."
+ }
+}
diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd
index 7c17312c4f0b..de34e758331b 100644
--- a/lib/galaxy/tool_util/xsd/galaxy.xsd
+++ b/lib/galaxy/tool_util/xsd/galaxy.xsd
@@ -29,10 +29,14 @@ List of behavior changes associated with profile versions:
- Disable default tool version of 1.0.0.
- Use non zero exit code as default stdio error condition (before non-empty stderr).
+#### 17.09
+
+- Introduce `provided_metadata_style` with default `"default"`. Restore legacy behavior by setting
+ this to `"legacy"`.
+
#### 18.01
- Use a separate home directory for each job.
-- Introduce `provided_metadata_style` with default `"default"` before `"legacy"`.
#### 18.09
diff --git a/packages/tool_util/setup.cfg b/packages/tool_util/setup.cfg
index 5b4f5619cfab..6fd52ba67a7f 100644
--- a/packages/tool_util/setup.cfg
+++ b/packages/tool_util/setup.cfg
@@ -50,6 +50,7 @@ python_requires = >=3.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
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/legacy_interpreter.xml b/test/functional/tools/legacy_interpreter.xml
new file mode 100644
index 000000000000..7ca65584d388
--- /dev/null
+++ b/test/functional/tools/legacy_interpreter.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/legacy_interpreter_write_output.py b/test/functional/tools/legacy_interpreter_write_output.py
new file mode 100644
index 000000000000..25791f7b3d86
--- /dev/null
+++ b/test/functional/tools/legacy_interpreter_write_output.py
@@ -0,0 +1,2 @@
+with open("output1", "w") as f:
+ f.write("hello world")
diff --git a/test/functional/tools/output_format_input.xml b/test/functional/tools/output_format_input.xml
index c346b10dedf3..6080f0c21ca2 100644
--- a/test/functional/tools/output_format_input.xml
+++ b/test/functional/tools/output_format_input.xml
@@ -1,6 +1,6 @@
+ (for legacy tools, i.e. profile <16.04, the format of a random input is used) -->
cat '$input' > '$output'
diff --git a/test/functional/tools/sample_tool_conf.xml b/test/functional/tools/sample_tool_conf.xml
index 80c032c93601..4afe5cf253f8 100644
--- a/test/functional/tools/sample_tool_conf.xml
+++ b/test/functional/tools/sample_tool_conf.xml
@@ -176,6 +176,7 @@
+
diff --git a/test/unit/tool_util/upgrade/__init__.py b/test/unit/tool_util/upgrade/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/test/unit/tool_util/upgrade/test_upgrade_advice.py b/test/unit/tool_util/upgrade/test_upgrade_advice.py
new file mode 100644
index 000000000000..190f6c395107
--- /dev/null
+++ b/test/unit/tool_util/upgrade/test_upgrade_advice.py
@@ -0,0 +1,164 @@
+# Not yet tested:
+# - 21_09_fix_from_work_dir_whitespace
+# - 23_0_consider_optional_text
+import os
+from typing import List
+
+import pytest
+
+from galaxy.tool_util.unittest_utils import functional_test_tool_path
+from galaxy.tool_util.upgrade import (
+ AdviceCode,
+ advise_on_upgrade,
+ TARGET_TOO_NEW,
+)
+
+FUTURE_GALAXY_VERSION = "29.6"
+
+
+def test_does_not_work_on_non_xml_tools():
+ simple_constructs = _tool_path("simple_constructs.yml")
+ with pytest.raises(Exception) as e:
+ advise_on_upgrade(simple_constructs, "16.04")
+ assert "XML tools" in str(e)
+
+
+def test_does_not_work_on_versions_too_new():
+ simple_constructs = _tool_path("simple_constructs.xml")
+ with pytest.raises(Exception) as e:
+ advise_on_upgrade(simple_constructs, FUTURE_GALAXY_VERSION)
+ assert TARGET_TOO_NEW in str(e)
+
+
+def test_old_advice_does_not_appear_when_upgrading_past_it():
+ a_16_01_tool = _tool_path("tool_provided_metadata_1.xml")
+ advice = advise_on_upgrade(a_16_01_tool)
+ assert_has_advice(advice, "16_04_consider_implicit_extra_file_collection")
+
+ a_17_09_tool = _tool_path("tool_provided_metadata_6.xml")
+ advice = advise_on_upgrade(a_17_09_tool)
+ assert_not_has_advice(advice, "16_04_consider_implicit_extra_file_collection")
+
+
+def test_interpreter_advice_positive():
+ legacy_interpreter = _tool_path("legacy_interpreter.xml")
+ advice = advise_on_upgrade(legacy_interpreter, "16.04")
+ assert_has_advice(advice, "16_04_fix_interpreter")
+ assert_not_has_advice(advice, "16_04_ready_interpreter")
+
+
+def test_interpreter_advice_negative():
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs, "16.04")
+ assert_not_has_advice(advice, "16_04_fix_interpreter")
+ assert_has_advice(advice, "16_04_ready_interpreter")
+
+
+def test_output_format_advice_positive():
+ output_format = _tool_path("output_format.xml")
+ advice = advise_on_upgrade(output_format, "16.04")
+ assert_has_advice(advice, "16_04_fix_output_format")
+
+
+def test_20_05_inputs_changes():
+ inputs_as_json = _tool_path("inputs_as_json.xml")
+ advice = advise_on_upgrade(inputs_as_json)
+ assert_has_advice(advice, "20_05_consider_inputs_as_json_changes")
+
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs)
+ assert_not_has_advice(advice, "20_05_consider_inputs_as_json_changes")
+
+
+def test_output_format_advice_negative():
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs, "16.04")
+ assert_not_has_advice(advice, "16_04_fix_output_format")
+
+
+def test_consider_implicit_extra_file_collection():
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs, "16.04")
+ assert_has_advice(advice, "16_04_consider_implicit_extra_file_collection")
+
+
+def test_consider_stdio():
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs, "16.04")
+ assert_has_advice(advice, "16_04_exit_code")
+
+ a_17_09_tool = _tool_path("tool_provided_metadata_6.xml")
+ advice = advise_on_upgrade(a_17_09_tool)
+ assert_not_has_advice(advice, "16_04_exit_code")
+
+
+def test_2009_output_collection_advice_positive():
+ collection_creates_list = _tool_path("collection_creates_list.xml")
+ advice = advise_on_upgrade(collection_creates_list)
+ assert_has_advice(advice, "20_09_consider_output_collection_order")
+
+
+def test_2009_output_collection_advice_negative():
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs)
+ assert_not_has_advice(advice, "20_09_consider_output_collection_order")
+
+
+def test_2009_consider_strict_shell_positive():
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs)
+ assert_has_advice(advice, "20_09_consider_set_e")
+
+
+def test_1801_consider_home_directory():
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs)
+ assert_has_advice(advice, "18_01_consider_home_directory")
+
+
+def test_1801_consider_structured_like():
+ simple_constructs = _tool_path("collection_paired_conditional_structured_like.xml")
+ advice = advise_on_upgrade(simple_constructs)
+ assert_has_advice(advice, "18_01_consider_structured_like")
+
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs)
+ assert_not_has_advice(advice, "18_01_consider_structured_like")
+
+
+def test_21_09_data_source_advice():
+ test_data_source = _tool_path("test_data_source.xml")
+ advice = advise_on_upgrade(test_data_source)
+ assert_has_advice(advice, "21_09_consider_python_environment")
+
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs)
+ assert_not_has_advice(advice, "21_09_consider_python_environment")
+
+
+def test_24_0_request_cleaning():
+ test_data_source = _tool_path("test_data_source.xml")
+ advice = advise_on_upgrade(test_data_source)
+ assert_has_advice(advice, "24_0_request_cleaning")
+
+ simple_constructs = _tool_path("simple_constructs.xml")
+ advice = advise_on_upgrade(simple_constructs)
+ assert_not_has_advice(advice, "24_0_request_cleaning")
+
+
+def _tool_path(tool_name: str):
+ return os.path.join(functional_test_tool_path(tool_name))
+
+
+def assert_has_advice(advice_list: List[AdviceCode], advice_code: str):
+ for advice in advice_list:
+ if advice["name"] == advice_code:
+ return
+
+ raise AssertionError(f"Was expecting advice {advice_code} in list of upgrade advice {advice_list}")
+
+
+def assert_not_has_advice(advice_list: List[AdviceCode], advice_code: str):
+ for advice in advice_list:
+ if advice["name"] == advice_code:
+ raise AssertionError(f"Was not expecting advice {advice_code} in list of upgrade advice {advice_list}")