From 0ba5b9ea54beb0e45f655da1c1dee5a06c12c2fd Mon Sep 17 00:00:00 2001
From: Michiel Cottaar <MichielCottaar@protonmail.com>
Date: Sat, 6 Jun 2020 16:26:02 +0100
Subject: [PATCH] ENH: update info to check multiple job IDs at once

---
 fsl/utils/fslsub.py  | 55 +++++++++++++++++++++++++---------
 tests/test_fslsub.py | 70 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 111 insertions(+), 14 deletions(-)

diff --git a/fsl/utils/fslsub.py b/fsl/utils/fslsub.py
index f6fa49557..e5fe94313 100644
--- a/fsl/utils/fslsub.py
+++ b/fsl/utils/fslsub.py
@@ -50,7 +50,7 @@ import tempfile
 import logging
 import importlib
 from dataclasses import dataclass, asdict
-from typing import Optional, Collection, Union, Tuple
+from typing import Optional, Collection, Union, Tuple, Dict
 import argparse
 import warnings
 
@@ -252,28 +252,55 @@ def submit(*command, **kwargs):
     return SubmitParams(**kwargs)(*command)
 
 
-def info(job_id):
+def info(job_ids) -> Dict[str, Optional[Dict[str, str]]]:
     """Gets information on a given job id
 
-    Uses `qstat -j <job_id>`
+    Uses `qstat -j <job_ids>`
 
-    :arg job_id: string with job id
-    :return:     dictionary with information on the submitted job (empty
-                 if job does not exist)
+    :arg job_ids: string with job id or (nested) sequence with jobs
+    :return: dictionary of jobid -> another dictionary with job information
+             (or None if job does not exist)
     """
     from fsl.utils.run import run
+    job_ids_string = _flatten_job_ids(job_ids)
     try:
-        result = run(['qstat', '-j', job_id], exitcode=True)[0]
+        result = run(['qstat', '-j', job_ids_string], exitcode=True)[0]
     except FileNotFoundError:
         log.debug("qstat not found; assuming not on cluster")
         return {}
-    if 'Following jobs do not exist:' in result:
-        return {}
-    res = {}
-    for line in result.splitlines()[1:]:
-        kv = line.split(':', 1)
-        if len(kv) == 2:
-            res[kv[0].strip()] = kv[1].strip()
+    return _parse_qstat(job_ids_string, result)
+
+
+def _parse_qstat(job_ids_string, qstat_stdout):
+    """
+    Parses the qstat output into a dictionary of dictionaries
+
+    :param job_ids_string: input job ids
+    :param qstat_stdout: qstat output
+    :return: dictionary of jobid -> another dictionary with job information
+             (or None if job does not exist)
+    """
+    res = {job_id: None for job_id in job_ids_string.split(',')}
+    current_job_id = None
+    for line in qstat_stdout.splitlines()[1:]:
+        line = line.strip()
+        if len(line) == 0:
+            continue
+        if line == '=' * len(line):
+            current_job_id = None
+        elif ':' in line:
+            current_key, value = [part.strip() for part in line.split(':', 1)]
+            if current_key == 'job_number':
+                current_job_id = value
+                if current_job_id not in job_ids_string:
+                    raise ValueError(f"Unexpected job ID in qstat output:\n{line}")
+                res[current_job_id] = {}
+            else:
+                if current_job_id is None:
+                    raise ValueError(f"Found job information before job ID in qstat output:\n{line}")
+                res[current_job_id][current_key] = value
+        else:
+            res[current_job_id][current_key] += '\n' + line
     return res
 
 
diff --git a/tests/test_fslsub.py b/tests/test_fslsub.py
index d7c6460ca..8e33d3824 100644
--- a/tests/test_fslsub.py
+++ b/tests/test_fslsub.py
@@ -13,6 +13,7 @@ import sys
 import textwrap as tw
 import contextlib
 import argparse
+import pytest
 
 import fsl
 from fsl.utils         import fslsub
@@ -189,3 +190,72 @@ def test_func_to_cmd():
 
         assert stdout.strip() == 'standard output'
         assert stderr.strip() == 'standard error'
+
+
+example_qstat_reply = """==============================================================
+job_number:                 9985061
+exec_file:                  job_scripts/9985061
+owner:                      user
+sge_o_home:                 /home/fs0/user
+sge_o_log_name:             user
+sge_o_shell:                /bin/bash
+sge_o_workdir:              /home/fs0/user
+account:                    sge
+cwd:                        /home/fs0/user
+mail_options:               a
+notify:                     FALSE
+job_name:                   echo
+jobshare:                   0
+hard_queue_list:            long.q
+restart:                    y
+job_args:                   test
+script_file:                echo
+binding:                    set linear:slots
+job_type:                   binary,noshell
+scheduling info:            queue instance "<some queue>" dropped because it is temporarily not available
+                            queue instance "<some queue>" dropped because it is disabled
+==============================================================
+job_number:                 9985062
+exec_file:                  job_scripts/9985062
+owner:                      user
+sge_o_home:                 /home/fs0/user
+sge_o_log_name:             user
+sge_o_shell:                /bin/bash
+sge_o_workdir:              /home/fs0/user
+account:                    sge
+cwd:                        /home/fs0/user
+mail_options:               a
+notify:                     FALSE
+job_name:                   echo
+jobshare:                   0
+hard_queue_list:            long.q
+restart:                    y
+job_args:                   test
+script_file:                echo
+binding:                    set linear:slots
+job_type:                   binary,noshell
+scheduling info:            queue instance "<some queue>" dropped because it is temporarily not available
+                            queue instance "<some queue>" dropped because it is disabled
+"""
+
+
+def test_info():
+    valid_job_ids = ['9985061', '9985062']
+    res = fslsub._parse_qstat(','.join(valid_job_ids), example_qstat_reply)
+    assert len(res) == 2
+    for job_id in valid_job_ids:
+        assert res[job_id] is not None
+        assert res[job_id]['account'] == 'sge'
+        assert res[job_id]['job_type'] == 'binary,noshell'
+        assert len(res[job_id]['scheduling info'].splitlines()) == 2
+        for line in res[job_id]['scheduling info'].splitlines():
+            assert line.startswith('queue instance ')
+
+    res2 = fslsub._parse_qstat(','.join(valid_job_ids + ['1']), example_qstat_reply)
+    assert len(res2) == 3
+    for job_id in valid_job_ids:
+        assert res[job_id] == res2[job_id]
+    assert res2['1'] is None
+
+    with pytest.raises(ValueError):
+        fslsub._parse_qstat(valid_job_ids[0], example_qstat_reply)
-- 
GitLab