Skip to content

Commit 799f95b

Browse files
feat: preventing overwrites (#358)
From this PR onwards, overwriting flags already used internally with the `slurm_extra` parameter. The plugin is feature rich and gets improved continuously. Overwriting internally used flags (via CLI or resources) already caused a number of issues. Now, we should have a reasonable error message, instead. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Centralized SLURM extra validation to block forbidden SLURM options; account/partition handling simplified to single-string arguments for submissions. * **Documentation** * Updated slurm_extra example (removed QoS) and added a note advising not to overwrite internal SLURM flags; recommend using profiles. * **Tests** * Added tests for slurm_extra validation, SBATCH formatting expectations, and related error messages. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent ba263ce commit 799f95b

File tree

4 files changed

+161
-11
lines changed

4 files changed

+161
-11
lines changed

docs/further.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,9 +486,11 @@ rule myrule:
486486
input: ...
487487
output: ...
488488
resources:
489-
slurm_extra="--qos=long --mail-type=ALL --mail-user=<your email>"
489+
slurm_extra="--mail-type=ALL --mail-user=<your email>"
490490
```
491491

492+
.. note:: We prevent you from overwriting all flags, which are used or configurable internally. This prevents conflicts and breaking job submission.
493+
492494
Again, rather use a [profile](https://snakemake.readthedocs.io/en/latest/executing/cli.html#profiles) to specify such resources.
493495

494496
### Software Recommendations

snakemake_executor_plugin_slurm/__init__.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from snakemake_interface_executor_plugins.executors.base import SubmittedJobInfo
2020
from snakemake_interface_executor_plugins.executors.remote import RemoteExecutor
21+
2122
from snakemake_interface_executor_plugins.settings import (
2223
ExecutorSettingsBase,
2324
CommonSettings,
@@ -34,6 +35,7 @@
3435
)
3536
from .efficiency_report import create_efficiency_report
3637
from .submit_string import get_submit_command
38+
from .validation import validate_slurm_extra
3739

3840

3941
@dataclass
@@ -682,6 +684,7 @@ def get_account_arg(self, job: JobExecutorInterface):
682684
# we have to quote the account, because it might
683685
# contain build-in shell commands - see issue #354
684686
for account in accounts:
687+
self.test_account(account)
685688
yield f" -A {shlex.quote(account)}"
686689
else:
687690
if self._fallback_account_arg is None:
@@ -820,13 +823,5 @@ def get_default_partition(self, job):
820823
return ""
821824

822825
def check_slurm_extra(self, job):
823-
jobname = re.compile(r"--job-name[=?|\s+]|-J\s?")
824-
if re.search(jobname, job.resources.slurm_extra):
825-
raise WorkflowError(
826-
"The --job-name option is not allowed in the 'slurm_extra' parameter. "
827-
"The job name is set by snakemake and must not be overwritten. "
828-
"It is internally used to check the stati of the all submitted jobs "
829-
"by this workflow."
830-
"Please consult the documentation if you are unsure how to "
831-
"query the status of your jobs."
832-
)
826+
"""Validate that slurm_extra doesn't contain executor-managed options."""
827+
validate_slurm_extra(job)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
SLURM parameter validation functions for the Snakemake executor plugin.
3+
"""
4+
5+
import re
6+
from snakemake_interface_common.exceptions import WorkflowError
7+
8+
9+
def get_forbidden_slurm_options():
10+
"""
11+
Return a dictionary of forbidden SLURM options that the executor manages.
12+
13+
Returns:
14+
dict: Mapping of regex patterns to human-readable option names
15+
"""
16+
return {
17+
# Job identification and output
18+
r"--job-name[=\s]|-J\s?": "job name",
19+
r"--output[=\s]|-o\s": "output file",
20+
r"--error[=\s]|-e\s": "error file",
21+
r"--parsable": "parsable output",
22+
r"--export[=\s]": "environment export",
23+
r"--comment[=\s]": "job comment",
24+
r"--workdir[=\s]|-D\s": "working directory",
25+
# Account and partition
26+
r"--account[=\s]|-A\s": "account",
27+
r"--partition[=\s]|-p\s": "partition",
28+
# Memory options
29+
r"--mem[=\s]": "memory",
30+
r"--mem-per-cpu[=\s]": "memory per CPU",
31+
# CPU and task options
32+
r"--ntasks[=\s]|-n\s": "number of tasks",
33+
r"--ntasks-per-gpu[=\s]": "tasks per GPU",
34+
r"--cpus-per-task[=\s]|-c\s": "CPUs per task",
35+
r"--cpus-per-gpu[=\s]": "CPUs per GPU",
36+
# Time and resource constraints
37+
r"--time[=\s]|-t\s": "runtime/time limit",
38+
r"--constraint[=\s]|-C\s": "node constraints",
39+
r"--qos[=\s]": "quality of service",
40+
r"--nodes[=\s]|-N\s": "number of nodes",
41+
r"--clusters[=\s]": "cluster specification",
42+
# GPU options
43+
r"--gres[=\s]": "generic resources (GRES)",
44+
r"--gpus[=\s]": "GPU allocation",
45+
}
46+
47+
48+
def validate_slurm_extra(job):
49+
"""
50+
Validate that slurm_extra doesn't contain executor-managed options.
51+
52+
Args:
53+
job: Snakemake job object with resources attribute
54+
55+
Raises:
56+
WorkflowError: If forbidden SLURM options are found in slurm_extra
57+
"""
58+
# skip testing if no slurm_extra is set
59+
slurm_extra = getattr(job.resources, "slurm_extra", None)
60+
if not slurm_extra:
61+
return
62+
63+
forbidden_options = get_forbidden_slurm_options()
64+
65+
for pattern, option_name in forbidden_options.items():
66+
if re.search(pattern, slurm_extra):
67+
raise WorkflowError(
68+
f"The --{option_name.replace(' ', '-')} option is not "
69+
f"allowed in the 'slurm_extra' parameter. "
70+
f"The {option_name} is set by the snakemake executor plugin "
71+
f"and must not be overwritten. "
72+
f"Please use the appropriate snakemake resource "
73+
f"specification instead. "
74+
f"Consult the documentation for proper resource configuration."
75+
)

tests/tests.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from snakemake_executor_plugin_slurm.utils import set_gres_string
1717
from snakemake_executor_plugin_slurm.submit_string import get_submit_command
18+
from snakemake_executor_plugin_slurm.validation import validate_slurm_extra
1819
from snakemake_interface_common.exceptions import WorkflowError
1920
import pandas as pd
2021

@@ -785,3 +786,80 @@ def test_wildcard_slash_replacement(self):
785786

786787
# Verify no slashes remain in the wildcard string
787788
assert "/" not in wildcard_str
789+
790+
791+
class TestSlurmExtraValidation:
792+
"""Test cases for the validate_slurm_extra function."""
793+
794+
@pytest.fixture
795+
def mock_job(self):
796+
"""Create a mock job with configurable slurm_extra resource."""
797+
798+
def _create_job(**resources):
799+
mock_resources = MagicMock()
800+
# Configure get method to return values from resources dict
801+
mock_resources.get.side_effect = lambda key, default=None: resources.get(
802+
key, default
803+
)
804+
# Add direct attribute access for certain resources
805+
for key, value in resources.items():
806+
setattr(mock_resources, key, value)
807+
808+
mock_job = MagicMock()
809+
mock_job.resources = mock_resources
810+
mock_job.name = "test_job"
811+
mock_job.wildcards = {}
812+
mock_job.is_group.return_value = False
813+
mock_job.jobid = 1
814+
return mock_job
815+
816+
return _create_job
817+
818+
def test_valid_slurm_extra(self, mock_job):
819+
"""Test that validation passes with allowed SLURM options."""
820+
job = mock_job(slurm_extra="--mail-type=END [email protected]")
821+
# Should not raise any exception
822+
validate_slurm_extra(job)
823+
824+
def test_forbidden_job_name_long_form(self, mock_job):
825+
"""Test that --job-name is rejected."""
826+
job = mock_job(slurm_extra="--job-name=my-job --mail-type=END")
827+
with pytest.raises(WorkflowError, match=r"job-name.*not allowed"):
828+
validate_slurm_extra(job)
829+
830+
def test_forbidden_job_name_short_form(self, mock_job):
831+
"""Test that -J is rejected."""
832+
job = mock_job(slurm_extra="-J my-job --mail-type=END")
833+
with pytest.raises(WorkflowError, match=r"job-name.*not allowed"):
834+
validate_slurm_extra(job)
835+
836+
def test_forbidden_account_long_form(self, mock_job):
837+
"""Test that --account is rejected."""
838+
job = mock_job(slurm_extra="--account=myaccount --mail-type=END")
839+
with pytest.raises(WorkflowError, match=r"account.*not allowed"):
840+
validate_slurm_extra(job)
841+
842+
def test_forbidden_account_short_form(self, mock_job):
843+
"""Test that -A is rejected."""
844+
job = mock_job(slurm_extra="-A myaccount --mail-type=END")
845+
with pytest.raises(WorkflowError, match=r"account.*not allowed"):
846+
validate_slurm_extra(job)
847+
848+
def test_forbidden_comment(self, mock_job):
849+
"""Test that --comment is rejected."""
850+
job = mock_job(slurm_extra="--comment='my comment' --mail-type=END")
851+
with pytest.raises(WorkflowError, match=r"job-comment.*not allowed"):
852+
validate_slurm_extra(job)
853+
854+
def test_forbidden_gres(self, mock_job):
855+
"""Test that --gres is rejected."""
856+
job = mock_job(slurm_extra="--gres=gpu:1 --mail-type=END")
857+
with pytest.raises(WorkflowError, match=r"generic-resources.*not allowed"):
858+
validate_slurm_extra(job)
859+
860+
def test_multiple_forbidden_options(self, mock_job):
861+
"""Test that the first forbidden option found is reported."""
862+
job = mock_job(slurm_extra="--job-name=test --account=myaccount")
863+
# Should raise error for job-name (first one encountered)
864+
with pytest.raises(WorkflowError, match=r"job-name.*not allowed"):
865+
validate_slurm_extra(job)

0 commit comments

Comments
 (0)