diff --git a/.ci/validate_test_tools.sh b/.ci/validate_test_tools.sh
index 258799dc95d4..45a32d3f6898 100755
--- a/.ci/validate_test_tools.sh
+++ b/.ci/validate_test_tools.sh
@@ -7,5 +7,7 @@ xsd_path="lib/galaxy/tools/xsd/galaxy.xsd"
xmllint --noout "$xsd_path"
test_tools_path='test/functional/tools'
-tool_files_list=$(ls "$test_tools_path"/*.xml | grep -v '_conf.xml$')
+# test all test tools except upload.xml which uses a non-standard conditional
+# (without param) which does not survive xsd validation
+tool_files_list=$(ls "$test_tools_path"/*.xml | grep -v '_conf.xml$' | grep -v upload.xml)
sh scripts/validate_tools.sh $tool_files_list
diff --git a/lib/galaxy/tool_util/lint.py b/lib/galaxy/tool_util/lint.py
index 54390031a232..bfde64c9d1bd 100644
--- a/lib/galaxy/tool_util/lint.py
+++ b/lib/galaxy/tool_util/lint.py
@@ -45,22 +45,31 @@
"""
import inspect
+from abc import (
+ ABC,
+ abstractmethod,
+)
from enum import IntEnum
from typing import (
Callable,
List,
Optional,
Type,
+ TYPE_CHECKING,
TypeVar,
Union,
)
+import galaxy.tool_util.linters
from galaxy.tool_util.parser import get_tool_source
from galaxy.util import (
Element,
submodules,
)
+if TYPE_CHECKING:
+ from galaxy.tool_util.parser.interface import ToolSource
+
class LintLevel(IntEnum):
SILENT = 5
@@ -71,14 +80,44 @@ class LintLevel(IntEnum):
ALL = 0
+class Linter(ABC):
+ """
+ a linter. needs to define a lint method and the code property.
+ optionally a fix method can be given
+ """
+
+ @classmethod
+ @abstractmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ """
+ should add at most one message to the lint context
+ """
+ pass
+
+ @classmethod
+ def name(cls) -> str:
+ """
+ get the linter name
+ """
+ return cls.__name__
+
+ @classmethod
+ def list_listers(cls) -> List[str]:
+ """
+ list the names of all linter derived from Linter
+ """
+ return [s.__name__ for s in cls.__subclasses__()]
+
+
class LintMessage:
"""
a message from the linter
"""
- def __init__(self, level: str, message: str, **kwargs):
+ def __init__(self, level: str, message: str, linter: Optional[str] = None, **kwargs):
self.level = level
self.message = message
+ self.linter = linter
def __eq__(self, other) -> bool:
"""
@@ -95,15 +134,19 @@ def __eq__(self, other) -> bool:
return False
def __str__(self) -> str:
- return f".. {self.level.upper()}: {self.message}"
+ if self.linter:
+ linter = f" ({self.linter})"
+ else:
+ linter = ""
+ return f".. {self.level.upper()}{linter}: {self.message}"
def __repr__(self) -> str:
return f"LintMessage({self.message})"
class XMLLintMessageLine(LintMessage):
- def __init__(self, level: str, message: str, node: Optional[Element] = None):
- super().__init__(level, message)
+ def __init__(self, level: str, message: str, linter: Optional[str] = None, node: Optional[Element] = None):
+ super().__init__(level, message, linter)
self.line = None
if node is not None:
self.line = node.sourceline
@@ -118,8 +161,8 @@ def __str__(self) -> str:
class XMLLintMessageXPath(LintMessage):
- def __init__(self, level: str, message: str, node: Optional[Element] = None):
- super().__init__(level, message)
+ def __init__(self, level: str, message: str, linter: Optional[str] = None, node: Optional[Element] = None):
+ super().__init__(level, message, linter)
self.xpath = None
if node is not None:
tool_xml = node.getroottree()
@@ -170,7 +213,8 @@ def found_warns(self) -> bool:
return len(self.warn_messages) > 0
def lint(self, name: str, lint_func: Callable[[LintTargetType, "LintContext"], None], lint_target: LintTargetType):
- name = name[len("lint_") :]
+ if name.startswith("lint_"):
+ name = name[len("lint_") :]
if name in self.skip_types:
return
@@ -187,37 +231,18 @@ def lint(self, name: str, lint_func: Callable[[LintTargetType, "LintContext"], N
lint_func(lint_target, self)
if self.level < LintLevel.SILENT:
- # TODO: colorful emoji if in click CLI.
- if self.error_messages:
- status = "FAIL"
- elif self.warn_messages:
- status = "WARNING"
- else:
- status = "CHECK"
-
- def print_linter_info(printed_linter_info):
- if printed_linter_info:
- return True
- print(f"Applying linter {name}... {status}")
- return True
-
- plf = False
for message in self.error_messages:
- plf = print_linter_info(plf)
print(f"{message}")
if self.level <= LintLevel.WARN:
for message in self.warn_messages:
- plf = print_linter_info(plf)
print(f"{message}")
if self.level <= LintLevel.INFO:
for message in self.info_messages:
- plf = print_linter_info(plf)
print(f"{message}")
if self.level <= LintLevel.VALID:
for message in self.valid_messages:
- plf = print_linter_info(plf)
print(f"{message}")
self.message_list = tmp_message_list + self.message_list
@@ -237,22 +262,22 @@ def warn_messages(self) -> List[LintMessage]:
def error_messages(self) -> List[LintMessage]:
return [x for x in self.message_list if x.level == "error"]
- def __handle_message(self, level: str, message: str, *args, **kwargs) -> None:
+ def __handle_message(self, level: str, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
if args:
message = message % args
- self.message_list.append(self.lint_message_class(level=level, message=message, **kwargs))
+ self.message_list.append(self.lint_message_class(level=level, message=message, linter=linter, **kwargs))
- def valid(self, message: str, *args, **kwargs) -> None:
- self.__handle_message("check", message, *args, **kwargs)
+ def valid(self, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
+ self.__handle_message("check", message, linter, *args, **kwargs)
- def info(self, message: str, *args, **kwargs) -> None:
- self.__handle_message("info", message, *args, **kwargs)
+ def info(self, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
+ self.__handle_message("info", message, linter, *args, **kwargs)
- def error(self, message: str, *args, **kwargs) -> None:
- self.__handle_message("error", message, *args, **kwargs)
+ def error(self, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
+ self.__handle_message("error", message, linter, *args, **kwargs)
- def warn(self, message: str, *args, **kwargs) -> None:
- self.__handle_message("warning", message, *args, **kwargs)
+ def warn(self, message: str, linter: Optional[str] = None, *args, **kwargs) -> None:
+ self.__handle_message("warning", message, linter, *args, **kwargs)
def failed(self, fail_level: Union[LintLevel, str]) -> bool:
if isinstance(fail_level, str):
@@ -319,12 +344,16 @@ def lint_xml(
def lint_tool_source_with(lint_context, tool_source, extra_modules=None) -> LintContext:
extra_modules = extra_modules or []
- import galaxy.tool_util.linters
- tool_xml = getattr(tool_source, "xml_tree", None)
- tool_type = tool_source.parse_tool_type() or "default"
linter_modules = submodules.import_submodules(galaxy.tool_util.linters)
linter_modules.extend(extra_modules)
+ return lint_tool_source_with_modules(lint_context, tool_source, linter_modules)
+
+
+def lint_tool_source_with_modules(lint_context: LintContext, tool_source, linter_modules) -> LintContext:
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ tool_type = tool_source.parse_tool_type() or "default"
+
for module in linter_modules:
lint_tool_types = getattr(module, "lint_tool_types", ["default", "manage_data"])
if not ("*" in lint_tool_types or tool_type in lint_tool_types):
@@ -344,6 +373,8 @@ def lint_tool_source_with(lint_context, tool_source, extra_modules=None) -> Lint
lint_context.lint(name, value, tool_xml)
else:
lint_context.lint(name, value, tool_source)
+ elif inspect.isclass(value) and issubclass(value, Linter) and not inspect.isabstract(value):
+ lint_context.lint(name, value.lint, tool_source)
return lint_context
diff --git a/lib/galaxy/tool_util/linters/citations.py b/lib/galaxy/tool_util/linters/citations.py
index 562691c15213..79e5b9cf5c80 100644
--- a/lib/galaxy/tool_util/linters/citations.py
+++ b/lib/galaxy/tool_util/linters/citations.py
@@ -1,42 +1,73 @@
-"""This module contains a citation lint function.
+"""This module contains citation linters.
Citations describe references that should be used when consumers
of the tool publish results.
"""
+from typing import TYPE_CHECKING
-def lint_citations(tool_xml, lint_ctx):
- """Ensure tool contains at least one valid citation."""
- root = tool_xml.find("./citations")
- if root is None:
- root = tool_xml.getroot()
-
- citations = tool_xml.findall("citations")
- if len(citations) > 1:
- lint_ctx.error("More than one citation section found, behavior undefined.", node=citations[1])
- return
-
- if len(citations) == 0:
- lint_ctx.warn("No citations found, consider adding citations to your tool.", node=root)
- return
-
- valid_citations = 0
- for citation in citations[0]:
- if citation.tag != "citation":
- lint_ctx.warn(
- f"Unknown tag discovered in citations block [{citation.tag}], will be ignored.", node=citation
- )
- continue
- citation_type = citation.attrib.get("type")
- if citation_type not in ("bibtex", "doi"):
- lint_ctx.warn(f"Unknown citation type discovered [{citation_type}], will be ignored.", node=citation)
- continue
- if citation.text is None or not citation.text.strip():
- lint_ctx.error(f"Empty {citation_type} citation.", node=citation)
- continue
- valid_citations += 1
-
- if valid_citations > 0:
- lint_ctx.valid(f"Found {valid_citations} likely valid citations.", node=root)
- else:
- lint_ctx.warn("Found no valid citations.", node=root)
+from galaxy.tool_util.lint import Linter
+
+if TYPE_CHECKING:
+ from galaxy.tool_util.lint import LintContext
+ from galaxy.tool_util.parser.interface import ToolSource
+
+
+class CitationsMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ root = tool_xml.find("./citations")
+ if root is None:
+ root = tool_xml.getroot()
+ citations = tool_xml.findall("citations")
+ if len(citations) == 0:
+ lint_ctx.warn("No citations found, consider adding citations to your tool.", linter=cls.name(), node=root)
+
+
+class CitationsNoText(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ citations = tool_xml.find("citations")
+ if citations is None:
+ return
+ for citation in citations:
+ citation_type = citation.attrib.get("type")
+ if citation_type in ["doi", "bibtex"] and (citation.text is None or not citation.text.strip()):
+ lint_ctx.error(f"Empty {citation_type} citation.", linter=cls.name(), node=citation)
+
+
+class CitationsFound(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ root = tool_xml.find("./citations")
+ if root is None:
+ root = tool_xml.getroot()
+ citations = tool_xml.find("citations")
+
+ if citations is not None and len(citations) > 0:
+ lint_ctx.valid(f"Found {len(citations)} citations.", linter=cls.name(), node=root)
+
+
+class CitationsNoValid(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ root = tool_xml.find("./citations")
+ if root is None:
+ root = tool_xml.getroot()
+ citations = tool_xml.findall("citations")
+ if len(citations) != 1:
+ return
+ if len(citations[0]) == 0:
+ lint_ctx.warn("Found no valid citations.", linter=cls.name(), node=root)
diff --git a/lib/galaxy/tool_util/linters/command.py b/lib/galaxy/tool_util/linters/command.py
index 9d2dadd1b844..eff900341cb1 100644
--- a/lib/galaxy/tool_util/linters/command.py
+++ b/lib/galaxy/tool_util/linters/command.py
@@ -1,54 +1,84 @@
-"""This module contains a linting function for a tool's command description.
+"""This module contains linters for a tool's command description.
A command description describes how to build the command-line to execute
from supplied inputs.
"""
+from typing import TYPE_CHECKING
-def lint_command(tool_xml, lint_ctx):
- """Ensure tool contains exactly one command and check attributes."""
- root = tool_xml.find("./command")
- if root is None:
- root = tool_xml.getroot()
-
- commands = tool_xml.findall("./command")
- if len(commands) > 1:
- lint_ctx.error("More than one command tag found, behavior undefined.", node=commands[1])
- return
-
- if len(commands) == 0:
- lint_ctx.error("No command tag found, must specify a command template to execute.", node=root)
- return
-
- command = get_command(tool_xml)
- if command.text is None:
- lint_ctx.error("Command is empty.", node=root)
- elif "TODO" in command.text:
- lint_ctx.warn("Command template contains TODO text.", node=command)
-
- command_attrib = command.attrib
- interpreter_type = None
- for key, value in command_attrib.items():
- if key == "interpreter":
- interpreter_type = value
- elif key == "detect_errors":
- detect_errors = value
- if detect_errors not in ["default", "exit_code", "aggressive"]:
- lint_ctx.warn(f"Unknown detect_errors attribute [{detect_errors}]", node=command)
-
- interpreter_info = ""
- if interpreter_type:
- interpreter_info = f" with interpreter of type [{interpreter_type}]"
- if interpreter_type:
- lint_ctx.warn("Command uses deprecated 'interpreter' attribute.", node=command)
- lint_ctx.info(f"Tool contains a command{interpreter_info}.", node=command)
-
-
-def get_command(tool_xml):
- """Get command XML element from supplied XML root."""
- root = tool_xml.getroot()
- commands = root.findall("command")
- command = None
- if len(commands) == 1:
- command = commands[0]
- return command
+from galaxy.tool_util.lint import Linter
+
+if TYPE_CHECKING:
+ from galaxy.tool_util.lint import LintContext
+ from galaxy.tool_util.parser.interface import ToolSource
+
+
+class CommandMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ root = tool_xml.find("./command")
+ if root is None:
+ root = tool_xml.getroot()
+ command = tool_xml.find("./command")
+ if command is None:
+ lint_ctx.error(
+ "No command tag found, must specify a command template to execute.", linter=cls.name(), node=root
+ )
+
+
+class CommandEmpty(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ root = tool_xml.find("./command")
+ if root is None:
+ root = tool_xml.getroot()
+ command = tool_xml.find("./command")
+ if command is not None and command.text is None:
+ lint_ctx.error("Command is empty.", linter=cls.name(), node=root)
+
+
+class CommandTODO(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ command = tool_xml.find("./command")
+ if command is not None and command.text is not None and "TODO" in command.text:
+ lint_ctx.warn("Command template contains TODO text.", linter=cls.name(), node=command)
+
+
+class CommandInterpreterDeprecated(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ command = tool_xml.find("./command")
+ if command is None:
+ return
+ interpreter_type = command.attrib.get("interpreter", None)
+ if interpreter_type is not None:
+ lint_ctx.warn("Command uses deprecated 'interpreter' attribute.", linter=cls.name(), node=command)
+
+
+class CommandInfo(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ command = tool_xml.find("./command")
+ if command is None:
+ return
+ interpreter_type = command.attrib.get("interpreter", None)
+ interpreter_info = ""
+ if interpreter_type:
+ interpreter_info = f" with interpreter of type [{interpreter_type}]"
+ lint_ctx.info(f"Tool contains a command{interpreter_info}.", linter=cls.name(), node=command)
diff --git a/lib/galaxy/tool_util/linters/cwl.py b/lib/galaxy/tool_util/linters/cwl.py
index edfc992f8887..f4e7f367ba56 100644
--- a/lib/galaxy/tool_util/linters/cwl.py
+++ b/lib/galaxy/tool_util/linters/cwl.py
@@ -1,48 +1,93 @@
"""Linter for CWL tools."""
+from typing import TYPE_CHECKING
+
lint_tool_types = ["cwl"]
from galaxy.tool_util.cwl.schema import schema_loader
+from galaxy.tool_util.lint import Linter
+
+if TYPE_CHECKING:
+ from galaxy.tool_util.lint import LintContext
+ from galaxy.tool_util.parser import ToolSource
-def lint_cwl_validation(tool_source, lint_ctx):
- """Determine in CWL tool validates against spec."""
- raw_reference = schema_loader.raw_process_reference(tool_source._source_path)
- validation_exception = None
- try:
- schema_loader.process_definition(raw_reference)
- except Exception as e:
- validation_exception = e
- if validation_exception:
- lint_ctx.error(f"Failed to valdiate CWL artifact: {validation_exception}")
- else:
+class CWLValid(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ raw_reference = schema_loader.raw_process_reference(tool_source.source_path)
+ try:
+ schema_loader.process_definition(raw_reference)
+ except Exception:
+ return
lint_ctx.info("CWL appears to be valid.")
-def lint_new_draft(tool_source, lint_ctx):
- """Determine in CWL tool is valid, modern draft."""
- raw_reference = schema_loader.raw_process_reference(tool_source._source_path)
- cwl_version = raw_reference.process_object.get("cwlVersion", None)
- if cwl_version is None:
- lint_ctx.error("CWL file does not contain a 'cwlVersion'")
- if cwl_version not in ["v1.0"]:
- lint_ctx.warn(f"CWL version [{cwl_version}] is unknown, we recommend the v1.0 the stable release.")
- else:
- lint_ctx.info(f"Modern CWL version [{cwl_version}].")
-
-
-def lint_docker_image(tool_source, lint_ctx):
- _, containers, *_ = tool_source.parse_requirements_and_containers()
- if len(containers) == 0:
- lint_ctx.warn("Tool does not specify a DockerPull source.")
- else:
- identifier = containers[0].identifier
- lint_ctx.info(f"Tool will run in Docker image [{identifier}].")
-
-
-def lint_description(tool_source, lint_ctx):
- help = tool_source.parse_help()
- if not help:
- lint_ctx.warn("Description of tool is empty or absent.")
- elif "TODO" in help:
- lint_ctx.warn("Help contains TODO text.")
+class CWLInValid(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ raw_reference = schema_loader.raw_process_reference(tool_source.source_path)
+ try:
+ schema_loader.process_definition(raw_reference)
+ except Exception as e:
+ lint_ctx.error(f"Failed to valdiate CWL artifact: {e}")
+
+
+class CWLVersionMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ raw_reference = schema_loader.raw_process_reference(tool_source.source_path)
+ cwl_version = raw_reference.process_object.get("cwlVersion", None)
+ if cwl_version is None:
+ lint_ctx.error("CWL file does not contain a 'cwlVersion'")
+
+
+class CWLVersionUnknown(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ raw_reference = schema_loader.raw_process_reference(tool_source.source_path)
+ cwl_version = raw_reference.process_object.get("cwlVersion", None)
+ if cwl_version not in ["v1.0"]:
+ lint_ctx.warn(f"CWL version [{cwl_version}] is unknown, we recommend the v1.0 the stable release.")
+
+
+class CWLVersionGood(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ raw_reference = schema_loader.raw_process_reference(tool_source.source_path)
+ cwl_version = raw_reference.process_object.get("cwlVersion", None)
+ if cwl_version in ["v1.0"]:
+ lint_ctx.info(f"Modern CWL version [{cwl_version}].")
+
+
+class CWLDockerMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, containers, *_ = tool_source.parse_requirements_and_containers()
+ if len(containers) == 0:
+ lint_ctx.warn("Tool does not specify a DockerPull source.")
+
+
+class CWLDockerGood(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, containers, *_ = tool_source.parse_requirements_and_containers()
+ if len(containers) > 0:
+ identifier = containers[0].identifier
+ lint_ctx.info(f"Tool will run in Docker image [{identifier}].")
+
+
+class CWLDescriptionMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ help = tool_source.parse_help()
+ if not help:
+ lint_ctx.warn("Description of tool is empty or absent.")
+
+
+class CWLHelpTODO(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ help = tool_source.parse_help()
+ if help and "TODO" in help:
+ lint_ctx.warn("Help contains TODO text.")
diff --git a/lib/galaxy/tool_util/linters/general.py b/lib/galaxy/tool_util/linters/general.py
index 8ca73c76ff76..72e2ad7b4861 100644
--- a/lib/galaxy/tool_util/linters/general.py
+++ b/lib/galaxy/tool_util/linters/general.py
@@ -1,88 +1,227 @@
"""This module contains linting functions for general aspects of the tool."""
import re
+from typing import (
+ Tuple,
+ TYPE_CHECKING,
+)
+from galaxy.tool_util.lint import Linter
from galaxy.tool_util.version import (
LegacyVersion,
parse_version,
)
-ERROR_VERSION_MSG = "Tool version is missing or empty."
-WARN_VERSION_MSG = "Tool version [%s] is not compliant with PEP 440."
-VALID_VERSION_MSG = "Tool defines a version [%s]."
-
-ERROR_NAME_MSG = "Tool name is missing or empty."
-VALID_NAME_MSG = "Tool defines a name [%s]."
-
-ERROR_ID_MSG = "Tool does not define an id attribute."
-VALID_ID_MSG = "Tool defines an id [%s]."
+if TYPE_CHECKING:
+ from galaxy.tool_util.lint import LintContext
+ from galaxy.tool_util.parser.interface import ToolSource
+ from galaxy.util.etree import (
+ Element,
+ ElementTree,
+ )
PROFILE_PATTERN = re.compile(r"^[12]\d\.\d{1,2}$")
-PROFILE_INFO_DEFAULT_MSG = "Tool targets 16.01 Galaxy profile."
-PROFILE_INFO_SPECIFIED_MSG = "Tool specifies profile version [%s]."
-PROFILE_INVALID_MSG = "Tool specifies an invalid profile version [%s]."
-WARN_WHITESPACE_MSG = "%s contains whitespace, this may cause errors: [%s]."
-WARN_WHITESPACE_PRESUFFIX = "%s is pre/suffixed by whitespace, this may cause errors: [%s]."
-WARN_ID_WHITESPACE_MSG = "Tool ID contains whitespace - this is discouraged: [%s]."
lint_tool_types = ["*"]
-def lint_general(tool_source, lint_ctx):
- """Check tool version, name, and id."""
- # determine line to report for general problems with outputs
+def _tool_xml_and_root(tool_source: "ToolSource") -> Tuple["ElementTree", "Element"]:
tool_xml = getattr(tool_source, "xml_tree", None)
if tool_xml:
tool_node = tool_xml.getroot()
else:
tool_node = None
- version = tool_source.parse_version() or ""
- parsed_version = parse_version(version)
- if not version:
- lint_ctx.error(ERROR_VERSION_MSG, node=tool_node)
- elif isinstance(parsed_version, LegacyVersion):
- lint_ctx.warn(WARN_VERSION_MSG % version, node=tool_node)
- elif version != version.strip():
- lint_ctx.warn(WARN_WHITESPACE_PRESUFFIX % ("Tool version", version), node=tool_node)
- else:
- lint_ctx.valid(VALID_VERSION_MSG % version, node=tool_node)
+ return tool_xml, tool_node
- name = tool_source.parse_name()
- if not name:
- lint_ctx.error(ERROR_NAME_MSG, node=tool_node)
- elif name != name.strip():
- lint_ctx.warn(WARN_WHITESPACE_PRESUFFIX % ("Tool name", name), node=tool_node)
- else:
- lint_ctx.valid(VALID_NAME_MSG % name, node=tool_node)
- tool_id = tool_source.parse_id()
- if not tool_id:
- lint_ctx.error(ERROR_ID_MSG, node=tool_node)
- elif re.search(r"\s", tool_id):
- lint_ctx.warn(WARN_ID_WHITESPACE_MSG % tool_id, node=tool_node)
- else:
- lint_ctx.valid(VALID_ID_MSG % tool_id, node=tool_node)
-
- profile = tool_source.parse_profile()
- profile_valid = PROFILE_PATTERN.match(profile) is not None
- if not profile_valid:
- lint_ctx.error(PROFILE_INVALID_MSG % profile, node=tool_node)
- elif profile == "16.01":
- lint_ctx.valid(PROFILE_INFO_DEFAULT_MSG, node=tool_node)
- else:
- lint_ctx.valid(PROFILE_INFO_SPECIFIED_MSG % profile, node=tool_node)
+class ToolVersionMissing(Linter):
+ """
+ Tools must have a version
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml, tool_node = _tool_xml_and_root(tool_source)
+ version = tool_source.parse_version() or ""
+ if not version:
+ lint_ctx.error("Tool version is missing or empty.", linter=cls.name(), node=tool_node)
+
+
+class ToolVersionPEP404(Linter):
+ """
+ Tools should have a PEP404 compliant version.
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml, tool_node = _tool_xml_and_root(tool_source)
+ version = tool_source.parse_version() or ""
+ parsed_version = parse_version(version)
+ if version and isinstance(parsed_version, LegacyVersion):
+ lint_ctx.warn(f"Tool version [{version}] is not compliant with PEP 440.", linter=cls.name(), node=tool_node)
+
+
+class ToolVersionWhitespace(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml, tool_node = _tool_xml_and_root(tool_source)
+ version = tool_source.parse_version() or ""
+ if version != version.strip():
+ lint_ctx.warn(
+ f"Tool version is pre/suffixed by whitespace, this may cause errors: [{version}].",
+ linter=cls.name(),
+ node=tool_node,
+ )
+
+
+class ToolVersionValid(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml, tool_node = _tool_xml_and_root(tool_source)
+ version = tool_source.parse_version() or ""
+ parsed_version = parse_version(version)
+ if version and not isinstance(parsed_version, LegacyVersion) and version == version.strip():
+ lint_ctx.valid(f"Tool defines a version [{version}].", linter=cls.name(), node=tool_node)
+
+
+class ToolNameMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ name = tool_source.parse_name()
+ if not name:
+ lint_ctx.error("Tool name is missing or empty.", linter=cls.name(), node=tool_node)
+
+
+class ToolNameWhitespace(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ name = tool_source.parse_name()
+ if name and name != name.strip():
+ lint_ctx.warn(
+ f"Tool name is pre/suffixed by whitespace, this may cause errors: [{name}].",
+ linter=cls.name(),
+ node=tool_node,
+ )
+
- requirements, containers, resource_requirements = tool_source.parse_requirements_and_containers()
- for r in requirements:
- if r.type == "package":
+class ToolNameValid(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ name = tool_source.parse_name()
+ if name and name == name.strip():
+ lint_ctx.valid(f"Tool defines a name [{name}].", linter=cls.name(), node=tool_node)
+
+
+class ToolIDMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ tool_id = tool_source.parse_id()
+ if not tool_id:
+ lint_ctx.error("Tool does not define an id attribute.", linter=cls.name(), node=tool_node)
+
+
+class ToolIDWhitespace(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ tool_id = tool_source.parse_id()
+ if tool_id and re.search(r"\s", tool_id):
+ lint_ctx.warn(
+ f"Tool ID contains whitespace - this is discouraged: [{tool_id}].", linter=cls.name(), node=tool_node
+ )
+
+
+class ToolIDValid(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ tool_id = tool_source.parse_id()
+ if tool_id and not re.search(r"\s", tool_id):
+ lint_ctx.valid(f"Tool defines an id [{tool_id}].", linter=cls.name(), node=tool_node)
+
+
+class ToolProfileInvalid(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ profile = tool_source.parse_profile()
+ profile_valid = PROFILE_PATTERN.match(profile) is not None
+ if not profile_valid:
+ lint_ctx.error(f"Tool specifies an invalid profile version [{profile}].", linter=cls.name(), node=tool_node)
+
+
+class ToolProfileLegacy(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ profile = tool_source.parse_profile()
+ profile_valid = PROFILE_PATTERN.match(profile) is not None
+ if profile_valid and profile == "16.01":
+ lint_ctx.valid("Tool targets 16.01 Galaxy profile.", linter=cls.name(), node=tool_node)
+
+
+class ToolProfileValid(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ profile = tool_source.parse_profile()
+ profile_valid = PROFILE_PATTERN.match(profile) is not None
+ if profile_valid and profile != "16.01":
+ lint_ctx.valid(f"Tool specifies profile version [{profile}].", linter=cls.name(), node=tool_node)
+
+
+class RequirementNameMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ requirements, containers, resource_requirements = tool_source.parse_requirements_and_containers()
+ for r in requirements:
+ if r.type != "package":
+ continue
if not r.name:
- lint_ctx.error("Requirement without name found")
+ lint_ctx.error("Requirement without name found", linter=cls.name(), node=tool_node)
+
+
+class RequirementVersionMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ requirements, containers, resource_requirements = tool_source.parse_requirements_and_containers()
+ for r in requirements:
+ if r.type != "package":
+ continue
if not r.version:
- lint_ctx.warn(f"Requirement {r.name} defines no version")
- # Warn requirement attributes with leading/trailing whitespace:
- elif r.version != r.version.strip():
- lint_ctx.warn(WARN_WHITESPACE_MSG % ("Requirement version", r.version))
- for rr in resource_requirements:
- if rr.runtime_required:
- lint_ctx.warn("Expressions in resource requirement not supported yet")
+ lint_ctx.warn(f"Requirement {r.name} defines no version", linter=cls.name(), node=tool_node)
+
+
+class RequirementVersionWhitespace(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ requirements, containers, resource_requirements = tool_source.parse_requirements_and_containers()
+ for r in requirements:
+ if r.type != "package":
+ continue
+ if r.version and r.version != r.version.strip():
+ lint_ctx.warn(
+ f"Requirement version contains whitespace, this may cause errors: [{r.version}].",
+ linter=cls.name(),
+ node=tool_node,
+ )
+
+
+class ResourceRequirementExpression(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ _, tool_node = _tool_xml_and_root(tool_source)
+ requirements, containers, resource_requirements = tool_source.parse_requirements_and_containers()
+ for rr in resource_requirements:
+ if rr.runtime_required:
+ lint_ctx.warn(
+ "Expressions in resource requirement not supported yet", linter=cls.name(), node=tool_node
+ )
diff --git a/lib/galaxy/tool_util/linters/help.py b/lib/galaxy/tool_util/linters/help.py
index 0008d465662e..38f7be7c10d1 100644
--- a/lib/galaxy/tool_util/linters/help.py
+++ b/lib/galaxy/tool_util/linters/help.py
@@ -1,43 +1,111 @@
"""This module contains a linting function for a tool's help."""
-from typing import Union
+from typing import (
+ TYPE_CHECKING,
+ Union,
+)
+from galaxy.tool_util.lint import Linter
from galaxy.util import (
rst_to_html,
unicodify,
)
+if TYPE_CHECKING:
+ from galaxy.tool_util.lint import LintContext
+ from galaxy.tool_util.parser.interface import ToolSource
+
+
+class HelpMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ root = tool_xml.find("./help")
+ if root is None:
+ root = tool_xml.getroot()
+ help = tool_xml.find("./help")
+ if help is None:
+ lint_ctx.warn(
+ "No help section found, consider adding a help section to your tool.", linter=cls.name(), node=root
+ )
+
+
+class HelpEmpty(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ help = tool_xml.find("./help")
+ if help is None:
+ return
+ help_text = help.text or ""
+ if not help_text.strip():
+ lint_ctx.warn("Help section appears to be empty.", linter=cls.name(), node=help)
+
+
+class HelpPresent(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ help = tool_xml.find("./help")
+ if help is None:
+ return
+ help_text = help.text or ""
+ if help_text.strip():
+ lint_ctx.valid("Tool contains help section.", linter=cls.name(), node=help)
+
+
+class HelpTODO(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ help = tool_xml.find("./help")
+ if help is None:
+ return
+ help_text = help.text or ""
+ if "TODO" in help_text:
+ lint_ctx.warn("Help contains TODO text.", linter=cls.name(), node=help)
+
+
+class HelpInvalidRST(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ help = tool_xml.find("./help")
+ if help is None:
+ return
+ help_text = help.text or ""
+ if not help_text.strip():
+ return
+ invalid_rst = rst_invalid(help_text)
+ if invalid_rst:
+ lint_ctx.warn(f"Invalid reStructuredText found in help - [{invalid_rst}].", linter=cls.name(), node=help)
+
-def lint_help(tool_xml, lint_ctx):
- """Ensure tool contains exactly one valid RST help block."""
- # determine node to report for general problems with help
- root = tool_xml.find("./help")
- if root is None:
- root = tool_xml.getroot()
- helps = tool_xml.findall("./help")
- if len(helps) > 1:
- lint_ctx.error("More than one help section found, behavior undefined.", node=helps[1])
- return
-
- if len(helps) == 0:
- lint_ctx.warn("No help section found, consider adding a help section to your tool.", node=root)
- return
-
- help_text = helps[0].text or ""
- if not help_text.strip():
- lint_ctx.warn("Help section appears to be empty.", node=helps[0])
- return
-
- lint_ctx.valid("Tool contains help section.", node=helps[0])
-
- if "TODO" in help_text:
- lint_ctx.warn("Help contains TODO text.", node=helps[0])
-
- invalid_rst = rst_invalid(help_text)
- if invalid_rst:
- lint_ctx.warn(f"Invalid reStructuredText found in help - [{invalid_rst}].", node=helps[0])
- else:
- lint_ctx.valid("Help contains valid reStructuredText.", node=helps[0])
+class HelpValidRST(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ help = tool_xml.find("./help")
+ if help is None:
+ return
+ help_text = help.text or ""
+ if not help_text.strip():
+ return
+ invalid_rst = rst_invalid(help_text)
+ if not invalid_rst:
+ lint_ctx.valid("Help contains valid reStructuredText.", linter=cls.name(), node=help)
def rst_invalid(text: str) -> Union[bool, str]:
diff --git a/lib/galaxy/tool_util/linters/inputs.py b/lib/galaxy/tool_util/linters/inputs.py
index 4fdfb89ea070..ae6c8fb71954 100644
--- a/lib/galaxy/tool_util/linters/inputs.py
+++ b/lib/galaxy/tool_util/linters/inputs.py
@@ -2,8 +2,15 @@
import ast
import re
-from typing import TYPE_CHECKING
+import warnings
+from typing import (
+ Iterator,
+ Optional,
+ Tuple,
+ TYPE_CHECKING,
+)
+from galaxy.tool_util.lint import Linter
from galaxy.util import string_as_bool
from ._util import (
is_datasource,
@@ -14,19 +21,12 @@
if TYPE_CHECKING:
from galaxy.tool_util.lint import LintContext
from galaxy.tool_util.parser import ToolSource
+ from galaxy.util.etree import (
+ Element,
+ ElementTree,
+ )
-FILTER_TYPES = [
- "data_meta",
- "param_value",
- "static_value",
- "regexp",
- "unique_value",
- "multiple_splitter",
- "attribute_value_splitter",
- "add_value",
- "remove_value",
- "sort_by",
-]
+lint_tool_types = ["*"]
ATTRIB_VALIDATOR_COMPATIBILITY = {
"check": ["metadata"],
@@ -123,308 +123,1016 @@
PARAM_TYPE_CHILD_COMBINATIONS = [
("./options", ["data", "select", "drill_down"]),
("./options/option", ["drill_down"]),
- ("./column", ["data_column"]),
+ ("./options/column", ["data", "select"]),
]
+# TODO lint for valid param type - attribute combinations
+# TODO check if dataset is available for filters referring other datasets
+# TODO check if ref input param is present for from_dataset
+
+
+class InputsNum(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tool_node = tool_xml.find("./inputs")
+ if tool_node is None:
+ tool_node = tool_xml.getroot()
+ num_inputs = len(tool_xml.findall("./inputs//param"))
+ if num_inputs:
+ lint_ctx.info(f"Found {num_inputs} input parameters.", linter=cls.name(), node=tool_node)
+
+
+class InputsMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tool_node = tool_xml.find("./inputs")
+ if tool_node is None:
+ tool_node = tool_xml.getroot()
+ num_inputs = len(tool_xml.findall("./inputs//param"))
+ if num_inputs == 0 and not is_datasource(tool_xml):
+ lint_ctx.warn("Found no input parameters.", linter=cls.name(), node=tool_node)
+
+
+class InputsMissingDataSource(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tool_node = tool_xml.find("./inputs")
+ if tool_node is None:
+ tool_node = tool_xml.getroot()
+ num_inputs = len(tool_xml.findall("./inputs//param"))
+ if num_inputs == 0 and is_datasource(tool_xml):
+ lint_ctx.info("No input parameters, OK for data sources", linter=cls.name(), node=tool_node)
+
-def lint_inputs(tool_source: "ToolSource", lint_ctx: "LintContext"):
- """Lint parameters in a tool's inputs block."""
- tool_xml = getattr(tool_source, "xml_tree", None)
- if tool_xml is None:
- return
- profile = tool_source.parse_profile()
- datasource = is_datasource(tool_xml)
- input_names = set()
- inputs = tool_xml.findall("./inputs//param")
- # determine node to report for general problems with outputs
- tool_node = tool_xml.find("./inputs")
- if tool_node is None:
- tool_node = tool_xml.getroot()
- num_inputs = 0
- for param in inputs:
- num_inputs += 1
- param_attrib = param.attrib
- if "name" not in param_attrib and "argument" not in param_attrib:
- lint_ctx.error("Found param input with no name specified.", node=param)
+class InputsDatasourceTags(Linter):
+ """
+ Lint that datasource tools have display and uihints tags
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tool_node = tool_xml.find("./inputs")
+ if tool_node is None:
+ tool_node = tool_xml.getroot()
+ inputs = tool_xml.findall("./inputs//param")
+ if is_datasource(tool_xml):
+ # TODO only display is subtag of inputs, uihints is a separate top level tag (supporting only attrib minwidth)
+ for datasource_tag in ("display", "uihints"):
+ if not any(param.tag == datasource_tag for param in inputs):
+ lint_ctx.info(
+ f"{datasource_tag} tag usually present in data sources", linter=cls.name(), node=tool_node
+ )
+
+
+def _iter_param(tool_xml: "ElementTree") -> Iterator[Tuple["Element", str]]:
+ for param in tool_xml.findall("./inputs//param"):
+ if "name" not in param.attrib and "argument" not in param.attrib:
continue
- param_name = _parse_name(param_attrib.get("name"), param_attrib.get("argument"))
- if "name" in param_attrib and "argument" in param_attrib:
- if param_attrib.get("name") == _parse_name(None, param_attrib.get("argument")):
- lint_ctx.warn(
- f"Param input [{param_name}] 'name' attribute is redundant if argument implies the same name.",
- node=param,
- )
- if param_name.strip() == "":
- lint_ctx.error("Param input with empty name.", node=param)
- elif not is_valid_cheetah_placeholder(param_name):
- lint_ctx.warn(f"Param input [{param_name}] is not a valid Cheetah placeholder.", node=param)
-
- # check for parameters with duplicate names
- path = [param_name]
- parent = param
- while True:
- parent = parent.getparent()
- if parent.tag == "inputs":
- break
- # parameters of the same name in different when branches are allowed
- # just add the value of the when branch to the path (this also allows
- # that the conditional select has the same name as params in the whens)
- if parent.tag == "when":
- path.append(str(parent.attrib.get("value")))
- else:
- path.append(str(parent.attrib.get("name")))
- path_str = ".".join(reversed(path))
- if path_str in input_names:
- lint_ctx.error(f"Tool defines multiple parameters with the same name: '{path_str}'", node=param)
- input_names.add(path_str)
-
- if "type" not in param_attrib:
- lint_ctx.error(f"Param input [{param_name}] input with no type specified.", node=param)
+ param_name = _parse_name(param.attrib.get("name"), param.attrib.get("argument"))
+ yield param, param_name
+
+
+def _iter_param_type(tool_xml: "ElementTree") -> Iterator[Tuple["Element", str, str]]:
+ for param, param_name in _iter_param(tool_xml):
+ if "type" not in param.attrib:
continue
- elif param_attrib["type"].strip() == "":
- lint_ctx.error(f"Param input [{param_name}] with empty type specified.", node=param)
+ param_type = param.attrib["type"]
+ if param_type == "":
continue
- param_type = param_attrib["type"]
+ yield param, param_name, param_type
+
+
+class InputsName(Linter):
+ """
+ Lint params define a name
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param in tool_xml.findall("./inputs//param"):
+ if "name" not in param.attrib and "argument" not in param.attrib:
+ lint_ctx.error("Found param input with no name specified.", linter=cls.name(), node=param)
+
+
+class InputsNameRedundantArgument(Linter):
+ """
+ Lint params define a name
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param in tool_xml.findall("./inputs//param"):
+ if "name" in param.attrib and "argument" in param.attrib:
+ param_name = _parse_name(param.attrib.get("name"), param.attrib.get("argument"))
+ if param.attrib.get("name") == _parse_name(None, param.attrib.get("argument")):
+ lint_ctx.warn(
+ f"Param input [{param_name}] 'name' attribute is redundant if argument implies the same name.",
+ linter=cls.name(),
+ node=param,
+ )
+
+
+# TODO redundant with InputsNameValid
+class InputsNameEmpty(Linter):
+ """
+ Lint params define a non-empty name
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name in _iter_param(tool_xml):
+ if param_name.strip() == "":
+ lint_ctx.error("Param input with empty name.", linter=cls.name(), node=param)
+
+
+class InputsNameValid(Linter):
+ """
+ Lint params define valid cheetah placeholder
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name in _iter_param(tool_xml):
+ if param_name != "" and not is_valid_cheetah_placeholder(param_name):
+ lint_ctx.warn(
+ f"Param input [{param_name}] is not a valid Cheetah placeholder.", linter=cls.name(), node=param
+ )
- # TODO lint for valid param type - attribute combinations
- # lint for valid param type - child node combinations
- for ptcc in PARAM_TYPE_CHILD_COMBINATIONS:
- if param.find(ptcc[0]) is not None and param_type not in ptcc[1]:
+def _param_path(param: "Element", param_name: str) -> str:
+ path = [param_name]
+ parent = param
+ while True:
+ parent = parent.getparent()
+ if parent.tag == "inputs":
+ break
+ # parameters of the same name in different when branches are allowed
+ # just add the value of the when branch to the path (this also allows
+ # that the conditional select has the same name as params in the whens)
+ if parent.tag == "when":
+ path.append(str(parent.attrib.get("value")))
+ else:
+ path.append(str(parent.attrib.get("name")))
+ return ".".join(reversed(path))
+
+
+class InputsNameDuplicate(Linter):
+ """
+ Lint params with duplicate names
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ input_names = set()
+ for param, param_name in _iter_param(tool_xml):
+ # check for parameters with duplicate names
+ path = _param_path(param, param_name)
+ if path in input_names:
lint_ctx.error(
- f"Parameter [{param_name}] '{ptcc[0]}' tags are only allowed for parameters of type {ptcc[1]}",
- node=param,
+ f"Tool defines multiple parameters with the same name: '{path}'", linter=cls.name(), node=param
)
+ input_names.add(path)
+
+
+class InputsNameDuplicateOutput(Linter):
+ """
+ Lint params names that are also used in outputs
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ input_names = set()
+ for param, param_name in _iter_param(tool_xml):
+ input_names.add(_param_path(param, param_name))
+
+ # check if there is an output with the same name as an input
+ outputs = tool_xml.findall("./outputs/*")
+ for output in outputs:
+ if output.get("name") in input_names:
+ lint_ctx.error(
+ f'Tool defines an output with a name equal to the name of an input: \'{output.get("name")}\'',
+ linter=cls.name(),
+ node=output,
+ )
+
+
+class InputsTypeChildCombination(Linter):
+ """
+ Lint for invalid parameter type child combinations
+ """
- # param type specific linting
- if param_type == "data":
- if "format" not in param_attrib:
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ # lint for valid param type - child node combinations
+ for ptcc in PARAM_TYPE_CHILD_COMBINATIONS:
+ if param.find(ptcc[0]) is not None and param_type not in ptcc[1]:
+ lint_ctx.error(
+ f"Parameter [{param_name}] '{ptcc[0]}' tags are only allowed for parameters of type {ptcc[1]}",
+ linter=cls.name(),
+ node=param,
+ )
+
+
+class InputsDataFormat(Linter):
+ """
+ Lint for data params wo format
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "data":
+ continue
+ if "format" not in param.attrib:
lint_ctx.warn(
- f"Param input [{param_name}] with no format specified - 'data' format will be assumed.", node=param
+ f"Param input [{param_name}] with no format specified - 'data' format will be assumed.",
+ linter=cls.name(),
+ node=param,
)
+
+
+class InputsDataOptionsMultiple(Linter):
+ """
+ Lint for data params with multiple options tags
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "data":
+ continue
options = param.findall("./options")
- has_options_filter_attribute = False
- if len(options) == 1:
- for oa in options[0].attrib:
- if oa == "options_filter_attribute":
- has_options_filter_attribute = True
- else:
- lint_ctx.error(f"Data parameter [{param_name}] uses invalid attribute: {oa}", node=param)
- elif len(options) > 1:
- lint_ctx.error(f"Data parameter [{param_name}] contains multiple options elements.", node=options[1])
- # for data params only filters with key='build' of type='data_meta' are allowed
- filters = param.findall("./options/filter")
- for f in filters:
- if not f.get("ref"):
+ if len(options) > 1:
+ lint_ctx.error(
+ f"Data parameter [{param_name}] contains multiple options elements.",
+ linter=cls.name(),
+ node=options[1],
+ )
+
+
+class InputsDataOptionsAttrib(Linter):
+ """
+ Lint for data params with options that have invalid attributes
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "data":
+ continue
+ options = param.find("./options")
+ if options is None:
+ continue
+ for oa in options.attrib:
+ if oa != "options_filter_attribute":
lint_ctx.error(
- f"Data parameter [{param_name}] filter needs to define a ref attribute",
- node=f,
+ f"Data parameter [{param_name}] uses invalid attribute: {oa}", linter=cls.name(), node=param
)
- if has_options_filter_attribute:
- if f.get("type") != "data_meta":
+
+
+class InputsDataOptionsFilterAttribFiltersType(Linter):
+ """
+ Lint for valid filter types for data parameters
+ if options set options_filter_attribute
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "data":
+ continue
+ options = param.find("./options")
+ if options is None:
+ continue
+ for filter in param.findall("./options/filter"):
+ if "options_filter_attribute" in options.attrib:
+ if filter.get("type") != "data_meta":
lint_ctx.error(
- f'Data parameter [{param_name}] for filters only type="data_meta" is allowed, found type="{f.get("type")}"',
- node=f,
+ f'Data parameter [{param_name}] for filters only type="data_meta" is allowed, found type="{filter.get("type")}"',
+ linter=cls.name(),
+ node=filter,
)
- else:
- if f.get("key") != "dbkey" or f.get("type") != "data_meta":
+
+
+class InputsDataOptionsFiltersType(Linter):
+ """
+ Lint for valid filter types for data parameters
+ if options do not set options_filter_attribute
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "data":
+ continue
+ options = param.find("./options")
+ if options is None:
+ continue
+ for filter in param.findall("./options/filter"):
+ if "options_filter_attribute" not in options.attrib:
+ if filter.get("key") != "dbkey" or filter.get("type") != "data_meta":
lint_ctx.error(
- f'Data parameter [{param_name}] for filters only type="data_meta" and key="dbkey" are allowed, found type="{f.get("type")}" and key="{f.get("key")}"',
- node=f,
+ f'Data parameter [{param_name}] for filters only type="data_meta" and key="dbkey" are allowed, found type="{filter.get("type")}" and key="{filter.get("key")}"',
+ linter=cls.name(),
+ node=filter,
)
- elif param_type == "select":
- # get dynamic/statically defined options
- dynamic_options = param.get("dynamic_options", None)
- options = param.findall("./options")
- filters = param.findall("./options/filter")
- select_options = param.findall("./option")
+class InputsDataOptionsFiltersRef(Linter):
+ """
+ Lint for set ref for filters of data parameters
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "data":
+ continue
+ options = param.find("./options")
+ if options is None:
+ continue
+ for filter in param.findall("./options/filter"):
+ if not filter.get("ref"):
+ lint_ctx.error(
+ f"Data parameter [{param_name}] filter needs to define a ref attribute",
+ linter=cls.name(),
+ node=filter,
+ )
+
+
+class InputsSelectDynamicOptions(Linter):
+ """
+ Lint for select with deprecated dynamic_options attribute
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ dynamic_options = param.get("dynamic_options", None)
if dynamic_options is not None:
lint_ctx.warn(
- f"Select parameter [{param_name}] uses deprecated 'dynamic_options' attribute.", node=param
+ f"Select parameter [{param_name}] uses deprecated 'dynamic_options' attribute.",
+ linter=cls.name(),
+ node=param,
)
- # check if options are defined by exactly one possibility
+
+class InputsSelectOptionsDef(Linter):
+ """
+ Lint for valid ways to define select options
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ dynamic_options = param.get("dynamic_options", None)
+ options = param.findall("./options")
+ select_options = param.findall("./option")
if param.getparent().tag != "conditional":
if (dynamic_options is not None) + (len(options) > 0) + (len(select_options) > 0) != 1:
lint_ctx.error(
f"Select parameter [{param_name}] options have to be defined by either 'option' children elements, a 'options' element or the 'dynamic_options' attribute.",
+ linter=cls.name(),
node=param,
)
- else:
- if len(select_options) == 0:
+
+
+class InputsSelectOptionsDefConditional(Linter):
+ """
+ Lint for valid ways to define select options
+ (for a select in a conditional)
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ dynamic_options = param.get("dynamic_options", None)
+ options = param.findall("./options")
+ select_options = param.findall("./option")
+ if param.getparent().tag == "conditional":
+ if len(select_options) == 0 or dynamic_options is not None or len(options) > 0:
lint_ctx.error(
f"Select parameter of a conditional [{param_name}] options have to be defined by 'option' children elements.",
+ linter=cls.name(),
node=param,
)
- # lint dynamic options
- if len(options) == 1:
- filters = options[0].findall("./filter")
- # lint filters
- # TODO check if dataset is available for filters referring other datasets
- filter_adds_options = False
- for f in filters:
- ftype = f.get("type", None)
- if ftype is None:
- lint_ctx.error(f"Select parameter [{param_name}] contains filter without type.", node=f)
- continue
- if ftype not in FILTER_TYPES:
- lint_ctx.error(
- f"Select parameter [{param_name}] contains filter with unknown type '{ftype}'.", node=f
- )
- continue
- if ftype in ["add_value", "data_meta"]:
- filter_adds_options = True
- # TODO more linting of filters
- from_file = options[0].get("from_file", None)
- from_parameter = options[0].get("from_parameter", None)
- from_dataset = options[0].get("from_dataset", None)
- from_data_table = options[0].get("from_data_table", None)
- # TODO check if input param is present for from_dataset
+# TODO xsd
+class InputsSelectOptionValueMissing(Linter):
+ """
+ Lint for select option tags without value
+ """
- if (
- from_file is None
- and from_parameter is None
- and from_dataset is None
- and from_data_table is None
- and not filter_adds_options
- ):
- lint_ctx.error(
- f"Select parameter [{param_name}] options tag defines no options. Use 'from_dataset', 'from_data_table', or a filter that adds values.",
- node=options[0],
- )
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ select_options = param.findall("./option")
+ if any("value" not in option.attrib for option in select_options):
+ lint_ctx.error(
+ f"Select parameter [{param_name}] has option without value", linter=cls.name(), node=param
+ )
- for deprecated_attr in ["from_file", "from_parameter", "options_filter_attribute", "transform_lines"]:
- if options[0].get(deprecated_attr) is not None:
- lint_ctx.warn(
- f"Select parameter [{param_name}] options uses deprecated '{deprecated_attr}' attribute.",
- node=options[0],
- )
- if from_dataset is not None and from_data_table is not None:
- lint_ctx.error(
- f"Select parameter [{param_name}] options uses 'from_dataset' and 'from_data_table' attribute.",
- node=options[0],
- )
+class InputsSelectOptionDuplicateValue(Linter):
+ """
+ Lint for select option with same value
+ """
- if options[0].get("meta_file_key", None) is not None and from_dataset is None:
- lint_ctx.error(
- f"Select parameter [{param_name}] 'meta_file_key' is only compatible with 'from_dataset'.",
- node=options[0],
- )
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ select_options = param.findall("./option")
+ select_options_values = list()
+ for option in select_options:
+ value = option.attrib.get("value", "")
+ select_options_values.append((value, option.attrib.get("selected", "false")))
+ if len(set(select_options_values)) != len(select_options_values):
+ lint_ctx.error(
+ f"Select parameter [{param_name}] has multiple options with the same value",
+ linter=cls.name(),
+ node=param,
+ )
- elif len(options) > 1:
- lint_ctx.error(f"Select parameter [{param_name}] contains multiple options elements.", node=options[1])
- # lint statically defined options
- if any("value" not in option.attrib for option in select_options):
- lint_ctx.error(f"Select parameter [{param_name}] has option without value", node=param)
- if any(option.text is None for option in select_options):
- lint_ctx.warn(f"Select parameter [{param_name}] has option without text", node=param)
+class InputsSelectOptionDuplicateText(Linter):
+ """
+ Lint for select option with same text
+ """
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ select_options = param.findall("./option")
select_options_texts = list()
- select_options_values = list()
for option in select_options:
- value = option.attrib.get("value", "")
if option.text is None:
- text = value.capitalize()
+ text = option.attrib.get("value", "").capitalize()
else:
text = option.text
select_options_texts.append((text, option.attrib.get("selected", "false")))
- select_options_values.append((value, option.attrib.get("selected", "false")))
if len(set(select_options_texts)) != len(select_options_texts):
lint_ctx.error(
- f"Select parameter [{param_name}] has multiple options with the same text content", node=param
+ f"Select parameter [{param_name}] has multiple options with the same text content",
+ linter=cls.name(),
+ node=param,
)
- if len(set(select_options_values)) != len(select_options_values):
- lint_ctx.error(f"Select parameter [{param_name}] has multiple options with the same value", node=param)
- if param_type == "boolean":
- truevalue = param_attrib.get("truevalue", "true")
- falsevalue = param_attrib.get("falsevalue", "false")
+
+class InputsSelectOptionsMultiple(Linter):
+ """
+ Lint dynamic options select for multiple options tags
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ options = param.findall("./options")
+ if len(options) > 1:
+ lint_ctx.error(
+ f"Select parameter [{param_name}] contains multiple options elements.",
+ linter=cls.name(),
+ node=options[1],
+ )
+
+
+class InputsSelectOptionsDefinesOptions(Linter):
+ """
+ Lint dynamic options select for the potential to define options
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ options = param.find("./options")
+ if options is None:
+ continue
+ filter_adds_options = any(
+ [f.get("type", None) in ["add_value", "data_meta"] for f in param.findall("./options/filter")]
+ )
+ from_file = options.get("from_file", None)
+ from_parameter = options.get("from_parameter", None)
+ # TODO check if input param is present for from_dataset
+ from_dataset = options.get("from_dataset", None)
+ from_data_table = options.get("from_data_table", None)
+
+ if (
+ from_file is None
+ and from_parameter is None
+ and from_dataset is None
+ and from_data_table is None
+ and not filter_adds_options
+ ):
+ lint_ctx.error(
+ f"Select parameter [{param_name}] options tag defines no options. Use 'from_dataset', 'from_data_table', or a filter that adds values.",
+ linter=cls.name(),
+ node=options,
+ )
+
+
+class InputsSelectOptionsDeprecatedAttr(Linter):
+ """
+ Lint dynamic options select deprecated attributes
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ options = param.find("./options")
+ if options is None:
+ continue
+ for deprecated_attr in ["from_file", "from_parameter", "options_filter_attribute", "transform_lines"]:
+ if options.get(deprecated_attr) is not None:
+ lint_ctx.warn(
+ f"Select parameter [{param_name}] options uses deprecated '{deprecated_attr}' attribute.",
+ linter=cls.name(),
+ node=options,
+ )
+
+
+class InputsSelectOptionsFromDatasetAndDatatable(Linter):
+ """
+ Lint dynamic options select for from_dataset and from_data_table
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ options = param.find("./options")
+ if options is None:
+ continue
+ from_dataset = options.get("from_dataset", None)
+ from_data_table = options.get("from_data_table", None)
+ if from_dataset is not None and from_data_table is not None:
+ lint_ctx.error(
+ f"Select parameter [{param_name}] options uses 'from_dataset' and 'from_data_table' attribute.",
+ linter=cls.name(),
+ node=options,
+ )
+
+
+class InputsSelectOptionsMetaFileKey(Linter):
+ """
+ Lint dynamic options select: meta_file_key attribute can only be used with from_dataset
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "select":
+ continue
+ options = param.find("./options")
+ if options is None:
+ continue
+ from_dataset = options.get("from_dataset", None)
+ if options.get("meta_file_key", None) is not None and from_dataset is None:
+ lint_ctx.error(
+ f"Select parameter [{param_name}] 'meta_file_key' is only compatible with 'from_dataset'.",
+ linter=cls.name(),
+ node=options,
+ )
+
+
+class InputsBoolDistinctValues(Linter):
+ """
+ Lint booleans for distinct true/false value
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "boolean":
+ continue
+ profile = tool_source.parse_profile()
+ truevalue = param.attrib.get("truevalue", "true")
+ falsevalue = param.attrib.get("falsevalue", "false")
problematic_booleans_allowed = profile < "23.1"
lint_level = lint_ctx.warn if problematic_booleans_allowed else lint_ctx.error
if truevalue == falsevalue:
lint_level(
- f"Boolean parameter [{param_name}] needs distinct 'truevalue' and 'falsevalue' values.", node=param
+ f"Boolean parameter [{param_name}] needs distinct 'truevalue' and 'falsevalue' values.",
+ linter=cls.name(),
+ node=param,
)
+
+
+class InputsBoolProblematic(Linter):
+ """
+ Lint booleans for problematic values, i.e. truevalue being false and vice versa
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type != "boolean":
+ continue
+ profile = tool_source.parse_profile()
+ truevalue = param.attrib.get("truevalue", "true")
+ falsevalue = param.attrib.get("falsevalue", "false")
+ problematic_booleans_allowed = profile < "23.1"
+ lint_level = lint_ctx.warn if problematic_booleans_allowed else lint_ctx.error
if truevalue.lower() == "false":
- lint_level(f"Boolean parameter [{param_name}] has invalid truevalue [{truevalue}].", node=param)
+ lint_level(
+ f"Boolean parameter [{param_name}] has invalid truevalue [{truevalue}].",
+ linter=cls.name(),
+ node=param,
+ )
if falsevalue.lower() == "true":
- lint_level(f"Boolean parameter [{param_name}] has invalid falsevalue [{falsevalue}].", node=param)
+ lint_level(
+ f"Boolean parameter [{param_name}] has invalid falsevalue [{falsevalue}].",
+ linter=cls.name(),
+ node=param,
+ )
+
+
+class InputsSelectSingleCheckboxes(Linter):
+ """
+ Lint selects that allow only a single selection but display as checkboxes
+ """
- if param_type in ["select", "data_column", "drill_down"]:
- multiple = string_as_bool(param_attrib.get("multiple", "false"))
- optional = string_as_bool(param_attrib.get("optional", multiple))
- if param_attrib.get("display") == "checkboxes":
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type not in ["select", "data_column", "drill_down"]:
+ continue
+ multiple = string_as_bool(param.attrib.get("multiple", "false"))
+ if param.attrib.get("display") == "checkboxes":
if not multiple:
lint_ctx.error(
f'Select [{param_name}] `display="checkboxes"` is incompatible with `multiple="false"`, remove the `display` attribute',
+ linter=cls.name(),
node=param,
)
+
+
+class InputsSelectMandatoryCheckboxes(Linter):
+ """
+ Lint selects that are mandatory but display as checkboxes
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type not in ["select", "data_column", "drill_down"]:
+ continue
+ multiple = string_as_bool(param.attrib.get("multiple", "false"))
+ optional = string_as_bool(param.attrib.get("optional", multiple))
+ if param.attrib.get("display") == "checkboxes":
if not optional:
lint_ctx.error(
f'Select [{param_name}] `display="checkboxes"` is incompatible with `optional="false"`, remove the `display` attribute',
+ linter=cls.name(),
node=param,
)
- if param_attrib.get("display") == "radio":
+
+
+class InputsSelectMultipleRadio(Linter):
+ """
+ Lint selects that allow only multiple selections but display as radio
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type not in ["select", "data_column", "drill_down"]:
+ continue
+ multiple = string_as_bool(param.attrib.get("multiple", "false"))
+ if param.attrib.get("display") == "radio":
if multiple:
lint_ctx.error(
- f'Select [{param_name}] display="radio" is incompatible with multiple="true"', node=param
+ f'Select [{param_name}] display="radio" is incompatible with multiple="true"',
+ linter=cls.name(),
+ node=param,
)
+
+
+class InputsSelectOptionalRadio(Linter):
+ """
+ Lint selects that are optional but display as radio
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param, param_name, param_type in _iter_param_type(tool_xml):
+ if param_type not in ["select", "data_column", "drill_down"]:
+ continue
+ multiple = string_as_bool(param.attrib.get("multiple", "false"))
+ optional = string_as_bool(param.attrib.get("optional", multiple))
+ if param.attrib.get("display") == "radio":
if optional:
lint_ctx.error(
- f'Select [{param_name}] display="radio" is incompatible with optional="true"', node=param
+ f'Select [{param_name}] display="radio" is incompatible with optional="true"',
+ linter=cls.name(),
+ node=param,
)
- # TODO: Validate type, much more...
- # lint validators
- # TODO check if dataset is available for validators referring other datasets
- validators = param.findall("./validator")
+
+def _iter_param_validator(tool_xml: "ElementTree") -> Iterator[Tuple[str, str, "Element", str]]:
+ input_params = tool_xml.findall("./inputs//param[@type]")
+ for param in input_params:
+ try:
+ param_name = _parse_name(param.attrib.get("name"), param.attrib.get("argument"))
+ except ValueError:
+ continue
+ param_type = param.attrib["type"]
+ validators = param.findall("./validator[@type]")
for validator in validators:
vtype = validator.attrib["type"]
+ yield (param_name, param_type, validator, vtype)
+
+
+class ValidatorParamIncompatible(Linter):
+ """
+ Lint for validator type - parameter type incompatibilities
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, param_type, validator, vtype in _iter_param_validator(tool_xml):
if param_type in PARAMETER_VALIDATOR_TYPE_COMPATIBILITY:
if vtype not in PARAMETER_VALIDATOR_TYPE_COMPATIBILITY[param_type]:
lint_ctx.error(
- f"Parameter [{param_name}]: validator with an incompatible type '{vtype}'", node=validator
+ f"Parameter [{param_name}]: validator with an incompatible type '{vtype}'",
+ linter=cls.name(),
+ node=validator,
)
+
+
+class ValidatorAttribIncompatible(Linter):
+ """
+ Lint for incompatibilities between validator type and given attributes
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
for attrib in ATTRIB_VALIDATOR_COMPATIBILITY:
if attrib in validator.attrib and vtype not in ATTRIB_VALIDATOR_COMPATIBILITY[attrib]:
lint_ctx.error(
f"Parameter [{param_name}]: attribute '{attrib}' is incompatible with validator of type '{vtype}'",
+ linter=cls.name(),
node=validator,
)
+
+
+class ValidatorHasText(Linter):
+ """
+ Lint that parameters that need text have text
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
if vtype in ["expression", "regex"]:
if validator.text is None:
lint_ctx.error(
- f"Parameter [{param_name}]: {vtype} validators are expected to contain text", node=validator
+ f"Parameter [{param_name}]: {vtype} validators are expected to contain text",
+ linter=cls.name(),
+ node=validator,
)
- else:
+
+
+class ValidatorHasNoText(Linter):
+ """
+ Lint that parameters that need no text have no text
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
+ if vtype not in ["expression", "regex"] and validator.text is not None:
+ lint_ctx.warn(
+ f"Parameter [{param_name}]: '{vtype}' validators are not expected to contain text (found '{validator.text}')",
+ linter=cls.name(),
+ node=validator,
+ )
+
+
+class ValidatorExpression(Linter):
+ """
+ Lint that checks expressions / regexp (ignoring FutureWarning)
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ with warnings.catch_warnings():
+ warnings.simplefilter("error", FutureWarning)
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
+ if vtype in ["expression", "regex"] and validator.text is not None:
try:
if vtype == "regex":
re.compile(validator.text)
else:
ast.parse(validator.text, mode="eval")
+ except FutureWarning:
+ pass
except Exception as e:
lint_ctx.error(
- f"Parameter [{param_name}]: '{validator.text}' is no valid regular expression: {str(e)}",
+ f"Parameter [{param_name}]: '{validator.text}' is no valid {vtype}: {str(e)}",
+ linter=cls.name(),
node=validator,
)
- if vtype not in ["expression", "regex"] and validator.text is not None:
- lint_ctx.warn(
- f"Parameter [{param_name}]: '{vtype}' validators are not expected to contain text (found '{validator.text}')",
- node=validator,
- )
+
+
+class ValidatorExpressionFuture(Linter):
+ """
+ Lint that checks expressions / regexp FutureWarnings
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ with warnings.catch_warnings():
+ warnings.simplefilter("error", FutureWarning)
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
+ if vtype in ["expression", "regex"] and validator.text is not None:
+ try:
+ if vtype == "regex":
+ re.compile(validator.text)
+ else:
+ ast.parse(validator.text, mode="eval")
+ except FutureWarning as e:
+ lint_ctx.warn(
+ f"Parameter [{param_name}]: '{validator.text}' is marked as deprecated {vtype}: {str(e)}",
+ linter=cls.name(),
+ node=validator,
+ )
+ except Exception:
+ pass
+
+
+class ValidatorMinMax(Linter):
+ """
+ Lint for min/max for validator that need it
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
if vtype in ["in_range", "length", "dataset_metadata_in_range"] and (
"min" not in validator.attrib and "max" not in validator.attrib
):
lint_ctx.error(
f"Parameter [{param_name}]: '{vtype}' validators need to define the 'min' or 'max' attribute(s)",
+ linter=cls.name(),
node=validator,
)
+
+
+class ValidatorDatasetMetadataEqualValue(Linter):
+ """
+ Lint dataset_metadata_equal needs value or value_json
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
if vtype in ["dataset_metadata_equal"]:
if (
not ("value" in validator.attrib or "value_json" in validator.attrib)
@@ -432,19 +1140,61 @@ def lint_inputs(tool_source: "ToolSource", lint_ctx: "LintContext"):
):
lint_ctx.error(
f"Parameter [{param_name}]: '{vtype}' validators need to define the 'value'/'value_json' and 'metadata_name' attributes",
+ linter=cls.name(),
node=validator,
)
+
+
+class ValidatorDatasetMetadataEqualValueOrJson(Linter):
+ """
+ Lint dataset_metadata_equal needs either value or value_json
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
+ if vtype in ["dataset_metadata_equal"]:
if "value" in validator.attrib and "value_json" in validator.attrib:
lint_ctx.error(
f"Parameter [{param_name}]: '{vtype}' validators must not define the 'value' and the 'value_json' attributes",
+ linter=cls.name(),
node=validator,
)
+
+class ValidatorMetadataCheckSkip(Linter):
+ """
+ Lint metadata needs check or skip
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
if vtype in ["metadata"] and ("check" not in validator.attrib and "skip" not in validator.attrib):
lint_ctx.error(
f"Parameter [{param_name}]: '{vtype}' validators need to define the 'check' or 'skip' attribute(s)",
+ linter=cls.name(),
node=validator,
)
+
+
+class ValidatorTableName(Linter):
+ """
+ Lint table_name is present if needed
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
if (
vtype
in [
@@ -457,8 +1207,22 @@ def lint_inputs(tool_source: "ToolSource", lint_ctx: "LintContext"):
):
lint_ctx.error(
f"Parameter [{param_name}]: '{vtype}' validators need to define the 'table_name' attribute",
+ linter=cls.name(),
node=validator,
)
+
+
+class ValidatorMetadataName(Linter):
+ """
+ Lint metadata_name is present if needed
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for param_name, _, validator, vtype in _iter_param_validator(tool_xml):
if (
vtype
in [
@@ -471,120 +1235,132 @@ def lint_inputs(tool_source: "ToolSource", lint_ctx: "LintContext"):
):
lint_ctx.error(
f"Parameter [{param_name}]: '{vtype}' validators need to define the 'metadata_name' attribute",
+ linter=cls.name(),
node=validator,
)
- conditional_selects = tool_xml.findall("./inputs//conditional")
- for conditional in conditional_selects:
+
+def _iter_conditional(tool_xml: "ElementTree") -> Iterator[Tuple["Element", Optional[str], "Element", Optional[str]]]:
+ conditionals = tool_xml.findall("./inputs//conditional")
+ for conditional in conditionals:
conditional_name = conditional.get("name")
- if not conditional_name:
- lint_ctx.error("Conditional without a name", node=conditional)
- if conditional.get("value_from"):
- # Probably only the upload tool use this, no children elements
+ if conditional.get("value_from"): # Probably only the upload tool use this, no children elements
continue
- first_param = conditional.findall("param")
- if len(first_param) != 1:
- lint_ctx.error(
- f"Conditional [{conditional_name}] needs exactly one child found {len(first_param)}",
- node=conditional,
- )
+ first_param = conditional.find("param")
+ if first_param is None:
continue
- first_param = first_param[0]
first_param_type = first_param.get("type")
- if first_param_type == "boolean":
- lint_ctx.warn(
- f'Conditional [{conditional_name}] first param of type="boolean" is discouraged, use a select',
- node=first_param,
- )
- elif first_param_type != "select":
- lint_ctx.error(f'Conditional [{conditional_name}] first param should have type="select"', node=first_param)
- continue
+ yield conditional, conditional_name, first_param, first_param_type
- if first_param_type == "select":
- select_options = _find_with_attribute(first_param, "option", "value")
- option_ids = [option.get("value") for option in select_options]
- else: # boolean
- option_ids = [first_param.get("truevalue", "true"), first_param.get("falsevalue", "false")]
- for incomp in ["optional", "multiple"]:
- if string_as_bool(first_param.get(incomp, False)):
+class ConditionalParamTypeBool(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for _, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml):
+ if first_param_type == "boolean":
lint_ctx.warn(
- f'Conditional [{conditional_name}] test parameter cannot be {incomp}="true"', node=first_param
+ f'Conditional [{conditional_name}] first param of type="boolean" is discouraged, use a select',
+ linter=cls.name(),
+ node=first_param,
)
- whens = conditional.findall("./when")
- if any("value" not in when.attrib for when in whens):
- lint_ctx.error(f"Conditional [{conditional_name}] when without value", node=conditional)
-
- when_ids = [w.get("value") for w in whens if w.get("value") is not None]
- for option_id in option_ids:
- if option_id not in when_ids:
- lint_ctx.warn(
- f"Conditional [{conditional_name}] no block found for {first_param_type} option '{option_id}'",
- node=conditional,
+class ConditionalParamType(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for _, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml):
+ if first_param_type not in ["boolean", "select"]:
+ lint_ctx.error(
+ f'Conditional [{conditional_name}] first param should have type="select"',
+ linter=cls.name(),
+ node=first_param,
)
- for when_id in when_ids:
- if when_id not in option_ids:
- if first_param_type == "select":
- lint_ctx.warn(
- f"Conditional [{conditional_name}] no found for when block '{when_id}'",
- node=conditional,
- )
- else:
+
+class ConditionalParamIncompatibleAttributes(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for _, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml):
+ if first_param_type not in ["boolean", "select"]:
+ continue
+ for incomp in ["optional", "multiple"]:
+ if string_as_bool(first_param.get(incomp, False)):
lint_ctx.warn(
- f"Conditional [{conditional_name}] no truevalue/falsevalue found for when block '{when_id}'",
- node=conditional,
+ f'Conditional [{conditional_name}] test parameter cannot be {incomp}="true"',
+ linter=cls.name(),
+ node=first_param,
)
- if datasource:
- # TODO only display is subtag of inputs, uihints is a separate top level tag (supporting only attrib minwidth)
- for datasource_tag in ("display", "uihints"):
- if not any(param.tag == datasource_tag for param in inputs):
- lint_ctx.info(f"{datasource_tag} tag usually present in data sources", node=tool_node)
-
- if num_inputs:
- lint_ctx.info(f"Found {num_inputs} input parameters.", node=tool_node)
- else:
- if datasource:
- lint_ctx.info("No input parameters, OK for data sources", node=tool_node)
- else:
- lint_ctx.warn("Found no input parameters.", node=tool_node)
- # check if there is an output with the same name as an input
- outputs = tool_xml.find("./outputs")
- if outputs is not None:
- for output in outputs:
- if output.get("name") in input_names:
- lint_ctx.error(
- f'Tool defines an output with a name equal to the name of an input: \'{output.get("name")}\'',
- node=output,
+class ConditionalWhenMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for conditional, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml):
+ if first_param_type not in ["boolean", "select"]:
+ continue
+ if first_param_type == "select":
+ options = first_param.findall("./option[@value]")
+ option_ids = set([option.get("value") for option in options])
+ else: # boolean
+ option_ids = set([first_param.get("truevalue", "true"), first_param.get("falsevalue", "false")])
+ whens = conditional.findall("./when[@value]")
+ when_ids = set([w.get("value") for w in whens if w.get("value") is not None])
+ for option_id in option_ids - when_ids:
+ lint_ctx.warn(
+ f"Conditional [{conditional_name}] no block found for {first_param_type} option '{option_id}'",
+ linter=cls.name(),
+ node=conditional,
)
-def lint_repeats(tool_source: "ToolSource", lint_ctx):
- """Lint repeat blocks in tool inputs."""
- tool_xml = getattr(tool_source, "xml_tree", None)
- if tool_xml is None:
- return
- repeats = tool_xml.findall("./inputs//repeat")
- for repeat in repeats:
- if "name" not in repeat.attrib:
- lint_ctx.error("Repeat does not specify name attribute.", node=repeat)
- if "title" not in repeat.attrib:
- lint_ctx.error("Repeat does not specify title attribute.", node=repeat)
+class ConditionalOptionMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for conditional, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml):
+ if first_param_type != "select":
+ continue
+ options = first_param.findall("./option[@value]")
+ option_ids = set([option.get("value") for option in options])
+ whens = conditional.findall("./when[@value]")
+ when_ids = set([w.get("value") for w in whens if w.get("value") is not None])
+ for when_id in when_ids - option_ids:
+ lint_ctx.warn(
+ f"Conditional [{conditional_name}] no found for when block '{when_id}'",
+ linter=cls.name(),
+ node=conditional,
+ )
-def _find_with_attribute(element, tag, attribute, test_value=None):
- rval = []
- for el in element.findall(f"./{tag}") or []:
- if attribute not in el.attrib:
- continue
- value = el.attrib[attribute]
- if test_value is not None:
- if value == test_value:
- rval.append(el)
- else:
- rval.append(el)
- return rval
+class ConditionalOptionMissingBoolean(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for conditional, conditional_name, first_param, first_param_type in _iter_conditional(tool_xml):
+ if first_param_type != "boolean":
+ continue
+ option_ids = set([first_param.get("truevalue", "true"), first_param.get("falsevalue", "false")])
+ whens = conditional.findall("./when[@value]")
+ when_ids = set([w.get("value") for w in whens if w.get("value")])
+ for when_id in when_ids - option_ids:
+ lint_ctx.warn(
+ f"Conditional [{conditional_name}] no truevalue/falsevalue found for when block '{when_id}'",
+ linter=cls.name(),
+ node=conditional,
+ )
diff --git a/lib/galaxy/tool_util/linters/outputs.py b/lib/galaxy/tool_util/linters/outputs.py
index bfb9e1674fd5..dc3efbb70c12 100644
--- a/lib/galaxy/tool_util/linters/outputs.py
+++ b/lib/galaxy/tool_util/linters/outputs.py
@@ -4,131 +4,227 @@
from packaging.version import Version
-from galaxy.util import (
- etree,
- string_as_bool,
-)
+from galaxy.tool_util.lint import Linter
from ._util import is_valid_cheetah_placeholder
from ..parser.output_collection_def import NAMED_PATTERNS
if TYPE_CHECKING:
from galaxy.tool_util.lint import LintContext
from galaxy.tool_util.parser import ToolSource
+ from galaxy.util.etree import (
+ Element,
+ ElementTree,
+ )
-def lint_output(tool_source: "ToolSource", lint_ctx: "LintContext"):
- """Check output elements, ensure there is at least one and check attributes."""
- tool_xml = getattr(tool_source, "xml_tree", None)
- if tool_xml is None:
- return
- profile = tool_source.parse_profile()
-
- outputs = tool_xml.findall("./outputs")
- # determine node to report for general problems with outputs
- tool_node = tool_xml.find("./outputs")
- if tool_node is None:
- tool_node = tool_xml.getroot()
- if len(outputs) == 0:
- lint_ctx.warn("Tool contains no outputs section, most tools should produce outputs.", node=tool_node)
- return
- if len(outputs) > 1:
- lint_ctx.warn("Tool contains multiple output sections, behavior undefined.", node=outputs[1])
- num_outputs = 0
- labels = set()
- names = set()
- for output in list(outputs[0]):
- if output.tag is etree.Comment:
- continue
- if output.tag not in ["data", "collection"]:
- lint_ctx.warn(f"Unknown element found in outputs [{output.tag}]", node=output)
- continue
- num_outputs += 1
- if "name" not in output.attrib:
- lint_ctx.warn("Tool output doesn't define a name - this is likely a problem.", node=output)
- # TODO make this an error if there is no discover_datasets / from_work_dir (is this then still a problem)
- elif not is_valid_cheetah_placeholder(output.attrib["name"]):
+class OutputsMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tool_node = tool_xml.find("./outputs")
+ if tool_node is None:
+ tool_node = tool_xml.getroot()
+ if len(tool_xml.findall("./outputs")) == 0:
lint_ctx.warn(
- f'Tool output name [{output.attrib["name"]}] is not a valid Cheetah placeholder.', node=output
+ "Tool contains no outputs section, most tools should produce outputs.",
+ linter=cls.name(),
+ node=tool_node,
)
- name = output.attrib.get("name")
- if name is not None:
+
+class OutputsOutput(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ output = tool_xml.find("./outputs/output")
+ if output is not None:
+ lint_ctx.warn(
+ "Avoid the use of 'output' and replace by 'data' or 'collection'", linter=cls.name(), node=output
+ )
+
+
+class OutputsNameInvalidCheetah(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for output in tool_xml.findall("./outputs/data[@name]") + tool_xml.findall("./outputs/collection[@name]"):
+ if not is_valid_cheetah_placeholder(output.attrib["name"]):
+ lint_ctx.warn(
+ f'Tool output name [{output.attrib["name"]}] is not a valid Cheetah placeholder.',
+ linter=cls.name(),
+ node=output,
+ )
+
+
+class OutputsNameDuplicated(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ names = set()
+ for output in tool_xml.findall("./outputs/data[@name]") + tool_xml.findall("./outputs/collection[@name]"):
+ name = output.attrib["name"]
if name in names:
- lint_ctx.error(f"Tool output [{name}] has duplicated name", node=output)
+ lint_ctx.error(f"Tool output [{name}] has duplicated name", linter=cls.name(), node=output)
names.add(name)
- label = output.attrib.get("label", "${tool.name} on ${on_string}")
- if label in labels:
- filter_node = output.find(".//filter")
- if filter_node is not None:
+
+class OutputsLabelDuplicatedFilter(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ labels = set()
+ for output in tool_xml.findall("./outputs/data") + tool_xml.findall("./outputs/collection"):
+ name = output.attrib.get("name", "")
+ label = output.attrib.get("label", "${tool.name} on ${on_string}")
+ if label in labels and output.find(".//filter") is not None:
lint_ctx.warn(
f"Tool output [{name}] uses duplicated label '{label}', double check if filters imply disjoint cases",
+ linter=cls.name(),
node=output,
)
- else:
- lint_ctx.warn(f"Tool output [{name}] uses duplicated label '{label}'", node=output)
- labels.add(label)
-
- format_set = False
- if __check_format(output, lint_ctx, profile):
- format_set = True
- if output.tag == "data":
- if "auto_format" in output.attrib and output.attrib["auto_format"]:
- format_set = True
+ labels.add(label)
+
+
+class OutputsLabelDuplicatedNoFilter(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ labels = set()
+ for output in tool_xml.findall("./outputs/data[@name]") + tool_xml.findall("./outputs/collection[@name]"):
+ name = output.attrib.get("name", "")
+ label = output.attrib.get("label", "${tool.name} on ${on_string}")
+ if label in labels and output.find(".//filter") is None:
+ lint_ctx.warn(f"Tool output [{name}] uses duplicated label '{label}'", linter=cls.name(), node=output)
+ labels.add(label)
+
- elif output.tag == "collection":
+class OutputsCollectionType(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for output in tool_xml.findall("./outputs/collection"):
if "type" not in output.attrib:
- lint_ctx.warn("Collection output with undefined 'type' found.", node=output)
- if "structured_like" in output.attrib and "inherit_format" in output.attrib:
- format_set = True
- for sub in output:
- if __check_pattern(sub):
- format_set = True
- elif __check_format(sub, lint_ctx, profile, allow_ext=True):
+ lint_ctx.warn("Collection output with undefined 'type' found.", linter=cls.name(), node=output)
+
+
+class OutputsNumber(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ outputs = tool_xml.findall("./outputs")
+ if len(outputs) == 0:
+ return
+ num_outputs = len(outputs[0].findall("./data")) + len(outputs[0].findall("./collection"))
+ lint_ctx.info(f"{num_outputs} outputs found.", linter=cls.name(), node=outputs[0])
+
+
+class OutputsFormatInput(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ def _report(output: "Element"):
+ message = f"Using format='input' on {output.tag} is deprecated. Use the format_source attribute."
+ if Version(str(profile)) <= Version("16.01"):
+ lint_ctx.warn(message, linter=cls.name(), node=output)
+ else:
+ lint_ctx.error(message, linter=cls.name(), node=output)
+
+ profile = tool_source.parse_profile()
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for output in tool_xml.findall("./outputs/data") + tool_xml.findall("./outputs/collection"):
+ fmt = output.attrib.get("format")
+ if fmt == "input":
+ _report(output)
+ for sub in output:
+ fmt = sub.attrib.get("format", sub.attrib.get("ext"))
+ if fmt == "input":
+ _report(output)
+
+
+class OutputsFormat(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for output in tool_xml.findall("./outputs/data") + tool_xml.findall("./outputs/collection"):
+ format_set = False
+ if _check_format(output):
format_set = True
+ if output.tag == "data":
+ if "auto_format" in output.attrib and output.attrib["auto_format"]:
+ format_set = True
- if not format_set:
- lint_ctx.warn(
- f"Tool {output.tag} output {output.attrib.get('name', 'with missing name')} doesn't define an output format.",
- node=output,
- )
+ elif output.tag == "collection":
+ if "structured_like" in output.attrib and "inherit_format" in output.attrib:
+ format_set = True
+ for sub in output:
+ if _check_pattern(sub) or _has_tool_provided_metadata(tool_xml):
+ format_set = True
+ elif _check_format(sub):
+ format_set = True
+
+ if not format_set:
+ lint_ctx.warn(
+ f"Tool {output.tag} output {output.attrib.get('name', 'with missing name')} doesn't define an output format.",
+ linter=cls.name(),
+ node=output,
+ )
+
+
+class OutputsFormatSourceIncomp(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ def _check_and_report(node):
+ if "format_source" in node.attrib and ("ext" in node.attrib or "format" in node.attrib):
+ lint_ctx.warn(
+ f"Tool {node.tag} output '{node.attrib.get('name', 'with missing name')}' should use either format_source or format/ext",
+ linter=cls.name(),
+ node=node,
+ )
- # TODO: check for different labels in case of multiple outputs
- lint_ctx.info(f"{num_outputs} outputs found.", node=outputs[0])
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ for output in tool_xml.findall("./outputs/data") + tool_xml.findall("./outputs/collection"):
+ _check_and_report(output)
+ for sub in output:
+ _check_and_report(sub)
-def __check_format(node, lint_ctx, profile: str, allow_ext=False):
+def _check_format(node):
"""
check if format/ext/format_source attribute is set in a given node
issue a warning if the value is input
return true (node defines format/ext) / false (else)
"""
- if "format_source" in node.attrib and ("ext" in node.attrib or "format" in node.attrib):
- lint_ctx.warn(
- f"Tool {node.tag} output '{node.attrib.get('name', 'with missing name')}' should use either format_source or format/ext",
- node=node,
- )
if "format_source" in node.attrib:
return True
if node.find(".//action[@type='format']") is not None:
return True
# if allowed (e.g. for discover_datasets), ext takes precedence over format
- fmt = None
- if allow_ext:
- fmt = node.attrib.get("ext")
- if fmt is None:
- fmt = node.attrib.get("format")
- if fmt == "input":
- message = f"Using format='input' on {node.tag} is deprecated. Use the format_source attribute."
- if Version(str(profile)) <= Version("16.01"):
- lint_ctx.warn(message, node=node)
- else:
- lint_ctx.error(message, node=node)
-
+ fmt = node.attrib.get("format", node.attrib.get("ext"))
return fmt is not None
-def __check_pattern(node):
+def _check_pattern(node):
"""
check if
- pattern attribute is set and defines the extension or
@@ -136,10 +232,6 @@ def __check_pattern(node):
"""
if node.tag != "discover_datasets":
return False
- if "from_tool_provided_metadata" in node.attrib and string_as_bool(
- node.attrib.get("from_tool_provided_metadata", "false")
- ):
- return True
if "pattern" not in node.attrib:
return False
pattern = node.attrib["pattern"]
@@ -147,3 +239,18 @@ def __check_pattern(node):
# TODO error on wrong pattern or non-regexp
if "(?P" in regex_pattern:
return True
+
+
+def _has_tool_provided_metadata(tool_xml: "ElementTree") -> bool:
+ outputs = tool_xml.find("./outputs")
+ if outputs is not None:
+ if "provided_metadata_file" in outputs.attrib or "provided_metadata_style" in outputs.attrib:
+ return True
+ command = tool_xml.find("./command")
+ if command is not None:
+ if "galaxy.json" in command.text:
+ return True
+ config = tool_xml.find("./configfiles/configfile[@filename='galaxy.json']")
+ if config is not None:
+ return True
+ return False
diff --git a/lib/galaxy/tool_util/linters/stdio.py b/lib/galaxy/tool_util/linters/stdio.py
index 04eec39e7fb8..9de0c5da6152 100644
--- a/lib/galaxy/tool_util/linters/stdio.py
+++ b/lib/galaxy/tool_util/linters/stdio.py
@@ -1,69 +1,82 @@
"""This module contains a linting functions for tool error detection."""
import re
+from typing import TYPE_CHECKING
-from galaxy.util import etree
-from .command import get_command
+from packaging.version import Version
+from galaxy.tool_util.lint import Linter
+
+if TYPE_CHECKING:
+ from galaxy.tool_util.lint import LintContext
+ from galaxy.tool_util.parser.interface import ToolSource
-def lint_stdio(tool_source, lint_ctx):
- tool_xml = getattr(tool_source, "xml_tree", None)
- if not tool_xml:
- # Can only lint XML tools at this point.
- # Should probably use tool_source.parse_stdio() to abstract away XML details
- return
- stdios = tool_xml.findall("./stdio") if tool_xml else []
- # determine node to report for general problems with stdio
- tool_node = tool_xml.find("./stdio")
- if tool_node is None:
- tool_node = tool_xml.getroot()
- if not stdios:
- command = get_command(tool_xml) if tool_xml else None
+class StdIOAbsenceLegacy(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ # Can only lint XML tools at this point.
+ # Should probably use tool_source.parse_stdio() to abstract away XML details
+ return
+ stdios = tool_xml.findall("./stdio") if tool_xml else []
+ if stdios:
+ return
+ tool_node = tool_xml.getroot()
+ command = tool_xml.find("./command")
if command is None or not command.get("detect_errors"):
- if tool_source.parse_profile() <= "16.01":
+ if Version(tool_source.parse_profile()) <= Version("16.01"):
lint_ctx.info(
"No stdio definition found, tool indicates error conditions with output written to stderr.",
+ linter=cls.name(),
node=tool_node,
)
- else:
+
+
+class StdIOAbsence(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ # Can only lint XML tools at this point.
+ # Should probably use tool_source.parse_stdio() to abstract away XML details
+ return
+ stdios = tool_xml.findall("./stdio") if tool_xml else []
+ if stdios:
+ return
+ tool_node = tool_xml.getroot()
+ command = tool_xml.find("./command")
+ if command is None or not command.get("detect_errors"):
+ if Version(tool_source.parse_profile()) > Version("16.01"):
lint_ctx.info(
"No stdio definition found, tool indicates error conditions with non-zero exit codes.",
+ linter=cls.name(),
node=tool_node,
)
- return
-
- if len(stdios) > 1:
- lint_ctx.error("More than one stdio tag found, behavior undefined.", node=stdios[1])
- return
-
- stdio = stdios[0]
- for child in list(stdio):
- if child.tag is etree.Comment:
- continue
- if child.tag == "regex":
- _lint_regex(tool_xml, child, lint_ctx)
- elif child.tag == "exit_code":
- _lint_exit_code(tool_xml, child, lint_ctx)
- else:
- message = "Unknown stdio child tag discovered [%s]. "
- message += "Valid options are exit_code and regex."
- lint_ctx.warn(message % child.tag, node=child)
-def _lint_exit_code(tool_xml, child, lint_ctx):
- for key in child.attrib.keys():
- if key not in ["description", "level", "range"]:
- lint_ctx.warn(f"Unknown attribute [{key}] encountered on exit_code tag.", node=child)
+class StdIORegex(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ # Can only lint XML tools at this point.
+ # Should probably use tool_source.parse_stdio() to abstract away XML details
+ return
+ stdios = tool_xml.findall("./stdio") if tool_xml else []
+ if len(stdios) != 1:
+ return
-def _lint_regex(tool_xml, child, lint_ctx):
- for key in child.attrib.keys():
- if key not in ["description", "level", "match", "source"]:
- lint_ctx.warn(f"Unknown attribute [{key}] encountered on regex tag.", node=child)
- match = child.attrib.get("match")
- if match:
- try:
- re.compile(match)
- except Exception as e:
- lint_ctx.error(f"Match '{match}' is no valid regular expression: {str(e)}", node=child)
+ stdio = stdios[0]
+ for child in list(stdio):
+ if child.tag == "regex":
+ match = child.attrib.get("match")
+ if match:
+ try:
+ re.compile(match)
+ except Exception as e:
+ lint_ctx.error(
+ f"Match '{match}' is no valid regular expression: {str(e)}", linter=cls.name(), node=child
+ )
diff --git a/lib/galaxy/tool_util/linters/tests.py b/lib/galaxy/tool_util/linters/tests.py
index 6f0680b2daf8..97de651b65bd 100644
--- a/lib/galaxy/tool_util/linters/tests.py
+++ b/lib/galaxy/tool_util/linters/tests.py
@@ -1,218 +1,507 @@
"""This module contains a linting functions for tool tests."""
-import typing
-from inspect import (
- Parameter,
- signature,
+from typing import (
+ Iterator,
+ List,
+ Tuple,
+ TYPE_CHECKING,
)
+from galaxy.tool_util.lint import Linter
from galaxy.util import asbool
from ._util import is_datasource
-from ..verify import asserts
-
-
-def check_compare_attribs(element, lint_ctx, test_idx):
- COMPARE_COMPATIBILITY = {
- "sort": ["diff", "re_match", "re_match_multiline"],
- "lines_diff": ["diff", "re_match", "contains"],
- "decompress": ["diff"],
- "delta": ["sim_size"],
- "delta_frac": ["sim_size"],
- }
- compare = element.get("compare", "diff")
- for attrib in COMPARE_COMPATIBILITY:
- if attrib in element.attrib and compare not in COMPARE_COMPATIBILITY[attrib]:
- lint_ctx.error(
- f'Test {test_idx}: Attribute {attrib} is incompatible with compare="{compare}".', node=element
- )
+if TYPE_CHECKING:
+ from galaxy.tool_util.lint import LintContext
+ from galaxy.tool_util.parser.interface import ToolSource
+ from galaxy.util.etree import Element
-def lint_tests(tool_xml, lint_ctx):
- # determine node to report for general problems with tests
- tests = tool_xml.findall("./tests/test")
- general_node = tool_xml.find("./tests")
- if general_node is None:
- general_node = tool_xml.getroot()
- datasource = is_datasource(tool_xml)
- if not tests:
- if not datasource:
- lint_ctx.warn("No tests found, most tools should define test cases.", node=general_node)
- elif datasource:
- lint_ctx.info("No tests found, that should be OK for data_sources.", node=general_node)
- return
-
- num_valid_tests = 0
- for test_idx, test in enumerate(tests, start=1):
- has_test = False
- test_expect = ("expect_failure", "expect_exit_code", "expect_num_outputs")
- for te in test_expect:
- if te in test.attrib:
- has_test = True
- break
- test_assert = ("assert_stdout", "assert_stderr", "assert_command")
- for ta in test_assert:
- assertions = test.findall(ta)
- if len(assertions) == 0:
- continue
- if len(assertions) > 1:
- lint_ctx.error(f"Test {test_idx}: More than one {ta} found. Only the first is considered.", node=test)
- has_test = True
- _check_asserts(test_idx, assertions, lint_ctx)
- _check_asserts(test_idx, test.findall(".//assert_contents"), lint_ctx)
-
- # check if expect_num_outputs is set if there are outputs with filters
- # (except for tests with expect_failure .. which can't have test outputs)
- filter = tool_xml.findall("./outputs//filter")
- if len(filter) > 0 and "expect_num_outputs" not in test.attrib:
- if not asbool(test.attrib.get("expect_failure", False)):
- lint_ctx.warn("Test should specify 'expect_num_outputs' if outputs have filters", node=test)
+lint_tool_types = ["default", "data_source", "manage_data"]
- # really simple test that test parameters are also present in the inputs
- for param in test.findall("param"):
- name = param.attrib.get("name", None)
- if not name:
- lint_ctx.error(f"Test {test_idx}: Found test param tag without a name defined.", node=param)
- continue
- name = name.split("|")[-1]
- xpaths = [f"@name='{name}'", f"@argument='{name}'", f"@argument='-{name}'", f"@argument='--{name}'"]
- if "_" in name:
- xpaths += [f"@argument='-{name.replace('_', '-')}'", f"@argument='--{name.replace('_', '-')}'"]
- found = False
- for xp in xpaths:
- inxpath = f".//inputs//param[{xp}]"
- inparam = tool_xml.findall(inxpath)
- if len(inparam) > 0:
- found = True
- break
- if not found:
- lint_ctx.error(f"Test {test_idx}: Test param {name} not found in the inputs", node=param)
- output_data_or_collection = _collect_output_names(tool_xml)
- found_output_test = False
- for output in test.findall("output") + test.findall("output_collection"):
- found_output_test = True
- name = output.attrib.get("name", None)
- if not name:
- lint_ctx.error(f"Test {test_idx}: Found {output.tag} tag without a name defined.", node=output)
- continue
- if name not in output_data_or_collection:
- lint_ctx.error(
- f"Test {test_idx}: Found {output.tag} tag with unknown name [{name}], valid names {list(output_data_or_collection)}",
- node=output,
- )
- continue
+class TestsMissing(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ root = tool_xml.find("./tests")
+ if root is None:
+ root = tool_xml.getroot()
+ if len(tests) == 0 and not is_datasource(tool_xml):
+ lint_ctx.warn("No tests found, most tools should define test cases.", linter=cls.name(), node=root)
- if output.tag == "output":
- check_compare_attribs(output, lint_ctx, test_idx)
- elements = output.findall("./element")
- if elements:
- for element in elements:
- check_compare_attribs(element, lint_ctx, test_idx)
-
- # check that
- # - test/output corresponds to outputs/data and
- # - test/collection to outputs/output_collection
- corresponding_output = output_data_or_collection[name]
- if output.tag == "output" and corresponding_output.tag != "data":
- lint_ctx.error(
- f"Test {test_idx}: test output {name} does not correspond to a 'data' output, but a '{corresponding_output.tag}'",
- node=output,
- )
- elif output.tag == "output_collection" and corresponding_output.tag != "collection":
- lint_ctx.error(
- f"Test {test_idx}: test collection output '{name}' does not correspond to a 'output_collection' output, but a '{corresponding_output.tag}'",
- node=output,
+
+class TestsMissingDatasource(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ root = tool_xml.find("./tests")
+ if root is None:
+ root = tool_xml.getroot()
+ if len(tests) == 0 and is_datasource(tool_xml):
+ lint_ctx.info("No tests found, that should be OK for data_sources.", linter=cls.name(), node=root)
+
+
+class TestsAssertsMultiple(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ # TODO same would be nice also for assert_contents
+ for ta in ("assert_stdout", "assert_stderr", "assert_command"):
+ if len(test.findall(ta)) > 1:
+ lint_ctx.error(
+ f"Test {test_idx}: More than one {ta} found. Only the first is considered.",
+ linter=cls.name(),
+ node=test,
+ )
+
+
+class TestsAssertsHasNQuant(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ for a in test.xpath(
+ ".//*[self::assert_contents or self::assert_stdout or self::assert_stderr or self::assert_command]//*"
+ ):
+ if a.tag not in ["has_n_lines", "has_n_columns"]:
+ continue
+ if not (set(a.attrib) & set(["n", "min", "max"])):
+ lint_ctx.error(
+ f"Test {test_idx}: '{a.tag}' needs to specify 'n', 'min', or 'max'", linter=cls.name(), node=a
+ )
+
+
+class TestsAssertsHasSizeQuant(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ for a in test.xpath(
+ ".//*[self::assert_contents or self::assert_stdout or self::assert_stderr or self::assert_command]//*"
+ ):
+ if a.tag != "has_size":
+ continue
+ if len(set(a.attrib) & set(["value", "min", "max"])) == 0:
+ lint_ctx.error(
+ f"Test {test_idx}: '{a.tag}' needs to specify 'value', 'min', or 'max'",
+ linter=cls.name(),
+ node=a,
+ )
+
+
+class TestsExpectNumOutputs(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ # check if expect_num_outputs is set if there are outputs with filters
+ # (except for tests with expect_failure .. which can't have test outputs)
+ filter = tool_xml.find("./outputs//filter")
+ if not (
+ filter is None
+ or "expect_num_outputs" in test.attrib
+ or asbool(test.attrib.get("expect_failure", False))
+ ):
+ lint_ctx.warn(
+ f"Test {test_idx}: should specify 'expect_num_outputs' if outputs have filters",
+ linter=cls.name(),
+ node=test,
)
- # check that discovered data is tested sufficiently
- discover_datasets = corresponding_output.find(".//discover_datasets")
- if discover_datasets is not None:
- if output.tag == "output":
- if "count" not in output.attrib and output.find("./discovered_dataset") is None:
+
+class TestsParamInInputs(Linter):
+ """
+ really simple linter that test parameters are also present in the inputs
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ for param in test.findall("param"):
+ name = param.attrib.get("name", None)
+ if not name:
+ continue
+ name = name.split("|")[-1]
+ xpaths = [f"@name='{name}'", f"@argument='{name}'", f"@argument='-{name}'", f"@argument='--{name}'"]
+ if "_" in name:
+ xpaths += [f"@argument='-{name.replace('_', '-')}'", f"@argument='--{name.replace('_', '-')}'"]
+ found = False
+ for xp in xpaths:
+ inxpath = f".//inputs//param[{xp}]"
+ inparam = tool_xml.findall(inxpath)
+ if len(inparam) > 0:
+ found = True
+ break
+ if not found:
+ lint_ctx.error(
+ f"Test {test_idx}: Test param {name} not found in the inputs", linter=cls.name(), node=param
+ )
+
+
+class TestsOutputName(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ # note output_collections are covered by xsd, but output is not required to have one by xsd
+ for output in test.findall("output"):
+ if not output.attrib.get("name", None):
+ lint_ctx.error(
+ f"Test {test_idx}: Found {output.tag} tag without a name defined.",
+ linter=cls.name(),
+ node=output,
+ )
+
+
+class TestsOutputDefined(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ output_data_or_collection = _collect_output_names(tool_xml)
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ for output in test.findall("output") + test.findall("output_collection"):
+ name = output.attrib.get("name", None)
+ if not name:
+ continue
+ if name not in output_data_or_collection:
+ lint_ctx.error(
+ f"Test {test_idx}: Found {output.tag} tag with unknown name [{name}], valid names {list(output_data_or_collection)}",
+ linter=cls.name(),
+ node=output,
+ )
+
+
+class TestsOutputCorresponding(Linter):
+ """
+ Linter checking if test/output corresponds to outputs/data
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ output_data_or_collection = _collect_output_names(tool_xml)
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ for output in test.findall("output") + test.findall("output_collection"):
+ name = output.attrib.get("name", None)
+ if not name:
+ continue
+ if name not in output_data_or_collection:
+ continue
+
+ # - test/collection to outputs/output_collection
+ corresponding_output = output_data_or_collection[name]
+ if output.tag == "output" and corresponding_output.tag != "data":
+ lint_ctx.error(
+ f"Test {test_idx}: test output {name} does not correspond to a 'data' output, but a '{corresponding_output.tag}'",
+ linter=cls.name(),
+ node=output,
+ )
+
+
+class TestsOutputCollectionCorresponding(Linter):
+ """
+ Linter checking if test/collection corresponds to outputs/output_collection
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ output_data_or_collection = _collect_output_names(tool_xml)
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ for output in test.findall("output") + test.findall("output_collection"):
+ name = output.attrib.get("name", None)
+ if not name:
+ continue
+ if name not in output_data_or_collection:
+ continue
+
+ # - test/collection to outputs/output_collection
+ corresponding_output = output_data_or_collection[name]
+ if output.tag == "output_collection" and corresponding_output.tag != "collection":
+ lint_ctx.error(
+ f"Test {test_idx}: test collection output '{name}' does not correspond to a 'output_collection' output, but a '{corresponding_output.tag}'",
+ linter=cls.name(),
+ node=output,
+ )
+
+
+class TestsOutputCompareAttrib(Linter):
+ """
+ Linter checking compatibility of output attributes with the value
+ of the compare attribute
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ COMPARE_COMPATIBILITY = {
+ "sort": ["diff", "re_match", "re_match_multiline"],
+ "lines_diff": ["diff", "re_match", "contains"],
+ "decompress": ["diff"],
+ "delta": ["sim_size"],
+ "delta_frac": ["sim_size"],
+ }
+ for test_idx, test in enumerate(tests, start=1):
+ for output in test.xpath(".//*[self::output or self::element or self::discovered_dataset]"):
+ compare = output.get("compare", "diff")
+ for attrib in COMPARE_COMPATIBILITY:
+ if attrib in output.attrib and compare not in COMPARE_COMPATIBILITY[attrib]:
lint_ctx.error(
- f"Test {test_idx}: test output '{name}' must have a 'count' attribute and/or 'discovered_dataset' children",
+ f'Test {test_idx}: Attribute {attrib} is incompatible with compare="{compare}".',
+ linter=cls.name(),
node=output,
)
- else:
- if "count" not in output.attrib and len(elements) == 0:
+
+
+class TestsOutputCheckDiscovered(Linter):
+ """
+ Linter checking that discovered elements of outputs are tested with
+ a count attribute or listing some discovered_dataset
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ output_data_or_collection = _collect_output_names(tool_xml)
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ for output in test.findall("output"):
+ name = output.attrib.get("name", None)
+ if not name:
+ continue
+ if name not in output_data_or_collection:
+ continue
+
+ # - test/collection to outputs/output_collection
+ corresponding_output = output_data_or_collection[name]
+ discover_datasets = corresponding_output.find(".//discover_datasets")
+ if discover_datasets is None:
+ continue
+ if "count" not in output.attrib and output.find("./discovered_dataset") is None:
+ lint_ctx.error(
+ f"Test {test_idx}: test output '{name}' must have a 'count' attribute and/or 'discovered_dataset' children",
+ linter=cls.name(),
+ node=output,
+ )
+
+
+class TestsOutputCollectionCheckDiscovered(Linter):
+ """
+ Linter checking that discovered elements of output collections
+ are tested with a count attribute or listing some elements
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ output_data_or_collection = _collect_output_names(tool_xml)
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ for output in test.findall("output_collection"):
+ name = output.attrib.get("name", None)
+ if not name:
+ continue
+ if name not in output_data_or_collection:
+ continue
+ # - test/collection to outputs/output_collection
+ corresponding_output = output_data_or_collection[name]
+ discover_datasets = corresponding_output.find(".//discover_datasets")
+ if discover_datasets is None:
+ continue
+ if "count" not in output.attrib and output.find("./element") is None:
+ lint_ctx.error(
+ f"Test {test_idx}: test collection '{name}' must have a 'count' attribute or 'element' children",
+ linter=cls.name(),
+ node=output,
+ )
+
+
+class TestsOutputCollectionCheckDiscoveredNested(Linter):
+ """ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ output_data_or_collection = _collect_output_names(tool_xml)
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ for output in test.findall("output_collection"):
+ name = output.attrib.get("name", None)
+ if not name:
+ continue
+ if name not in output_data_or_collection:
+ continue
+
+ # - test/collection to outputs/output_collection
+ corresponding_output = output_data_or_collection[name]
+ if corresponding_output.find(".//discover_datasets") is None:
+ continue
+ if corresponding_output.get("type", "") in ["list:list", "list:paired"]:
+ nested_elements = output.find("./element/element")
+ element_with_count = output.find("./element[@count]")
+ if nested_elements is None and element_with_count is None:
lint_ctx.error(
- f"Test {test_idx}: test collection '{name}' must have a 'count' attribute or 'element' children",
+ f"Test {test_idx}: test collection '{name}' must contain nested 'element' tags and/or element children with a 'count' attribute",
+ linter=cls.name(),
node=output,
)
- if corresponding_output.get("type", "") in ["list:list", "list:paired"]:
- nested_elements = output.find("./element/element")
- element_with_count = output.find("./element[@count]")
- if nested_elements is None and element_with_count is None:
- lint_ctx.error(
- f"Test {test_idx}: test collection '{name}' must contain nested 'element' tags and/or element childen with a 'count' attribute",
- node=output,
- )
- if asbool(test.attrib.get("expect_failure", False)):
- if found_output_test:
- lint_ctx.error(f"Test {test_idx}: Cannot specify outputs in a test expecting failure.", node=test)
+
+class TestsOutputFailing(Linter):
+ """ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ if not asbool(test.attrib.get("expect_failure", False)):
+ continue
+ if test.find("output") is not None or test.find("output_collection") is not None:
+ lint_ctx.error(
+ f"Test {test_idx}: Cannot specify outputs in a test expecting failure.",
+ linter=cls.name(),
+ node=test,
+ )
+
+
+class TestsExpectNumOutputsFailing(Linter):
+ """ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in enumerate(tests, start=1):
+ if not asbool(test.attrib.get("expect_failure", False)):
+ continue
+ if test.find("output") is not None or test.find("output_collection") is not None:
continue
if "expect_num_outputs" in test.attrib:
lint_ctx.error(
f"Test {test_idx}: Cannot make assumptions on the number of outputs in a test expecting failure.",
+ linter=cls.name(),
node=test,
)
- continue
- has_test = has_test or found_output_test
- if not has_test:
+
+class TestsHasExpectations(Linter):
+ """ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tests = tool_xml.findall("./tests/test")
+ for test_idx, test in _iter_tests(tests, valid=False):
lint_ctx.warn(
f"Test {test_idx}: No outputs or expectations defined for tests, this test is likely invalid.",
+ linter=cls.name(),
node=test,
)
- else:
- num_valid_tests += 1
- if num_valid_tests or datasource:
- lint_ctx.valid(f"{num_valid_tests} test(s) found.", node=general_node)
- else:
- lint_ctx.warn("No valid test(s) found.", node=general_node)
+class TestsNoValid(Linter):
+ """ """
-def _check_asserts(test_idx, assertions, lint_ctx):
- """
- assertions is a list of assert_contents, assert_stdout, assert_stderr, assert_command
- in practice only for the first case the list may be longer than one
- """
- for assertion in assertions:
- for i, a in enumerate(assertion.iter()):
- if i == 0: # skip root note itself
- continue
- assert_function_name = "assert_" + a.tag
- if assert_function_name not in asserts.assertion_functions:
- lint_ctx.error(f"Test {test_idx}: unknown assertion '{a.tag}'", node=a)
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ general_node = tool_xml.find("./tests")
+ if general_node is None:
+ general_node = tool_xml.getroot()
+ tests = tool_xml.findall("./tests/test")
+ if not tests:
+ return
+ num_valid_tests = len(list(_iter_tests(tests, valid=True)))
+ if num_valid_tests or is_datasource(tool_xml):
+ lint_ctx.valid(f"{num_valid_tests} test(s) found.", linter=cls.name(), node=general_node)
+
+
+class TestsValid(Linter):
+ """ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ general_node = tool_xml.find("./tests")
+ if general_node is None:
+ general_node = tool_xml.getroot()
+ tests = tool_xml.findall("./tests/test")
+ if not tests:
+ return
+ num_valid_tests = len(list(_iter_tests(tests, valid=True)))
+ if not (num_valid_tests or is_datasource(tool_xml)):
+ lint_ctx.warn("No valid test(s) found.", linter=cls.name(), node=general_node)
+
+
+def _iter_tests(tests: List["Element"], valid: bool) -> Iterator[Tuple[int, "Element"]]:
+ for test_idx, test in enumerate(tests, start=1):
+ is_valid = False
+ is_valid |= bool(set(test.attrib) & set(("expect_failure", "expect_exit_code", "expect_num_outputs")))
+ for ta in ("assert_stdout", "assert_stderr", "assert_command"):
+ if test.find(ta) is not None:
+ is_valid = True
+ found_output_test = test.find("output") is not None or test.find("output_collection") is not None
+ if asbool(test.attrib.get("expect_failure", False)):
+ if found_output_test or "expect_num_outputs" in test.attrib:
continue
- assert_function_sig = signature(asserts.assertion_functions[assert_function_name])
- # check of the attributes
- for attrib in a.attrib:
- if attrib not in assert_function_sig.parameters:
- lint_ctx.error(f"Test {test_idx}: unknown attribute '{attrib}' for '{a.tag}'", node=a)
- continue
- # check missing required attributes
- for p in assert_function_sig.parameters:
- if p in ["output", "output_bytes", "verify_assertions_function", "children"]:
- continue
- if assert_function_sig.parameters[p].default is Parameter.empty and p not in a.attrib:
- lint_ctx.error(f"Test {test_idx}: missing attribute '{p}' for '{a.tag}'", node=a)
- # has_n_lines, has_n_columns, and has_size need to specify n/value, min, or max
- if a.tag in ["has_n_lines", "has_n_columns"]:
- if "n" not in a.attrib and "min" not in a.attrib and "max" not in a.attrib:
- lint_ctx.error(f"Test {test_idx}: '{a.tag}' needs to specify 'n', 'min', or 'max'", node=a)
- if a.tag == "has_size":
- if "value" not in a.attrib and "min" not in a.attrib and "max" not in a.attrib:
- lint_ctx.error(f"Test {test_idx}: '{a.tag}' needs to specify 'value', 'min', or 'max'", node=a)
-
-
-def _handle_optionals(annotation):
- as_dict = annotation.__dict__
- if "__origin__" in as_dict and as_dict["__origin__"] == typing.Union:
- return as_dict["__args__"][0]
- return annotation
+ is_valid |= found_output_test
+ if is_valid == valid:
+ yield (test_idx, test)
def _collect_output_names(tool_xml):
diff --git a/lib/galaxy/tool_util/linters/xml_order.py b/lib/galaxy/tool_util/linters/xml_order.py
index 7ae29cd21dbf..af3f1f2cdd66 100644
--- a/lib/galaxy/tool_util/linters/xml_order.py
+++ b/lib/galaxy/tool_util/linters/xml_order.py
@@ -1,9 +1,17 @@
-"""This module contains a linting functions for tool XML block order.
+"""This module contains a linter for tool XML block order.
For more information on the IUC standard for XML block order see -
https://github.com/galaxy-iuc/standards.
"""
+from typing import TYPE_CHECKING
+
+from galaxy.tool_util.lint import Linter
+
+if TYPE_CHECKING:
+ from galaxy.tool_util.lint import LintContext
+ from galaxy.tool_util.parser.interface import ToolSource
+
from typing import Optional
# https://github.com/galaxy-iuc/standards
@@ -11,6 +19,7 @@
TAG_ORDER = [
"description",
"macros",
+ "options",
"edam_topics",
"edam_operations",
"xrefs",
@@ -45,28 +54,31 @@
]
-# Ensure the XML blocks appear in the correct order prescribed
-# by the tool author best practices.
-def lint_xml_order(tool_xml, lint_ctx):
- tool_root = tool_xml.getroot()
+class XMLOrder(Linter):
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ tool_root = tool_xml.getroot()
- if tool_root.attrib.get("tool_type", "") == "data_source":
- tag_ordering = DATASOURCE_TAG_ORDER
- else:
- tag_ordering = TAG_ORDER
-
- last_tag = None
- last_key: Optional[int] = None
- for elem in tool_root:
- tag = elem.tag
- if tag in tag_ordering:
+ if tool_root.attrib.get("tool_type", "") == "data_source":
+ tag_ordering = DATASOURCE_TAG_ORDER
+ else:
+ tag_ordering = TAG_ORDER
+ last_tag = None
+ last_key: Optional[int] = None
+ for elem in tool_root:
+ tag = elem.tag
+ if tag not in tag_ordering:
+ continue
key = tag_ordering.index(tag)
if last_key:
if last_key > key:
lint_ctx.warn(
- f"Best practice violation [{tag}] elements should come before [{last_tag}]", node=elem
+ f"Best practice violation [{tag}] elements should come before [{last_tag}]",
+ linter=cls.name(),
+ node=elem,
)
last_tag = tag
last_key = key
- else:
- lint_ctx.info(f"Unknown tag [{tag}] encountered, this may result in a warning in the future.", node=elem)
diff --git a/lib/galaxy/tool_util/linters/xsd.py b/lib/galaxy/tool_util/linters/xsd.py
new file mode 100644
index 000000000000..aa17453d0bcf
--- /dev/null
+++ b/lib/galaxy/tool_util/linters/xsd.py
@@ -0,0 +1,33 @@
+import os.path
+from typing import TYPE_CHECKING
+
+from galaxy import tool_util
+from galaxy.tool_util.lint import Linter
+from galaxy.util import etree
+
+if TYPE_CHECKING:
+ from galaxy.tool_util.lint import LintContext
+ from galaxy.tool_util.parser import ToolSource
+
+TOOL_XSD = os.path.join(os.path.dirname(tool_util.__file__), "xsd", "galaxy.xsd")
+
+
+class XSD(Linter):
+ """
+ Lint an XML tool against XSD
+ """
+
+ @classmethod
+ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
+ tool_xml = getattr(tool_source, "xml_tree", None)
+ if not tool_xml:
+ return
+ xmlschema = etree.parse(TOOL_XSD)
+ xsd = etree.XMLSchema(xmlschema)
+ if not xsd.validate(tool_xml):
+ for error in xsd.error_log: # type: ignore[attr-defined] # https://github.com/lxml/lxml-stubs/pull/100
+ # the validation error contains the path which allows
+ # us to lookup the node that is reported in the lint context
+ node = tool_xml.xpath(error.path)
+ node = node[0]
+ lint_ctx.error(f"Invalid XML: {error.message}", linter=cls.name(), node=node)
diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd
index d9885d4cce71..226ba2c29ec2 100644
--- a/lib/galaxy/tool_util/xsd/galaxy.xsd
+++ b/lib/galaxy/tool_util/xsd/galaxy.xsd
@@ -115,8 +115,7 @@ the tool menu immediately following the hyperlink for the tool (based on the
-
-
+
@@ -270,6 +269,11 @@ is ``data_source`` or ``data_source_async`` - this attribute defines the HTTP re
communicating with an external data source application (the default is ``get``).
+
+
+ Documentation needed
+
+
@@ -343,6 +347,19 @@ Usage example:
]]>
+
+
+
+ ` is identical to ``,
+`` is identical to ``, and
+`` is identical to ``.
+
+Note that
+ ]]>
+
+
@@ -2981,10 +2998,10 @@ between paired inputs of single stranded inputs.
-
+
-
+
Name for this element
diff --git a/test/unit/tool_util/test_tool_linters.py b/test/unit/tool_util/test_tool_linters.py
index 7b2f134192a9..0600ae32c4f2 100644
--- a/test/unit/tool_util/test_tool_linters.py
+++ b/test/unit/tool_util/test_tool_linters.py
@@ -5,8 +5,10 @@
from galaxy.tool_util.lint import (
lint_tool_source_with,
+ lint_tool_source_with_modules,
lint_xml_with,
LintContext,
+ Linter,
XMLLintMessageLine,
XMLLintMessageXPath,
)
@@ -20,6 +22,7 @@
stdio,
tests,
xml_order,
+ xsd,
)
from galaxy.tool_util.loader_directory import load_tool_sources_from_path
from galaxy.tool_util.parser.xml import XmlToolSource
@@ -32,28 +35,26 @@
# TODO tests tool xml for general linter
# tests tool xml for citations linter
CITATIONS_MULTIPLE = """
-
+
"""
CITATIONS_ABSENT = """
-
+
"""
CITATIONS_ERRORS = """
-
+
-
-
"""
CITATIONS_VALID = """
-
+
DOI
@@ -62,23 +63,23 @@
# tests tool xml for command linter
COMMAND_MULTIPLE = """
-
-
-
+
+ ls
+ df
"""
COMMAND_MISSING = """
-
+
"""
COMMAND_TODO = """
-
+
## TODO
"""
COMMAND_DETECT_ERRORS_INTERPRETER = """
-
+
"""
@@ -91,7 +92,7 @@
"""
GENERAL_WHITESPACE_IN_VERSIONS_AND_NAMES = """
-
+
bwa
@@ -99,7 +100,7 @@
"""
GENERAL_REQUIREMENT_WO_VERSION = """
-
+
bwa
@@ -119,31 +120,31 @@
# test tool xml for help linter
HELP_MULTIPLE = """
-
+
Help
More help
"""
HELP_ABSENT = """
-
+
"""
HELP_EMPTY = """
-
+
"""
HELP_TODO = """
-
+
TODO
"""
HELP_INVALID_RST = """
-
+
**xxl__
@@ -152,7 +153,7 @@
# test tool xml for inputs linter
INPUTS_NO_INPUTS = """
-
+
"""
@@ -163,7 +164,7 @@
"""
INPUTS_VALID = """
-
+
@@ -172,7 +173,7 @@
"""
INPUTS_PARAM_NAME = """
-
+
@@ -184,7 +185,7 @@
"""
INPUTS_PARAM_TYPE = """
-
+
@@ -193,7 +194,7 @@
"""
INPUTS_DATA_PARAM = """
-
+
@@ -201,7 +202,7 @@
"""
INPUTS_DATA_PARAM_OPTIONS = """
-
+
@@ -213,7 +214,7 @@
"""
INPUTS_DATA_PARAM_OPTIONS_FILTER_ATTRIBUTE = """
-
+
@@ -225,12 +226,12 @@
"""
INPUTS_DATA_PARAM_INVALIDOPTIONS = """
-
+
-
+
@@ -238,7 +239,7 @@
"""
INPUTS_BOOLEAN_PARAM_SWAPPED_LABELS = """
-
+
@@ -246,7 +247,7 @@
"""
INPUTS_BOOLEAN_PARAM_DUPLICATE_LABELS = """
-
+
@@ -254,7 +255,7 @@
"""
INPUTS_CONDITIONAL = """
-
+
@@ -309,7 +310,7 @@
"""
INPUTS_SELECT_INCOMPATIBLE_DISPLAY = """
-
+
@@ -329,7 +330,7 @@
"""
INPUTS_SELECT_DUPLICATED_OPTIONS = """
-
+
@@ -340,7 +341,7 @@
"""
SELECT_DUPLICATED_OPTIONS_WITH_DIFF_SELECTED = """
-
+
@@ -351,7 +352,7 @@
"""
INPUTS_SELECT_DEPRECATIONS = """
-
+
@@ -365,7 +366,7 @@
"""
INPUTS_SELECT_OPTION_DEFINITIONS = """
-
+
@@ -391,7 +392,7 @@
"""
INPUTS_SELECT_FILTER = """
-
+
@@ -404,7 +405,7 @@
"""
INPUTS_VALIDATOR_INCOMPATIBILITIES = """
-
+
TEXT
@@ -424,7 +425,7 @@
"""
INPUTS_VALIDATOR_CORRECT = """
-
+
@@ -436,7 +437,7 @@
-
+
@@ -469,7 +470,7 @@
"""
INPUTS_TYPE_CHILD_COMBINATIONS = """
-
+
@@ -480,19 +481,26 @@
-
+
+
+
+
+
+
+
+
"""
INPUTS_DUPLICATE_NAMES = """
-
+
-
+
@@ -517,38 +525,39 @@
# test tool xml for outputs linter
OUTPUTS_MISSING = """
-
+
"""
OUTPUTS_MULTIPLE = """
-
+
"""
OUTPUTS_UNKNOWN_TAG = """
-
+
-
+
"""
OUTPUTS_UNNAMED_INVALID_NAME = """
-
+
+
"""
OUTPUTS_FORMAT_INPUT_LEGACY = """
-
+
"""
OUTPUTS_FORMAT_INPUT = """
-
+
@@ -558,7 +567,7 @@
# check that linter accepts format source for collection elements as means to specify format
# and that the linter warns if format and format_source are used
OUTPUTS_COLLECTION_FORMAT_SOURCE = """
-
+
@@ -570,7 +579,7 @@
# check that setting format with actions is supported
OUTPUTS_FORMAT_ACTION = """
-
+
@@ -589,16 +598,18 @@
# check that linter does not complain about missing format if from_tool_provided_metadata is used
OUTPUTS_DISCOVER_TOOL_PROVIDED_METADATA = """
-
+
+ galaxy.json
-
+
"""
+
OUTPUTS_DUPLICATED_NAME_LABEL = """
-
+
@@ -614,7 +625,7 @@
# tool xml for repeats linter
REPEATS = """
-
+
@@ -625,34 +636,34 @@
# tool xml for stdio linter
STDIO_DEFAULT_FOR_DEFAULT_PROFILE = """
-
+
"""
STDIO_DEFAULT_FOR_NONLEGACY_PROFILE = """
-
+
"""
STDIO_MULTIPLE_STDIO = """
-
+
"""
STDIO_INVALID_CHILD_OR_ATTRIB = """
-
+
-
+
"""
STDIO_INVALID_MATCH = """
-
+
@@ -661,13 +672,13 @@
# check that linter does complain about tests wo assumptions
TESTS_ABSENT = """
-
+
"""
TESTS_ABSENT_DATA_SOURCE = """
-
+
"""
TESTS_WO_EXPECTATIONS = """
-
+
@@ -676,12 +687,13 @@
"""
TESTS_PARAM_OUTPUT_NAMES = """
-
+
-
-
-
-
+
+
+
+
+
@@ -707,7 +719,7 @@
"""
TESTS_EXPECT_FAILURE_OUTPUT = """
-
+
@@ -721,7 +733,7 @@
"""
ASSERTS = """
-
+
@@ -740,7 +752,7 @@
"""
TESTS_VALID = """
-
+
@@ -770,7 +782,7 @@
"""
TESTS_OUTPUT_TYPE_MISMATCH = """
-
+
@@ -784,7 +796,7 @@
"""
TESTS_DISCOVER_OUTPUTS = """
-
+
@@ -833,9 +845,9 @@
"""
TESTS_EXPECT_NUM_OUTPUTS_FILTER = """
-
+
-
+
@@ -847,7 +859,7 @@
"""
TESTS_COMPARE_ATTRIB_INCOMPATIBILITY = """
-
+
@@ -870,7 +882,7 @@
# tool xml for xml_order linter
XML_ORDER = """
-
+
@@ -920,27 +932,29 @@ def get_tool_xml_exact(xml_string: str):
return parse_xml(tool_path, strip_whitespace=False, remove_comments=False)
+def run_lint_module(lint_ctx, lint_module, lint_target):
+ lint_tool_source_with_modules(lint_ctx, lint_target, list(set([lint_module, xsd])))
+
+
def run_lint(lint_ctx, lint_func, lint_target):
lint_ctx.lint(name="test_lint", lint_func=lint_func, lint_target=lint_target)
# check if the lint messages have the line
for message in lint_ctx.message_list:
- if lint_func != general.lint_general:
- assert message.line is not None, f"No context found for message: {message.message}"
+ assert message.line is not None, f"No context found for message: {message.message}"
def test_citations_multiple(lint_ctx):
- tool_xml_tree = get_xml_tree(CITATIONS_MULTIPLE)
- run_lint(lint_ctx, citations.lint_citations, tool_xml_tree)
- assert "More than one citation section found, behavior undefined." in lint_ctx.error_messages
+ tool_source = get_xml_tool_source(CITATIONS_MULTIPLE)
+ run_lint_module(lint_ctx, citations, tool_source)
+ assert lint_ctx.error_messages == ["Invalid XML: Element 'citations': This element is not expected."]
assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
assert not lint_ctx.warn_messages
- assert len(lint_ctx.error_messages) == 1
def test_citations_absent(lint_ctx):
- tool_xml_tree = get_xml_tree(CITATIONS_ABSENT)
- run_lint(lint_ctx, citations.lint_citations, tool_xml_tree)
+ tool_source = get_xml_tool_source(CITATIONS_ABSENT)
+ run_lint_module(lint_ctx, citations, tool_source)
assert lint_ctx.warn_messages == ["No citations found, consider adding citations to your tool."]
assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
@@ -948,61 +962,67 @@ def test_citations_absent(lint_ctx):
def test_citations_errors(lint_ctx):
- tool_xml_tree = get_xml_tree(CITATIONS_ERRORS)
- run_lint(lint_ctx, citations.lint_citations, tool_xml_tree)
- assert "Unknown tag discovered in citations block [nonsense], will be ignored." in lint_ctx.warn_messages
- assert "Unknown citation type discovered [hoerensagen], will be ignored." in lint_ctx.warn_messages
- assert "Empty doi citation." in lint_ctx.error_messages
- assert "Found no valid citations." in lint_ctx.warn_messages
- assert len(lint_ctx.warn_messages) == 3
+ tool_source = get_xml_tool_source(CITATIONS_ERRORS)
+ run_lint_module(lint_ctx, citations, tool_source)
+ assert lint_ctx.error_messages == ["Empty doi citation."]
+ assert not lint_ctx.warn_messages
assert not lint_ctx.info_messages
- assert not lint_ctx.valid_messages
+ assert len(lint_ctx.valid_messages) == 1
def test_citations_valid(lint_ctx):
- tool_xml_tree = get_xml_tree(CITATIONS_VALID)
- run_lint(lint_ctx, citations.lint_citations, tool_xml_tree)
- assert "Found 1 likely valid citations." in lint_ctx.valid_messages
+ tool_source = get_xml_tool_source(CITATIONS_VALID)
+ run_lint_module(lint_ctx, citations, tool_source)
+ assert "Found 1 citations." in lint_ctx.valid_messages
assert len(lint_ctx.valid_messages) == 1
assert not lint_ctx.info_messages
assert not lint_ctx.error_messages
def test_command_multiple(lint_ctx):
- tool_xml_tree = get_xml_tree(COMMAND_MULTIPLE)
- run_lint(lint_ctx, command.lint_command, tool_xml_tree)
- assert "More than one command tag found, behavior undefined." in lint_ctx.error_messages
- assert len(lint_ctx.error_messages) == 1
- assert not lint_ctx.info_messages
+ tool_source = get_xml_tool_source(COMMAND_MULTIPLE)
+ run_lint_module(lint_ctx, command, tool_source)
+ assert lint_ctx.error_messages == ["Invalid XML: Element 'command': This element is not expected."]
+ assert lint_ctx.info_messages == ["Tool contains a command."]
assert not lint_ctx.valid_messages
assert not lint_ctx.warn_messages
def test_command_missing(lint_ctx):
- tool_xml_tree = get_xml_tree(COMMAND_MISSING)
- run_lint(lint_ctx, command.lint_command, tool_xml_tree)
- assert "No command tag found, must specify a command template to execute." in lint_ctx.error_messages
+ tool_source = get_xml_tool_source(COMMAND_MISSING)
+ run_lint_module(lint_ctx, command, tool_source)
+ assert lint_ctx.error_messages == ["No command tag found, must specify a command template to execute."]
+ assert not lint_ctx.info_messages
+ assert not lint_ctx.valid_messages
+ assert not lint_ctx.warn_messages
def test_command_todo(lint_ctx):
- tool_xml_tree = get_xml_tree(COMMAND_TODO)
- run_lint(lint_ctx, command.lint_command, tool_xml_tree)
- assert "Tool contains a command." in lint_ctx.info_messages
- assert "Command template contains TODO text." in lint_ctx.warn_messages
+ tool_source = get_xml_tool_source(COMMAND_TODO)
+ run_lint_module(lint_ctx, command, tool_source)
+ assert not lint_ctx.error_messages
+ assert lint_ctx.info_messages == ["Tool contains a command."]
+ assert lint_ctx.warn_messages == ["Command template contains TODO text."]
+ assert not lint_ctx.valid_messages
def test_command_detect_errors_interpreter(lint_ctx):
- tool_xml_tree = get_xml_tree(COMMAND_DETECT_ERRORS_INTERPRETER)
- run_lint(lint_ctx, command.lint_command, tool_xml_tree)
- assert "Command uses deprecated 'interpreter' attribute." in lint_ctx.warn_messages
- assert "Tool contains a command with interpreter of type [python]." in lint_ctx.info_messages
- assert "Unknown detect_errors attribute [nonsense]" in lint_ctx.warn_messages
+ tool_source = get_xml_tool_source(COMMAND_DETECT_ERRORS_INTERPRETER)
+ run_lint_module(lint_ctx, command, tool_source)
assert "Command is empty." in lint_ctx.error_messages
+ assert (
+ "Invalid XML: Element 'command', attribute 'detect_errors': [facet 'enumeration'] The value 'nonsense' is not an element of the set {'default', 'exit_code', 'aggressive'}."
+ in lint_ctx.error_messages
+ )
+ assert lint_ctx.warn_messages == ["Command uses deprecated 'interpreter' attribute."]
+ assert lint_ctx.info_messages == ["Tool contains a command with interpreter of type [python]."]
+ assert not lint_ctx.valid_messages
+ assert len(lint_ctx.error_messages) == 2
def test_general_missing_tool_id_name_version(lint_ctx):
tool_source = get_xml_tool_source(GENERAL_MISSING_TOOL_ID_NAME_VERSION)
- run_lint(lint_ctx, general.lint_general, tool_source)
+ run_lint_module(lint_ctx, general, tool_source)
assert "Tool version is missing or empty." in lint_ctx.error_messages
assert "Tool name is missing or empty." in lint_ctx.error_messages
assert "Tool does not define an id attribute." in lint_ctx.error_messages
@@ -1011,7 +1031,7 @@ def test_general_missing_tool_id_name_version(lint_ctx):
def test_general_whitespace_in_versions_and_names(lint_ctx):
tool_source = get_xml_tool_source(GENERAL_WHITESPACE_IN_VERSIONS_AND_NAMES)
- run_lint(lint_ctx, general.lint_general, tool_source)
+ run_lint_module(lint_ctx, general, tool_source)
assert "Tool version is pre/suffixed by whitespace, this may cause errors: [ 1.0.1 ]." in lint_ctx.warn_messages
assert "Tool name is pre/suffixed by whitespace, this may cause errors: [ BWA Mapper ]." in lint_ctx.warn_messages
assert "Requirement version contains whitespace, this may cause errors: [ 1.2.5 ]." in lint_ctx.warn_messages
@@ -1021,7 +1041,7 @@ def test_general_whitespace_in_versions_and_names(lint_ctx):
def test_general_requirement_without_version(lint_ctx):
tool_source = get_xml_tool_source(GENERAL_REQUIREMENT_WO_VERSION)
- run_lint(lint_ctx, general.lint_general, tool_source)
+ run_lint_module(lint_ctx, general, tool_source)
assert "Tool version [1.0.1blah] is not compliant with PEP 440." in lint_ctx.warn_messages
assert "Requirement bwa defines no version" in lint_ctx.warn_messages
assert "Requirement without name found" in lint_ctx.error_messages
@@ -1036,7 +1056,7 @@ def test_general_requirement_without_version(lint_ctx):
def test_general_valid(lint_ctx):
tool_source = get_xml_tool_source(GENERAL_VALID)
- run_lint(lint_ctx, general.lint_general, tool_source)
+ run_lint_module(lint_ctx, general, tool_source)
assert "Tool defines a version [1.0+galaxy1]." in lint_ctx.valid_messages
assert "Tool specifies profile version [21.09]." in lint_ctx.valid_messages
assert "Tool defines an id [valid_id]." in lint_ctx.valid_messages
@@ -1049,7 +1069,7 @@ def test_general_valid(lint_ctx):
def test_general_valid_new_profile_fmt(lint_ctx):
tool_source = get_xml_tool_source(GENERAL_VALID_NEW_PROFILE_FMT)
- run_lint(lint_ctx, general.lint_general, tool_source)
+ run_lint_module(lint_ctx, general, tool_source)
assert "Tool defines a version [1.0+galaxy1]." in lint_ctx.valid_messages
assert "Tool specifies profile version [23.0]." in lint_ctx.valid_messages
assert "Tool defines an id [valid_id]." in lint_ctx.valid_messages
@@ -1061,18 +1081,17 @@ def test_general_valid_new_profile_fmt(lint_ctx):
def test_help_multiple(lint_ctx):
- tool_xml_tree = get_xml_tree(HELP_MULTIPLE)
- run_lint(lint_ctx, help.lint_help, tool_xml_tree)
- assert "More than one help section found, behavior undefined." in lint_ctx.error_messages
+ tool_source = get_xml_tool_source(HELP_MULTIPLE)
+ run_lint_module(lint_ctx, help, tool_source)
assert not lint_ctx.info_messages
- assert not lint_ctx.valid_messages
+ assert len(lint_ctx.valid_messages) == 2 # has help and valid rst
assert not lint_ctx.warn_messages
- assert len(lint_ctx.error_messages) == 1
+ assert lint_ctx.error_messages == ["Invalid XML: Element 'help': This element is not expected."]
def test_help_absent(lint_ctx):
- tool_xml_tree = get_xml_tree(HELP_ABSENT)
- run_lint(lint_ctx, help.lint_help, tool_xml_tree)
+ tool_source = get_xml_tool_source(HELP_ABSENT)
+ run_lint_module(lint_ctx, help, tool_source)
assert "No help section found, consider adding a help section to your tool." in lint_ctx.warn_messages
assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
@@ -1081,8 +1100,8 @@ def test_help_absent(lint_ctx):
def test_help_empty(lint_ctx):
- tool_xml_tree = get_xml_tree(HELP_EMPTY)
- run_lint(lint_ctx, help.lint_help, tool_xml_tree)
+ tool_source = get_xml_tool_source(HELP_EMPTY)
+ run_lint_module(lint_ctx, help, tool_source)
assert "Help section appears to be empty." in lint_ctx.warn_messages
assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
@@ -1091,8 +1110,8 @@ def test_help_empty(lint_ctx):
def test_help_todo(lint_ctx):
- tool_xml_tree = get_xml_tree(HELP_TODO)
- run_lint(lint_ctx, help.lint_help, tool_xml_tree)
+ tool_source = get_xml_tool_source(HELP_TODO)
+ run_lint_module(lint_ctx, help, tool_source)
assert "Tool contains help section." in lint_ctx.valid_messages
assert "Help contains valid reStructuredText." in lint_ctx.valid_messages
assert "Help contains TODO text." in lint_ctx.warn_messages
@@ -1103,8 +1122,8 @@ def test_help_todo(lint_ctx):
def test_help_invalid_rst(lint_ctx):
- tool_xml_tree = get_xml_tree(HELP_INVALID_RST)
- run_lint(lint_ctx, help.lint_help, tool_xml_tree)
+ tool_source = get_xml_tool_source(HELP_INVALID_RST)
+ run_lint_module(lint_ctx, help, tool_source)
assert "Tool contains help section." in lint_ctx.valid_messages
assert (
"Invalid reStructuredText found in help - [:2: (WARNING/2) Inline strong start-string without end-string.\n]."
@@ -1118,7 +1137,7 @@ def test_help_invalid_rst(lint_ctx):
def test_inputs_no_inputs(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_NO_INPUTS)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found no input parameters." in lint_ctx.warn_messages
assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
@@ -1128,7 +1147,7 @@ def test_inputs_no_inputs(lint_ctx):
def test_inputs_no_inputs_datasource(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_NO_INPUTS_DATASOURCE)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "No input parameters, OK for data sources" in lint_ctx.info_messages
assert "display tag usually present in data sources" in lint_ctx.info_messages
assert "uihints tag usually present in data sources" in lint_ctx.info_messages
@@ -1140,7 +1159,7 @@ def test_inputs_no_inputs_datasource(lint_ctx):
def test_inputs_valid(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_VALID)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 2 input parameters." in lint_ctx.info_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
@@ -1150,7 +1169,7 @@ def test_inputs_valid(lint_ctx):
def test_inputs_param_name(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_PARAM_NAME)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 5 input parameters." in lint_ctx.info_messages
assert "Param input [2] is not a valid Cheetah placeholder." in lint_ctx.warn_messages
assert "Found param input with no name specified." in lint_ctx.error_messages
@@ -1167,10 +1186,13 @@ def test_inputs_param_name(lint_ctx):
def test_inputs_param_type(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_PARAM_TYPE)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 2 input parameters." in lint_ctx.info_messages
- assert "Param input [valid_name] input with no type specified." in lint_ctx.error_messages
- assert "Param input [another_valid_name] with empty type specified." in lint_ctx.error_messages
+ assert "Invalid XML: Element 'param': The attribute 'type' is required but missing." in lint_ctx.error_messages
+ assert (
+ "Invalid XML: Element 'param', attribute 'type': [facet 'enumeration'] The value '' is not an element of the set {'text', 'integer', 'float', 'color', 'boolean', 'genomebuild', 'select', 'data_column', 'hidden', 'hidden_data', 'baseurl', 'file', 'data', 'drill_down', 'group_tag', 'data_collection', 'directory_uri'}."
+ in lint_ctx.error_messages
+ )
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
assert not lint_ctx.warn_messages
@@ -1179,7 +1201,7 @@ def test_inputs_param_type(lint_ctx):
def test_inputs_data_param(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_DATA_PARAM)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 1 input parameters." in lint_ctx.info_messages
assert (
"Param input [valid_name] with no format specified - 'data' format will be assumed." in lint_ctx.warn_messages
@@ -1192,7 +1214,7 @@ def test_inputs_data_param(lint_ctx):
def test_inputs_boolean_param(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_BOOLEAN_PARAM_DUPLICATE_LABELS)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 1 input parameters." in lint_ctx.info_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
@@ -1202,7 +1224,7 @@ def test_inputs_boolean_param(lint_ctx):
def test_inputs_data_param_options(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_OPTIONS)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert not lint_ctx.valid_messages
assert "Found 1 input parameters." in lint_ctx.info_messages
assert len(lint_ctx.info_messages) == 1
@@ -1212,7 +1234,7 @@ def test_inputs_data_param_options(lint_ctx):
def test_inputs_data_param_options_filter_attribute(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_OPTIONS_FILTER_ATTRIBUTE)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert not lint_ctx.valid_messages
assert "Found 1 input parameters." in lint_ctx.info_messages
assert len(lint_ctx.info_messages) == 1
@@ -1222,7 +1244,7 @@ def test_inputs_data_param_options_filter_attribute(lint_ctx):
def test_inputs_data_param_invalid_options(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_INVALIDOPTIONS)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert not lint_ctx.valid_messages
assert "Found 1 input parameters." in lint_ctx.info_messages
assert len(lint_ctx.info_messages) == 1
@@ -1230,7 +1252,7 @@ def test_inputs_data_param_invalid_options(lint_ctx):
assert "Data parameter [valid_name] contains multiple options elements." in lint_ctx.error_messages
assert "Data parameter [valid_name] filter needs to define a ref attribute" in lint_ctx.error_messages
assert (
- 'Data parameter [valid_name] for filters only type="data_meta" and key="dbkey" are allowed, found type="expression" and key="None"'
+ 'Data parameter [valid_name] for filters only type="data_meta" and key="dbkey" are allowed, found type="regexp" and key="None"'
in lint_ctx.error_messages
)
assert len(lint_ctx.error_messages) == 3
@@ -1238,15 +1260,22 @@ def test_inputs_data_param_invalid_options(lint_ctx):
def test_inputs_conditional(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_CONDITIONAL)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 10 input parameters." in lint_ctx.info_messages
- assert "Conditional without a name" in lint_ctx.error_messages
+ assert (
+ "Invalid XML: Element 'conditional': The attribute 'name' is required but missing." in lint_ctx.error_messages
+ )
assert (
"Select parameter of a conditional [select] options have to be defined by 'option' children elements."
in lint_ctx.error_messages
)
- assert "Conditional [cond_wo_param] needs exactly one child found 0" in lint_ctx.error_messages
- assert "Conditional [cond_w_mult_param] needs exactly one child found 2" in lint_ctx.error_messages
+ assert (
+ "Invalid XML: Element 'param': This element is not expected. Expected is ( when )." in lint_ctx.error_messages
+ ) # 2x param
+ assert (
+ "Invalid XML: Element 'conditional': Missing child element(s). Expected is ( param )."
+ in lint_ctx.error_messages
+ )
assert 'Conditional [cond_text] first param should have type="select"' in lint_ctx.error_messages
assert (
'Conditional [cond_boolean] first param of type="boolean" is discouraged, use a select'
@@ -1255,7 +1284,7 @@ def test_inputs_conditional(lint_ctx):
assert "Conditional [cond_boolean] no truevalue/falsevalue found for when block 'False'" in lint_ctx.warn_messages
assert 'Conditional [cond_w_optional_select] test parameter cannot be optional="true"' in lint_ctx.warn_messages
assert 'Conditional [cond_w_multiple_select] test parameter cannot be multiple="true"' in lint_ctx.warn_messages
- assert "Conditional [when_wo_value] when without value" in lint_ctx.error_messages
+ assert "Invalid XML: Element 'when': The attribute 'value' is required but missing." in lint_ctx.error_messages
assert "Conditional [missing_when] no block found for select option 'none'" in lint_ctx.warn_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
@@ -1265,7 +1294,7 @@ def test_inputs_conditional(lint_ctx):
def test_inputs_select_incompatible_display(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_SELECT_INCOMPATIBLE_DISPLAY)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 3 input parameters." in lint_ctx.info_messages
assert 'Select [radio_select] display="radio" is incompatible with optional="true"' in lint_ctx.error_messages
assert 'Select [radio_select] display="radio" is incompatible with multiple="true"' in lint_ctx.error_messages
@@ -1285,7 +1314,7 @@ def test_inputs_select_incompatible_display(lint_ctx):
def test_inputs_duplicated_options(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_SELECT_DUPLICATED_OPTIONS)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 1 input parameters." in lint_ctx.info_messages
assert "Select parameter [select] has multiple options with the same text content" in lint_ctx.error_messages
assert "Select parameter [select] has multiple options with the same value" in lint_ctx.error_messages
@@ -1297,14 +1326,14 @@ def test_inputs_duplicated_options(lint_ctx):
def test_inputs_duplicated_options_with_different_select(lint_ctx):
tool_source = get_xml_tool_source(SELECT_DUPLICATED_OPTIONS_WITH_DIFF_SELECTED)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert not lint_ctx.warn_messages
assert not lint_ctx.error_messages
-def test_inputs_select_deprections(lint_ctx):
+def test_inputs_select_deprecations(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_SELECT_DEPRECATIONS)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 3 input parameters." in lint_ctx.info_messages
assert "Select parameter [select_do] uses deprecated 'dynamic_options' attribute." in lint_ctx.warn_messages
assert "Select parameter [select_ff] options uses deprecated 'from_file' attribute." in lint_ctx.warn_messages
@@ -1322,7 +1351,8 @@ def test_inputs_select_deprections(lint_ctx):
def test_inputs_select_option_definitions(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_SELECT_OPTION_DEFINITIONS)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
+ run_lint_module(lint_ctx, xsd, tool_source)
assert "Found 6 input parameters." in lint_ctx.info_messages
assert (
"Select parameter [select_noopt] options have to be defined by either 'option' children elements, a 'options' element or the 'dynamic_options' attribute."
@@ -1342,24 +1372,23 @@ def test_inputs_select_option_definitions(lint_ctx):
in lint_ctx.error_messages
)
assert "Select parameter [select_noval_notext] has option without value" in lint_ctx.error_messages
- assert "Select parameter [select_noval_notext] has option without text" in lint_ctx.warn_messages
assert (
"Select parameter [select_meta_file_key_incomp] 'meta_file_key' is only compatible with 'from_dataset'."
in lint_ctx.error_messages
)
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
- assert len(lint_ctx.warn_messages) == 1
+ assert not lint_ctx.warn_messages
assert len(lint_ctx.error_messages) == 7
def test_inputs_select_filter(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_SELECT_FILTER)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 1 input parameters." in lint_ctx.info_messages
- assert "Select parameter [select_filter_types] contains filter without type." in lint_ctx.error_messages
+ assert "Invalid XML: Element 'filter': The attribute 'type' is required but missing." in lint_ctx.error_messages
assert (
- "Select parameter [select_filter_types] contains filter with unknown type 'unknown_filter_type'."
+ "Invalid XML: Element 'filter', attribute 'type': [facet 'enumeration'] The value 'unknown_filter_type' is not an element of the set {'data_meta', 'param_value', 'static_value', 'regexp', 'unique_value', 'multiple_splitter', 'attribute_value_splitter', 'add_value', 'remove_value', 'sort_by'}."
in lint_ctx.error_messages
)
assert len(lint_ctx.info_messages) == 1
@@ -1370,7 +1399,7 @@ def test_inputs_select_filter(lint_ctx):
def test_inputs_validator_incompatibilities(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_VALIDATOR_INCOMPATIBILITIES)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 2 input parameters." in lint_ctx.info_messages
assert (
"Parameter [param_name]: 'in_range' validators are not expected to contain text (found 'TEXT')"
@@ -1388,10 +1417,10 @@ def test_inputs_validator_incompatibilities(lint_ctx):
assert "Parameter [param_name]: expression validators are expected to contain text" in lint_ctx.error_messages
assert "Parameter [param_name]: regex validators are expected to contain text" in lint_ctx.error_messages
assert (
- "Parameter [param_name]: '[' is no valid regular expression: unterminated character set at position 0"
+ "Parameter [param_name]: '[' is no valid regex: unterminated character set at position 0"
in lint_ctx.error_messages
)
- assert "Parameter [param_name]: '(' is no valid regular expression" in lint_ctx.error_messages
+ assert "Parameter [param_name]: '(' is no valid expression" in lint_ctx.error_messages
assert (
"Parameter [another_param_name]: 'metadata' validators need to define the 'check' or 'skip' attribute(s)"
in lint_ctx.error_messages
@@ -1416,7 +1445,7 @@ def test_inputs_validator_incompatibilities(lint_ctx):
def test_inputs_validator_correct(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_VALIDATOR_CORRECT)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert "Found 5 input parameters." in lint_ctx.info_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
@@ -1426,7 +1455,7 @@ def test_inputs_validator_correct(lint_ctx):
def test_inputs_type_child_combinations(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_TYPE_CHILD_COMBINATIONS)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
assert not lint_ctx.warn_messages
@@ -1439,15 +1468,19 @@ def test_inputs_type_child_combinations(lint_ctx):
in lint_ctx.error_messages
)
assert (
- "Parameter [data_param] './column' tags are only allowed for parameters of type ['data_column']"
+ "Parameter [int_param] './options' tags are only allowed for parameters of type ['data', 'select', 'drill_down']"
in lint_ctx.error_messages
)
- assert len(lint_ctx.error_messages) == 3
+ assert (
+ "Parameter [int_param] './options/column' tags are only allowed for parameters of type ['data', 'select']"
+ in lint_ctx.error_messages
+ )
+ assert len(lint_ctx.error_messages) == 4
def test_inputs_duplicate_names(lint_ctx):
tool_source = get_xml_tool_source(INPUTS_DUPLICATE_NAMES)
- run_lint(lint_ctx, inputs.lint_inputs, tool_source)
+ run_lint_module(lint_ctx, inputs, tool_source)
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
assert not lint_ctx.warn_messages
@@ -1459,19 +1492,22 @@ def test_inputs_duplicate_names(lint_ctx):
def test_inputs_repeats(lint_ctx):
+ """
+ this was previously covered by a linter in inputs
+ """
tool_source = get_xml_tool_source(REPEATS)
- run_lint(lint_ctx, inputs.lint_repeats, tool_source)
- assert "Repeat does not specify name attribute." in lint_ctx.error_messages
- assert "Repeat does not specify title attribute." in lint_ctx.error_messages
- assert not lint_ctx.info_messages
+ run_lint_module(lint_ctx, inputs, tool_source)
+ assert lint_ctx.info_messages == ["Found 1 input parameters."]
assert not lint_ctx.valid_messages
assert not lint_ctx.warn_messages
+ assert "Invalid XML: Element 'repeat': The attribute 'name' is required but missing." in lint_ctx.error_messages
+ assert "Invalid XML: Element 'repeat': The attribute 'title' is required but missing." in lint_ctx.error_messages
assert len(lint_ctx.error_messages) == 2
def test_outputs_missing(lint_ctx):
tool_source = get_xml_tool_source(OUTPUTS_MISSING)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
+ run_lint_module(lint_ctx, outputs, tool_source)
assert "Tool contains no outputs section, most tools should produce outputs." in lint_ctx.warn_messages
assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
@@ -1481,20 +1517,26 @@ def test_outputs_missing(lint_ctx):
def test_outputs_multiple(lint_ctx):
tool_source = get_xml_tool_source(OUTPUTS_MULTIPLE)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
+ run_lint_module(lint_ctx, outputs, tool_source)
assert "0 outputs found." in lint_ctx.info_messages
- assert "Tool contains multiple output sections, behavior undefined." in lint_ctx.warn_messages
+ assert "Invalid XML: Element 'outputs': This element is not expected." in lint_ctx.error_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
- assert len(lint_ctx.warn_messages) == 1
- assert not lint_ctx.error_messages
+ assert not lint_ctx.warn_messages
+ assert len(lint_ctx.error_messages) == 1
def test_outputs_unknown_tag(lint_ctx):
+ """
+ In the past it has been assumed that output tags are not allowed, but only data.
+ But, actually they are covered by xsd which is also tested here.
+ Still output tags are not covered in the user facing xml schema docs and should
+ probably be avoided.
+ """
tool_source = get_xml_tool_source(OUTPUTS_UNKNOWN_TAG)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
+ lint_tool_source_with_modules(lint_ctx, tool_source, [outputs, xsd])
assert "0 outputs found." in lint_ctx.info_messages
- assert "Unknown element found in outputs [output]" in lint_ctx.warn_messages
+ assert "Avoid the use of 'output' and replace by 'data' or 'collection'" in lint_ctx.warn_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
assert len(lint_ctx.warn_messages) == 1
@@ -1503,22 +1545,23 @@ def test_outputs_unknown_tag(lint_ctx):
def test_outputs_unnamed_invalid_name(lint_ctx):
tool_source = get_xml_tool_source(OUTPUTS_UNNAMED_INVALID_NAME)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
- assert "2 outputs found." in lint_ctx.info_messages
- assert "Tool output doesn't define a name - this is likely a problem." in lint_ctx.warn_messages
+ run_lint_module(lint_ctx, outputs, tool_source)
+ assert "3 outputs found." in lint_ctx.info_messages
+ assert "Invalid XML: Element 'data': The attribute 'name' is required but missing." in lint_ctx.error_messages
+ assert "Invalid XML: Element 'collection': The attribute 'name' is required but missing." in lint_ctx.error_messages
assert "Tool data output with missing name doesn't define an output format." in lint_ctx.warn_messages
assert "Tool output name [2output] is not a valid Cheetah placeholder." in lint_ctx.warn_messages
assert "Collection output with undefined 'type' found." in lint_ctx.warn_messages
assert "Tool collection output 2output doesn't define an output format." in lint_ctx.warn_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
- assert len(lint_ctx.warn_messages) == 5
- assert not lint_ctx.error_messages
+ assert len(lint_ctx.warn_messages) == 4
+ assert len(lint_ctx.error_messages) == 2
def test_outputs_format_input_legacy(lint_ctx):
tool_source = get_xml_tool_source(OUTPUTS_FORMAT_INPUT_LEGACY)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
+ run_lint_module(lint_ctx, outputs, tool_source)
assert "1 outputs found." in lint_ctx.info_messages
assert "Using format='input' on data is deprecated. Use the format_source attribute." in lint_ctx.warn_messages
assert len(lint_ctx.info_messages) == 1
@@ -1529,7 +1572,7 @@ def test_outputs_format_input_legacy(lint_ctx):
def test_outputs_format_input(lint_ctx):
tool_source = get_xml_tool_source(OUTPUTS_FORMAT_INPUT)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
+ run_lint_module(lint_ctx, outputs, tool_source)
assert "1 outputs found." in lint_ctx.info_messages
assert "Using format='input' on data is deprecated. Use the format_source attribute." in lint_ctx.error_messages
assert len(lint_ctx.info_messages) == 1
@@ -1540,7 +1583,7 @@ def test_outputs_format_input(lint_ctx):
def test_outputs_collection_format_source(lint_ctx):
tool_source = get_xml_tool_source(OUTPUTS_COLLECTION_FORMAT_SOURCE)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
+ run_lint_module(lint_ctx, outputs, tool_source)
assert "Tool data output 'reverse' should use either format_source or format/ext" in lint_ctx.warn_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
@@ -1550,7 +1593,7 @@ def test_outputs_collection_format_source(lint_ctx):
def test_outputs_format_action(lint_ctx):
tool_source = get_xml_tool_source(OUTPUTS_FORMAT_ACTION)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
+ run_lint_module(lint_ctx, outputs, tool_source)
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
assert not lint_ctx.warn_messages
@@ -1559,7 +1602,7 @@ def test_outputs_format_action(lint_ctx):
def test_outputs_discover_tool_provided_metadata(lint_ctx):
tool_source = get_xml_tool_source(OUTPUTS_DISCOVER_TOOL_PROVIDED_METADATA)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
+ run_lint_module(lint_ctx, outputs, tool_source)
assert "1 outputs found." in lint_ctx.info_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
@@ -1568,8 +1611,9 @@ def test_outputs_discover_tool_provided_metadata(lint_ctx):
def test_outputs_duplicated_name_label(lint_ctx):
+ """ """
tool_source = get_xml_tool_source(OUTPUTS_DUPLICATED_NAME_LABEL)
- run_lint(lint_ctx, outputs.lint_output, tool_source)
+ run_lint_module(lint_ctx, outputs, tool_source)
assert "4 outputs found." in lint_ctx.info_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
@@ -1585,7 +1629,7 @@ def test_outputs_duplicated_name_label(lint_ctx):
def test_stdio_default_for_default_profile(lint_ctx):
tool_source = get_xml_tool_source(STDIO_DEFAULT_FOR_DEFAULT_PROFILE)
- run_lint(lint_ctx, stdio.lint_stdio, tool_source)
+ run_lint_module(lint_ctx, stdio, tool_source)
assert (
"No stdio definition found, tool indicates error conditions with output written to stderr."
in lint_ctx.info_messages
@@ -1598,7 +1642,7 @@ def test_stdio_default_for_default_profile(lint_ctx):
def test_stdio_default_for_nonlegacy_profile(lint_ctx):
tool_source = get_xml_tool_source(STDIO_DEFAULT_FOR_NONLEGACY_PROFILE)
- run_lint(lint_ctx, stdio.lint_stdio, tool_source)
+ run_lint_module(lint_ctx, stdio, tool_source)
assert (
"No stdio definition found, tool indicates error conditions with non-zero exit codes." in lint_ctx.info_messages
)
@@ -1610,8 +1654,8 @@ def test_stdio_default_for_nonlegacy_profile(lint_ctx):
def test_stdio_multiple_stdio(lint_ctx):
tool_source = get_xml_tool_source(STDIO_MULTIPLE_STDIO)
- run_lint(lint_ctx, stdio.lint_stdio, tool_source)
- assert "More than one stdio tag found, behavior undefined." in lint_ctx.error_messages
+ run_lint_module(lint_ctx, stdio, tool_source)
+ assert "Invalid XML: Element 'stdio': This element is not expected." in lint_ctx.error_messages
assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
assert not lint_ctx.warn_messages
@@ -1619,22 +1663,33 @@ def test_stdio_multiple_stdio(lint_ctx):
def test_stdio_invalid_child_or_attrib(lint_ctx):
+ """
+ note the test name is due to that this was previously covered by linting code in stdio
+ """
tool_source = get_xml_tool_source(STDIO_INVALID_CHILD_OR_ATTRIB)
- run_lint(lint_ctx, stdio.lint_stdio, tool_source)
- assert (
- "Unknown stdio child tag discovered [reqex]. Valid options are exit_code and regex." in lint_ctx.warn_messages
- )
- assert "Unknown attribute [descriptio] encountered on exit_code tag." in lint_ctx.warn_messages
- assert "Unknown attribute [descriptio] encountered on regex tag." in lint_ctx.warn_messages
+ run_lint_module(lint_ctx, xsd, tool_source)
assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
- assert len(lint_ctx.warn_messages) == 3
- assert not lint_ctx.error_messages
+ assert not len(lint_ctx.warn_messages) == 3
+ assert "Invalid XML: Element 'reqex': This element is not expected." in lint_ctx.error_messages
+ assert (
+ "Invalid XML: Element 'regex', attribute 'source': [facet 'enumeration'] The value 'stdio' is not an element of the set {'stdout', 'stderr', 'both'}."
+ in lint_ctx.error_messages
+ )
+ assert (
+ "Invalid XML: Element 'regex', attribute 'descriptio': The attribute 'descriptio' is not allowed."
+ in lint_ctx.error_messages
+ )
+ assert (
+ "Invalid XML: Element 'exit_code', attribute 'descriptio': The attribute 'descriptio' is not allowed."
+ in lint_ctx.error_messages
+ )
+ assert len(lint_ctx.error_messages) == 4
def test_stdio_invalid_match(lint_ctx):
tool_source = get_xml_tool_source(STDIO_INVALID_MATCH)
- run_lint(lint_ctx, stdio.lint_stdio, tool_source)
+ run_lint_module(lint_ctx, stdio, tool_source)
assert (
"Match '[' is no valid regular expression: unterminated character set at position 0" in lint_ctx.error_messages
)
@@ -1645,8 +1700,8 @@ def test_stdio_invalid_match(lint_ctx):
def test_tests_absent(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_ABSENT)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
+ tool_source = get_xml_tool_source(TESTS_ABSENT)
+ run_lint_module(lint_ctx, tests, tool_source)
assert "No tests found, most tools should define test cases." in lint_ctx.warn_messages
assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
@@ -1655,8 +1710,8 @@ def test_tests_absent(lint_ctx):
def test_tests_data_source(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_ABSENT_DATA_SOURCE)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
+ tool_source = get_xml_tool_source(TESTS_ABSENT_DATA_SOURCE)
+ run_lint_module(lint_ctx, tests, tool_source)
assert "No tests found, that should be OK for data_sources." in lint_ctx.info_messages
assert len(lint_ctx.info_messages) == 1
assert not lint_ctx.valid_messages
@@ -1665,17 +1720,20 @@ def test_tests_data_source(lint_ctx):
def test_tests_param_output_names(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_PARAM_OUTPUT_NAMES)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
+ tool_source = get_xml_tool_source(TESTS_PARAM_OUTPUT_NAMES)
+ run_lint_module(lint_ctx, tests, tool_source)
assert "1 test(s) found." in lint_ctx.valid_messages
- assert "Test 1: Found test param tag without a name defined." in lint_ctx.error_messages
+ assert "Invalid XML: Element 'param': The attribute 'name' is required but missing." in lint_ctx.error_messages
assert "Test 1: Test param non_existent_test_name not found in the inputs" in lint_ctx.error_messages
assert "Test 1: Found output tag without a name defined." in lint_ctx.error_messages
assert (
"Test 1: Found output tag with unknown name [nonexistent_output], valid names ['existent_output', 'existent_collection']"
in lint_ctx.error_messages
)
- assert "Test 1: Found output_collection tag without a name defined." in lint_ctx.error_messages
+ assert (
+ "Invalid XML: Element 'output_collection': The attribute 'name' is required but missing."
+ in lint_ctx.error_messages
+ )
assert (
"Test 1: Found output_collection tag with unknown name [nonexistent_collection], valid names ['existent_output', 'existent_collection']"
in lint_ctx.error_messages
@@ -1687,8 +1745,8 @@ def test_tests_param_output_names(lint_ctx):
def test_tests_expect_failure_output(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_EXPECT_FAILURE_OUTPUT)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
+ tool_source = get_xml_tool_source(TESTS_EXPECT_FAILURE_OUTPUT)
+ run_lint_module(lint_ctx, tests, tool_source)
assert "No valid test(s) found." in lint_ctx.warn_messages
assert "Test 1: Cannot specify outputs in a test expecting failure." in lint_ctx.error_messages
assert (
@@ -1702,8 +1760,8 @@ def test_tests_expect_failure_output(lint_ctx):
def test_tests_without_expectations(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_WO_EXPECTATIONS)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
+ tool_source = get_xml_tool_source(TESTS_WO_EXPECTATIONS)
+ run_lint_module(lint_ctx, tests, tool_source)
assert (
"Test 1: No outputs or expectations defined for tests, this test is likely invalid." in lint_ctx.warn_messages
)
@@ -1715,8 +1773,8 @@ def test_tests_without_expectations(lint_ctx):
def test_tests_valid(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_VALID)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
+ tool_source = get_xml_tool_source(TESTS_VALID)
+ run_lint_module(lint_ctx, tests, tool_source)
assert "1 test(s) found." in lint_ctx.valid_messages
assert not lint_ctx.info_messages
assert len(lint_ctx.valid_messages) == 1
@@ -1725,26 +1783,32 @@ def test_tests_valid(lint_ctx):
def test_tests_asserts(lint_ctx):
- tool_xml_tree = get_xml_tree(ASSERTS)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
- assert "Test 1: unknown assertion 'invalid'" in lint_ctx.error_messages
- assert "Test 1: unknown attribute 'invalid_attrib' for 'has_text'" in lint_ctx.error_messages
- assert "Test 1: missing attribute 'text' for 'has_text'" in lint_ctx.error_messages
- assert "Test 1: attribute 'value' for 'has_size' needs to be 'int' got '500k'" not in lint_ctx.error_messages
+ tool_source = get_xml_tool_source(ASSERTS)
+ run_lint_module(lint_ctx, tests, tool_source)
+ assert "Invalid XML: Element 'invalid': This element is not expected." in lint_ctx.error_messages
+ assert (
+ "Invalid XML: Element 'has_text', attribute 'invalid_attrib': The attribute 'invalid_attrib' is not allowed."
+ in lint_ctx.error_messages
+ )
+ assert "Invalid XML: Element 'has_text': The attribute 'text' is required but missing." in lint_ctx.error_messages
+ assert (
+ "Invalid XML: Element 'has_size', attribute 'delta': '1X' is not a valid value of the union type 'Bytes'."
+ in lint_ctx.error_messages
+ )
assert (
- "Test 1: unknown attribute 'invalid_attrib_also_checked_in_nested_asserts' for 'not_has_text'"
+ "Invalid XML: Element 'not_has_text', attribute 'invalid_attrib_also_checked_in_nested_asserts': The attribute 'invalid_attrib_also_checked_in_nested_asserts' is not allowed."
in lint_ctx.error_messages
)
assert "Test 1: 'has_size' needs to specify 'value', 'min', or 'max'" in lint_ctx.error_messages
assert "Test 1: 'has_n_columns' needs to specify 'n', 'min', or 'max'" in lint_ctx.error_messages
assert "Test 1: 'has_n_lines' needs to specify 'n', 'min', or 'max'" in lint_ctx.error_messages
assert not lint_ctx.warn_messages
- assert len(lint_ctx.error_messages) == 7
+ assert len(lint_ctx.error_messages) == 8
def test_tests_output_type_mismatch(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_OUTPUT_TYPE_MISMATCH)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
+ tool_source = get_xml_tool_source(TESTS_OUTPUT_TYPE_MISMATCH)
+ run_lint_module(lint_ctx, tests, tool_source)
assert (
"Test 1: test output collection_name does not correspond to a 'data' output, but a 'collection'"
in lint_ctx.error_messages
@@ -1758,8 +1822,8 @@ def test_tests_output_type_mismatch(lint_ctx):
def test_tests_discover_outputs(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_DISCOVER_OUTPUTS)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
+ tool_source = get_xml_tool_source(TESTS_DISCOVER_OUTPUTS)
+ run_lint_module(lint_ctx, tests, tool_source)
assert (
"Test 3: test output 'data_name' must have a 'count' attribute and/or 'discovered_dataset' children"
in lint_ctx.error_messages
@@ -1769,11 +1833,11 @@ def test_tests_discover_outputs(lint_ctx):
in lint_ctx.error_messages
)
assert (
- "Test 3: test collection 'collection_name' must contain nested 'element' tags and/or element childen with a 'count' attribute"
+ "Test 3: test collection 'collection_name' must contain nested 'element' tags and/or element children with a 'count' attribute"
in lint_ctx.error_messages
)
assert (
- "Test 5: test collection 'collection_name' must contain nested 'element' tags and/or element childen with a 'count' attribute"
+ "Test 5: test collection 'collection_name' must contain nested 'element' tags and/or element children with a 'count' attribute"
in lint_ctx.error_messages
)
assert not lint_ctx.warn_messages
@@ -1781,16 +1845,16 @@ def test_tests_discover_outputs(lint_ctx):
def test_tests_expect_num_outputs_filter(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_EXPECT_NUM_OUTPUTS_FILTER)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
- assert "Test should specify 'expect_num_outputs' if outputs have filters" in lint_ctx.warn_messages
+ tool_source = get_xml_tool_source(TESTS_EXPECT_NUM_OUTPUTS_FILTER)
+ run_lint_module(lint_ctx, tests, tool_source)
+ assert "Test 1: should specify 'expect_num_outputs' if outputs have filters" in lint_ctx.warn_messages
assert len(lint_ctx.warn_messages) == 1
assert len(lint_ctx.error_messages) == 0
def test_tests_compare_attrib_incompatibility(lint_ctx):
- tool_xml_tree = get_xml_tree(TESTS_COMPARE_ATTRIB_INCOMPATIBILITY)
- run_lint(lint_ctx, tests.lint_tests, tool_xml_tree)
+ tool_source = get_xml_tool_source(TESTS_COMPARE_ATTRIB_INCOMPATIBILITY)
+ run_lint_module(lint_ctx, tests, tool_source)
assert 'Test 1: Attribute decompress is incompatible with compare="re_match".' in lint_ctx.error_messages
assert 'Test 1: Attribute sort is incompatible with compare="contains".' in lint_ctx.error_messages
assert not lint_ctx.info_messages
@@ -1800,17 +1864,15 @@ def test_tests_compare_attrib_incompatibility(lint_ctx):
def test_xml_order(lint_ctx):
- tool_xml_tree = get_xml_tree(XML_ORDER)
- run_lint(lint_ctx, xml_order.lint_xml_order, tool_xml_tree)
- assert "Unknown tag [wrong_tag] encountered, this may result in a warning in the future." in lint_ctx.info_messages
- assert "Best practice violation [stdio] elements should come before [command]" in lint_ctx.warn_messages
- assert len(lint_ctx.info_messages) == 1
+ tool_source = get_xml_tool_source(XML_ORDER)
+ run_lint_module(lint_ctx, xml_order, tool_source)
+ assert not lint_ctx.info_messages
assert not lint_ctx.valid_messages
- assert len(lint_ctx.warn_messages) == 1
- assert not lint_ctx.error_messages
+ assert lint_ctx.warn_messages == ["Best practice violation [stdio] elements should come before [command]"]
+ assert lint_ctx.error_messages == ["Invalid XML: Element 'wrong_tag': This element is not expected."]
-DATA_MANAGER = """
+DATA_MANAGER = """
@@ -1840,7 +1902,7 @@ def test_data_manager(lint_ctx_xpath, lint_ctx):
assert len(lint_ctx.error_messages) == 3
-COMPLETE = """
+COMPLETE = """
macros.xml
@@ -1849,10 +1911,11 @@ def test_data_manager(lint_ctx_xpath, lint_ctx):
+ blah fasel
-
+
@@ -1890,12 +1953,25 @@ def test_tool_and_macro_xml(lint_ctx_xpath, lint_ctx):
lint_tool_source_with(lint_ctx, tool_source)
asserts = (
- ("Select parameter [select] has multiple options with the same value", 5, "/tool/inputs/param[1]"),
- ("Found param input with no name specified.", 13, "/tool/inputs/param[2]"),
- ("Param input [No_type] input with no type specified.", 3, "/tool/inputs/param[3]"),
+ (
+ "Select parameter [select] has multiple options with the same value",
+ 5,
+ "/tool/inputs/param[1]",
+ "InputsSelectOptionDuplicateValue",
+ ),
+ ("Found param input with no name specified.", 14, "/tool/inputs/param[2]", "InputsName"),
+ (
+ "Invalid XML: Element 'param': The attribute 'type' is required but missing.",
+ 3,
+ "/tool/inputs/param[3]",
+ "XSD",
+ ),
)
for a in asserts:
- message, line, xpath = a
+ message, line, xpath, linter = a
+ # check message + path combinations
+ # found = set([(lint_message.message, lint_message.xpath) for lint_message in lint_ctx_xpath.message_list])
+ # path_asserts = set([(a[0], a[2]) for a in asserts])
found = False
for lint_message in lint_ctx_xpath.message_list:
if lint_message.message != message:
@@ -1904,12 +1980,14 @@ def test_tool_and_macro_xml(lint_ctx_xpath, lint_ctx):
assert (
lint_message.xpath == xpath
), f"Assumed xpath {xpath}; found xpath {lint_message.xpath} for: {message}"
+ assert lint_message.linter == linter
assert found, f"Did not find {message}"
for lint_message in lint_ctx.message_list:
if lint_message.message != message:
continue
found = True
assert lint_message.line == line, f"Assumed line {line}; found line {lint_message.line} for: {message}"
+ assert lint_message.linter == linter
assert found, f"Did not find {message}"
@@ -1993,3 +2071,25 @@ def test_xml_comments_are_ignored(lint_ctx: LintContext):
lint_xml_with(lint_ctx, tool_xml)
for lint_message in lint_ctx.message_list:
assert "Comment" not in lint_message.message
+
+
+def test_list_linters():
+ linter_names = Linter.list_listers()
+ # make sure to add/remove a test for new/removed linters if this number changes
+ assert len(linter_names) == 129
+ assert "Linter" not in linter_names
+ # make sure that linters from all modules are available
+ for prefix in [
+ "Citations",
+ "Command",
+ "CWL",
+ "ToolProfile",
+ "Help",
+ "Inputs",
+ "Outputs",
+ "StdIO",
+ "Tests",
+ "XMLOrder",
+ "XSD",
+ ]:
+ assert len([x for x in linter_names if x.startswith(prefix)])