| ############################################################################ | |
| # Copyright (C) SchedMD LLC. | |
| ############################################################################ | |
| import pytest | |
| import atf | |
| # Setup | |
| @pytest.fixture(scope="module", autouse=True) | |
| def setup(): | |
| atf.require_auto_config("wants to create custom topology.conf") | |
| atf.require_nodes(8) | |
| atf.require_config_parameter("SelectType", "select/cons_tres") | |
| atf.require_config_parameter("SelectTypeParameters", "CR_CPU") | |
| atf.require_config_parameter("TopologyPlugin", "topology/block") | |
| atf.require_version((25, 11), "sbin/slurmctld") | |
| # Mark topology for teardown and overwrite with proper data. | |
| # require_config_parameter marks a file for teardown, | |
| # but it doesn't allow us to write multiple lines easily to an external conf. | |
| # We're using it to create/mark the file. | |
| atf.require_config_parameter("", "", source="topology") | |
| # This is where we write the actual data | |
| overwrite_topology_conf() | |
| atf.require_config_parameter_includes("SchedulerParameters", ("bf_interval", 1)) | |
| atf.require_config_parameter_includes("SchedulerParameters", ("sched_interval", 1)) | |
| atf.require_slurm_running() | |
| def overwrite_topology_conf(): | |
| conf = atf.properties["slurm-config-dir"] + "/topology.conf" | |
| content = """ | |
| BlockName=b1 Nodes=node[1-4] | |
| BlockName=b2 Nodes=node[5-8] | |
| BlockSizes=4,8 | |
| """ | |
| atf.run_command(f"cat > {conf}", input=content, user="slurm", fatal=True) | |
| def test_segment_nodelist(): | |
| """Test simplest segment and nodelist""" | |
| job_id_1 = atf.submit_job_sbatch( | |
| '-N 4 -w node[1-4] --segment=2 --exclusive --mem=1 --wrap="exit 0"' | |
| ) | |
| atf.wait_for_job_state(job_id_1, "COMPLETED", fatal=True, timeout=5) | |
| job_id_2 = atf.submit_job_sbatch( | |
| '-N 4 -w node[1,2,5,6] --segment=2 --exclusive --mem=1 --wrap="exit 0"' | |
| ) | |
| atf.wait_for_job_state(job_id_2, "COMPLETED", fatal=True, timeout=5) | |
| job_id_3 = atf.submit_job_sbatch( | |
| '-N 6 -w node[1-8] --segment=3 --exclusive --mem=1 --wrap="exit 0"' | |
| ) | |
| atf.wait_for_job_state(job_id_3, "COMPLETED", fatal=True, timeout=5) | |
| job_id_4 = atf.submit_job_sbatch( | |
| '-N 6 -w node[1-3,5-7] --segment=3 --exclusive --mem=1 --wrap="exit 0"' | |
| ) | |
| atf.wait_for_job_state(job_id_4, "COMPLETED", fatal=True, timeout=5) | |
| assert ( | |
| atf.submit_job_sbatch( | |
| '-N 6 -w node[1-6] --segment=3 --exclusive --mem=1 --wrap="sleep 20"', | |
| xfail=True, | |
| ) | |
| == 0 | |
| ), "Job should fail segment #2 not fit on b2 " | |
| assert ( | |
| atf.submit_job_sbatch( | |
| '-N 6 -w node[1-5] --segment=3 --exclusive --mem=1 --wrap="sleep 20"', | |
| xfail=True, | |
| ) | |
| == 0 | |
| ), "Job should fail requested node count > number of specified nodes" | |
| def test_segment_update(): | |
| """Test scontrol update job SegmentSize on a pending job.""" | |
| atf.require_version((26, 11), "bin/scontrol") | |
| job_id = atf.submit_job_sbatch( | |
| '--hold -N 4 --segment=2 --exclusive --mem=1 --wrap="true"', | |
| fatal=True, | |
| ) | |
| assert atf.get_job_parameter(job_id, "SegmentSize") == 2 | |
| # SegmentSize=4 -> success (4 % 4 == 0). | |
| atf.run_command(f"scontrol update JobId={job_id} SegmentSize=4", fatal=True) | |
| assert atf.get_job_parameter(job_id, "SegmentSize") == 4 | |
| # SegmentSize > min_nodes is allowed. | |
| atf.run_command(f"scontrol update JobId={job_id} SegmentSize=8", fatal=True) | |
| assert atf.get_job_parameter(job_id, "SegmentSize") == 8 | |
| # SegmentSize=3 -> rejected (4 % 3 != 0), value unchanged. | |
| result = atf.run_command( | |
| f"scontrol update JobId={job_id} SegmentSize=3", xfail=True | |
| ) | |
| assert result["exit_code"] != 0, "SegmentSize=3 must be rejected for N=4" | |
| assert atf.get_job_parameter(job_id, "SegmentSize") == 8 | |
| # SegmentSize=0 clears segmentation. | |
| atf.run_command(f"scontrol update JobId={job_id} SegmentSize=0", fatal=True) | |
| assert atf.get_job_parameter(job_id, "SegmentSize") is None | |
| atf.cancel_jobs([job_id], quiet=True) | |
| def test_segment_update_not_pending(): | |
| """SegmentSize cannot be updated on a non-pending job.""" | |
| atf.require_version((26, 11), "bin/scontrol") | |
| job_id = atf.submit_job_sbatch( | |
| "-N 2 --exclusive --mem=1 --wrap='sleep infinity'", fatal=True | |
| ) | |
| atf.wait_for_job_state(job_id, "RUNNING", fatal=True) | |
| result = atf.run_command( | |
| f"scontrol update JobId={job_id} SegmentSize=2", xfail=True | |
| ) | |
| assert result["exit_code"] != 0, "SegmentSize update must fail for non-pending job" | |
| atf.cancel_jobs([job_id], quiet=True) | |
| def test_spread_consolidate_segments_update(): | |
| """Test scontrol update job SpreadSegments and ConsolidateSegments.""" | |
| atf.require_version((26, 11), "bin/scontrol") | |
| job_id = atf.submit_job_sbatch( | |
| '--hold -N 4 --segment=2 --exclusive --mem=1 --wrap="true"', | |
| fatal=True, | |
| ) | |
| assert atf.get_job_parameter(job_id, "SpreadSegments") is None | |
| assert atf.get_job_parameter(job_id, "ConsolidateSegments") is None | |
| # Enable both flags. | |
| atf.run_command(f"scontrol update JobId={job_id} SpreadSegments=Yes", fatal=True) | |
| assert atf.get_job_parameter(job_id, "SpreadSegments") == "Yes" | |
| atf.run_command( | |
| f"scontrol update JobId={job_id} ConsolidateSegments=Yes", fatal=True | |
| ) | |
| assert atf.get_job_parameter(job_id, "ConsolidateSegments") == "Yes" | |
| # Setting an already-set flag is a silent no-op. | |
| atf.run_command(f"scontrol update JobId={job_id} SpreadSegments=Yes", fatal=True) | |
| assert atf.get_job_parameter(job_id, "SpreadSegments") == "Yes" | |
| # Clear flags. | |
| atf.run_command(f"scontrol update JobId={job_id} SpreadSegments=No", fatal=True) | |
| assert atf.get_job_parameter(job_id, "SpreadSegments") is None | |
| atf.run_command( | |
| f"scontrol update JobId={job_id} ConsolidateSegments=No", fatal=True | |
| ) | |
| assert atf.get_job_parameter(job_id, "ConsolidateSegments") is None | |
| atf.cancel_jobs([job_id], quiet=True) | |
| def test_spread_consolidate_segments_update_not_pending(): | |
| """SpreadSegments/ConsolidateSegments cannot be updated on a non-pending job.""" | |
| atf.require_version((26, 11), "bin/scontrol") | |
| job_id = atf.submit_job_sbatch( | |
| "-N 2 --exclusive --mem=1 --wrap='sleep infinity'", fatal=True | |
| ) | |
| atf.wait_for_job_state(job_id, "RUNNING", fatal=True) | |
| result = atf.run_command( | |
| f"scontrol update JobId={job_id} SpreadSegments=Yes", xfail=True | |
| ) | |
| assert ( | |
| result["exit_code"] != 0 | |
| ), "SpreadSegments update must fail for non-pending job" | |
| result = atf.run_command( | |
| f"scontrol update JobId={job_id} ConsolidateSegments=Yes", xfail=True | |
| ) | |
| assert ( | |
| result["exit_code"] != 0 | |
| ), "ConsolidateSegments update must fail for non-pending job" | |
| atf.cancel_jobs([job_id], quiet=True) |