diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 82bdf39e12..85cbd02188 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -20,6 +20,11 @@ on: release: types: [published] workflow_dispatch: + inputs: + nextflow-version: + description: "Nextflow version to test against" + required: false + default: "latest-stable" # Cancel if a newer run with the same workflow name is queued concurrency: @@ -46,7 +51,7 @@ jobs: tests: ${{ steps.list_tests.outputs.tests }} test: - name: Run ${{matrix.test}} with Python ${{ matrix.python-version }} on ubuntu-latest + name: Run ${{matrix.test}} with Python ${{ matrix.python-version }} and Nextflow ${{ matrix.nextflow-version }} needs: list_tests runs-on: - runs-on=${{ github.run_id }}-run-test @@ -54,8 +59,9 @@ jobs: strategy: matrix: # On main branch test with 3.10 and 3.14, otherwise just 3.10 - python-version: ${{ github.base_ref == 'main' && fromJson('["3.10", "3.14"]') || fromJson('["3.10"]') }} + python-version: ${{ (github.base_ref == 'main' || github.event_name == 'push') && fromJson('["3.10", "3.14"]') || fromJson('["3.10"]') }} test: ${{ fromJson(needs.list_tests.outputs.tests).test }} + nextflow-version: ${{ (github.base_ref == 'main' || github.event_name == 'push') && fromJson('["25.10.4", "latest-stable"]') || fromJson('["25.10.4"]') }} fail-fast: false # run all tests even if one fails steps: - name: go to subdirectory and change nextflow workdir @@ -84,18 +90,17 @@ jobs: - name: Set up Apptainer if: ${{ startsWith(matrix.test, 'pipelines/download/') }} uses: eWaterCycle/setup-apptainer@4bb22c52d4f63406c49e94c804632975787312b3 # v2.0.0 - with: - apptainer-version: 1.3.4 - - - name: Get current date - id: date - run: echo "date=$(date +'%Y-%m')" >> $GITHUB_ENV - name: Install Nextflow uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 + with: + version: ${{ github.event.inputs.nextflow-version || matrix.nextflow-version }} - name: Install nf-test uses: nf-core/setup-nf-test@4069fbbaabe94c08faba4ad261bfa88225ba133f # v2 + with: + version: 0.9.5 + install-fast-diff: true - name: move coveragerc file up run: | diff --git a/nf_core/components/components_test.py b/nf_core/components/components_test.py index 3932d88ebb..587c724384 100644 --- a/nf_core/components/components_test.py +++ b/nf_core/components/components_test.py @@ -201,7 +201,8 @@ def generate_snapshot(self) -> bool: # set verbose flag if self.verbose is True verbose = "--verbose --debug" if self.verbose else "" - update = "--update-snapshot" if self.update else "" + update_snapshot = self.update + update = "--update-snapshot" if update_snapshot else "" self.update = False # reset self.update to False to test if the new snapshot is stable tag = f"subworkflows/{self.component_name}" if self.component_type == "subworkflows" else self.component_name profile = self.profile if self.profile else os.environ["PROFILE"] @@ -221,8 +222,9 @@ def generate_snapshot(self) -> bool: self.obsolete_snapshots = True # check if nf-test was successful if "Assertion failed:" in nftest_out.decode(): - if "Different Snapshot:" not in nftest_err.decode(): - self.errors.append("Assertion failed.") + if "Different Snapshot:" in nftest_err.decode(): + return update_snapshot # snapshot was updated return False only if we don't want to update the snapshot + self.errors.append("Assertion failed.") return False elif "No tests to execute." in nftest_out.decode(): log.error("Nothing to execute. Is the file 'main.nf.test' missing?") diff --git a/nf_core/pipelines/containers_utils.py b/nf_core/pipelines/containers_utils.py index 93e6aeda1d..2ab297b091 100644 --- a/nf_core/pipelines/containers_utils.py +++ b/nf_core/pipelines/containers_utils.py @@ -112,7 +112,13 @@ def generate_container_configs( raise UserWarning("Failed to run `nextflow inspect`. Please check your Nextflow installation.") out, _ = cmd_out - out_json = json.loads(out) + out_str = out.decode("utf-8", errors="replace") + try: + # Newer Nextflow versions print [PIPELINE]/[WORKDIR] headers before and [SUCCESS] after the JSON + json_start = out_str.find("{") + out_json, _ = json.JSONDecoder().raw_decode(out_str, json_start if json_start >= 0 else 0) + except json.JSONDecodeError: + out_json = json.loads(out) except RuntimeError as e: log.error("Running 'nextflow inspect' failed with the following error:") diff --git a/nf_core/pipelines/download/download.py b/nf_core/pipelines/download/download.py index 43d706f117..fbd051f65d 100644 --- a/nf_core/pipelines/download/download.py +++ b/nf_core/pipelines/download/download.py @@ -693,10 +693,25 @@ def run_nextflow_inspect(params_file: Path | None = None) -> dict[str, Any]: try: out_json = run_nextflow_inspect() except RuntimeError as e: + # Extract Nextflow stdout from the chained CalledProcessError (errors go to stdout in NF) + nf_stdout = getattr(e.__cause__, "output", None) or b"" + + # Nextflow >= 26.04 enforces strict process directive syntax and rejects old-style + # if/else container blocks with "Invalid process directive". Users need an older NF. + if b"Invalid process directive" in nf_stdout: + raise DownloadError( + "nextflow inspect failed because the pipeline uses old-style process syntax " + "that the default strict syntax of Nextflow >= 26.04 no longer accepts.\n" + "Please downgrade to Nextflow 25.10 or lower to download this pipeline, " + "or ask the pipeline maintainers to update their container directives." + ) from e + # Some workflow revisions explicitly require an outdir parameter. If this is the # only issue, retry inspect with an ephemeral params file that provides one. if re.search( - r"missing required parameter\s*:\s*(?:--outdir|params\.outdir|outdir)\b", str(e), flags=re.IGNORECASE + r"missing required parameter\s*:\s*(?:--outdir|params\.outdir|outdir)\b", + nf_stdout.decode("utf-8", errors="replace"), + flags=re.IGNORECASE, ): try: with tempfile.TemporaryDirectory(prefix="nf-core-inspect-") as tmp_dir: diff --git a/nf_core/pipelines/list.py b/nf_core/pipelines/list.py index 2dfb5a9b01..bc9bf30205 100644 --- a/nf_core/pipelines/list.py +++ b/nf_core/pipelines/list.py @@ -25,13 +25,44 @@ def _get_nextflow_assets_dir() -> Path: """Return the Nextflow assets directory used for local workflow caches.""" nxf_assets = os.environ.get("NXF_ASSETS") if nxf_assets: - return Path(nxf_assets) + base = Path(nxf_assets) + elif nxf_home := os.environ.get("NXF_HOME"): + base = Path(nxf_home, "assets") + else: + base = Path(os.environ.get("HOME", ""), ".nextflow", "assets") + + # Newer Nextflow versions store clones under assets/.repos/ + repos_dir = base / ".repos" + if repos_dir.is_dir(): + return repos_dir + return base + - nxf_home = os.environ.get("NXF_HOME") - if nxf_home: - return Path(nxf_home, "assets") +def _resolve_wf_path(path: Path) -> Path: + """Resolve the actual pipeline working tree for a given assets dir / workflow path. - return Path(os.environ.get("HOME", ""), ".nextflow", "assets") + Nextflow 26.04+ uses a worktree layout under .repos: + //clones// ← working tree + //bare/ ← bare git repo + Prefers the clone matching the bare repo's HEAD; falls back to the most + recently modified clone if HEAD's clone is missing or the bare repo is unreadable. + Returns path unchanged for the old (pre-26.04) flat layout. + """ + clones_dir = path / "clones" + bare_dir = path / "bare" + if clones_dir.is_dir(): + if bare_dir.is_dir(): + try: + sha = git.Repo(bare_dir).head.commit.hexsha + clone = clones_dir / sha + if clone.is_dir(): + return clone + except git.GitCommandError: + pass + sha_dirs = sorted(clones_dir.iterdir(), key=lambda p: p.stat().st_mtime) + if sha_dirs: + return sha_dirs[-1] + return path def list_workflows(filter_by=None, sort_by="release", as_json=False, show_archived=False): @@ -79,7 +110,7 @@ def get_local_wf(workflow: str | Path, revision=None) -> Path | None: workflow = Path("nf-core", workflow) local_wf = LocalWorkflow(str(workflow)) - local_wf_path = _get_nextflow_assets_dir() / workflow + local_wf_path = _resolve_wf_path(_get_nextflow_assets_dir() / workflow) if local_wf_path.is_dir(): local_wf.local_path = local_wf_path local_wf.get_local_nf_workflow_details() @@ -103,7 +134,11 @@ def get_local_wf(workflow: str | Path, revision=None) -> Path | None: pull_cmd = f"pull {workflow}" if revision is not None: pull_cmd += f" -r {revision}" - nf_core.utils.run_cmd("nextflow", pull_cmd) + try: + nf_core.utils.run_cmd("nextflow", pull_cmd) + except RuntimeError as e: + log.warning(f"Could not pull workflow '{workflow}': {e}") + return None local_wf = LocalWorkflow(str(workflow)) local_wf.get_local_nf_workflow_details() return local_wf.local_path @@ -191,7 +226,7 @@ def compare_remote_local(self): if rwf.full_name == lwf.full_name: rwf.local_wf = lwf if rwf.releases: - if rwf.releases[-1]["tag_sha"] == lwf.commit_sha: + if rwf.releases[0]["tag_sha"] == lwf.commit_sha: rwf.local_is_latest = True else: rwf.local_is_latest = False @@ -229,7 +264,7 @@ def print_summary(self): if not self.sort_workflows_by or self.sort_workflows_by == "release": filtered_workflows.sort( key=lambda wf: ( - (wf.releases[-1].get("published_at_timestamp", 0) if len(wf.releases) > 0 else 0) * -1, + (wf.releases[0].get("published_at_timestamp", 0) if len(wf.releases) > 0 else 0) * -1, wf.archived, wf.full_name.lower(), ) @@ -365,7 +400,7 @@ def get_local_nf_workflow_details(self): if self.local_path is None: # Try to guess the local cache directory - nf_wfdir = _get_nextflow_assets_dir() / self.full_name + nf_wfdir = _resolve_wf_path(_get_nextflow_assets_dir() / self.full_name) if nf_wfdir.is_dir(): log.debug(f"Guessed nextflow assets workflow directory: {nf_wfdir}") self.local_path = nf_wfdir @@ -379,7 +414,10 @@ def get_local_nf_workflow_details(self): for key, pattern in re_patterns.items(): m = re.search(pattern, str(nfinfo_raw)) if m: - setattr(self, key, m.group(1)) + value = Path(m.group(1)) if key == "local_path" else m.group(1) + if key == "local_path": + value = _resolve_wf_path(value) + setattr(self, key, value) # Pull information from the local git repository if self.local_path is not None: @@ -388,7 +426,8 @@ def get_local_nf_workflow_details(self): repo = git.Repo(self.local_path) self.commit_sha = str(repo.head.commit.hexsha) self.remote_url = str(repo.remotes.origin.url) - self.last_pull = (self.local_path / ".git" / "FETCH_HEAD").stat().st_mtime + self.last_pull = Path(repo.common_dir) / "FETCH_HEAD" + self.last_pull = self.last_pull.stat().st_mtime self.last_pull_date = datetime.fromtimestamp(self.last_pull).strftime("%Y-%m-%d %H:%M:%S") self.last_pull_pretty = pretty_date(self.last_pull) @@ -410,7 +449,7 @@ def get_local_nf_workflow_details(self): f"Could not fetch status of local Nextflow copy of '{self.full_name}':" f"\n [red]{type(e).__name__}:[/] {str(e)}" "\n\nThis git repository looks broken. It's probably a good idea to delete this local copy and pull again:" - f"\n [magenta]rm -rf {self.local_path}" + f"\n [magenta]rm -rf {_get_nextflow_assets_dir() / self.full_name}" f"\n [magenta]nextflow pull {self.full_name}", ) diff --git a/nf_core/pipelines/schema.py b/nf_core/pipelines/schema.py index 090373a11f..0c42b718dc 100644 --- a/nf_core/pipelines/schema.py +++ b/nf_core/pipelines/schema.py @@ -67,8 +67,10 @@ def _update_validation_plugin_from_config(self) -> None: plugin = "nf-schema" if self.schema_filename: conf = nf_core.utils.fetch_wf_config(Path(self.schema_filename).parent) - else: + elif self.pipeline_dir is not None: conf = nf_core.utils.fetch_wf_config(Path(self.pipeline_dir)) + else: + return plugins = str(conf.get("plugins", "")).strip("'\"").strip(" ").split(",") plugin_found = False @@ -137,7 +139,7 @@ def get_schema_path(self, path: str | Path, local_only: bool = False, revision: self.pipeline_dir = nf_core.pipelines.list.get_local_wf(path, revision=revision) self.schema_filename = Path(self.pipeline_dir or "", "nextflow_schema.json") # check if the schema file exists - if not self.schema_filename.exists(): + if self.schema_filename is not None and not self.schema_filename.exists(): self.schema_filename = None # Only looking for local paths, overwrite with None to be safe else: diff --git a/nf_core/utils.py b/nf_core/utils.py index 55f2ad3447..d376559e53 100644 --- a/nf_core/utils.py +++ b/nf_core/utils.py @@ -479,8 +479,14 @@ def run_cmd(executable: str, cmd: str) -> tuple[bytes, bytes] | None: full_cmd = f"{executable} {cmd}" log.debug(f"Running command: {full_cmd}") try: - proc = subprocess.run(shlex.split(full_cmd), capture_output=True, check=True) + proc = subprocess.run(shlex.split(full_cmd), capture_output=True, check=False) + if proc.returncode != 0: + if executable == "nf-test": + return (proc.stdout, proc.stderr) + raise subprocess.CalledProcessError(proc.returncode, proc.args, output=proc.stdout, stderr=proc.stderr) return (proc.stdout, proc.stderr) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Command '{full_cmd}' failed: {e}") from e except OSError as e: if e.errno == errno.ENOENT: raise RuntimeError( @@ -488,14 +494,6 @@ def run_cmd(executable: str, cmd: str) -> tuple[bytes, bytes] | None: ) from e else: return None - except subprocess.CalledProcessError as e: - log.debug(f"Command '{full_cmd}' returned non-zero error code '{e.returncode}':\n[red]> {e.stderr.decode()}") - if executable == "nf-test": - return (e.stdout, e.stderr) - else: - raise RuntimeError( - f"Command '{full_cmd}' returned non-zero error code '{e.returncode}':\n[red]> {e.stderr.decode()}{e.stdout.decode()}" - ) from e def setup_nfcore_dir() -> bool: diff --git a/tests/pipelines/download/test_download.py b/tests/pipelines/download/test_download.py index 5e4908432e..40228994cc 100644 --- a/tests/pipelines/download/test_download.py +++ b/tests/pipelines/download/test_download.py @@ -1,10 +1,12 @@ """Tests for the download subcommand of nf-core tools""" +import contextlib import json import logging import os import re import shutil +import subprocess import tempfile import unittest from pathlib import Path @@ -20,12 +22,22 @@ from nf_core.pipelines.download.utils import DownloadError from nf_core.pipelines.download.workflow_repo import WorkflowRepo from nf_core.synced_repo import SyncedRepo -from nf_core.utils import NF_INSPECT_MIN_NF_VERSION, check_nextflow_version +from nf_core.utils import NF_INSPECT_MIN_NF_VERSION, check_nextflow_version, get_nf_version from ...utils import TEST_DATA_DIR, with_temporary_folder +NF_STRICT_SYNTAX_VERSION = (26, 4, 0, False) + class DownloadTest(unittest.TestCase): + def setUp(self): + nf_ver = get_nf_version() + self.nf_strict_syntax = nf_ver is not None and nf_ver >= NF_STRICT_SYNTAX_VERSION + + def _strict_syntax_ctx(self, match: str = "nextflow inspect"): + """Return a context manager that expects a DownloadError on NF >= 26.04, or a no-op otherwise.""" + return pytest.raises(DownloadError, match=match) if self.nf_strict_syntax else contextlib.nullcontext() + @pytest.fixture(autouse=True) def use_caplog(self, caplog): self._caplog = caplog @@ -253,22 +265,20 @@ def test_containers_pipeline_singularity(self, tmp_path, mock_fetch_wf_config): mock_fetch_wf_config.return_value = {} # Run get containers with `nextflow inspect` + # NF >= 26.04 rejects old-style if/else container directives (mock_dsl2_old uses them) entrypoint = "main_passing_test.nf" - download_obj.find_container_images(mock_pipeline_dir, "dummy-revision", entrypoint=entrypoint) - - # Store the containers found by the new method - found_containers = set(download_obj.containers) - - # Load the reference containers - with open(refererence_json_dir / f"{container_system}_containers.json") as fh: - ref_containers = json.load(fh) - ref_container_strs = set(ref_containers.values()) - - # Now check that they contain the same containers - assert found_containers == ref_container_strs, ( - f"Containers found in pipeline by `nextflow inspect`: {found_containers}\n" - f"Containers that should've been found: {ref_container_strs}" - ) + with self._strict_syntax_ctx(match="downgrade to Nextflow"): + download_obj.find_container_images(mock_pipeline_dir, "dummy-revision", entrypoint=entrypoint) + + if not self.nf_strict_syntax: + found_containers = set(download_obj.containers) + with open(refererence_json_dir / f"{container_system}_containers.json") as fh: + ref_containers = json.load(fh) + ref_container_strs = set(ref_containers.values()) + assert found_containers == ref_container_strs, ( + f"Containers found in pipeline by `nextflow inspect`: {found_containers}\n" + f"Containers that should've been found: {ref_container_strs}" + ) # # Test that `find_container_images` (uses `nextflow inspect`) fetches the correct Docker images @@ -287,27 +297,24 @@ def test_containers_pipeline_docker(self, tmp_path, mock_fetch_wf_config): container_system = "docker" mock_pipeline_dir = TEST_DATA_DIR / "mock_pipeline_containers" refererence_json_dir = mock_pipeline_dir / "per_profile_output" - # First check that `-profile singularity` produces the same output as the reference + # First check that `-profile docker` produces the same output as the reference download_obj = DownloadWorkflow(pipeline="dummy", outdir=tmp_path, container_system=container_system) mock_fetch_wf_config.return_value = {} - # Run get containers with `nextflow inspect` + # NF >= 26.04 rejects old-style if/else container directives (mock_dsl2_old uses them) entrypoint = "main_passing_test.nf" - download_obj.find_container_images(mock_pipeline_dir, "dummy-revision", entrypoint=entrypoint) - - # Store the containers found by the new method - found_containers = set(download_obj.containers) - - # Load the reference containers - with open(refererence_json_dir / f"{container_system}_containers.json") as fh: - ref_containers = json.load(fh) - ref_container_strs = set(ref_containers.values()) - - # Now check that they contain the same containers - assert found_containers == ref_container_strs, ( - f"Containers found in pipeline by `nextflow inspect`: {found_containers}\n" - f"Containers that should've been found: {ref_container_strs}" - ) + with self._strict_syntax_ctx(match="downgrade to Nextflow"): + download_obj.find_container_images(mock_pipeline_dir, "dummy-revision", entrypoint=entrypoint) + + if not self.nf_strict_syntax: + found_containers = set(download_obj.containers) + with open(refererence_json_dir / f"{container_system}_containers.json") as fh: + ref_containers = json.load(fh) + ref_container_strs = set(ref_containers.values()) + assert found_containers == ref_container_strs, ( + f"Containers found in pipeline by `nextflow inspect`: {found_containers}\n" + f"Containers that should've been found: {ref_container_strs}" + ) @mock.patch("nf_core.pipelines.download.download.run_cmd") @mock.patch("nf_core.pipelines.list.Workflows.get_remote_workflows") @@ -325,11 +332,13 @@ def _run_cmd_side_effect(_executable, cmd_params): nonlocal has_retried if not has_retried: has_retried = True - raise RuntimeError( - """The following invalid input values have been detected:\n - * Missing required parameter: --outdir - """ + nf_output = ( + b"The following invalid input values have been detected:\n * Missing required parameter: --outdir\n" ) + cause = subprocess.CalledProcessError(1, ["nextflow", "inspect"], output=nf_output) + exc = RuntimeError("Command failed") + exc.__cause__ = cause + raise exc assert "-params-file" in cmd_params params_file_match = re.search(r'-params-file\s+"([^"]+)"', cmd_params) @@ -358,6 +367,22 @@ def test_find_container_images_raises_on_unexpected_runtime_error(self, mock_get assert mock_run_cmd.call_count == 1 mock_get_remote_workflows.assert_called_once() + @mock.patch("nf_core.pipelines.download.download.run_cmd") + @mock.patch("nf_core.pipelines.list.Workflows.get_remote_workflows") + def test_find_container_images_raises_on_strict_syntax_error(self, mock_get_remote_workflows, mock_run_cmd): + """Nextflow >= 26.04 rejects old if/else container directives; tool should give actionable guidance.""" + download_obj = DownloadWorkflow(container_system="docker") + cause = subprocess.CalledProcessError(1, ["nextflow", "inspect"], output=b"Error: Invalid process directive\n") + exc = RuntimeError("Command failed") + exc.__cause__ = cause + mock_run_cmd.side_effect = exc + + with pytest.raises(DownloadError, match="downgrade to Nextflow"): + download_obj.find_container_images(Path("workflow"), "dummy-revision") + + assert mock_run_cmd.call_count == 1 + mock_get_remote_workflows.assert_called_once() + # # Tests for the main entry method 'download_workflow' # @@ -386,7 +411,9 @@ def test_download_workflow_with_success(self, tmp_dir, mock_check_and_set_implem ) download_obj.include_configs = True # suppress prompt, because stderr.is_interactive doesn't. - download_obj.download_workflow() + # NF >= 26.04 fails on old-style container directives used in bamtofastq 2.2.0 + with self._strict_syntax_ctx(): + download_obj.download_workflow() # # Test Download for Seqera Platform @@ -436,8 +463,11 @@ def test_download_workflow_for_platform( assert bool(re.search(r"nf-core-rnaseq_\d{4}-\d{2}-\d{1,2}_\d{1,2}-\d{1,2}", str(download_obj.outdir), re.S)) download_obj.output_filename = download_obj.outdir / "rnaseq.git" - download_obj.download_workflow_platform(location=tmp_dir) + # NF >= 26.04 fails on old-style container directives used in rnaseq 3.17.0 / 3.19.0 + with self._strict_syntax_ctx(): + download_obj.download_workflow_platform(location=tmp_dir) + # workflow_repo is populated before find_container_images is called, so it's accessible in both paths assert download_obj.workflow_repo assert isinstance(download_obj.workflow_repo, WorkflowRepo) assert issubclass(type(download_obj.workflow_repo), SyncedRepo) @@ -450,12 +480,13 @@ def test_download_workflow_for_platform( # assert that the download has a "latest" branch. assert "latest" in all_heads - # download_obj.download_workflow_platform(location=tmp_dir) will run `nextflow inspect` for each revision - # This means that the containers in download_obj.containers are the containers the last specified revision i.e. 3.17 - assert isinstance(download_obj.containers, list) and len(download_obj.containers) == 39 - assert ( - "https://depot.galaxyproject.org/singularity/bbmap:39.10--h92535d8_0" in download_obj.containers - ) # direct definition + if not self.nf_strict_syntax: + # download_obj.download_workflow_platform(location=tmp_dir) will run `nextflow inspect` for each revision + # This means that the containers in download_obj.containers are the containers the last specified revision i.e. 3.17 + assert isinstance(download_obj.containers, list) and len(download_obj.containers) == 39 + assert ( + "https://depot.galaxyproject.org/singularity/bbmap:39.10--h92535d8_0" in download_obj.containers + ) # direct definition # clean-up # remove "nf-core-rnaseq*" directories diff --git a/tests/pipelines/lint/test_nextflow_config.py b/tests/pipelines/lint/test_nextflow_config.py index 60de0432d9..63c58b149b 100644 --- a/tests/pipelines/lint/test_nextflow_config.py +++ b/tests/pipelines/lint/test_nextflow_config.py @@ -59,6 +59,7 @@ def test_nextflow_config_missing_test_profile_failed(self): fail_content = re.sub(r"\btest\b", "testfail", content) with open(nf_conf_file, "w") as f: f.write(fail_content) + Path(self.new_pipeline, "conf", "testfail.config").touch() lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline) lint_obj.load_pipeline_config() result = lint_obj.nextflow_config() @@ -155,7 +156,7 @@ def test_default_values_float(self): content = f.read() fail_content = re.sub( r"validate_params\s*=\s*true", - "params.validate_params = true\ndummy = 0.000000001", + "validate_params = true\ndummy = 0.000000001", content, ) with open(nf_conf_file, "w") as f: @@ -187,7 +188,7 @@ def test_default_values_float_fail(self): content = f.read() fail_content = re.sub( r"validate_params\s*=\s*true", - "params.validate_params = true\ndummy = 0.000000001", + "validate_params = true\ndummy = 0.000000001", content, ) with open(nf_conf_file, "w") as f: diff --git a/tests/pipelines/test_container_configs.py b/tests/pipelines/test_container_configs.py index af9ab0064c..e90b39a021 100644 --- a/tests/pipelines/test_container_configs.py +++ b/tests/pipelines/test_container_configs.py @@ -83,7 +83,7 @@ def test_generate_container_configs_new_module_injected(self) -> None: patch("nf_core.pipelines.containers_utils.check_nextflow_version", return_value=True), patch( "nf_core.pipelines.containers_utils.run_cmd", - return_value=('{"processes": []}', ""), + return_value=(b'{"processes": []}', b""), ), ): self.container_configs.generate_container_configs( @@ -109,7 +109,7 @@ def test_generate_container_configs_removes_stale_entries(self) -> None: patch("nf_core.pipelines.containers_utils.check_nextflow_version", return_value=True), patch( "nf_core.pipelines.containers_utils.run_cmd", - return_value=('{"processes": []}', ""), + return_value=(b'{"processes": []}', b""), ), ): self.container_configs.generate_container_configs() diff --git a/tests/pipelines/test_list.py b/tests/pipelines/test_list.py index 02d95cb146..8d32436393 100644 --- a/tests/pipelines/test_list.py +++ b/tests/pipelines/test_list.py @@ -111,7 +111,7 @@ def test_parse_local_workflow_and_succeed(self): if not test_path.is_dir(): test_path.mkdir(parents=True) # Create a dummy workflow directory (not just a file) - dummy_wf_path = self.tmp_nxf / "nf-core" / "dummy-wf" + dummy_wf_path = self.tmp_nxf / ".repos" / "nf-core" / "dummy-wf" dummy_wf_path.mkdir(parents=True, exist_ok=True) assert os.environ["NXF_ASSETS"] == self.tmp_nxf_str workflows_obj = nf_core.pipelines.list.Workflows() @@ -140,6 +140,7 @@ def test_local_workflow_investigation(self, mock_repo): (git_dir / "FETCH_HEAD").touch() mock_repo.return_value.head.commit.hexsha = "h00r4y" mock_repo.return_value.remotes.origin.url = "https://github.com/nf-core/dummy" + mock_repo.return_value.common_dir = str(git_dir) local_wf.get_local_nf_workflow_details() @mock.patch("git.Repo") diff --git a/tests/pipelines/test_rocrate.py b/tests/pipelines/test_rocrate.py index 8b04eb539b..30df70f821 100644 --- a/tests/pipelines/test_rocrate.py +++ b/tests/pipelines/test_rocrate.py @@ -276,14 +276,10 @@ def test_get_git_email_for_name(self): def test_parse_manifest_contributors_logs_parse_errors(self): """Emit a clear error when manifest.contributors cannot be normalised into valid JSON""" - self._set_manifest_identity( - """contributors = [ - [ - name: 'Alice Example', - github: alice - ] - ] - """ + # Set nf_config directly to avoid running nextflow config -flat on invalid Groovy syntax + # (unquoted `alice` is valid Groovy but references an undefined variable, causing nextflow to fail) + self.rocrate_obj.pipeline_obj.nf_config["manifest.contributors"] = ( + "[[\n name: 'Alice Example',\n github: alice\n]]" ) with self.assertLogs("nf_core.pipelines.rocrate", level="ERROR") as logs: @@ -383,14 +379,14 @@ def test_parse_manifest_contributors_requires_names(self): with self.assertRaises(SystemExit): self.rocrate_obj.parse_manifest_contributors() - def test_rocrate_creation_for_fetchngs(self): - """Run the nf-core rocrate command with nf-core/fetchngs""" + def test_rocrate_creation_for_demo(self): + """Run the nf-core rocrate command with nf-core/demo""" tmp_dir = Path(tempfile.mkdtemp()) - # git clone nf-core/fetchngs - git.Repo.clone_from("https://github.com/nf-core/fetchngs", tmp_dir / "fetchngs") + # git clone nf-core/demo + git.Repo.clone_from("https://github.com/nf-core/demo", tmp_dir / "demo") # Run the command - self.rocrate_obj = nf_core.pipelines.rocrate.ROCrate(tmp_dir / "fetchngs", version="1.12.0") - assert self.rocrate_obj.create_rocrate(tmp_dir / "fetchngs", self.pipeline_dir) + self.rocrate_obj = nf_core.pipelines.rocrate.ROCrate(tmp_dir / "demo", version="1.1.0") + assert self.rocrate_obj.create_rocrate(tmp_dir / "demo", self.pipeline_dir) # Check that Sateesh Peri is mentioned in creator field diff --git a/tests/pipelines/test_schema.py b/tests/pipelines/test_schema.py index 78324ee642..5965a5c011 100644 --- a/tests/pipelines/test_schema.py +++ b/tests/pipelines/test_schema.py @@ -68,7 +68,7 @@ def test_load_lint_schema(self): def test_load_lint_schema_nofile(self): """Check that linting raises properly if a non-existent file is given""" - with pytest.raises(RuntimeError): + with pytest.raises(AssertionError): self.schema_obj.get_schema_path("fake_file") def test_load_lint_schema_notjson(self): @@ -104,7 +104,7 @@ def test_get_schema_path_path_notexist(self): def test_get_schema_path_name(self): """Get schema file from the name of a remote pipeline""" - self.schema_obj.get_schema_path("atacseq") + self.schema_obj.get_schema_path("proteinfamilies") def test_get_schema_path_name_notexist(self): """