diff --git a/lib/galaxy/tool_util/linters/required_files.py b/lib/galaxy/tool_util/linters/required_files.py new file mode 100644 index 00000000000..cf4426a4e99 --- /dev/null +++ b/lib/galaxy/tool_util/linters/required_files.py @@ -0,0 +1,43 @@ +import os +from typing import TYPE_CHECKING + +from galaxy.tool_util.lint import Linter +from galaxy.tool_util.parser.interface import RequiredFiles + +if TYPE_CHECKING: + from galaxy.tool_util.lint import LintContext + from galaxy.tool_util.parser import ToolSource + + +class RequiredFilesExist(Linter): + """Check that required_files include patterns match existing files.""" + + @classmethod + def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): + if not tool_source.source_path: + return + tool_dir = os.path.dirname(tool_source.source_path) + required_files = tool_source.parse_required_files() + if required_files is None: + return + for include in required_files.includes: + per_include = RequiredFiles.from_dict( + { + "includes": [include], + "excludes": [], + "extend_default_excludes": False, + } + ) + if not per_include.find_required_files(tool_dir): + path = include["path"] + path_type = include.get("path_type", "literal") + if path_type == "literal": + lint_ctx.error( + f"Required file [{path}] does not exist", + linter=cls.name(), + ) + else: + lint_ctx.error( + f"Required files pattern [{path}] (type {path_type}) does not match any files", + linter=cls.name(), + ) diff --git a/test/unit/tool_util/test_tool_linters.py b/test/unit/tool_util/test_tool_linters.py index 20b82c6567e..d14234b70f3 100644 --- a/test/unit/tool_util/test_tool_linters.py +++ b/test/unit/tool_util/test_tool_linters.py @@ -21,6 +21,7 @@ from galaxy.tool_util.linters import ( help, inputs, output, + required_files, stdio, tests, xml_order, @@ -2424,7 +2425,7 @@ def test_skip_by_module(lint_ctx): 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) == 142 + assert len(linter_names) == 143 assert "Linter" not in linter_names # make sure that linters from all modules are available for prefix in [ @@ -2500,3 +2501,84 @@ def test_linter_module_list(): elif inspect.isclass(value) and issubclass(value, Linter) and not inspect.isabstract(value): linter_cnt += 1 assert linter_cnt >= old_linters[module_name] + + +REQUIRED_FILES_LITERAL = """ + + + + + +""" + +REQUIRED_FILES_LITERAL_MISSING = """ + + + + + +""" + +REQUIRED_FILES_GLOB = """ + + + + + +""" + +REQUIRED_FILES_GLOB_NO_MATCH = """ + + + + + +""" + + +def _write_file(directory, filename, content): + """Write content to a file in directory, return full path.""" + path = os.path.join(directory, filename) + with open(path, "w") as f: + f.write(content) + return path + + +def _load_and_run_lint(lint_ctx, tool_path, lint_module): + """Load a tool XML from disk and run a lint module on it.""" + tool_xml, _ = load_with_references(tool_path) + tool_source = XmlToolSource(tool_xml, source_path=tool_path) + run_lint_module(lint_ctx, lint_module, tool_source) + + +def test_required_files_literal_exist(lint_ctx): + with tempfile.TemporaryDirectory() as tool_dir: + tool_path = _write_file(tool_dir, "tool.xml", REQUIRED_FILES_LITERAL) + _write_file(tool_dir, "my_script.R", "# R script") + _load_and_run_lint(lint_ctx, tool_path, required_files) + assert not lint_ctx.error_messages + + +def test_required_files_literal_missing(lint_ctx): + with tempfile.TemporaryDirectory() as tool_dir: + tool_path = _write_file(tool_dir, "tool.xml", REQUIRED_FILES_LITERAL_MISSING) + _load_and_run_lint(lint_ctx, tool_path, required_files) + assert "Required file [nonexistent.py] does not exist" in lint_ctx.error_messages + assert len(lint_ctx.error_messages) == 1 + + +def test_required_files_glob_match(lint_ctx): + with tempfile.TemporaryDirectory() as tool_dir: + tool_path = _write_file(tool_dir, "tool.xml", REQUIRED_FILES_GLOB) + _write_file(tool_dir, "my_script.R", "# R script") + _load_and_run_lint(lint_ctx, tool_path, required_files) + assert not lint_ctx.error_messages + + +def test_required_files_glob_no_match(lint_ctx): + with tempfile.TemporaryDirectory() as tool_dir: + tool_path = _write_file(tool_dir, "tool.xml", REQUIRED_FILES_GLOB_NO_MATCH) + _write_file(tool_dir, "my_script.R", "# R script") + _load_and_run_lint(lint_ctx, tool_path, required_files) + assert "Required files pattern [*.py] (type glob) does not match any files" in lint_ctx.error_messages + assert len(lint_ctx.error_messages) == 1