blob: 42e461f68f39c3d73e45c7567db28a4ae4dd5b5f [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test sacct names the user that cancelled a job or step.
slurmctld records the name of the user that asked for the cancel, which
sacct shows in the State column as "CANCELLED by <user>".
"""
import pytest
import atf
@pytest.fixture(scope="module", autouse=True)
def setup():
# Before 26.11, sacct shows the uid of the user instead of the name.
# Remove this when 26.05 is no longer supported.
atf.require_version(
(26, 11), "bin/sacct", reason="sacct shows the canceller's name"
)
atf.require_accounting()
# --stepmgr is silently dropped without it, see PrologFlags in slurm.conf(5)
atf.require_config_parameter_includes("PrologFlags", "Contain")
atf.require_slurm_running()
def job_owner(job_id):
"""Return the name of the user that owns the job, as accounting has it."""
owner = ""
for _ in atf.timer(fatal=True):
owner = atf.run_command_output(f"sacct -nPX -j {job_id} -o user").strip()
if owner:
break
return owner
def cancelled_state(sacct_args):
"""Wait for accounting to record the cancel, then return the State."""
state = ""
for _ in atf.timer(fatal=True):
state = atf.run_command_output(f"sacct -nP {sacct_args} -o state%80").strip()
if state.startswith("CANCELLED"):
break
return state
def test_owner_cancels_job():
"""The owner of a job is named when it cancels the job."""
job_id = atf.submit_job_sbatch('--wrap "sleep infinity"', fatal=True)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
owner = job_owner(job_id)
atf.run_command(f"scancel {job_id}", fatal=True)
state = cancelled_state(f"-X -j {job_id}")
assert (
state == f"CANCELLED by {owner}"
), f"Expected {owner} as the canceller, got {state!r}"
def test_other_user_cancels_job():
"""A user that cancels another user's job is named, not the owner."""
job_id = atf.submit_job_sbatch('--wrap "sleep infinity"', fatal=True)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
owner = job_owner(job_id)
canceller = atf.properties["slurm-user"]
if canceller == owner:
canceller = "root"
atf.run_command(f"scancel {job_id}", user=canceller, fatal=True)
state = cancelled_state(f"-X -j {job_id}")
assert (
state == f"CANCELLED by {canceller}"
), f"Expected {canceller} as the canceller of {owner}'s job, got {state!r}"
def test_owner_cancels_step():
"""The owner of a job is named when it cancels one of the job's steps."""
job_id = atf.submit_job_sbatch('--wrap "srun sleep infinity"', fatal=True)
atf.wait_for_step(job_id, 0, fatal=True)
owner = job_owner(job_id)
atf.run_command(f"scancel {job_id}.0", fatal=True)
state = cancelled_state(f"-j {job_id}.0")
assert (
state == f"CANCELLED by {owner}"
), f"Expected {owner} as the canceller, got {state!r}"
def test_owner_cancels_stepmgr_step():
"""The owner of a --stepmgr job is named when it cancels one of its steps.
slurmstepd manages the steps of such a job, and slurmctld forwards the
cancel to it through slurmd.
"""
job_id = atf.submit_job_sbatch('--stepmgr --wrap "srun sleep infinity"', fatal=True)
atf.wait_for_step(job_id, 0, fatal=True)
assert (
atf.get_job_parameter(job_id, "StepMgrEnabled") == "Yes"
), "The job should have a stepmgr"
owner = job_owner(job_id)
atf.run_command(f"scancel {job_id}.0", fatal=True)
state = cancelled_state(f"-j {job_id}.0")
assert (
state == f"CANCELLED by {owner}"
), f"Expected {owner} as the canceller, got {state!r}"