changeset 958:b3b5f8990265

merge
author Alain Mazy <am@orthanc.team>
date Tue, 25 Aug 2026 09:31:32 +0200
parents df6abc13ad0b (current diff) fed1678dc2bf (diff)
children 54377a1367ca
files
diffstat 12 files changed, 199 insertions(+), 66 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.github/workflows/mirror.yml	Tue Aug 25 09:31:32 2026 +0200
@@ -0,0 +1,53 @@
+name: Mirror Mercurial to GitHub
+
+env:
+  HG_SOURCE_URL: https://orthanc.uclouvain.be/hg/orthanc-tests/
+  GITHUB_TARGET: jodogne/mirror-orthanc-tests
+  FAST_EXPORT_VERSION: fast-export-260405
+
+on:
+  schedule:
+    - cron: '0 3 * * *'
+  workflow_dispatch: {}
+
+concurrency:
+  group: hg-mirror
+  cancel-in-progress: false
+
+permissions:
+  contents: write  # GITHUB_TOKEN is read-only by default; this grants push access
+
+jobs:
+  mirror:
+    if: ${{ github.repository == 'jodogne/mirror-orthanc-tests' }}
+    runs-on: ubuntu-slim
+    steps:
+      - name: Install Mercurial and fast-export
+        run: |
+          sudo apt-get update
+          DEBIAN_FRONTEND=noninteractive sudo apt-get install -y mercurial git wget
+          cd /tmp/
+          wget https://orthanc.uclouvain.be/downloads/third-party-downloads/${FAST_EXPORT_VERSION}.tar.gz
+          tar xvf ${FAST_EXPORT_VERSION}.tar.gz
+
+      - name: Full clone of Mercurial source
+        run: |
+          hg clone "${HG_SOURCE_URL}" hg-repo
+
+      - name: Full conversion (fresh git repo every run)
+        run: |
+          git init --bare git-repo
+          cd git-repo
+          /tmp/${FAST_EXPORT_VERSION}/hg-fast-export.sh -r ../hg-repo --hg-hash --hgtags
+
+      - name: Push (never force, so it can't rewrite or delete history)
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          cd git-repo
+          git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_TARGET}"
+          git push origin 'refs/heads/*:refs/heads/*'
+          git push origin 'refs/tags/*:refs/tags/*'
+          if git rev-parse --verify refs/notes/hg >/dev/null 2>&1; then
+            git push origin refs/notes/hg:refs/notes/hg
+          fi
--- a/.hgtags	Tue Aug 25 09:31:20 2026 +0200
+++ b/.hgtags	Tue Aug 25 09:31:32 2026 +0200
@@ -52,3 +52,4 @@
 467d73ff9c02807f8f7f3c029e209fb445f6ef8b Orthanc-1.12.9
 655347b0274bea00576ed2f647acf7ce9ee49b5c Orthanc-1.12.10
 0cc8f543108ede9301b42cb94148ce10db68180d Orthanc-1.12.11
+f3f666d432bf7c319c9004e8930f7d1ec3c63b31 Orthanc-1.13.0
--- a/CITATION.cff	Tue Aug 25 09:31:20 2026 +0200
+++ b/CITATION.cff	Tue Aug 25 09:31:32 2026 +0200
@@ -10,5 +10,5 @@
 doi: "10.1007/s10278-018-0082-y"
 license: "GPL-3.0-or-later"
 repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
-version: 1.12.11
-date-released: 2026-04-14
+version: 1.13.0
+date-released: 2026-08-15
--- a/Database/WSI/NOTES.txt	Tue Aug 25 09:31:20 2026 +0200
+++ b/Database/WSI/NOTES.txt	Tue Aug 25 09:31:32 2026 +0200
@@ -1,3 +1,12 @@
+
+Create a pyramid from the Orthanc logo
+--------------------------------------
+
+$ convert /tmp/Orthanc-LOGO.png -background grey -alpha remove -alpha off /tmp/FlatLogo.png
+$ gdal_translate /tmp/FlatLogo.png ./OrthancLogoRaw.tiff -co TILED=YES -co BLOCKXSIZE=512 -co BLOCKYSIZE=512
+$ gdal_translate /tmp/FlatLogo.png ./OrthancLogoJpeg.tiff -co TILED=YES -co BLOCKXSIZE=512 -co BLOCKYSIZE=512 -co COMPRESS=JPEG
+
+
 Create a small image pyramid
 ----------------------------
 
