Skip to content

Commit

Permalink
add cluster cancel script
Browse files Browse the repository at this point in the history
  • Loading branch information
mbhall88 committed Jun 15, 2022
1 parent 19dfe45 commit c0dd024
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 1 deletion.
136 changes: 136 additions & 0 deletions tests/test_lsf_cancel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import unittest
from unittest.mock import patch

from tests.src.OSLayer import OSLayer
from tests.src.lsf_cancel import kill_jobs, parse_input, KILL


class TestParseInput(unittest.TestCase):
script = "lsf_cancel.py"

def test_parse_input_no_args(self):
fake_args = [self.script]
with patch("sys.argv", fake_args):
actual = parse_input()

assert not actual

def test_parse_input_one_job_no_log(self):
fake_args = [self.script, "1234"]
with patch("sys.argv", fake_args):
actual = parse_input()

expected = fake_args[1:]
assert actual == expected

def test_parse_input_one_job_and_log(self):
fake_args = [self.script, "1234", "log/file.out"]
with patch("sys.argv", fake_args):
actual = parse_input()

expected = [fake_args[1]]
assert actual == expected

def test_parse_input_two_jobs_and_log(self):
fake_args = [self.script, "1234", "log/file.out", "9090", "log/other.out"]
with patch("sys.argv", fake_args):
actual = parse_input()

expected = [fake_args[1], fake_args[3]]
assert actual == expected

def test_parse_input_two_jobs_and_digits_in_log(self):
fake_args = [self.script, "1234", "log/file.out", "9090", "log/123"]
with patch("sys.argv", fake_args):
actual = parse_input()

expected = [fake_args[1], fake_args[3]]
assert actual == expected

def test_parse_input_multiple_args_but_no_jobs(self):
fake_args = [self.script, "log/file.out", "log/123"]
with patch("sys.argv", fake_args):
actual = parse_input()

assert not actual


class TestKillJobs(unittest.TestCase):
@patch.object(
OSLayer,
OSLayer.run_process.__name__,
return_value=(
"Job <123> is being terminated",
"",
),
)
def test_kill_jobs_one_job(
self,
run_process_mock,
):
jobids = ["123"]
expected_kill_cmd = "{} {}".format(KILL, " ".join(jobids))

kill_jobs(jobids)

run_process_mock.assert_called_once_with(expected_kill_cmd)

@patch.object(
OSLayer,
OSLayer.run_process.__name__,
return_value=(
"Job <123> is being terminated\nJob <456> is being terminated",
"",
),
)
def test_kill_jobs_two_jobs(
self,
run_process_mock,
):
jobids = ["123", "456"]
expected_kill_cmd = "{} {}".format(KILL, " ".join(jobids))

kill_jobs(jobids)

run_process_mock.assert_called_once_with(expected_kill_cmd)

@patch.object(
OSLayer,
OSLayer.run_process.__name__,
return_value=("", ""),
)
def test_kill_jobs_no_jobs(
self,
run_process_mock,
):
jobids = []

kill_jobs(jobids)

run_process_mock.assert_not_called()

@patch.object(
OSLayer,
OSLayer.run_process.__name__,
return_value=("", ""),
)
def test_kill_jobs_empty_jobs(self, run_process_mock):
jobids = ["", ""]

kill_jobs(jobids)

run_process_mock.assert_not_called()

@patch.object(
OSLayer,
OSLayer.run_process.__name__,
return_value=("", ""),
)
def test_kill_jobs_empty_job_and_non_empty_job(self, run_process_mock):
jobids = ["", "123"]

expected_kill_cmd = "{} {}".format(KILL, " ".join(jobids))

kill_jobs(jobids)

run_process_mock.assert_called_once_with(expected_kill_cmd)
4 changes: 4 additions & 0 deletions {{cookiecutter.profile_name}}/OSLayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ def run_process(cmd: str) -> Tuple[stdout, stderr]:
def print(string: str):
print(string)

@staticmethod
def eprint(string: str):
print(string, file=sys.stderr)

@staticmethod
def get_uuid4_string() -> str:
return str(uuid.uuid4())
Expand Down
2 changes: 1 addition & 1 deletion {{cookiecutter.profile_name}}/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ restart-times: "{{cookiecutter.restart_times}}"
jobs: "{{cookiecutter.jobs}}"
cluster: "lsf_submit.py"
cluster-status: "lsf_status.py"
cluster-cancel: "bkill"
cluster-cancel: "lsf_cancel.py"
max-jobs-per-second: "{{cookiecutter.max_jobs_per_second}}"
max-status-checks-per-second: "{{cookiecutter.max_status_checks_per_second}}"
44 changes: 44 additions & 0 deletions {{cookiecutter.profile_name}}/lsf_cancel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
import re
import shlex
import sys
from pathlib import Path
from typing import List

if not __name__.startswith("tests.src."):
sys.path.append(str(Path(__file__).parent.absolute()))
from OSLayer import OSLayer
else:
from .OSLayer import OSLayer

KILL = "bkill"


def kill_jobs(ids_to_kill: List[str]):
# we don't want to run bkill with no argument as this will kill the last job
if any(ids_to_kill):
cmd = "{} {}".format(KILL, " ".join(ids_to_kill))
_ = OSLayer.run_process(cmd)


def parse_input() -> List[str]:
# need to support quoted and unquoted jobid
# see https://github.com/Snakemake-Profiles/lsf/issues/45
split_args = shlex.split(" ".join(sys.argv[1:]))
valid_ids = []
for arg in map(str.strip, split_args):
if re.fullmatch(r"\d+", arg):
valid_ids.append(arg)

return valid_ids


if __name__ == "__main__":
jobids = parse_input()

if jobids:
kill_jobs(jobids)
else:
OSLayer.eprint(
"[cluster-cancel error] Did not get any valid jobids to cancel..."
)

0 comments on commit c0dd024

Please sign in to comment.