diff --git a/CHANGELOG.md b/CHANGELOG.md index 21750bf270..51afe36aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Remove `nfcore_external_java_deps.jar` from lib directory in pipeline template ([#2675](https://github.com/nf-core/tools/pull/2675)) - Add function to check `-profile` is well formatted ([#2678](https://github.com/nf-core/tools/pull/2678)) - Add new pipeline error message pointing to docs when 'requirement exceeds available memory' error message ([#2680](https://github.com/nf-core/tools/pull/2680)) +- add 👀👍🏻🎉😕 reactions to fix-linting-bot action ([#2692](https://github.com/nf-core/tools/pull/2692)) ### Download @@ -21,7 +22,8 @@ - Fix linting of a pipeline with patched custom module ([#2669](https://github.com/nf-core/tools/pull/2669)) - linting a pipeline also lints the installed subworkflows ([#2677](https://github.com/nf-core/tools/pull/2677)) - environment.yml name must be lowercase ([#2676](https://github.com/nf-core/tools/pull/2676)) -- template: add 👀👍🏻🎉😕 reactions to fix-bot action ([#2692](https://github.com/nf-core/tools/pull/2692)) +- lint `nextflow.config` default values match the ones specified in `nextflow_schema.json` ([#2684](https://github.com/nf-core/tools/pull/2684)) + ### Modules diff --git a/nf_core/lint/nextflow_config.py b/nf_core/lint/nextflow_config.py index 328bc03759..1e0a6c4995 100644 --- a/nf_core/lint/nextflow_config.py +++ b/nf_core/lint/nextflow_config.py @@ -1,6 +1,9 @@ import logging import os import re +from pathlib import Path + +from nf_core.schema import PipelineSchema log = logging.getLogger(__name__) @@ -113,6 +116,18 @@ def nextflow_config(self): * A ``test`` configuration profile should exist. + **The default values in ``nextflow.config`` should match the default values defined in the ``nextflow_schema.json``.** + + .. tip:: You can choose to ignore tests for the default value of an specific parameter + by creating a file called ``.nf-core.yml`` in the root of your pipeline and creating + a list the config parameters that should be ignored. For example to ignore the default value for the input parameter: + + .. code-block:: yaml + + lint: + nextflow_config: + - config_defaults: + - params.input """ passed = [] warned = [] @@ -347,4 +362,40 @@ def nextflow_config(self): else: failed.append("nextflow.config does not contain configuration profile `test`") + # Check that the default values in nextflow.config match the default values defined in the nextflow_schema.json + ignore_defaults = [] + for item in ignore_configs: + if isinstance(item, dict) and "config_defaults" in item: + ignore_defaults = item.get("config_defaults", []) + schema_path = Path(self.wf_path) / "nextflow_schema.json" + schema = PipelineSchema() + schema.schema_filename = schema_path + schema.no_prompts = True + schema.load_schema() + schema.get_schema_defaults() # Get default values from schema + self.nf_config.keys() # Params in nextflow.config + for param_name in schema.schema_defaults.keys(): + param = "params." + param_name + # Convert booleans to strings if needed + schema_default = ( + "true" + if str(schema.schema_defaults[param_name]) == "True" + else "false" + if str(schema.schema_defaults[param_name]) == "False" + else str(schema.schema_defaults[param_name]) + ) + if param in ignore_defaults: + ignored.append(f"Config default ignored: {param}") + elif param in self.nf_config.keys(): + if str(self.nf_config[param]) == schema_default: + passed.append(f"Config default value correct: {param}") + else: + failed.append( + f"Config default value incorrect: `{param}` is set as {self._wrap_quotes(schema_default)} in `nextflow_schema.json` but is {self._wrap_quotes(self.nf_config[param])} in `nextflow.config`." + ) + else: + failed.append( + f"Default value from the Nextflow schema '{param} = {self._wrap_quotes(schema_default)}' not found in `nextflow.config`." + ) + return {"passed": passed, "warned": warned, "failed": failed, "ignored": ignored} diff --git a/tests/lint/nextflow_config.py b/tests/lint/nextflow_config.py index 5d5f8e7345..60aaee5243 100644 --- a/tests/lint/nextflow_config.py +++ b/tests/lint/nextflow_config.py @@ -1,5 +1,6 @@ import os import re +from pathlib import Path import nf_core.create import nf_core.lint @@ -53,3 +54,66 @@ def test_nextflow_config_missing_test_profile_failed(self): result = lint_obj.nextflow_config() assert len(result["failed"]) > 0 assert len(result["warned"]) == 0 + + +def test_default_values_match(self): + """Test that the default values in nextflow.config match the default values defined in the nextflow_schema.json.""" + new_pipeline = self._make_pipeline_copy() + lint_obj = nf_core.lint.PipelineLint(new_pipeline) + lint_obj._load_pipeline_config() + result = lint_obj.nextflow_config() + assert len(result["failed"]) == 0 + assert len(result["warned"]) == 0 + assert "Config default value correct: params.max_cpus" in result["passed"] + assert "Config default value correct: params.validate_params" in result["passed"] + + +def test_default_values_fail(self): + """Test linting fails if the default values in nextflow.config do not match the ones defined in the nextflow_schema.json.""" + new_pipeline = self._make_pipeline_copy() + # Change the default value of max_cpus in nextflow.config + nf_conf_file = Path(new_pipeline) / "nextflow.config" + with open(nf_conf_file) as f: + content = f.read() + fail_content = re.sub(r"\bmax_cpus = 16\b", "max_cpus = 0", content) + with open(nf_conf_file, "w") as f: + f.write(fail_content) + # Change the default value of max_memory in nextflow_schema.json + nf_schema_file = Path(new_pipeline) / "nextflow_schema.json" + with open(nf_schema_file) as f: + content = f.read() + fail_content = re.sub(r'"default": "128.GB"', '"default": "18.GB"', content) + print(fail_content) + with open(nf_schema_file, "w") as f: + f.write(fail_content) + lint_obj = nf_core.lint.PipelineLint(new_pipeline) + lint_obj._load_pipeline_config() + result = lint_obj.nextflow_config() + assert len(result["failed"]) == 2 + assert ( + "Config default value incorrect: `params.max_cpus` is set as `16` in `nextflow_schema.json` but is `0` in `nextflow.config`." + in result["failed"] + ) + assert ( + "Config default value incorrect: `params.max_memory` is set as `18.GB` in `nextflow_schema.json` but is `128.GB` in `nextflow.config`." + in result["failed"] + ) + + +def test_default_values_ignored(self): + """Test ignoring linting of default values.""" + new_pipeline = self._make_pipeline_copy() + # Add max_cpus to the ignore list + nf_core_yml = Path(new_pipeline) / ".nf-core.yml" + with open(nf_core_yml, "w") as f: + f.write( + "repository_type: pipeline\nlint:\n nextflow_config:\n - config_defaults:\n - params.max_cpus\n" + ) + lint_obj = nf_core.lint.PipelineLint(new_pipeline) + lint_obj._load_pipeline_config() + lint_obj._load_lint_config() + result = lint_obj.nextflow_config() + assert len(result["failed"]) == 0 + assert len(result["ignored"]) == 1 + assert "Config default value correct: params.max_cpus" not in result["passed"] + assert "Config default ignored: params.max_cpus" in result["ignored"] diff --git a/tests/test_lint.py b/tests/test_lint.py index 32913bda0d..ff7e56744d 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -219,6 +219,9 @@ def test_sphinx_md_files(self): test_multiqc_incorrect_export_plots, ) from .lint.nextflow_config import ( # type: ignore[misc] + test_default_values_fail, + test_default_values_ignored, + test_default_values_match, test_nextflow_config_bad_name_fail, test_nextflow_config_dev_in_release_mode_failed, test_nextflow_config_example_pass,