# HG changeset patch # User Alain Mazy # Date 1792666516 -7200 # Node ID 0bfb716551230ac7e592c405dad05234eaa2bc60 # Parent bf0201d555a10e6465a9c51737f3d7fffd8878ec new tests for worklists invalid/deleted files diff -r bf0201d555a1 -r 0bfb71655123 .hgignore --- 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 diff -r bf0201d555a1 -r 0bfb71655123 Database/Worklists/Working/.keep diff -r bf0201d555a1 -r 0bfb71655123 Database/Worklists/Working/orig.7814.other.station.dump.wl Binary file Database/Worklists/Working/orig.7814.other.station.dump.wl has changed diff -r bf0201d555a1 -r 0bfb71655123 GenerateConfigurationForTests.py --- 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'] = { diff -r bf0201d555a1 -r 0bfb71655123 Plugins/Worklists/Run.py --- 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...') diff -r bf0201d555a1 -r 0bfb71655123 Tests/Tests.py --- 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 diff -r bf0201d555a1 -r 0bfb71655123 Tests/Toolbox.py --- 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)