Binary file Database/WSI/OrthancLogoJpeg.tiff has changed
Binary file Database/WSI/OrthancLogoRaw.tiff has changed
--- a/GenerateConfigurationForTests.py	Tue Aug 25 09:31:20 2026 +0200
+++ b/GenerateConfigurationForTests.py	Tue Aug 25 09:31:32 2026 +0200
@@ -123,7 +123,7 @@
          'LocalAet': 'SELF-FROM-CONFIG'
      },
      'orthanctest-with-local-aet': {
-         'AET': 'ORTHANCTEST',
+         'AET': 'ORTHANCTEST2',
          'Host': ip,
          'Port': 5001,
          'LocalAet': 'OT-FROM-CONFIG'
@@ -162,7 +162,6 @@
 config['MediaArchiveSize'] = 1
 config['SaveJobs'] = False
 config['ExecuteLuaEnabled'] = True
-config['HttpTimeout'] = 2
 config['SyncStorageArea'] = False  # For tests to run more quickly
 config['WebDavEnabled'] = True
 config['WebDavDeleteAllowed'] = True
@@ -171,8 +170,16 @@
 config['CheckRevisions'] = True
 config['HttpsCACertificates'] = "/etc/ssl/certs/ca-certificates.crt"   # for HTTPS lua tests (note: this path is valid only on linux !)
 
+# Release network resources as soon as possible
+config['HttpTimeout'] = 2
+config['HttpRequestTimeout'] = 5
+config['DicomScpTimeout'] = 5
+config['DicomScuTimeout'] = 5
+
 del config['DeidentifyLogsDicomVersion']
-del config['KeepAlive']
+
+if 'KeepAlive' in config:  # It could be absent, as this option has been moved to "AdvancedConfiguration.json"
+    del config['KeepAlive']
 
 config['Dictionary'] = {
     '00e1,10c2' : [ 'UI', 'PET-CT Multi Modality Name', 1, 1, 'ELSCINT1' ],
--- a/NewTests/ExtraMainDicomTags/test_extra_main_dicom_tags.py	Tue Aug 25 09:31:20 2026 +0200
+++ b/NewTests/ExtraMainDicomTags/test_extra_main_dicom_tags.py	Tue Aug 25 09:31:32 2026 +0200
@@ -105,7 +105,7 @@
         
         count = int(m.get('orthanc_storage_cache_hit_count') or '0') + int(m.get('orthanc_storage_cache_miss_count') or '0')
 
-        if self.o.get_system()["ApiVersion"] >= 34:  # 1.13.0 and 34 = streaming branch
+        if self.o.is_orthanc_version_at_least(1, 13, 0):
             count += int(m.get('orthanc_dicom_parser_cache_hit_count') or '0')
 
         print(f"storage access count = {count}")
--- a/NewTests/InterruptedDownloads/test_interrupted_downloads.py	Tue Aug 25 09:31:20 2026 +0200
+++ b/NewTests/InterruptedDownloads/test_interrupted_downloads.py	Tue Aug 25 09:31:32 2026 +0200
@@ -81,7 +81,7 @@
     def test_interrupting_a_download_should_cancel_the_archive_job(self):
         
         # note: there was a regression between 1.12.10 and 1.12.11
-        if self.o.is_orthanc_version_at_least(1, 12, 12) and self.o.get_system()["ApiVersion"] >= 34:  # 1.13.0 and 34 = streaming branch
+        if self.o.is_orthanc_version_at_least(1, 13, 0):
             self.o.delete_all_content()
             populator = OrthancTestDbPopulator(self.o, studies_count=1, series_count=2, instances_count=200, random_seed=65)
             populator.execute()
@@ -98,7 +98,7 @@
     def test_interrupting_a_download_should_release_the_http_thread(self):
         
         # note: there was a regression between 1.12.10 and 1.12.11
-        if self.o.is_orthanc_version_at_least(1, 12, 12) and self.o.get_system()["ApiVersion"] >= 34:  # 1.13.0 and 34 = streaming branch
+        if self.o.is_orthanc_version_at_least(1, 13, 0):
             self.o.delete_all_content()
             populator = OrthancTestDbPopulator(self.o, studies_count=1, series_count=2, instances_count=200, random_seed=65)
             populator.execute()
--- a/Tests/CheckHttpServerSecurity.py	Tue Aug 25 09:31:20 2026 +0200
+++ b/Tests/CheckHttpServerSecurity.py	Tue Aug 25 09:31:32 2026 +0200
@@ -43,11 +43,14 @@
 os.mkdir(TMP)
 
 
-ORTHANC = Toolbox.DefineOrthanc(username = 'orthanc',
-                                password = 'orthanc')
 
 
-def IsHttpServerSecure(config):
+def IsHttpServerSecure(config,
+                       username = 'orthanc',
+                       password = 'orthanc'):
+    ORTHANC = Toolbox.DefineOrthanc(username = username,
+                                    password = password)
+
     with open(CONFIG, 'w') as f:
         f.write(json.dumps(config))
     
@@ -59,66 +62,93 @@
         #shell=True
         )
 
-    time.sleep(1)
+    success = False
 
-    while True:
+    while process.poll() is None:  # Orthanc is still running
         try:
             system = Toolbox.DoGet(ORTHANC, '/system')
+            success = True
             break
         except:
-            time.sleep(0.1)
+            pass
+
+        time.sleep(0.1)
 
     process.terminate()
     process.wait()
 
-    return system['IsHttpServerSecure']
+    if success:
+        return system['IsHttpServerSecure']
+    else:
+        return None  # Orthanc has not started
 
 
-def Assert(b):
-    if not b:
+def Assert(expected, actual):
+    if expected != actual:
         raise Exception('Bad result')
 
 
 print('==== TEST 1 ====')
-Assert(IsHttpServerSecure({
+Assert(True, IsHttpServerSecure({
             'RemoteAccessAllowed': False,
             'RegisteredUsers' : { }
             }))
 
 print('==== TEST 2 ====')
-Assert(IsHttpServerSecure({
+Assert(True, IsHttpServerSecure({
             'RemoteAccessAllowed': False,
             'AuthenticationEnabled': False,
             'RegisteredUsers' : { }
             }))
 
-print('==== TEST 3 ====')
-Assert(IsHttpServerSecure({
+print('==== TEST 3a ====')
+Assert(False, IsHttpServerSecure({
             'RemoteAccessAllowed': False,
             'AuthenticationEnabled': True,
             'RegisteredUsers' : { 'orthanc' : 'orthanc' }
             }))
 
-print('==== TEST 4 ====')
-Assert(not IsHttpServerSecure({
+print('==== TEST 3b ====')
+Assert(False, IsHttpServerSecure({
+            'RemoteAccessAllowed': False,
+            'AuthenticationEnabled': True,
+            'RegisteredUsers' : { 'alice' : 'orthanctest' }
+            }, 'alice', 'orthanctest'))
+
+print('==== TEST 3c ====')
+Assert(True, IsHttpServerSecure({
+            'RemoteAccessAllowed': False,
+            'AuthenticationEnabled': True,
+            'RegisteredUsers' : { 'orthanc' : 'SECURE' }
+            }, 'orthanc', 'SECURE'))
+
+print('==== TEST 4 ====')  # Orthanc refuses to start since Orthanc 1.13.0
+Assert(None, IsHttpServerSecure({
             'RemoteAccessAllowed': True
             }))
 
 print('==== TEST 5 (server application scenario) ====')
-Assert(not IsHttpServerSecure({
+Assert(False, IsHttpServerSecure({
             'RemoteAccessAllowed': True,
             'AuthenticationEnabled': False,
             }))
 
-print('==== TEST 6 ====')
-Assert(IsHttpServerSecure({
+print('==== TEST 6a ====')
+Assert(False, IsHttpServerSecure({
             'RemoteAccessAllowed': True,
             'AuthenticationEnabled': True,
             'RegisteredUsers' : { 'orthanc' : 'orthanc' }
             }))
 
-print('==== TEST 7 (Docker scenario) ====')
-Assert(not IsHttpServerSecure({
+print('==== TEST 6b ====')
+Assert(True, IsHttpServerSecure({
+            'RemoteAccessAllowed': True,
+            'AuthenticationEnabled': True,
+            'RegisteredUsers' : { 'orthanc' : 'SECURE' }
+            }, 'orthanc', 'SECURE'))
+
+print('==== TEST 7 (Docker scenario) ====')  # Orthanc refuses to start since Orthanc 1.13.0
+Assert(None, IsHttpServerSecure({
             'RemoteAccessAllowed': True,
             'AuthenticationEnabled': True
             }))
--- a/Tests/Tests.py	Tue Aug 25 09:31:20 2026 +0200
+++ b/Tests/Tests.py	Tue Aug 25 09:31:32 2026 +0200
@@ -761,7 +761,7 @@
 
         z, resp = GetArchive(_REMOTE, '/patients/%s/archive' % kneePatient)
         self.assertEqual(2, len(z.namelist()))
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             self.assertIn('887 - KNEE/A10003245599 - IRM DU GENOU/301 - MR - T1W_aTSE/MR000001.dcm', z.namelist())
             self.assertIn('887 - KNEE/A10003245599 - IRM DU GENOU/601 - MR - T2W_TSE/MR000003.dcm', z.namelist())
         elif IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
@@ -770,7 +770,7 @@
 
         z, resp = GetArchive(_REMOTE, '/studies/%s/archive' % kneeStudy)
         self.assertEqual(2, len(z.namelist()))
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             self.assertIn('887 - KNEE/A10003245599 - IRM DU GENOU/301 - MR - T1W_aTSE/MR000001.dcm', z.namelist())
             self.assertIn('887 - KNEE/A10003245599 - IRM DU GENOU/601 - MR - T2W_TSE/MR000003.dcm', z.namelist())
         elif IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
@@ -779,7 +779,7 @@
 
         z, resp = GetArchive(_REMOTE, '/series/%s/archive' % kneeSeries)
         self.assertEqual(1, len(z.namelist()))
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             self.assertIn('887 - KNEE/A10003245599 - IRM DU GENOU/301 - MR - T1W_aTSE/MR000001.dcm', z.namelist())
         elif IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
             self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
@@ -797,7 +797,7 @@
             'Utf8' : False,  # Necessary since Orthanc 1.12.11
             })
         self.assertEqual(3, len(z.namelist()))
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             self.assertIn('5Yp0E - BRAINIX/0 - IRM crbrale, neuro-crne/401 - MR - sT2W FLAIR/MR000001.dcm', z.namelist())
             self.assertIn('887 - KNEE/A10003245599 - IRM DU GENOU/301 - MR - T1W_aTSE/MR000001.dcm', z.namelist())
         elif IsOrthancVersionAbove(_REMOTE, 1, 12, 11):
@@ -815,7 +815,7 @@
             'Utf8' : False,  # Necessary since Orthanc 1.12.11
             })
         self.assertEqual(3, len(z.namelist()))
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             self.assertIn('5Yp0E - BRAINIX/0 - IRM crbrale, neuro-crne/401 - MR - sT2W FLAIR/MR000001.dcm', z.namelist())
             self.assertIn('887 - KNEE/A10003245599 - IRM DU GENOU/301 - MR - T1W_aTSE/MR000001.dcm', z.namelist())
         elif IsOrthancVersionAbove(_REMOTE, 1, 12, 11):
@@ -828,7 +828,7 @@
             'Utf8' : False,  # Necessary since Orthanc 1.12.11
             })
         self.assertEqual(3, len(z.namelist()))
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             self.assertIn('5Yp0E - BRAINIX/0 - IRM crbrale, neuro-crne/401 - MR - sT2W FLAIR/MR000001.dcm', z.namelist())
             self.assertIn('887 - KNEE/A10003245599 - IRM DU GENOU/301 - MR - T1W_aTSE/MR000001.dcm', z.namelist())
         elif IsOrthancVersionAbove(_REMOTE, 1, 12, 11):
@@ -857,14 +857,14 @@
             # when downloading studies individually, we want to have the PatientName that appears in the study
             z, resp = GetArchive(_REMOTE, '/studies/%s/archive' % helloStudy)
             self.assertEqual(1, len(z.namelist()))
-            if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+            if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
                 self.assertIn('COMMON - HELLO/HELLO^SERIES/Unknown Series/00000000.dcm', z.namelist())
             elif IsOrthancVersionAbove(_REMOTE, 1, 12, 11):
                 self.assertIn('COMMON HELLO/HELLO^SERIES/Unknown Series/00000000.dcm', z.namelist())
 
             z, resp = GetArchive(_REMOTE, '/studies/%s/archive' % worldStudy)
             self.assertEqual(1, len(z.namelist()))
-            if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+            if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
                 self.assertIn('COMMON - WORLD/WORLD^SERIES/Unknown Series/00000000.dcm', z.namelist())
             elif IsOrthancVersionAbove(_REMOTE, 1, 12, 11):
                 self.assertIn('COMMON WORLD/WORLD^SERIES/Unknown Series/00000000.dcm', z.namelist())
@@ -1922,7 +1922,7 @@
         # Test returning sequence values (only since Orthanc 0.9.5)
         i = CallFindScu([ '-k', '0008,0052=SERIES', '-k', '0008,2112' ])  # "ColorTestImageJ" has this sequence tag
         sequences = re.findall(r'\(0008,2112\)', i)
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):  # the content of sequences was not returned prior to this version
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):  # the content of sequences was not returned prior to this version
             # From this version, Orthanc returns an empty sequence for the instance that does not contain the sequence
             # and a sequence with a content for the instance that contains the sequence
             self.assertEqual(2, len(sequences))
@@ -1931,7 +1931,7 @@
             # and no sequence at all for the instance that contained the sequence
             self.assertEqual(1, len(sequences))
 
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):  # the content of sequences was not returned prior to this version
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):  # the content of sequences was not returned prior to this version
             sequences = re.findall(r'\(0008,0016\)', i)
             self.assertEqual(1, len(sequences))
             sequences = re.findall(r'\(0008,0018\)', i)
@@ -1960,7 +1960,7 @@
     def test_incoming_findscu_private_tags_and_sequences(self):
         UploadInstance(_REMOTE, 'ZeissOCT.dcm')
 
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):  # this query requests a private sequence and a private tag that is not present in the data
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):  # this query requests a private sequence and a private tag that is not present in the data
             i = CallFindScu([ GetDatabasePath('ZeissOCT-cfind-query.dcm') ])
             self.assertIn('Find Response: 1', i)
             sequences = re.findall(r'\(0407,1013\)', i)
@@ -3399,11 +3399,11 @@
         WaitJobDone(_REMOTE, j['ID'])
         self.assertEqual(1, len(DoGet(_LOCAL, '/instances')))
         self.assertEqual('ORTHANC', DoGet(_LOCAL, '/instances/%s/metadata/RemoteAET' % c['ID']))
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             self.assertEqual('ORTHANC', DoGet(_REMOTE, '/jobs/%s' % j['ID'])['Content']['LocalAet'])
 
         # use the LocalAet from the "DicomModalities" config
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             DropOrthanc(_LOCAL)
             self.assertEqual(0, len(DoGet(_LOCAL, '/instances')))
 
@@ -3419,7 +3419,7 @@
 
 
         # use the LocalAet from the payload
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             DropOrthanc(_LOCAL)
             j = DoPost(_REMOTE, '/modalities/orthanctest-with-local-aet/store', {
                 'LocalAet' : 'FROM-PAYLOAD',
@@ -3429,7 +3429,7 @@
 
             WaitJobDone(_REMOTE, j['ID'])
             self.assertEqual(2, len(DoGet(_LOCAL, '/instances')))
-            if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+            if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
                 self.assertEqual('FROM-PAYLOAD', DoGet(_LOCAL, '/instances/%s/metadata/RemoteAET' % a['ID']))
                 self.assertEqual('FROM-PAYLOAD', DoGet(_REMOTE, '/jobs/%s' % j['ID'])['Content']['LocalAet'])
 
@@ -4389,7 +4389,7 @@
 
     def test_move_scu_local_aet(self):
         # test localAet wrt Move
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             DropOrthanc(_LOCAL)
             DropOrthanc(_REMOTE)
             DropOrthancJobs(_REMOTE)
@@ -12683,10 +12683,10 @@
             })
         self.assertEqual(1, len(z.namelist()))
 
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             self.assertIn('5Yp0E - BRAINIX/0 - IRM crbrale, neuro-crne/401 - MR - sT2W FLAIR/MR000001.dcm', z.namelist())
 
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             z = PostArchive(_REMOTE, '/tools/create-archive', {
                 'Resources' : [ brainix ],
                 'Utf8' : True,
@@ -12697,6 +12697,7 @@
 
     def test_od_of_ol_array(self):
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 11):
+            # Issue reported by Yusuf Sayıta (Philips)
             instance = UploadInstance(_REMOTE, '2026-03-16-OD-OF-OL.dcm') ['ID']
 
             tags = DoGet(_REMOTE, '/instances/%s/tags' % instance)
@@ -12796,10 +12797,12 @@
             self.assertEqual(42, v[2])
 
     def test_deeply_nested_sequence(self):
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if (IsThirdPartyLibraryVersionAbove(_REMOTE, 'dcmtk', 3, 7, 1) or
+            IsPatchApplied(_REMOTE, 'dcmtk-3.7.0-max-nested-sequence.patch')):
+            # note: unable to test with storescu since it does not support deeply nested sequences and crashes
             self.assertRaises(Exception, lambda: UploadInstance(_REMOTE, '2026-05-06-Deeply-Nested-Sequence.dcm'))
-
-            # note: unable to test with storescu since it does not support deeply nested sequences and crashes
+        else:
+            self.skipTest('Skipping because the required resource is unavailable')
 
     
     # small functional test, mainly to detect memory leaks (https://github.com/orthanc-server/orthanc-builder/issues/36)
@@ -12822,7 +12825,7 @@
                                                         'Level': 'Study' })
 
     def test_oob_monochrome_2(self):
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             r = UploadInstance(_REMOTE, '2026-05-26-oob-monochrome-65535.dcm')
             self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/instances/%s/preview' % r['ID']))
 
@@ -12853,7 +12856,7 @@
 
 
     def test_multiframe_rgb48_raw(self):
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             i = UploadInstance(_REMOTE, 'US-multiframe-RGB48-RAW.dcm')['ID']
 
             # note: the execution paths are different when we are using GDCM or not (at least in 1.12.11), even if the TS is RAW !!!
@@ -12884,7 +12887,7 @@
 
 
     def test_multiframe_rgb48_rle(self):
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 12):
+        if IsOrthancVersionAbove(_REMOTE, 1, 13, 0):
             i = UploadInstance(_REMOTE, 'US-multiframe-RGB48-RLE.dcm')['ID']
 
             # note: the execution paths are different when we are using GDCM or not (at least in 1.12.11), even if the TS is RAW !!!
--- a/Tests/Toolbox.py	Tue Aug 25 09:31:20 2026 +0200
+++ b/Tests/Toolbox.py	Tue Aug 25 09:31:32 2026 +0200
@@ -347,20 +347,6 @@
 
     return name
 
-def IsOrthancVersionAbove(orthanc, major, minor, revision):
-    v = DoGet(orthanc, '/system')['Version']
-
-    if v.startswith('mainline'):
-        return True
-    else:
-        tmp = v.split('.')
-        a = int(tmp[0])
-        b = int(tmp[1])
-        c = int(tmp[2])
-        return (a > major or
-                (a == major and b > minor) or
-                (a == major and b == minor and c >= revision))
-
 
 def HasExtendedFind(orthanc):
     v = DoGet(orthanc, '/system')
@@ -588,3 +574,47 @@
                 raise Exception()
         
         return result
+
+
+def IsVersionAbove(version, major, minor, revision = None):
+    tmp = version.split('.')
+
+    if revision == None:
+        assert(len(tmp) == 2)
+        a = int(tmp[0])
+        b = int(tmp[1])
+        return (a > major or
+                (a == major and b > minor))
+    else:
+        assert(len(tmp) == 3)
+        a = int(tmp[0])
+        b = int(tmp[1])
+        c = int(tmp[2])
+        return (a > major or
+                (a == major and b > minor) or
+                (a == major and b == minor and c >= revision))
+
+
+def IsOrthancVersionAbove(orthanc, major, minor, revision):
+    v = DoGet(orthanc, '/system')['Version']
+
+    if v.startswith('mainline'):
+        return True
+    else:
+        return IsVersionAbove(v, major, minor, revision)
+
+
+def IsThirdPartyLibraryVersionAbove(orthanc, library, major, minor, revision):
+    try:
+        v = DoGet(orthanc, '/system') ['ThirdPartyVersions'][library]
+        return IsVersionAbove(v, major, minor, revision)
+    except:
+        return False
+
+
+def IsPatchApplied(orthanc, patch):
+    try:
+        patches = DoGet(orthanc, '/system') ['ThirdPartyPatches']
+        return patch in patches
+    except:
+        return False