diff --git a/eessi_bot_job_manager.py b/eessi_bot_job_manager.py index 85fba369..5319a22e 100644 --- a/eessi_bot_job_manager.py +++ b/eessi_bot_job_manager.py @@ -272,6 +272,9 @@ def parse_scontrol_show_job_output(self, output): """ The output of 'scontrol --oneliner show job' is a list of key=value pairs separated by whitespaces. + Note that with newer Slurm versions (25.11), some values can also contain whitespaces + (e.g. for SubmitLine), making it complex to distinguish between keys and values. + To solve this, we assume that all Slurm keys start with an uppercase letter. Args: output (string): the output of the scontrol command @@ -281,8 +284,20 @@ def parse_scontrol_show_job_output(self, output): """ job_info = {} stripped_output = output.strip() - for pair in stripped_output.split(): - key, value = pair.split('=', 1) + + # Match keys that start with uppercase and continue until '=' + key_pattern = re.compile(r'([A-Z][A-Za-z0-9:/_]*)=') + + keys_matches = list(key_pattern.finditer(stripped_output)) + + for idx, key_match in enumerate(keys_matches): + # The key is what actually got matched by the regex + key = key_match.group(1) + # The value starts where the key ends... + value_start = key_match.end() + # and it ends where the next key starts (or, if it's the last one, where the string ends) + value_end = keys_matches[idx+1].start() if (idx + 1) < len(keys_matches) else len(stripped_output) + value = stripped_output[value_start:value_end].strip() job_info[key] = value return job_info diff --git a/tests/test_eessi_bot_job_manager.py b/tests/test_eessi_bot_job_manager.py index 5c5a9c05..aae9d038 100644 --- a/tests/test_eessi_bot_job_manager.py +++ b/tests/test_eessi_bot_job_manager.py @@ -135,3 +135,31 @@ def test_determine_finished_jobs(): assert job_manager.determine_finished_jobs(known_jobs, current_jobs_all_jobs) == [] assert job_manager.determine_finished_jobs(known_jobs, current_jobs_one_job) == ['1', '2'] assert job_manager.determine_finished_jobs(known_jobs, {}) == ['0', '1', '2'] + + +def test_parse_scontrol_show_job_output(): + # Dummy output (shortened) from Slurm 25.11.3 for "scontrol show job " + scontrol_output = 'JobId=123 JobName=bot_test_job UserId=eessibot(12345) MCS_label=N/A EligibleTime=Unknown' \ + ' AllocNode:Sid=my.node.name:123456 SubmitLine=/opt/slurm/25.11.3/bin/sbatch --hold' \ + ' --time=10-0:0:0 --nodes=1 --exclusive --cpus-per-task=1 --job-name=bot_test_job ' \ + '/home/eessibot/job.slurm WorkDir=/jobs/2026.01/pr_123/event_123-456-789/run_000/riscv64/' \ + 'generic/dev.eessi.io-riscv StdErr= StdIn=/dev/null StdOut=/jobs/2026.01/pr_123/' \ + 'event_123-456-789/run_000/riscv64/generic/dev.eessi.io-riscv/slurm-123.out TresPerTask=cpu=1' + job_manager = EESSIBotSoftwareLayerJobManager() + job_info = job_manager.parse_scontrol_show_job_output(scontrol_output) + job_info_expected = { + 'JobId': '123', + 'JobName': 'bot_test_job', + 'UserId': 'eessibot(12345)', + 'MCS_label': 'N/A', + 'EligibleTime': 'Unknown', + 'AllocNode:Sid': 'my.node.name:123456', + 'SubmitLine': '/opt/slurm/25.11.3/bin/sbatch --hold --time=10-0:0:0 --nodes=1 --exclusive --cpus-per-task=1 ' + '--job-name=bot_test_job /home/eessibot/job.slurm', + 'WorkDir': '/jobs/2026.01/pr_123/event_123-456-789/run_000/riscv64/generic/dev.eessi.io-riscv', + 'StdErr': '', + 'StdIn': '/dev/null', + 'StdOut': '/jobs/2026.01/pr_123/event_123-456-789/run_000/riscv64/generic/dev.eessi.io-riscv/slurm-123.out', + 'TresPerTask': 'cpu=1', + } + assert job_info == job_info_expected