| ############################################################################ |
| # 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']}" |