blob: b6a0c73d12dc8528a8f5815d48916c314a7469cb [file]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test how slurmctld handles bad topology.yaml files"""
import os
import re
import pytest
import atf
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 5, 3),
"sbin/slurmctld",
reason="Fixed xassert instead of fatal for bad formatted topology.yaml fails, and enforce BlockSizes",
)
atf.require_nodes(17)
# intentionally not starting slurm
testcase_dir = os.path.splitext(__file__)[0] + "_testcases"
def read_testcase(filename):
"""Return the contents of a topology.yaml testcase file"""
with open(f"{testcase_dir}/{filename}") as f:
return f.read()
# One case per parse_error() call site reachable from PARSER_ARRAY(TOPOLOGY_CONF)
#
# Note "mutually exclusive" replaced with "mutually [a-z]*" because it is
# misspelled before data_parser/v0.0.46
@pytest.mark.parametrize(
("topology_file", "expected_error"),
[
(
"root_not_list.yaml",
"source: #/, description: Unexpected type string when expecting a list, rc: 9202",
),
(
"block_not_dict.yaml",
"source: #/block/, description: Rejecting list when dictionary expected, rc: 9209",
),
(
"block_blocks_not_list.yaml",
"source: #/block/blocks/, description: Unexpected type string when expecting a list, rc: 9202",
),
(
"flat_bool_mutually_exclusive.yaml",
"source: #/flat/, description: Field flat is mutually [a-z]* with other plugins, rc: -1",
),
(
"flat_dict_mutually_exclusive.yaml",
"source: #/flat/, description: Field flat is mutually [a-z]* with other plugins, rc: -1",
),
(
"ring_not_dict.yaml",
"source: #/ring/, description: Rejecting string when dictionary expected, rc: 9209",
),
(
"ring_mutually_exclusive.yaml",
"source: #/ring/, description: Field ring is mutually [a-z]* with fields block, tree and flat, rc: -1",
),
(
"ring_rings_not_list.yaml",
"source: #/ring/rings/, description: Unexpected type string when expecting a list, rc: 9202",
),
(
"torus3d_not_dict.yaml",
"source: #/torus3d/, description: Rejecting string when dictionary expected, rc: 9209",
),
(
"torus3d_mutually_exclusive.yaml",
"source: #/torus3d/, description: Field torus3d is mutually [a-z]* with fields block, tree, flat and ring, rc: -1",
),
(
"torus3d_toruses_not_list.yaml",
"source: #/torus3d/toruses/, description: Unexpected type string when expecting a list, rc: 9202",
),
(
"torus3d_placements_not_list.yaml",
"source: #/torus3d/toruses/placements/, description: Unexpected type string when expecting a list, rc: 9202",
),
(
"torus3d_regions_not_list.yaml",
"source: #/torus3d/toruses/regions/, description: Unexpected type string when expecting a list, rc: 9202",
),
(
"tree_not_dict.yaml",
"source: #/tree/, description: Rejecting string when dictionary expected, rc: 9209",
),
(
"tree_mutually_exclusive.yaml",
"source: #/tree/, description: Field tree is mutually [a-z]* with fields block and flat, rc: -1",
),
(
"tree_switches_not_list.yaml",
"source: #/tree/switches/, description: Unexpected type string when expecting a list, rc: 9202",
),
],
ids=[
"root_not_list",
"block_not_dict",
"block_blocks_not_list",
"flat_bool_mutually_exclusive",
"flat_dict_mutually_exclusive",
"ring_not_dict",
"ring_mutually_exclusive",
"ring_rings_not_list",
"torus3d_not_dict",
"torus3d_mutually_exclusive",
"torus3d_toruses_not_list",
"torus3d_placements_not_list",
"torus3d_regions_not_list",
"tree_not_dict",
"tree_mutually_exclusive",
"tree_switches_not_list",
],
)
def test_failed_parsing(topology_file, expected_error):
atf.require_config_file("topology.yaml", read_testcase(topology_file))
result = atf.run_command(
f"{atf.properties['slurm-sbin-dir']}/slurmctld -D",
user=atf.properties["slurm-user"],
xfail=True,
quiet=True,
)
# Prevent the slurmctld continuing to run on timeout since it is launched
# under sudo
if result["exit_code"] == 110:
atf.stop_slurmctld()
assert re.search(
expected_error, result["stderr"]
), f"slurmctld should log {expected_error} but didn't, got: {result['stderr']}"
assert (
"error: Failed to load topology.yaml" in result["stderr"]
), f"slurmctld should report a topology.yaml load failure, got: {result['stderr']}"
assert (
"fatal: Failed to initialize topology plugin" in result["stderr"]
), f"slurmctld should fatal on topology init, got: {result['stderr']}"
assert (
result["exit_code"] == 1
), f"slurmctld should fatal (exit 1), got {result['exit_code']}"
@pytest.mark.parametrize(
("topology_file", "fatal_message"),
[
("tree_bad_options.yaml", "fatal: Switch configuration sw_root has invalid"),
(
"block_bad_options.yaml",
"fatal: Invalid BlockSizes value in topology block_bad_options",
),
("ring_bad_options.yaml", "fatal: Ring (ring0) is bigger than 16"),
("flat_bad_options.yaml", "fatal: Failed to initialize topology plugin"),
("torus_bad_options.yaml", "fatal: Torus3d (pod1) has invalid configuration"),
],
ids=["tree", "block", "ring", "flat", "torus"],
)
def test_bad_options(topology_file, fatal_message):
atf.require_config_file("topology.yaml", read_testcase(topology_file))
result = atf.run_command(
f"{atf.properties['slurm-sbin-dir']}/slurmctld -D",
user=atf.properties["slurm-user"],
xfail=True,
quiet=True,
)
# Prevent the slurmctld continuing to run on timeout since it is launched
# under sudo
if result["exit_code"] == 110:
atf.stop_slurmctld()
assert (
fatal_message in result["stderr"]
), f"Expected '{fatal_message}' in slurmctld stderr, got: {result['stderr']}"
assert (
result["exit_code"] == 1
), f"slurmctld should fatal (exit 1), got {result['exit_code']}"