changeset 991:0bfb71655123 default tip

new tests for worklists invalid/deleted files
author Alain Mazy <am@orthanc.team>
date Thu, 22 Oct 2026 12:55:16 +0200
parents bf0201d555a1
children
files .hgignore Database/Worklists/Working/.keep Database/Worklists/Working/orig.7814.other.station.dump.wl GenerateConfigurationForTests.py Plugins/Worklists/Run.py Tests/Tests.py Tests/Toolbox.py
diffstat 6 files changed, 92 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Fri Oct 09 17:13:39 2026 +0200
+++ b/.hgignore	Thu Oct 22 12:55:16 2026 +0200
@@ -16,3 +16,4 @@
 Tests/*.key
 Tests/dicom-tls.json
 *.orig
+Database/Worklists/Working/*.dump.wl
\ No newline at end of file
Binary file Database/Worklists/Working/orig.7814.other.station.dump.wl has changed
--- a/GenerateConfigurationForTests.py	Fri Oct 09 17:13:39 2026 +0200
+++ b/GenerateConfigurationForTests.py	Thu Oct 22 12:55:16 2026 +0200
@@ -228,6 +228,7 @@
 config['Worklists'] = {
     'Enable': True,
     'Database': os.path.abspath(os.path.join(os.path.dirname(__file__), './Database/Worklists/Working')),
+    'HousekeepingInterval': 1,  # for the "orthanc-worklists" plugin (cf. "test_housekeeper")
 }
 
 config['PostgreSQL'] = {
--- a/Plugins/Worklists/Run.py	Fri Oct 09 17:13:39 2026 +0200
+++ b/Plugins/Worklists/Run.py	Thu Oct 22 12:55:16 2026 +0200
@@ -142,7 +142,7 @@
                 if not isQuery:
                     tag = as_ascii[4:13].lower()
                     if not tag in ignoreTags:
-                        line_without_comment = re.sub(b'\s*#.*', b'', line)
+                        line_without_comment = re.sub(rb'\s*#.*', b'', line)
                         line_without_comment = line_without_comment.replace(b'\0', b'')
                         current.append(line_without_comment[3:])
 
@@ -168,6 +168,15 @@
     return False
 
 
+def GetUpTime():
+    metrics = DoGetRaw(ORTHANC, '/tools/metrics-prometheus')[1].decode()
+    for line in metrics.split('\n'):
+        m = line.split(' ')
+        if m[0] == 'orthanc_up_time_s':
+            return int(m[1])
+    raise Exception('Metric "orthanc_up_time_s" is not available')
+
+
 def ParseTopLevelTags(answer):
     tags = {}
     for line in answer:
@@ -383,7 +392,75 @@
         self.assertEqual('PatientID', a[0]['0010,0020']['Name'])
         self.assertEqual('ISO_IR 100', a[0]['0008,0005']['Value'])
         self.assertEqual('SpecificCharacterSet', a[0]['0008,0005']['Name'])
- 
+
+
+    def test_housekeeper(self):
+        # The "WL HOUSEKEEPER" thread of the "orthanc-worklists" plugin
+        # deletes a worklist once its study is stable ("DeleteWorklistsOnStableStudy",
+        # true by default).  A malformed ".wl" file (e.g. one being written by a
+        # RIS while it is read) used to terminate Orthanc from this thread.
+        # This requires the standalone plugin (not the "libModalityWorklists"
+        # sample), with a short "HousekeepingInterval" and "StableAge"
+        # (cf. "GenerateConfigurationForTests.py").
+        if not HasNewWorklistsPlugin(ORTHANC) or not IsPluginVersionAtLeast(ORTHANC, "orthanc-worklists", 0, 9, 3):
+            self.skipTest('Requires the new "orthanc-worklists" plugin')
+
+        start = time.time()
+        upTime = GetUpTime()
+
+        AddToDatabase('Dcmtk/Database/wklist1.dump')
+        valid = os.path.join(WORKING, 'wklist1.dump.wl')
+        empty = os.path.join(WORKING, 'empty.wl')
+        garbage = os.path.join(WORKING, 'garbage.wl')
+        open(empty, 'wb').close()
+        with open(garbage, 'wb') as f:
+            f.write(b'This is not a DICOM file')
+
+        # The malformed worklists are skipped, the valid one is still returned
+        a = DoPost(ORTHANC, '/modalities/self/find-worklist', {
+            'Query' : {
+                'PatientID' : ''
+                }
+            })
+        self.assertEqual(1, len(a))
+        self.assertEqual('AV35674', a[0]['PatientID'])
+
+        # Simulate the modality sending the images of the scheduled procedure:
+        # store a study with the same PatientID and StudyInstanceUID as the
+        # valid worklist (cf. "wklist1.dump").
+        #
+        # This relies on a short "StableAge" (1 second)
+        instance = DoPost(ORTHANC, '/tools/create-dicom', {
+            'Tags' : {
+                'PatientID' : 'AV35674',
+                'StudyInstanceUID' : '1.2.276.0.7230010.3.2.101',
+                },
+            'Force' : True
+            })['ID']
+
+        try:
+            # Once the study is stable ("StableAge") and the housekeeper has run
+            # ("HousekeepingInterval"), the worklist of the completed study is deleted
+            for i in range(30):
+                if not os.path.exists(valid):
+                    break
+                time.sleep(1)
+
+            self.assertFalse(os.path.exists(valid))
+
+            # The housekeeper has skipped the malformed worklists without crashing
+            self.assertTrue(os.path.exists(empty))
+            self.assertTrue(os.path.exists(garbage))
+
+            # Orthanc has not been restarted in the meantime
+            self.assertGreaterEqual(GetUpTime(), upTime + int(time.time() - start) - 1)
+
+            # The REST API skips the malformed worklists instead of failing
+            self.assertEqual(0, len(DoGet(ORTHANC, '/worklists')))
+
+        finally:
+            DoDelete(ORTHANC, '/instances/%s' % instance)
+
         
 try:
     print('\nStarting the tests...')
--- a/Tests/Tests.py	Fri Oct 09 17:13:39 2026 +0200
+++ b/Tests/Tests.py	Thu Oct 22 12:55:16 2026 +0200
@@ -12953,9 +12953,8 @@
         # to true, so they can't be checked this way, regardless of whether
         # "LocalAet" is honored).
 
-        if not HasWorklistsPlugin(_REMOTE):
-            print("skipping worklists tests, plugin is not installed")
-            return
+        if not HasAnyWorklistsPlugin(_REMOTE):
+            self.skipTest("skipping worklists tests, plugin is not installed")
 
         if not IsOrthancVersionAbove(_REMOTE, 1, 13, 1):
             return
--- a/Tests/Toolbox.py	Fri Oct 09 17:13:39 2026 +0200
+++ b/Tests/Toolbox.py	Thu Oct 22 12:55:16 2026 +0200
@@ -483,10 +483,18 @@
     plugins = DoGet(orthanc, '/plugins')
     return ('postgresql-index' in plugins)
 
-def HasWorklistsPlugin(orthanc):
+def HasLegacyWorklistsPlugin(orthanc):
     plugins = DoGet(orthanc, '/plugins')
     return ('worklists' in plugins)
 
+def HasNewWorklistsPlugin(orthanc):
+    plugins = DoGet(orthanc, '/plugins')
+    return ('orthanc-worklists' in plugins)
+
+def HasAnyWorklistsPlugin(orthanc):
+    plugins = DoGet(orthanc, '/plugins')
+    return ('orthanc-worklists' in plugins) or ('worklists' in plugins)
+
 def HasMySQLIndexPlugin(orthanc):
     plugins = DoGet(orthanc, '/plugins')
     return ('mysql-index' in plugins)