blob: b0cbca6040841efe2be7647f4efe6769deb15727 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test that SchedulerParameters=max_array_tasks caps the tasks in a job array.
max_array_tasks limits how many tasks one array may hold, independently of
MaxArraySize limiting the highest index. An array over the limit has to be
refused, and refused for the limit rather than for its syntax.
"""
import pytest
import atf
MAX_ARRAY_TASKS_ERROR = "Job Array exceeds the maximum number of tasks"
# Held below MaxArraySize so that the task count, not the index, is what
# rejects the array.
max_array_tasks = 4
max_array_size = 100
# Each holds exactly max_array_tasks tasks: a dense range, and a sparse one
# whose highest index is far above max_array_tasks, which is only accepted if
# the limit counts tasks rather than the highest index.
AT_LIMIT_ARRAYS = [
pytest.param(f"0-{max_array_tasks - 1}", id="range"),
pytest.param(
f"0-{max_array_size - 1}:{max_array_size // max_array_tasks}",
id="sparse",
),
]
# Each holds one task more than the limit: a dense range, and a sparse list
# that stays below the highest legal index so the task count is what rejects
# it rather than the index.
OVER_LIMIT_ARRAYS = [
pytest.param(f"0-{max_array_tasks}", id="range"),
pytest.param(
",".join(str(index) for index in range(0, 2 * (max_array_tasks + 1), 2)),
id="sparse",
),
]
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_config_parameter("MaxArraySize", max_array_size)
atf.require_config_parameter_includes(
"SchedulerParameters", ("max_array_tasks", max_array_tasks)
)
atf.require_slurm_running()
def sbatch_args(array):
"""Returns the sbatch arguments submitting a job array."""
return f"--array='{array}' -N1 -o /dev/null --wrap 'sleep infinity'"
@pytest.mark.parametrize("array", AT_LIMIT_ARRAYS)
def test_at_max_array_tasks(array):
"""Test that an array holding exactly max_array_tasks tasks is accepted."""
job_id = atf.submit_job_sbatch(sbatch_args(array))
assert job_id, f"--array={array} should be accepted"
@pytest.mark.parametrize("array", OVER_LIMIT_ARRAYS)
def test_above_max_array_tasks(array):
"""Test that an array holding more than max_array_tasks is rejected."""
result = atf.run_command(f"sbatch {sbatch_args(array)}", xfail=True)
assert (
result["exit_code"] != 0
), f"--array={array} should be rejected, but printed {result['stdout']!r}"
# The limit itself is not new, only the error naming it. sbatch renders the
# error from its own copy of the error table, so an older sbatch prints
# "Unknown error". slurmctld is never older than its client commands, so
# gating on sbatch also covers a slurmctld that still reports the generic
# invalid array.
@pytest.mark.skipif(
atf.get_version("bin/sbatch") < (26, 11),
reason="Ticket 25423: an array over max_array_tasks was reported as a generic invalid array before 26.11",
)
@pytest.mark.parametrize("array", OVER_LIMIT_ARRAYS)
def test_above_max_array_tasks_message(array):
"""Test that sbatch names the task limit rather than the syntax."""
result = atf.run_command(f"sbatch {sbatch_args(array)}", xfail=True)
assert (
MAX_ARRAY_TASKS_ERROR in result["stderr"]
), f"sbatch should report {MAX_ARRAY_TASKS_ERROR!r}, but printed {result['stderr']!r}"