blob: 2909c5f229534765d719d07b89e4136c828d600a [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test the sbatch, salloc and srun --kv option.
--kv attaches a KV metadata document to a job at submission. The clients refuse
anything that is not a JSON object before the job is submitted, and the
document can then be read back with scontrol.
"""
import json
import shlex
import pytest
import atf
# Nested objects, arrays, booleans, null and characters a shell treats
# specially must all come back unchanged.
KV = {
"team": "science",
"nested": {"attempt": 2, "enabled": True},
"values": [1, None, "quote ' and slash \\"],
}
@pytest.fixture(scope="module", autouse=True)
def setup():
for command in ["sbatch", "salloc", "srun"]:
atf.require_version(
(26, 11),
f"bin/{command}",
reason=f"Issue 51034: {command} --kv was added in 26.11",
)
atf.require_version(
(26, 11), "bin/scontrol", reason="Issue 51034: KV was added in 26.11"
)
atf.require_slurm_running()
def kv_arg(kv):
"""Format a --kv=<json> argument, quoted for the shell."""
return f"--kv={shlex.quote(json.dumps(kv))}"
def decode_kv(job):
"""Return the KV document a job carries in --json output, or None."""
return job.get("kv") or None
def job_kv(job_id):
return decode_kv(atf.get_jobs(job_id, use_json=True, fatal=True)[job_id])
def test_sbatch_kv():
"""sbatch --kv gives the job the document."""
job_id = atf.submit_job_sbatch(f"{kv_arg(KV)} --hold --wrap true", fatal=True)
assert job_kv(job_id) == KV, "sbatch --kv should set the job's KV document"
def test_salloc_kv():
"""salloc --kv gives the job the document."""
job_id = atf.submit_job_salloc(f"{kv_arg(KV)} true", fatal=True)
assert job_id, "salloc should have been granted an allocation"
assert job_kv(job_id) == KV, "salloc --kv should set the job's KV document"
def test_srun_kv():
"""srun --kv gives the job it allocates the document."""
job_id = atf.submit_job_srun(f"{kv_arg(KV)} true", fatal=True)
assert job_id, "srun should have been granted an allocation"
assert job_kv(job_id) == KV, "srun --kv should set the job's KV document"
def test_empty_kv():
"""An empty --kv= gives the job no document."""
job_id = atf.submit_job_sbatch("--kv= --hold --wrap true", fatal=True)
assert job_kv(job_id) is None, "--kv= should leave the job without KV"
@pytest.mark.parametrize("command", ["sbatch", "salloc", "srun"])
@pytest.mark.parametrize("value", ["{", "[]", '"text"'])
def test_invalid_kv_refused(command, value):
"""A value that is not a JSON object is refused before submission."""
args = "--wrap true" if command == "sbatch" else "true"
# No fatal=True: the refusal is what is being asserted.
result = atf.run_command(f"{command} --kv={shlex.quote(value)} {args}")
assert result["exit_code"] != 0, (
f"{command} should refuse --kv={value}, got exit_code "
f"{result['exit_code']} and stdout: {result['stdout']}"
)
assert (
"Invalid --kv" in result["stderr"]
), f"The refusal should name --kv, got: {result['stderr']}"
def test_kv_in_batch_script():
"""#SBATCH --kv takes the same quoting as the command line."""
script = f"{atf.module_tmp_path}/kv_job.sh"
atf.make_bash_script(
script,
"""#SBATCH --kv='{"a b": "c d", "n": [1, null]}'
true
""",
)
job_id = atf.submit_job_sbatch(f"--hold {script}", fatal=True)
assert job_kv(job_id) == {
"a b": "c d",
"n": [1, None],
}, "#SBATCH --kv should set the job's KV document"
def test_kv_hetjob():
"""Each hetjob component gets only the --kv given for it."""
first = {"component": "first"}
third = {"component": "third"}
leader_id = atf.submit_job_sbatch(
f"--hold {kv_arg(first)} -n1 : -n1 : -n1 {kv_arg(third)} --wrap true",
fatal=True,
)
components = sorted(
atf.get_jobs(leader_id, use_json=True, fatal=True).values(),
key=lambda job: job["het_job_offset"]["number"],
)
assert [decode_kv(job) for job in components] == [
first,
None,
third,
], "A hetjob component should not inherit --kv from the one before it"
def test_srun_kv_ignored_for_steps():
"""srun --kv inside an allocation does not change the job's document."""
job_id = atf.submit_job_sbatch(
f"{kv_arg(KV)} --wrap {shlex.quote('srun ' + kv_arg({'step': 1}) + ' true')}",
fatal=True,
)
atf.wait_for_job_state(job_id, "COMPLETED", fatal=True)
assert job_kv(job_id) == KV, "srun --kv for a step should not change job KV"