import inspect import os import tempfile import pytest import galaxy.tool_util.linters from galaxy.tool_util.lint import ( lint_tool_source_with, lint_tool_source_with_modules, LintContext, Linter, XMLLintMessageLine, XMLLintMessageXPath, ) from galaxy.tool_util.linters import ( citations, command, datatypes, general, help, inputs, output, required_files, stdio, tests, xml_order, xsd, ) from galaxy.tool_util.loader_directory import load_tool_sources_from_path from galaxy.tool_util.parser.interface import ToolSource from galaxy.tool_util.parser.xml import XmlToolSource from galaxy.tool_util.unittest_utils import functional_test_tool_path from galaxy.util import ( ElementTree, submodules, ) from galaxy.util.unittest_utils import skip_if_site_down from galaxy.util.xml_macros import load_with_references # TODO tests tool xml for general linter # tests tool xml for citations linter CITATIONS_MULTIPLE = """ """ CITATIONS_ABSENT = """ """ CITATIONS_ERRORS = """ """ CITATIONS_VALID = """ DOI """ # tests tool xml for command linter COMMAND_MULTIPLE = """ ls df """ COMMAND_MISSING = """ """ COMMAND_TODO = """ ## TODO """ COMMAND_DETECT_ERRORS_INTERPRETER = """ """ # tests tool xml for general linter GENERAL_MISSING_TOOL_ID_NAME_VERSION = """ """ GENERAL_WHITESPACE_IN_VERSIONS_AND_NAMES = """ """ GENERAL_REQUIREMENT_WO_VERSION = """ """ GENERAL_VALID = """ """ GENERAL_VALID_NEW_PROFILE_FMT = """ """ GENERAL_VALID_BIOTOOLS = """ bwa johnscoolbowtie """ GENERAL_VALID_EDAM = """ topic_2269 topic_000 operation_3434 operation_000 """ # test tool xml for help linter HELP_MULTIPLE = """ Help More help """ HELP_ABSENT = """ """ HELP_EMPTY = """ """ HELP_TODO = """ TODO """ HELP_INVALID_RST = """ **xxl__ """ # test tool xml for inputs linter INPUTS_NO_INPUTS = """ """ INPUTS_NO_INPUTS_DATASOURCE = """ """ INPUTS_VALID = """ """ INPUTS_PARAM_NAME = """ """ INPUTS_PARAM_TYPE = """ """ INPUTS_DATA_PARAM = """ """ INPUTS_DATA_PARAM_OPTIONS = """ """ INPUTS_DATA_PARAM_OPTIONS_FILTER_ATTRIBUTE = """ """ INPUTS_DATA_PARAM_INVALIDOPTIONS = """ """ INPUTS_BOOLEAN_PARAM_SWAPPED_LABELS = """ """ INPUTS_BOOLEAN_PARAM_DUPLICATE_LABELS = """ """ INPUTS_CONDITIONAL = """ """ INPUTS_SELECT_INCOMPATIBLE_DISPLAY = """ """ INPUTS_SELECT_DUPLICATED_OPTIONS = """ """ SELECT_DUPLICATED_OPTIONS_WITH_DIFF_SELECTED = """ """ INPUTS_SELECT_DEPRECATIONS = """ """ INPUTS_SELECT_OPTION_DEFINITIONS = """ """ INPUTS_SELECT_FILTER = """ """ INPUTS_VALIDATOR_INCOMPATIBILITIES = """ TEXT [ ( value and "," not in value """ INPUTS_VALIDATOR_CORRECT = """ reg.xp somepythonexpression reg.xp somepythonexpression somepythonexpression """ INPUTS_TYPE_CHILD_COMBINATIONS = """ """ INPUTS_DUPLICATE_NAMES = """
""" INPUTS_FILTER_INCORRECT = """ """ INPUTS_FILTER_CORRECT = """ """ # test tool xml for outputs linter OUTPUTS_MISSING = """ """ OUTPUTS_MULTIPLE = """ """ OUTPUTS_UNKNOWN_TAG = """ """ OUTPUTS_UNNAMED_INVALID_NAME = """ """ OUTPUTS_FORMAT_INPUT_LEGACY = """ """ OUTPUTS_FORMAT_INPUT = """ """ # 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 = """ """ # check that setting format with actions is supported OUTPUTS_FORMAT_ACTION = """ """ # 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 = """ a and condition another or condition """ # check if filters are valid python expressions OUTPUTS_FILTER_EXPRESSION = """ an invalid condition an and condition another invalid condition another or condition """ # tool xml for repeats linter REPEATS = """ """ # 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 = """ """ # check that linter does complain about tests wo assumptions TESTS_ABSENT = """ """ TESTS_ABSENT_YAML = """ class: GalaxyTool name: name id: id """ TESTS_ABSENT_DATA_SOURCE = """ """ TESTS_WO_EXPECTATIONS = """ """ TESTS_PARAM_OUTPUT_NAMES = """ """ TESTS_EXPECT_FAILURE_OUTPUT = """ """ ASSERTS = """ """ INVALID_CENTER_OF_MASS = """ """ VALID_CENTER_OF_MASS = """ """ ASSERTS_STRING_COERCION = """ """ TESTS_VALID = """ """ TESTS_OUTPUT_TYPE_MISMATCH = """ """ TESTS_DISCOVER_OUTPUTS = """ """ TESTS_EXPECT_NUM_OUTPUTS_FILTER = """ """ TESTS_COMPARE_ATTRIB_INCOMPATIBILITY = """ """ # tool xml for xml_order linter XML_ORDER = """ """ TOOL_WITH_COMMENTS = """ """ @pytest.fixture() def lint_ctx(): return LintContext("all", lint_message_class=XMLLintMessageLine) @pytest.fixture() def lint_ctx_xpath(): return LintContext("all", lint_message_class=XMLLintMessageXPath) def get_xml_tree(xml_string: str) -> ElementTree: with tempfile.NamedTemporaryFile(mode="w", suffix="tool.xml") as tmp: tmp.write(xml_string) tmp.flush() tool_path = tmp.name return load_with_references(tool_path)[0] def get_xml_tool_source(xml_string: str) -> XmlToolSource: return XmlToolSource(get_xml_tree(xml_string)) def get_tool_source(source_contents: str) -> ToolSource: if "GalaxyTool" in source_contents: with tempfile.NamedTemporaryFile(mode="w", suffix="tool.yml") as tmp: tmp.write(source_contents) tmp.flush() tool_sources = load_tool_sources_from_path(tmp.name) assert len(tool_sources) == 1, "Expected 1 tool source" tool_source = tool_sources[0][1] return tool_source else: return get_xml_tool_source(source_contents) def run_lint_module(lint_ctx, lint_module, lint_target): lint_tool_source_with_modules(lint_ctx, lint_target, list({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: assert message.line is not None, f"No context found for message: {message.message}" def test_citations_multiple(lint_ctx): 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 def test_citations_absent(lint_ctx): 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 assert not lint_ctx.error_messages def test_citations_errors(lint_ctx): 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 len(lint_ctx.valid_messages) == 1 def test_citations_valid(lint_ctx): 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_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_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_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_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_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 assert "Tool specifies an invalid profile version [2109]." in lint_ctx.error_messages def test_general_whitespace_in_versions_and_names(lint_ctx): tool_source = get_xml_tool_source(GENERAL_WHITESPACE_IN_VERSIONS_AND_NAMES) 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 assert "Tool ID contains whitespace - this is discouraged: [bwa tool]." in lint_ctx.warn_messages assert "Tool targets 16.01 Galaxy profile." in lint_ctx.valid_messages def test_general_requirement_without_version(lint_ctx): tool_source = get_xml_tool_source(GENERAL_REQUIREMENT_WO_VERSION) 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 assert "Tool specifies profile version [20.09]." in lint_ctx.valid_messages assert "Tool defines an id [bwa_tool]." in lint_ctx.valid_messages assert "Tool defines a name [BWA Mapper]." in lint_ctx.valid_messages assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 3 assert len(lint_ctx.warn_messages) == 2 assert len(lint_ctx.error_messages) == 1 def test_general_valid(lint_ctx): tool_source = get_xml_tool_source(GENERAL_VALID) 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 assert "Tool defines a name [valid name]." in lint_ctx.valid_messages assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 4 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_general_valid_new_profile_fmt(lint_ctx): tool_source = get_xml_tool_source(GENERAL_VALID_NEW_PROFILE_FMT) 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 assert "Tool defines a name [valid name]." in lint_ctx.valid_messages assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 4 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages @skip_if_site_down("https://bio.tools/") def test_general_valid_biotools(lint_ctx): tool_source = get_xml_tool_source(GENERAL_VALID_BIOTOOLS) run_lint_module(lint_ctx, general, tool_source) assert "No entry johnscoolbowtie in bio.tools." in lint_ctx.warn_messages assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 4 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_general_text_spaces_comments(lint_ctx): tool_source = get_xml_tool_source(TOOL_WITH_COMMENTS) run_lint_module(lint_ctx, general, tool_source) assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 4 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_general_valid_edam(lint_ctx): tool_source = get_xml_tool_source(GENERAL_VALID_EDAM) run_lint_module(lint_ctx, general, tool_source) assert "No entry 'operation_000' in EDAM." in lint_ctx.warn_messages assert "No entry 'topic_000' in EDAM." in lint_ctx.warn_messages assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 4 assert len(lint_ctx.warn_messages) == 2 # accept 2 xsd errors due to malformed topic/operation ID # which should make sure that the id never becomes valid assert len(lint_ctx.error_messages) == 2 def test_help_multiple(lint_ctx): tool_source = get_xml_tool_source(HELP_MULTIPLE) run_lint_module(lint_ctx, help, tool_source) assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 2 # has help and valid rst assert not lint_ctx.warn_messages assert lint_ctx.error_messages == ["Invalid XML: Element 'help': This element is not expected."] def test_help_absent(lint_ctx): 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 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_help_empty(lint_ctx): 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 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_help_todo(lint_ctx): 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 assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 2 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_help_invalid_rst(lint_ctx): 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]." in lint_ctx.warn_messages ) assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 1 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_inputs_no_inputs(lint_ctx): tool_source = get_xml_tool_source(INPUTS_NO_INPUTS) 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 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_inputs_no_inputs_datasource(lint_ctx): tool_source = get_xml_tool_source(INPUTS_NO_INPUTS_DATASOURCE) 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 assert len(lint_ctx.info_messages) == 3 assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_inputs_valid(lint_ctx): tool_source = get_xml_tool_source(INPUTS_VALID) 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 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_inputs_param_name(lint_ctx): tool_source = get_xml_tool_source(INPUTS_PARAM_NAME) 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 assert "Param input with empty name." in lint_ctx.error_messages assert ( "Param input [param_name] 'name' attribute is redundant if argument implies the same name." in lint_ctx.warn_messages ) assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert len(lint_ctx.warn_messages) == 2 assert len(lint_ctx.error_messages) == 2 def test_inputs_param_type(lint_ctx): tool_source = get_xml_tool_source(INPUTS_PARAM_TYPE) run_lint_module(lint_ctx, inputs, tool_source) assert "Found 2 input parameters." in lint_ctx.info_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 assert len(lint_ctx.error_messages) == 2 def test_inputs_data_param(lint_ctx): tool_source = get_xml_tool_source(INPUTS_DATA_PARAM) 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 ) 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 def test_inputs_boolean_param_duplicate_labels(lint_ctx): tool_source = get_xml_tool_source(INPUTS_BOOLEAN_PARAM_DUPLICATE_LABELS) 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 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_inputs_boolean_param_swapped_labels(lint_ctx): tool_source = get_xml_tool_source(INPUTS_BOOLEAN_PARAM_SWAPPED_LABELS) 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 assert len(lint_ctx.warn_messages) == 2 assert not lint_ctx.error_messages def test_inputs_data_param_options(lint_ctx): tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_OPTIONS) run_lint_module(lint_ctx, inputs, tool_source) assert not lint_ctx.valid_messages assert "Found 2 input parameters." in lint_ctx.info_messages assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_inputs_data_param_options_filter_attribute(lint_ctx): tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_OPTIONS_FILTER_ATTRIBUTE) run_lint_module(lint_ctx, inputs, tool_source) assert not lint_ctx.valid_messages assert "Found 2 input parameters." in lint_ctx.info_messages assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_inputs_data_param_invalid_options(lint_ctx): tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_INVALIDOPTIONS) 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 assert not lint_ctx.warn_messages 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="regexp" and key="None"' in lint_ctx.error_messages ) assert len(lint_ctx.error_messages) == 3 def test_inputs_conditional(lint_ctx): tool_source = get_xml_tool_source(INPUTS_CONDITIONAL) run_lint_module(lint_ctx, inputs, tool_source) assert "Found 10 input parameters." in lint_ctx.info_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 ( "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' in lint_ctx.warn_messages ) 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 "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 assert len(lint_ctx.warn_messages) == 6 assert len(lint_ctx.error_messages) == 6 def test_inputs_select_incompatible_display(lint_ctx): tool_source = get_xml_tool_source(INPUTS_SELECT_INCOMPATIBLE_DISPLAY) 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 assert ( 'Select [checkboxes_select] `display="checkboxes"` is incompatible with `optional="false"`, remove the `display` attribute' in lint_ctx.error_messages ) assert ( 'Select [checkboxes_select] `display="checkboxes"` is incompatible with `multiple="false"`, remove the `display` attribute' in lint_ctx.error_messages ) assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert len(lint_ctx.error_messages) == 4 def test_inputs_duplicated_options(lint_ctx): tool_source = get_xml_tool_source(INPUTS_SELECT_DUPLICATED_OPTIONS) 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 assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert len(lint_ctx.error_messages) == 2 def test_inputs_duplicated_options_with_different_select(lint_ctx): tool_source = get_xml_tool_source(SELECT_DUPLICATED_OPTIONS_WITH_DIFF_SELECTED) run_lint_module(lint_ctx, inputs, tool_source) assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_inputs_select_deprecations(lint_ctx): tool_source = get_xml_tool_source(INPUTS_SELECT_DEPRECATIONS) 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 assert "Select parameter [select_fp] options uses deprecated 'from_parameter' attribute." in lint_ctx.warn_messages assert "Select parameter [select_ff] options uses deprecated 'transform_lines' attribute." in lint_ctx.warn_messages assert ( "Select parameter [select_fp] options uses deprecated 'options_filter_attribute' attribute." 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 def test_inputs_select_option_definitions(lint_ctx): tool_source = get_xml_tool_source(INPUTS_SELECT_OPTION_DEFINITIONS) 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." in lint_ctx.error_messages ) assert ( "Select parameter [select_noopts] options tag defines no options. Use 'from_dataset', 'from_data_table', or a filter that adds values." in lint_ctx.error_messages ) assert ( "Select parameter [select_fd_op] options have to be defined by either 'option' children elements, a 'options' element or the 'dynamic_options' attribute." in lint_ctx.error_messages ) assert "Select parameter [select_fd_op] contains multiple options elements." in lint_ctx.error_messages assert ( "Select parameter [select_fd_fdt] options uses 'from_dataset' and 'from_data_table' attribute." in lint_ctx.error_messages ) assert "Select parameter [select_noval_notext] has option without value" in lint_ctx.error_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 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_module(lint_ctx, inputs, tool_source) assert "Found 1 input parameters." in lint_ctx.info_messages assert "Invalid XML: Element 'filter': The attribute 'type' is required but missing." in lint_ctx.error_messages assert ( "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', 'data_table'}." in lint_ctx.error_messages ) assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert len(lint_ctx.error_messages) == 2 def test_inputs_validator_incompatibilities(lint_ctx): tool_source = get_xml_tool_source(INPUTS_VALIDATOR_INCOMPATIBILITIES) 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')" in lint_ctx.warn_messages ) assert "Parameter [param_name]: validator with an incompatible type 'in_range'" in lint_ctx.error_messages assert ( "Parameter [param_name]: 'in_range' validators need to define the 'min' or 'max' attribute(s)" in lint_ctx.error_messages ) assert ( "Parameter [param_name]: attribute 'filename' is incompatible with validator of type 'regex'" in lint_ctx.error_messages ) 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 regex: unterminated character set at position 0" 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 ) assert ( "Parameter [param_name]: 'value_in_data_table' validators need to define the 'table_name' attribute" in lint_ctx.error_messages ) assert ( "Parameter [another_param_name]: attribute 'value' is incompatible with validator of type 'metadata'" in lint_ctx.error_messages ) assert ( "Parameter [another_param_name]: 'dataset_metadata_equal' validators must not define the 'value' and the 'value_json' attributes" 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 len(lint_ctx.error_messages) == 11 def test_inputs_validator_correct(lint_ctx): tool_source = get_xml_tool_source(INPUTS_VALIDATOR_CORRECT) 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 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_inputs_type_child_combinations(lint_ctx): tool_source = get_xml_tool_source(INPUTS_TYPE_CHILD_COMBINATIONS) 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 assert ( "Parameter [text_param] './options' tags are only allowed for parameters of type ['data', 'select', 'drill_down']" in lint_ctx.error_messages ) assert ( "Parameter [select_param] './options/option' tags are only allowed for parameters of type ['drill_down']" in lint_ctx.error_messages ) assert ( "Parameter [int_param] './options' tags are only allowed for parameters of type ['data', 'select', 'drill_down']" in lint_ctx.error_messages ) 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_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 assert "Tool defines multiple parameters with the same name: 'dup'" in lint_ctx.error_messages assert ( "Tool defines an output with a name equal to the name of an input: 'dup_in_output'" in lint_ctx.error_messages ) assert len(lint_ctx.error_messages) == 2 def test_inputs_filter_correct(lint_ctx): tool_source = get_xml_tool_source(INPUTS_FILTER_CORRECT) 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 assert not lint_ctx.error_messages def test_inputs_filter_incorrect(lint_ctx): tool_source = get_xml_tool_source(INPUTS_FILTER_INCORRECT) run_lint_module(lint_ctx, inputs, tool_source) assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert ( "Select parameter [select_param] 'regexp' filter specifies unnecessary attribute 'multiple'" in lint_ctx.warn_messages ) assert len(lint_ctx.warn_messages) == 1 assert "Invalid XML: Element 'filter': The attribute 'type' is required but missing." in lint_ctx.error_messages assert ( "Invalid XML: Element 'filter', attribute 'type': [facet 'enumeration'] The value 'typo' 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', 'data_table'}." in lint_ctx.error_messages ) assert ( "Select parameter [select_param] 'static_value' filter misses required attribute 'column'" in lint_ctx.error_messages ) assert ( "Select parameter [select_param] 'remove_value'' filter needs either the 'value'; 'ref'; or 'meta' and 'key' attribute(s)" in lint_ctx.error_messages ) assert ( "Select parameter [select_param] 'data_meta'' filter attribute 'ref' refers to non existing parameter 'input'" in lint_ctx.error_messages ) assert ( "Select parameter [select_param] 'regexp'' filter 'value' is not a valid regular expression (nothing to repeat at position 1)'" in lint_ctx.error_messages ) assert len(lint_ctx.error_messages) == 6 def test_inputs_repeats(lint_ctx): """ this was previously covered by a linter in inputs """ tool_source = get_xml_tool_source(REPEATS) 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_module(lint_ctx, output, 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 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_outputs_multiple(lint_ctx): tool_source = get_xml_tool_source(OUTPUTS_MULTIPLE) run_lint_module(lint_ctx, output, tool_source) assert "0 outputs found." in lint_ctx.info_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 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) lint_tool_source_with_modules(lint_ctx, tool_source, [output, xsd]) assert "0 outputs found." in lint_ctx.info_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 assert not lint_ctx.error_messages def test_outputs_unnamed_invalid_name(lint_ctx): tool_source = get_xml_tool_source(OUTPUTS_UNNAMED_INVALID_NAME) run_lint_module(lint_ctx, output, 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) == 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_module(lint_ctx, output, 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 assert not lint_ctx.valid_messages assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_outputs_format_input(lint_ctx): tool_source = get_xml_tool_source(OUTPUTS_FORMAT_INPUT) run_lint_module(lint_ctx, output, 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 assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert len(lint_ctx.error_messages) == 1 def test_outputs_collection_format_source(lint_ctx): tool_source = get_xml_tool_source(OUTPUTS_COLLECTION_FORMAT_SOURCE) run_lint_module(lint_ctx, output, 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 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_outputs_format_action(lint_ctx): tool_source = get_xml_tool_source(OUTPUTS_FORMAT_ACTION) run_lint_module(lint_ctx, output, tool_source) assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_outputs_discover_tool_provided_metadata(lint_ctx): tool_source = get_xml_tool_source(OUTPUTS_DISCOVER_TOOL_PROVIDED_METADATA) run_lint_module(lint_ctx, output, tool_source) assert "1 outputs found." in lint_ctx.info_messages assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_outputs_duplicated_name_label(lint_ctx): """ """ tool_source = get_xml_tool_source(OUTPUTS_DUPLICATED_NAME_LABEL) run_lint_module(lint_ctx, output, tool_source) assert "4 outputs found." in lint_ctx.info_messages assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert len(lint_ctx.warn_messages) == 2 assert "Tool output [valid_name] uses duplicated label '${tool.name} on ${on_string}'" in lint_ctx.warn_messages assert ( "Tool output [yet_another_valid_name] uses duplicated label 'same label may be OK if there is a filter', double check if filters imply disjoint cases" in lint_ctx.warn_messages ) assert "Tool output [valid_name] has duplicated name" in lint_ctx.error_messages assert len(lint_ctx.error_messages) == 1 def test_outputs_filter_expression(lint_ctx): """ """ tool_source = get_xml_tool_source(OUTPUTS_FILTER_EXPRESSION) run_lint_module(lint_ctx, output, tool_source) assert "2 outputs found." in lint_ctx.info_messages assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert ( "Filter 'another invalid condition' is no valid expression: invalid syntax (, line 1)" in lint_ctx.warn_messages ) assert ( "Filter 'another invalid condition' is no valid expression: invalid syntax (, line 1)" in lint_ctx.warn_messages ) assert len(lint_ctx.warn_messages) == 2 assert not lint_ctx.error_messages def test_stdio_default_for_default_profile(lint_ctx): tool_source = get_xml_tool_source(STDIO_DEFAULT_FOR_DEFAULT_PROFILE) 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 ) assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_stdio_default_for_nonlegacy_profile(lint_ctx): tool_source = get_xml_tool_source(STDIO_DEFAULT_FOR_NONLEGACY_PROFILE) 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 ) assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_stdio_multiple_stdio(lint_ctx): tool_source = get_xml_tool_source(STDIO_MULTIPLE_STDIO) 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 assert len(lint_ctx.error_messages) == 1 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_module(lint_ctx, xsd, tool_source) assert not lint_ctx.info_messages assert not lint_ctx.valid_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_module(lint_ctx, stdio, tool_source) assert ( "Match '[' is no valid regular expression: unterminated character set at position 0" in lint_ctx.error_messages ) 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_tests_absent(lint_ctx): for test_contents in [TESTS_ABSENT, TESTS_ABSENT_YAML]: tool_source = get_tool_source(test_contents) 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 assert len(lint_ctx.warn_messages) == 1 assert not lint_ctx.error_messages def test_tests_data_source(lint_ctx): 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 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_tests_param_output_names(lint_ctx): 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 "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 ( "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 ) assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 1 assert len(lint_ctx.error_messages) == 6 def test_tests_expect_failure_output(lint_ctx): 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 ( "Test 2: Cannot make assumptions on the number of outputs in a test expecting failure." in lint_ctx.error_messages ) assert not lint_ctx.info_messages assert not lint_ctx.valid_messages assert len(lint_ctx.warn_messages) == 3 assert len(lint_ctx.error_messages) == 2 def test_tests_without_expectations(lint_ctx): 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 ) assert "No valid test(s) found." in lint_ctx.warn_messages assert not lint_ctx.info_messages assert not lint_ctx.valid_messages assert len(lint_ctx.warn_messages) == 2 assert not lint_ctx.error_messages def test_tests_valid(lint_ctx): 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 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_tests_asserts(lint_ctx): 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 ( "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 'size', 'min', or 'max'" in lint_ctx.error_messages assert "Test 1: 'has_size' must not specify 'value' and 'size'" 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 len(lint_ctx.error_messages) == 9 def test_tests_asserts_string_coercion(lint_ctx): tool_source = get_xml_tool_source(ASSERTS_STRING_COERCION) run_lint_module(lint_ctx, tests, tool_source) assert len(lint_ctx.warn_messages) == 0, lint_ctx.warn_messages def test_tests_assertion_models_valid(lint_ctx): tool_source = get_xml_tool_source(VALID_CENTER_OF_MASS) run_lint_module(lint_ctx, tests, tool_source) assert len(lint_ctx.error_messages) == 0 assert len(lint_ctx.warn_messages) == 0 def test_tests_assertion_models_invalid(lint_ctx): tool_source = get_xml_tool_source(INVALID_CENTER_OF_MASS) run_lint_module(lint_ctx, tests, tool_source) assert len(lint_ctx.error_messages) == 0 assert len(lint_ctx.warn_messages) == 1 assert "Test 1: failed to validate assertions. Validation errors are " in lint_ctx.warn_messages def test_tests_output_type_mismatch(lint_ctx): 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 ) assert ( "Test 1: test collection output 'data_name' does not correspond to a 'output_collection' output, but a 'data'" in lint_ctx.error_messages ) assert len(lint_ctx.error_messages) == 2 def test_tests_discover_outputs(lint_ctx): 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/min/max' attribute and/or 'discovered_dataset' children" in lint_ctx.error_messages ) assert ( "Test 3: test collection 'collection_name' must have a 'count/min/max' attribute or 'element' children" in lint_ctx.error_messages ) assert ( "Test 3: test collection 'collection_name' must contain nested 'element' tags and/or element children with a 'count/min/max' attribute" in lint_ctx.error_messages ) assert ( "Test 5: test collection 'collection_name' must contain nested 'element' tags and/or element children with a 'count/min/max' attribute" in lint_ctx.error_messages ) assert len(lint_ctx.error_messages) == 4 def test_tests_expect_num_outputs_filter(lint_ctx): 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_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 assert len(lint_ctx.valid_messages) == 1 assert len(lint_ctx.error_messages) == 2 def test_xml_order(lint_ctx): 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 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."] VALID_DATATYPES = """ """ DATATYPES_CONF = """ """ def test_valid_datatypes(lint_ctx): """ test datatypes linters """ with tempfile.TemporaryDirectory() as tmp: tool_path = os.path.join(tmp, "tool.xml") datatypes_path = os.path.join(tmp, "datatypes_conf.xml") with open(tool_path, "w") as tmpf: tmpf.write(VALID_DATATYPES) with open(datatypes_path, "w") as tmpf: tmpf.write(DATATYPES_CONF) tool_xml, _ = load_with_references(tool_path) tool_source = XmlToolSource(tool_xml, source_path=tool_path) run_lint_module(lint_ctx, datatypes, tool_source) assert not lint_ctx.info_messages assert not lint_ctx.valid_messages assert "Tool uses a custom datatypes_conf.xml which is discouraged" in lint_ctx.warn_messages assert len(lint_ctx.warn_messages) == 1 assert "Unknown datatype [invalid] used in param" in lint_ctx.error_messages assert "Unknown datatype [another_invalid] used in data" in lint_ctx.error_messages assert "Unknown datatype [just_another_invalid] used in when" in lint_ctx.error_messages assert "Unknown datatype [collection_format] used in collection" in lint_ctx.error_messages assert "Unknown datatype [invalid] used in param" in lint_ctx.error_messages assert "Unknown datatype [invalid] used in discover_datasets" in lint_ctx.error_messages assert "Invalid format (auto or input) in tool or tool test inputs" in lint_ctx.error_messages # 2x assert len(lint_ctx.error_messages) == 8 DATA_MANAGER = """ """ def test_data_manager(lint_ctx_xpath, lint_ctx): """ test that all (not really testing 'all', but more than the general linter which was the only one applied to data managers until 23.0) linters are applied """ tool_source = get_xml_tool_source(DATA_MANAGER) lint_tool_source_with(lint_ctx, tool_source) assert "No tests found, most tools should define test cases." in lint_ctx.warn_messages assert "Tool contains no outputs section, most tools should produce outputs." in lint_ctx.warn_messages assert "No help section found, consider adding a help section to your tool." in lint_ctx.warn_messages assert "No citations found, consider adding citations to your tool." in lint_ctx.warn_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 assert "No command tag found, must specify a command template to execute." in lint_ctx.error_messages assert lint_ctx.valid_messages assert len(lint_ctx.warn_messages) == 4 assert len(lint_ctx.error_messages) == 3 COMPLETE = """ macros.xml """ COMPLETE_MACROS = """ """ def test_tool_and_macro_xml(lint_ctx_xpath, lint_ctx): """ test linters (all of them via lint_tool_source_with) on a tool and macro xml file checking a list of asserts, where each assert is a 4-tuple: - expected message - filename suffix (tool.xml or macro.xml) - line number (1 based) - xpath """ with tempfile.TemporaryDirectory() as tmp: tool_path = os.path.join(tmp, "tool.xml") macros_path = os.path.join(tmp, "macros.xml") with open(tool_path, "w") as tmpf: tmpf.write(COMPLETE) with open(macros_path, "w") as tmpf: tmpf.write(COMPLETE_MACROS) tool_xml, _ = load_with_references(tool_path) tool_source = XmlToolSource(tool_xml) # lint once with the lint context using XMLLintMessageXPath and XMLLintMessageLine lint_tool_source_with(lint_ctx_xpath, tool_source) lint_tool_source_with(lint_ctx, tool_source) asserts = ( ( "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, 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: continue found = True 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}" # TODO COPIED from test/unit/app/tools/test_tool_deserialization.py CWL_TOOL = """ cwlVersion: v1.0 class: CommandLineTool baseCommand: echo inputs: message: type: string inputBinding: position: 1 outputs: [] """ YAML_TOOL = """ class: GalaxyTool id: simple_constructs_y name: simple_constructs_y version: 1.0 command: > echo "$booltest" >> $out_file1; inputs: - name: booltest type: boolean truevalue: booltrue falsevalue: boolfalse checked: false outputs: out_file1: format: txt """ def test_linting_yml_tool(lint_ctx): tool_source = get_tool_source(YAML_TOOL) lint_tool_source_with(lint_ctx, tool_source) assert "Tool defines a version [1.0]." in lint_ctx.valid_messages assert "Tool defines a name [simple_constructs_y]." in lint_ctx.valid_messages assert "Tool defines an id [simple_constructs_y]." in lint_ctx.valid_messages assert "Tool specifies profile version [24.2]." in lint_ctx.valid_messages assert not lint_ctx.info_messages assert len(lint_ctx.valid_messages) == 4 assert not lint_ctx.warn_messages assert not lint_ctx.error_messages def test_linting_cwl_tool(lint_ctx): with tempfile.TemporaryDirectory() as tmp: tool_path = os.path.join(tmp, "tool.cwl") with open(tool_path, "w") as tmpf: tmpf.write(CWL_TOOL) tool_sources = load_tool_sources_from_path(tmp) assert len(tool_sources) == 1, "Expected 1 tool source" tool_source = tool_sources[0][1] lint_tool_source_with(lint_ctx, tool_source) assert "Tool defines a version [0.0.1]." in lint_ctx.valid_messages assert "Tool defines a name [tool.cwl]." in lint_ctx.valid_messages assert "Tool defines an id [tool.cwl]." in lint_ctx.valid_messages assert "Tool specifies profile version [17.09]." in lint_ctx.valid_messages assert "CWL appears to be valid." in lint_ctx.info_messages assert "Description of tool is empty or absent." in lint_ctx.warn_messages assert "Tool does not specify a DockerPull source." in lint_ctx.warn_messages assert "Modern CWL version [v1.0]." in lint_ctx.info_messages assert len(lint_ctx.info_messages) == 2 assert len(lint_ctx.valid_messages) == 4 assert len(lint_ctx.warn_messages) == 2 assert not lint_ctx.error_messages def test_skip_by_name(lint_ctx): # add a linter class name to the skip list lint_ctx.skip_types = ["CitationsMissing"] tool_source = get_xml_tool_source(CITATIONS_ABSENT) run_lint_module(lint_ctx, citations, tool_source) assert not lint_ctx.warn_messages assert not lint_ctx.info_messages assert not lint_ctx.valid_messages assert not lint_ctx.error_messages def test_skip_by_module(lint_ctx): # add a module name to the skip list -> all linters in this module are skipped lint_ctx.skip_types = ["citations"] tool_source = get_xml_tool_source(CITATIONS_ABSENT) run_lint_module(lint_ctx, citations, tool_source) assert not lint_ctx.warn_messages assert not lint_ctx.info_messages assert not lint_ctx.valid_messages assert not lint_ctx.error_messages 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) == 143 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)]) def test_linting_functional_tool_multi_select(lint_ctx): tool_source = functional_test_tool_source("multi_select.xml") run_lint_module(lint_ctx, tests, tool_source) warn_message = lint_ctx.warn_messages[0] assert ( "Test 2: failed to validate test parameters against inputs - tests won't run on a modern Galaxy tool profile version. Validation errors are [5 validation errors for" in str(warn_message) ) def functional_test_tool_source(name: str) -> ToolSource: tool_sources = load_tool_sources_from_path(functional_test_tool_path(name)) assert len(tool_sources) == 1, "Expected 1 tool source" tool_source = tool_sources[0][1] return tool_source def test_linter_module_list(): linter_modules = submodules.import_submodules(galaxy.tool_util.linters) linter_module_names = [m.__name__.split(".")[-1] for m in linter_modules] linter_module_names = [n for n in linter_module_names if not n.startswith("_")] assert len(linter_module_names) >= 11 # until 23.2 the linters were implemented as functions lint_xyz contained in a module named xyz # with 24.0 the functions were split in multiple classes (1 per linter message) # in order to keep the skip functionality of lint contexts working (which is used eg in planemo) # with the old names, we now also check for module name if a linter should be skipped # therefore we test here that the module names are not changed # the keys of the following dict represent the linter names before the switch and the values give # the number of linter classes when we switched # so adding new functionality to a linter module is fine. But splitting one or removing functionality # should raise an error here to point to the possible consequences of renaming old_linters = { "citations": 4, "command": 5, "cwl": 9, "general": 17, "help": 6, "inputs": 52, "output": 11, "stdio": 3, "tests": 21, "xml_order": 1, } assert len(set(linter_module_names).intersection(set(old_linters.keys()))) == len(old_linters.keys()) for module in linter_modules: module_name = module.__name__.split(".")[-1] if module_name not in old_linters: continue linter_cnt = 0 for name, value in inspect.getmembers(module): if callable(value) and name.startswith("lint_"): continue 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