blob: f8cbfe0df1c9925dc61f896bc4c09cfeb14d1ce3 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Verify scancel restricts cancellation by job name."""
import os
import pexpect
import pytest
import atf
test_name = os.path.splitext(os.path.basename(__file__))[0]
name1 = f"{test_name}_job1"
name2 = f"{test_name}_job2"
name3 = f"{test_name}_job3"
requires_j = pytest.mark.skipif(
atf.get_version("bin/scancel") < (26, 11),
reason="Ticket 25236: scancel -J/--job-name was added in 26.11",
)
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_slurm_running()
@pytest.fixture
def jobs():
job1 = atf.submit_job_sbatch(
f"-H --job-name={name1} --wrap 'sleep infinity'", fatal=True
)
job2 = atf.submit_job_sbatch(
f"-H --job-name={name2} --wrap 'sleep infinity'", fatal=True
)
return job1, job2
@pytest.mark.parametrize(
"opt",
[
pytest.param("-J", marks=requires_j),
pytest.param("--job-name", marks=requires_j),
"--jobname",
"--name",
"-n",
],
)
def test_job_name_filter(jobs, opt):
"""Verify scancel name filters cancel only the matching job"""
job1, job2 = jobs
result = atf.run_command(f"scancel {opt} {name1}")
assert result["exit_code"] == 0, f"scancel should accept {opt}"
atf.wait_for_job_state(job1, "CANCELLED", fatal=True)
assert (
atf.get_job_parameter(job2, "JobState", fatal=True) == "PENDING"
), f"scancel {opt} {name1} should not have cancelled job {job2}"
@pytest.mark.parametrize(
"names", [name1.upper(), f"{name1},{name2}"], ids=["case", "list"]
)
@pytest.mark.parametrize("opt", [pytest.param("-J", marks=requires_j), "--name"])
def test_job_name_exact_match(jobs, opt, names):
"""Verify scancel matches one name exactly, with no case folding or list"""
job1, job2 = jobs
atf.run_command(f"scancel {opt} {names}")
for job in (job1, job2):
assert (
atf.get_job_parameter(job, "JobState", fatal=True) == "PENDING"
), f"scancel {opt} {names} should not have cancelled job {job}"
@pytest.fixture
def same_name_jobs():
running = atf.submit_job_sbatch(
f"--job-name={name3} --wrap 'sleep infinity'", fatal=True
)
atf.wait_for_job_state(running, "RUNNING", fatal=True)
pending = atf.submit_job_sbatch(
f"-H --job-name={name3} --wrap 'sleep infinity'", fatal=True
)
return running, pending
@pytest.mark.parametrize("opt", [pytest.param("-J", marks=requires_j), "--name"])
def test_job_name_with_state_filter(same_name_jobs, opt):
"""Verify scancel signals only jobs satisfying every supplied filter"""
running, pending = same_name_jobs
result = atf.run_command(f"scancel {opt} {name3} --state=PENDING")
assert result["exit_code"] == 0, f"scancel should accept {opt} with --state"
atf.wait_for_job_state(pending, "CANCELLED", fatal=True)
assert (
atf.get_job_parameter(running, "JobState", fatal=True) == "RUNNING"
), f"scancel {opt} {name3} --state=PENDING should not have cancelled {running}"
@pytest.fixture
def interactive_scancel(request, jobs):
child = atf.run_command_pexpect(f"scancel {request.param} {name1} --interactive")
yield request.param, child
child.close()
@pytest.mark.parametrize(
"interactive_scancel",
[pytest.param("-J", marks=requires_j), "--name"],
indirect=True,
)
def test_job_name_interactive(jobs, interactive_scancel):
"""Verify the name filter narrows the --interactive prompt to one job"""
job1, job2 = jobs
opt, child = interactive_scancel
index = child.expect(
[rf"Cancel job_id={job1} .*\[y/n\]\? ", pexpect.TIMEOUT, pexpect.EOF]
)
assert index == 0, f"scancel {opt} {name1} --interactive should prompt for {job1}"
child.sendline("y")
index = child.expect([pexpect.EOF, pexpect.TIMEOUT])
assert index == 0, f"scancel should not prompt again after cancelling job {job1}"
child.close()
assert child.exitstatus == 0, "scancel --interactive should exit 0"
atf.wait_for_job_state(job1, "CANCELLED", fatal=True)
assert (
atf.get_job_parameter(job2, "JobState", fatal=True) == "PENDING"
), f"scancel {opt} {name1} --interactive should not have cancelled job {job2}"
def test_job_name_env(jobs):
"""Verify SCANCEL_NAME cancels only the matching job"""
job1, job2 = jobs
result = atf.run_command("scancel", env_vars=f"SCANCEL_NAME={name1}")
assert result["exit_code"] == 0, "scancel should accept SCANCEL_NAME"
atf.wait_for_job_state(job1, "CANCELLED", fatal=True)
assert (
atf.get_job_parameter(job2, "JobState", fatal=True) == "PENDING"
), f"SCANCEL_NAME={name1} should not have cancelled job {job2}"
@pytest.mark.parametrize("opt", [pytest.param("-J", marks=requires_j), "--name"])
def test_job_name_overrides_env(jobs, opt):
"""Verify a name option on the command line overrides SCANCEL_NAME"""
job1, job2 = jobs
result = atf.run_command(f"scancel {opt} {name2}", env_vars=f"SCANCEL_NAME={name1}")
assert result["exit_code"] == 0, f"scancel should accept {opt} with SCANCEL_NAME"
atf.wait_for_job_state(job2, "CANCELLED", fatal=True)
assert (
atf.get_job_parameter(job1, "JobState", fatal=True) == "PENDING"
), f"{opt} {name2} should override SCANCEL_NAME={name1}"