blob: 4416793591a86963444883327bee653e08497c23 [file]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test the --runtime job submission option and its runtime plugins."""
import json
import pytest
import atf
# Setup
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 11),
"bin/srun",
reason="Issue 50190: --runtime plugin ownership added in 26.11",
)
atf.require_version(
(26, 11),
"bin/sbatch",
reason="Issue 50190: --runtime plugin ownership added in 26.11",
)
atf.require_version(
(26, 11),
"bin/scontrol",
reason="Issue 50190: --runtime plugin ownership added in 26.11",
)
atf.require_version(
(26, 11),
"bin/squeue",
reason="Issue 50190: squeue -O Runtime added in 26.11",
)
atf.require_auto_config("deletes oci.conf directly on teardown")
atf.require_slurm_running()
@pytest.mark.parametrize("command", ["salloc", "sbatch", "srun"])
def test_runtime_recorded(command):
"""salloc/sbatch/srun record --runtime on the job, normalized to
runtime/<name>."""
job_id = atf.submit_job(
command, "--runtime=oci", "sleep 60", background=True, fatal=True
)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
assert (
atf.get_job_parameter(job_id, "Runtime", fatal=True) == "runtime/oci"
), f"{command}: --runtime not recorded as runtime/oci"
runtime = atf.run_command_output(
f"squeue -j {job_id} -O Runtime --noheader", fatal=True
).strip()
assert runtime == "runtime/oci", f"{command}: squeue Runtime should be runtime/oci"
def test_runtime_json_key_rename():
"""The job runtime is keyed 'runtime' in v0.0.46 and 'container_type' in
v0.0.45 (the same struct member was renamed)."""
job_id = atf.submit_job_sbatch('--runtime=oci --hold --wrap "true"', fatal=True)
job = json.loads(
atf.run_command_output(f"scontrol --json show job {job_id}", fatal=True)
)["jobs"][0]
assert job.get("runtime") == "runtime/oci", "v0.0.46 'runtime' key missing"
assert "container_type" not in job, "v0.0.46 should not use 'container_type'"
job45 = json.loads(
atf.run_command_output(f"scontrol --json=v0.0.45 show job {job_id}", fatal=True)
)["jobs"][0]
assert (
job45.get("container_type") == "runtime/oci"
), "v0.0.45 'container_type' key missing"
def test_runtime_bogus_fails():
"""An unknown runtime plugin fails when the step is launched.
Only srun is exercised: it launches a step under slurmstepd, which loads
the runtime plugin and fails. salloc and sbatch do not validate the
runtime plugin name at submission time, so they are not tested here.
"""
error = atf.run_command_error("srun --runtime=bogus true", xfail=True, fatal=True)
assert (
"launch failed" in error
), "srun should report a step launch failure for an unknown runtime plugin"
def test_runtime_none_ignores_container():
"""runtime/none runs tasks on the host, ignoring the --container value."""
# runtime/none ignores --container (even an invalid bundle) and runs the
# task directly on the host, exiting 0.
atf.run_command("srun --runtime=none --container=invalid hostname", fatal=True)
@pytest.fixture
def oci_enabled():
"""Enable OCI via a minimal runc oci.conf and remove it on teardown.
OCI support is global (oci.conf enables --container processing for every
step), so it is scoped to this fixture to keep it away from the other tests,
which assume OCI is disabled.
"""
atf.require_config_file(
"oci.conf",
'EnvExclude="^(SLURM_CONF|SLURM_CONF_SERVER)="\n'
'RunTimeQuery="runc --rootless=true --root=/run/user/%U/ state %n.%u.%j.%s.%t"\n'
'RunTimeKill="runc --rootless=true --root=/run/user/%U/ kill -a %n.%u.%j.%s.%t"\n'
'RunTimeDelete="runc --rootless=true --root=/run/user/%U/ delete --force %n.%u.%j.%s.%t"\n'
'RunTimeRun="runc --rootless=true --root=/run/user/%U/ run %n.%u.%j.%s.%t -b %b"\n',
)
yield
atf.run_command(
f"rm -f {atf.properties['slurm-config-dir']}/oci.conf",
user=atf.properties["slurm-user"],
fatal=True,
quiet=True,
)
def test_runtime_oci_uses_container(oci_enabled):
"""runtime/oci engages the --container value, unlike runtime/none.
runtime/oci does not ignore --container, so an invalid bundle fails the
step launch where runtime/none exits 0 on the same input. A full
containerized run additionally needs a valid OCI bundle and is out of scope
here.
"""
atf.run_command_error(
"srun --runtime=oci --container=invalid hostname", xfail=True, fatal=True
)
def test_runtime_env_vars():
"""SLURM_RUNTIME is exported to the job and SBATCH_RUNTIME sets --runtime."""
output = atf.run_command_output("srun --runtime=none env", fatal=True)
assert (
"SLURM_RUNTIME=runtime/none" in output
), "SLURM_RUNTIME should be exported to the job environment"
job_id = atf.submit_job_sbatch(
'--hold --wrap "true"', env_vars="SBATCH_RUNTIME=oci", fatal=True
)
assert (
atf.get_job_parameter(job_id, "Runtime", fatal=True) == "runtime/oci"
), "SBATCH_RUNTIME should set the job runtime"
def test_runtime_env_var_input_and_negative():
"""SRUN_RUNTIME sets --runtime, and SLURM_RUNTIME is only exported when
--runtime is specified."""
output = atf.run_command_output(
"srun env", env_vars="SRUN_RUNTIME=none", fatal=True
)
assert (
"SLURM_RUNTIME=runtime/none" in output
), "SRUN_RUNTIME should set the job runtime"
output = atf.run_command_output("srun env", fatal=True)
assert (
"SLURM_RUNTIME=" not in output
), "SLURM_RUNTIME should not be exported when --runtime is not specified"
def test_def_runtime_plugin_reported():
"""scontrol show config reports DefRuntimePlugin, defaulting to runtime/oci."""
atf.set_config_parameter("DefRuntimePlugin", None)
assert (
atf.get_config_parameter("DefRuntimePlugin", live=True) == "runtime/oci"
), "an unset DefRuntimePlugin should report the documented default runtime/oci"
atf.set_config_parameter("DefRuntimePlugin", "runtime/none")
assert (
atf.get_config_parameter("DefRuntimePlugin", live=True) == "runtime/none"
), "DefRuntimePlugin should reflect the configured runtime/none"
def test_def_runtime_plugin_takes_effect(oci_enabled):
"""A job with no --runtime uses the configured DefRuntimePlugin.
With oci.conf enabled, DefRuntimePlugin=runtime/oci engages the
--container value and an invalid bundle fails the step launch, while
DefRuntimePlugin=runtime/none ignores --container and the same job
succeeds. The jobs request no --runtime, so the contrast proves the
configured default selects the plugin.
"""
atf.set_config_parameter("DefRuntimePlugin", "runtime/oci")
atf.run_command_error("srun --container=invalid hostname", xfail=True, fatal=True)
atf.set_config_parameter("DefRuntimePlugin", "runtime/none")
atf.run_command("srun --container=invalid hostname", fatal=True)