Mercurial > hg > orthanc-tests
changeset 816:b359461f750d attach-custom-data
merged default -> attach-custom-data
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Tue, 20 May 2025 10:56:10 +0200 |
parents | f0cc40ca6fb1 (current diff) 1aa6cfc19cc8 (diff) |
children | 1562c38ab7aa |
files | NewTests/PostgresUpgrades/test_pg_upgrades.py NewTests/requirements.txt Tests/Tests.py |
diffstat | 10 files changed, 304 insertions(+), 78 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgtags Mon Mar 17 17:02:56 2025 +0100 +++ b/.hgtags Tue May 20 10:56:10 2025 +0200 @@ -47,3 +47,4 @@ 7bfc8992ab8fc44bd811bc60ebf3332303bc87ed Orthanc-1.12.4 847b3c6b360b9b0daeab327133703c60e14e51f0 Orthanc-1.12.5 287aae544b3133f6cecdf768f5a09dacbd75cf91 Orthanc-1.12.6 +2eca398d9676e2378343c48769a4b3938ba96005 Orthanc-1.12.7
--- a/CITATION.cff Mon Mar 17 17:02:56 2025 +0100 +++ b/CITATION.cff Tue May 20 10:56:10 2025 +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.6 -date-released: 2025-01-22 +version: 1.12.7 +date-released: 2025-04-07
--- a/NewTests/Authorization/test_authorization.py Mon Mar 17 17:02:56 2025 +0100 +++ b/NewTests/Authorization/test_authorization.py Tue May 20 10:56:10 2025 +0200 @@ -99,6 +99,7 @@ cls.label_a_instance_id = o.upload_file(here / "../../Database/Knix/Loc/IM-0001-0001.dcm")[0] cls.label_a_study_id = o.instances.get_parent_study_id(cls.label_a_instance_id) cls.label_a_series_id = o.instances.get_parent_series_id(cls.label_a_instance_id) + cls.label_a_patient_dicom_id = o.studies.get_tags(cls.label_a_study_id)["PatientID"] cls.label_a_study_dicom_id = o.studies.get_tags(cls.label_a_study_id)["StudyInstanceUID"] cls.label_a_series_dicom_id = o.series.get_tags(cls.label_a_series_id)["SeriesInstanceUID"] cls.label_a_instance_dicom_id = o.instances.get_tags(cls.label_a_instance_id)["SOPInstanceUID"] @@ -107,6 +108,7 @@ cls.label_b_instance_id = o.upload_file(here / "../../Database/Brainix/Epi/IM-0001-0001.dcm")[0] cls.label_b_study_id = o.instances.get_parent_study_id(cls.label_b_instance_id) cls.label_b_series_id = o.instances.get_parent_series_id(cls.label_b_instance_id) + cls.label_b_patient_dicom_id = o.studies.get_tags(cls.label_b_study_id)["PatientID"] cls.label_b_study_dicom_id = o.studies.get_tags(cls.label_b_study_id)["StudyInstanceUID"] cls.label_b_series_dicom_id = o.series.get_tags(cls.label_b_series_id)["SeriesInstanceUID"] cls.label_b_instance_dicom_id = o.instances.get_tags(cls.label_b_instance_id)["SOPInstanceUID"] @@ -118,6 +120,7 @@ cls.no_label_instance_id = o.upload_file(here / "../../Database/Comunix/Pet/IM-0001-0001.dcm")[0] cls.no_label_study_id = o.instances.get_parent_study_id(cls.no_label_instance_id) cls.no_label_series_id = o.instances.get_parent_series_id(cls.no_label_instance_id) + cls.no_label_patient_dicom_id = o.studies.get_tags(cls.no_label_study_id)["PatientID"] cls.no_label_study_dicom_id = o.studies.get_tags(cls.no_label_study_id)["StudyInstanceUID"] cls.no_label_series_dicom_id = o.series.get_tags(cls.no_label_series_id)["SeriesInstanceUID"] cls.no_label_instance_dicom_id = o.instances.get_tags(cls.no_label_instance_id)["SOPInstanceUID"] @@ -315,6 +318,15 @@ self.assertEqual(1, len(r["Labels"])) self.assertEqual("label_a", r["Labels"][0]) + if o_admin.is_plugin_version_at_least("authorization", 0, 9, 2): + i = o.get_json(f"dicom-web/studies?StudyInstanceUID={self.label_a_study_dicom_id}") + + # this one is forbidden because we specify the study (and the study is forbidden) + self.assert_is_forbidden(lambda: o.get_json(f"dicom-web/studies?StudyInstanceUID={self.label_b_study_dicom_id}")) + + # this one is empty because no studies are specified + self.assertEqual(0, len(o.get_json(f"dicom-web/studies?PatientID={self.label_b_patient_dicom_id}"))) + def test_uploader_a(self): o_admin = OrthancApiClient(self.o._root_url, headers={"user-token-key": "token-admin"}) @@ -341,6 +353,16 @@ o.upload_files_dicom_web(paths = [here / "../../Database/Beaufix/IM-0001-0001.dcm"]) o_admin.instances.delete(orthanc_ids=instances_ids) + if o_admin.is_plugin_version_at_least("authorization", 0, 9, 1): + + # uploader-a shall not be able to upload a study through DICOMWeb using /dicom-web/studies/<StudyInstanceUID of label_b> + self.assert_is_forbidden(lambda: o.upload_files_dicom_web(paths = [here / "../../Database/Knix/Loc/IM-0001-0002.dcm"], endpoint=f"/dicom-web/studies/{self.label_b_study_dicom_id}")) + + # uploader-a shall be able to upload a study through DICOMWeb using /dicom-web/studies/<StudyInstanceUID of label_a> + o.upload_files_dicom_web(paths = [here / "../../Database/Knix/Loc/IM-0001-0002.dcm"], endpoint=f"/dicom-web/studies/{self.label_a_study_dicom_id}") + + # note that, uploader-a is allowed to upload to /dicom-web/studies without checking any labels :-() + o.upload_files_dicom_web(paths = [here / "../../Database/Knix/Loc/IM-0001-0002.dcm"], endpoint=f"/dicom-web/studies") def test_resource_token(self):
--- a/NewTests/DelayedDeletion/test_delayed_deletion.py Mon Mar 17 17:02:56 2025 +0100 +++ b/NewTests/DelayedDeletion/test_delayed_deletion.py Tue May 20 10:56:10 2025 +0200 @@ -8,6 +8,7 @@ import pathlib import glob +import pprint here = pathlib.Path(__file__).parent.resolve() @@ -23,7 +24,8 @@ "DelayedDeletion": { "Enable": True, "ThrottleDelayMs": 200 - } + }, + "DatabaseServerIdentifier": "delayed-test" } config_path = cls.generate_configuration( @@ -98,11 +100,15 @@ def test_resumes_pending_deletion(self): + # plugin_status = self.o.get_json("plugins/delayed-deletion/status") + # pprint.pprint(plugin_status) + completed = False while not completed: print('-------------- waiting for DelayedDeletion to finish processing') time.sleep(1) plugin_status = self.o.get_json("plugins/delayed-deletion/status") + # pprint.pprint(plugin_status) completed = plugin_status["FilesPendingDeletion"] == 0 self.assertTrue(completed)
--- a/NewTests/PostgresUpgrades/test_pg_upgrades.py Mon Mar 17 17:02:56 2025 +0100 +++ b/NewTests/PostgresUpgrades/test_pg_upgrades.py Tue May 20 10:56:10 2025 +0200 @@ -71,6 +71,8 @@ subprocess.run(["docker", "compose", "stop", "orthanc-pg-15-under-tests"], check=True) time.sleep(2) + + # test a full upgrade from a very old version, and a downgrade to the previous version (where we run the integration tests) def test_upgrades_downgrades_with_pg_15(self): # remove everything including the DB from previous tests @@ -144,10 +146,11 @@ o.wait_started() # time.sleep(10000) - subprocess.run(["docker", "compose", "up", "orthanc-tests"], check=True) + subprocess.run(["docker", "compose", "up", "orthanc-tests", "--exit-code-from", "orthanc-tests"], check=True) + # make sure we can still start a new Orthanc with an Old PG server def test_latest_orthanc_with_pg_9(self): # remove everything including the DB from previous tests
--- a/NewTests/requirements.txt Mon Mar 17 17:02:56 2025 +0100 +++ b/NewTests/requirements.txt Tue May 20 10:56:10 2025 +0200 @@ -1,3 +1,3 @@ -orthanc-api-client>=0.18.4 +orthanc-api-client>=0.18.5 orthanc-tools>=0.16.5 uvicorn \ No newline at end of file
--- a/Plugins/DicomWeb/DicomWeb.py Mon Mar 17 17:02:56 2025 +0100 +++ b/Plugins/DicomWeb/DicomWeb.py Tue May 20 10:56:10 2025 +0200 @@ -41,7 +41,7 @@ body += bytearray('\r\n', 'ascii') -def SendStowRaw(orthanc, uri, dicom): +def SendStowRaw(orthanc, uri, dicom, partsContentType='application/dicom'): # We do not use Python's "email" package, as it uses LF (\n) for line # endings instead of CRLF (\r\n) for binary messages, as required by # RFC 1341 @@ -54,9 +54,9 @@ if isinstance(dicom, list): for i in range(dicom): - _AttachPart(body, dicom[i], 'application/dicom', boundary) + _AttachPart(body, dicom[i], partsContentType, boundary) else: - _AttachPart(body, dicom, 'application/dicom', boundary) + _AttachPart(body, dicom, partsContentType, boundary) # Closing boundary body += bytearray('--%s--' % boundary, 'ascii') @@ -72,8 +72,8 @@ return (response.status, DecodeJson(content)) -def SendStow(orthanc, uri, dicom): - (status, content) = SendStowRaw(orthanc, uri, dicom) +def SendStow(orthanc, uri, dicom, partsContentType='application/dicom'): + (status, content) = SendStowRaw(orthanc, uri, dicom, partsContentType) if not (status in [ 200 ]): raise Exception('Bad status: %d' % status) else:
--- a/Plugins/DicomWeb/Run.py Mon Mar 17 17:02:56 2025 +0100 +++ b/Plugins/DicomWeb/Run.py Tue May 20 10:56:10 2025 +0200 @@ -189,7 +189,10 @@ a = SendStow(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Phenix/IM-0001-0001.dcm')) self.assertEqual(1, len(DoGet(ORTHANC, '/instances'))) - self.assertEqual(4, len(a)) + if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 19, 0): + self.assertEqual(3, len(a)) # DICOM_TAG_FAILED_SOP_SEQUENCE has been removed in 1.19 + else: + self.assertEqual(4, len(a)) # Specific character set self.assertTrue('00080005' in a) @@ -198,8 +201,11 @@ self.assertTrue(a['00081190']['Value'][0].endswith('studies/2.16.840.1.113669.632.20.1211.10000098591')) self.assertEqual('UR', a['00081190']['vr']) - self.assertFalse('Value' in a['00081198']) # No error => empty sequence - self.assertEqual('SQ', a['00081198']['vr']) + if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 19, 0): + self.assertNotIn('00081198', a) # No errors => the DICOM_TAG_FAILED_SOP_SEQUENCE shall not be present + else: + self.assertFalse('Value' in a['00081198']) # No error => empty sequence + self.assertEqual('SQ', a['00081198']['vr']) self.assertEqual(1, len(a['00081199']['Value'])) # 1 success self.assertEqual('SQ', a['00081199']['vr']) @@ -236,6 +242,16 @@ self.assertEqual(1, len(parts)) self.assertEqual(os.path.getsize(GetDatabasePath('Phenix/IM-0001-0001.dcm')), int(parts[0])) + def test_stow_like_dcm4chee(self): + # https://discourse.orthanc-server.org/t/orthanc-dicomweb-stowrs-server-request-response-compatibility/5763 + + self.assertEqual(0, len(DoGet(ORTHANC, '/instances'))) + a = SendStow(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Phenix/IM-0001-0001.dcm'), 'application/dicom;transfer-syntax=1.2.840.10008.1.2.1') + self.assertEqual(1, len(DoGet(ORTHANC, '/instances'))) + + self.assertNotIn('00081198', a) # No errors => the DICOM_TAG_FAILED_SOP_SEQUENCE shall not be present + + def test_server_get(self): try: @@ -296,56 +312,85 @@ def test_server_stow(self): - UploadInstance(ORTHANC, 'Knee/T1/IM-0001-0001.dcm') + # UploadInstance(ORTHANC, 'Knee/T1/IM-0001-0001.dcm') + + # self.assertRaises(Exception, lambda: + # DoPost(ORTHANC, '/dicom-web/servers/sample/stow', + # { 'Resources' : [ 'nope' ], + # 'Synchronous' : True })) # inexisting resource - self.assertRaises(Exception, lambda: - DoPost(ORTHANC, '/dicom-web/servers/sample/stow', - { 'Resources' : [ 'nope' ], - 'Synchronous' : True })) # inexisting resource + # if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 18, 0): + # l = 4 # "Server" has been added + # else: + # l = 3 # For >= 1.10.1 + + # # study + # r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', + # { 'Resources' : [ '0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918' ], + # 'Synchronous' : True }) - if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 18, 0): - l = 4 # "Server" has been added - else: - l = 3 # For >= 1.10.1 + # self.assertEqual(l, len(r)) + # self.assertEqual("0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918", r['Resources']['Studies'][0]) + # if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 18, 0): + # self.assertEqual("sample", r['Server']) - # study - r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', - { 'Resources' : [ '0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918' ], - 'Synchronous' : True }) + # # series + # r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', + # { 'Resources' : [ '6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285' ], + # 'Synchronous' : True }) + # self.assertEqual(l, len(r)) + # self.assertEqual("6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285", r['Resources']['Series'][0]) - self.assertEqual(l, len(r)) - self.assertEqual("0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918", r['Resources']['Studies'][0]) - if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 18, 0): - self.assertEqual("sample", r['Server']) + # # instances + # r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', + # { 'Resources' : [ 'c8df6478-d7794217-0f11c293-a41237c9-31d98357' ], + # 'Synchronous' : True }) + # self.assertEqual(l, len(r)) + # self.assertEqual("c8df6478-d7794217-0f11c293-a41237c9-31d98357", r['Resources']['Instances'][0]) - # series - r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', - { 'Resources' : [ '6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285' ], - 'Synchronous' : True }) - self.assertEqual(l, len(r)) - self.assertEqual("6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285", r['Resources']['Series'][0]) + # # altogether + # r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', + # { 'Resources' : [ + # 'ca29faea-b6a0e17f-067743a1-8b778011-a48b2a17', + # '0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918', + # '6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285', + # 'c8df6478-d7794217-0f11c293-a41237c9-31d98357' ], + # 'Synchronous' : True }) + # # pprint.pprint(r) + # self.assertEqual(l, len(r)) + # self.assertEqual("ca29faea-b6a0e17f-067743a1-8b778011-a48b2a17", r['Resources']['Patients'][0]) + # self.assertEqual("0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918", r['Resources']['Studies'][0]) + # self.assertEqual("6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285", r['Resources']['Series'][0]) + # self.assertEqual("c8df6478-d7794217-0f11c293-a41237c9-31d98357", r['Resources']['Instances'][0]) - # instances - r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', - { 'Resources' : [ 'c8df6478-d7794217-0f11c293-a41237c9-31d98357' ], - 'Synchronous' : True }) - self.assertEqual(l, len(r)) - self.assertEqual("c8df6478-d7794217-0f11c293-a41237c9-31d98357", r['Resources']['Instances'][0]) + + if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 20, 0): + a = UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0001.dcm') + b = UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0002.dcm') - # altogether - r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', - { 'Resources' : [ - 'ca29faea-b6a0e17f-067743a1-8b778011-a48b2a17', - '0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918', - '6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285', - 'c8df6478-d7794217-0f11c293-a41237c9-31d98357' ], - 'Synchronous' : True }) - # pprint.pprint(r) - self.assertEqual(l, len(r)) - self.assertEqual("ca29faea-b6a0e17f-067743a1-8b778011-a48b2a17", r['Resources']['Patients'][0]) - self.assertEqual("0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918", r['Resources']['Studies'][0]) - self.assertEqual("6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285", r['Resources']['Series'][0]) - self.assertEqual("c8df6478-d7794217-0f11c293-a41237c9-31d98357", r['Resources']['Instances'][0]) + # study + r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', + { 'Resources' : [ a['ParentStudy'] ], + 'Synchronous' : True }) + + self.assertEqual(1, len(r['Resources']['Studies'])) + self.assertNotIn('Series', r['Resources']) + self.assertEqual(2, len(r['Resources']['Instances'])) + self.assertEqual(a['ParentStudy'], r['Resources']['Studies'][0]) + self.assertIn(a['ID'], r['Resources']['Instances']) + self.assertIn(b['ID'], r['Resources']['Instances']) + + # series + r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', + { 'Resources' : [ a['ParentSeries'] ], + 'Synchronous' : True }) + + self.assertEqual(1, len(r['Resources']['Series'])) + self.assertNotIn('Studies', r['Resources']) + self.assertEqual(2, len(r['Resources']['Instances'])) + self.assertEqual(a['ParentSeries'], r['Resources']['Series'][0]) + self.assertIn(a['ID'], r['Resources']['Instances']) + self.assertIn(b['ID'], r['Resources']['Instances']) @@ -708,19 +753,24 @@ def test_stow_errors(self): - def CheckSequences(a): - self.assertEqual(3, len(a)) + def CheckSequences(a, expectFailedSopSequence): + if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 19, 0) and not expectFailedSopSequence: + self.assertEqual(2, len(a)) + self.assertNotIn('00081198', a) + else: + self.assertEqual(3, len(a)) + self.assertTrue('00081198' in a) + self.assertEqual('SQ', a['00081198']['vr']) + self.assertTrue('00080005' in a) - self.assertTrue('00081198' in a) self.assertTrue('00081199' in a) self.assertEqual('CS', a['00080005']['vr']) - self.assertEqual('SQ', a['00081198']['vr']) self.assertEqual('SQ', a['00081199']['vr']) # Pushing an instance to a study that is not its parent (status, a) = SendStowRaw(ORTHANC, args.dicomweb + '/studies/nope', GetDatabasePath('Phenix/IM-0001-0001.dcm')) self.assertEqual(409, status) - CheckSequences(a) + CheckSequences(a, True) self.assertFalse('Value' in a['00081199']) # No success instance @@ -735,23 +785,21 @@ # Pushing an instance with missing tags (status, a) = SendStowRaw(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Issue111.dcm')) self.assertEqual(400, status) - CheckSequences(a) + CheckSequences(a, False) # No failed instance, as tags are missing - self.assertFalse('Value' in a['00081198']) # No failed instance, as tags are missing self.assertFalse('Value' in a['00081199']) # No success instance # Pushing a file that is not in the DICOM format (status, a) = SendStowRaw(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Issue111.dump')) self.assertEqual(400, status) - CheckSequences(a) + CheckSequences(a, False) # No failed instance, as non-DICOM - self.assertFalse('Value' in a['00081198']) # No failed instance, as non-DICOM self.assertFalse('Value' in a['00081199']) # No success instance # Pushing a DICOM instance with only SOP class and instance UID (status, a) = SendStowRaw(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Issue196.dcm')) self.assertEqual(400, status) - CheckSequences(a) + CheckSequences(a, True) self.assertFalse('Value' in a['00081199']) # No success instance @@ -1196,6 +1244,39 @@ self.assertEqual(len(a), len(c)) self.assertEqual(a, c) + if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 20, 0): + # test with 2 instances: https://discourse.orthanc-server.org/t/thumbnail-orthanc-stone-viewer-issue/5827/3 + i = UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0001.dcm') ['ID'] + UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0002.dcm') ['ID'] + + study = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['StudyInstanceUID'] + series = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['SeriesInstanceUID'] + instance = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['SOPInstanceUID'] + + a = DoPost(ORTHANC, '/dicom-web/servers/sample/get', { + 'Uri' : '/studies/%s/series/%s/instances/%s/rendered' % (study, series, instance) + }) + + im = UncompressImage(a) + self.assertEqual("L", im.mode) + self.assertEqual(256, im.size[0]) + self.assertEqual(256, im.size[1]) + + b = DoPost(ORTHANC, '/dicom-web/servers/sample/get', { + 'Uri' : '/studies/%s/series/%s/rendered' % (study, series) + }) + + self.assertEqual(len(a), len(b)) + self.assertEqual(a, b) + + c = DoPost(ORTHANC, '/dicom-web/servers/sample/get', { + 'Uri' : '/studies/%s/rendered' % study + }) + + self.assertEqual(len(a), len(c)) + self.assertEqual(a, c) + + def test_multiple_mime_accept_wado_rs(self): # "Multiple MIME type Accept Headers for Wado-RS"
--- a/Plugins/Transfers/Run.py Mon Mar 17 17:02:56 2025 +0100 +++ b/Plugins/Transfers/Run.py Tue May 20 10:56:10 2025 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python3 # -*- coding: utf-8 -*-
--- a/Tests/Tests.py Mon Mar 17 17:02:56 2025 +0100 +++ b/Tests/Tests.py Tue May 20 10:56:10 2025 +0200 @@ -538,6 +538,32 @@ self.assertEqual(0, len(DoGet(_REMOTE, '/patients'))) + def test_delete_cascade_with_multiple_instances(self): + # make sure deleting the last instance of a study deletes the series, study and patient + + self.assertEqual(0, len(DoGet(_REMOTE, '/instances'))) # make sure orthanc is empty when starting the test + a = UploadInstance(_REMOTE, 'Knix/Loc/IM-0001-0001.dcm') + b = UploadInstance(_REMOTE, 'Knix/Loc/IM-0001-0002.dcm') + + self.assertEqual(2, len(DoGet(_REMOTE, '/instances'))) + self.assertEqual(1, len(DoGet(_REMOTE, '/series'))) + self.assertEqual(1, len(DoGet(_REMOTE, '/studies'))) + self.assertEqual(1, len(DoGet(_REMOTE, '/patients'))) + + DoDelete(_REMOTE, '/instances/%s' % b['ID']) + + self.assertEqual(1, len(DoGet(_REMOTE, '/instances'))) + self.assertEqual(1, len(DoGet(_REMOTE, '/series'))) + self.assertEqual(1, len(DoGet(_REMOTE, '/studies'))) + self.assertEqual(1, len(DoGet(_REMOTE, '/patients'))) + + DoDelete(_REMOTE, '/instances/%s' % a['ID']) + + self.assertEqual(0, len(DoGet(_REMOTE, '/instances'))) + self.assertEqual(0, len(DoGet(_REMOTE, '/series'))) + self.assertEqual(0, len(DoGet(_REMOTE, '/studies'))) + self.assertEqual(0, len(DoGet(_REMOTE, '/patients'))) + def test_multiframe(self): i = UploadInstance(_REMOTE, 'Multiframe.dcm')['ID'] self.assertEqual(76, len(DoGet(_REMOTE, '/instances/%s/frames' % i))) @@ -2562,30 +2588,30 @@ a = ExtractDicomTags(Anonymize(u, { 'PatientName' : 'toto' }), tags) for i in range(4): self.assertNotEqual(ids[i], a[i]) - self.assertFalse(a[4].startswith('Orthanc')) + self.assertNotIn('PS 3.15', a[4]) a = ExtractDicomTags(Anonymize(u, { 'SOPInstanceUID' : 'instance' }), tags) self.assertEqual('instance', a[3]) - self.assertFalse(a[4].startswith('Orthanc')) + self.assertNotIn('PS 3.15', a[4]) a = ExtractDicomTags(Anonymize(u, { 'SeriesInstanceUID' : 'series' }), tags) self.assertEqual('series', a[2]) - self.assertFalse(a[4].startswith('Orthanc')) + self.assertNotIn('PS 3.15', a[4]) a = ExtractDicomTags(Anonymize(u, { 'StudyInstanceUID' : 'study' }), tags) self.assertEqual('study', a[1]) - self.assertFalse(a[4].startswith('Orthanc')) + self.assertNotIn('PS 3.15', a[4]) a = ExtractDicomTags(Anonymize(u, { 'PatientID' : 'patient' }), tags) self.assertEqual('patient', a[0]) - self.assertFalse(a[4].startswith('Orthanc')) + self.assertNotIn('PS 3.15', a[4]) a = ExtractDicomTags(Anonymize(u, { 'PatientID' : 'patient', 'StudyInstanceUID' : 'study', 'SeriesInstanceUID' : 'series', 'SOPInstanceUID' : 'instance' }), tags) self.assertEqual('patient', a[0]) - self.assertFalse(a[4].startswith('Orthanc')) + self.assertNotIn('PS 3.15', a[4]) self.assertEqual(1, len(DoGet(_REMOTE, '/instances'))) @@ -6702,7 +6728,6 @@ SYNTAXES = [ '1.2.840.10008.1.2', '1.2.840.10008.1.2.1', - '1.2.840.10008.1.2.1.99', # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5) '1.2.840.10008.1.2.2', '1.2.840.10008.1.2.4.50', '1.2.840.10008.1.2.4.51', @@ -6710,6 +6735,10 @@ '1.2.840.10008.1.2.4.70', ] + if IsOrthancVersionAbove(_REMOTE, 1, 12, 7): + SYNTAXES.append('1.2.840.10008.1.2.1.99') # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5) + + if HasGdcmPlugin(_REMOTE): SYNTAXES = SYNTAXES + [ '1.2.840.10008.1.2.4.80', # This makes DCMTK 3.6.2 crash @@ -7377,7 +7406,6 @@ SYNTAXES = [ '1.2.840.10008.1.2', '1.2.840.10008.1.2.1', - '1.2.840.10008.1.2.1.99', # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5) '1.2.840.10008.1.2.2', '1.2.840.10008.1.2.4.50', '1.2.840.10008.1.2.4.51', @@ -7385,6 +7413,11 @@ '1.2.840.10008.1.2.4.70', ] + if IsOrthancVersionAbove(_REMOTE, 1, 12, 7): + SYNTAXES.append('1.2.840.10008.1.2.1.99') # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5) + + + if HasGdcmPlugin(_REMOTE): SYNTAXES = SYNTAXES + [ '1.2.840.10008.1.2.4.80', # This makes DCMTK 3.6.2 crash @@ -8356,6 +8389,7 @@ tags2021b = GetTags(study, { 'DicomVersion' : '2021b' }) tags2023b = GetTags(study, { 'DicomVersion' : '2023b' }) tagsDefault = GetTags(study, {}) + tagsReplace = GetTags(study, { 'Replace' : { 'StationName': 'tutu' }}) orthancVersion = DoGet(_REMOTE, '/system') ['Version'] if orthancVersion.startswith('mainline-'): # happens in unstable orthancteam/orthanc images @@ -8365,6 +8399,9 @@ self.assertEqual('Orthanc %s - PS 3.15-2017c Table E.1-1 Basic Profile' % orthancVersion, tags2017c['0012,0063']) self.assertEqual('Orthanc %s - PS 3.15-2021b Table E.1-1 Basic Profile' % orthancVersion, tags2021b['0012,0063']) self.assertEqual('Orthanc %s - PS 3.15-2023b Table E.1-1 Basic Profile' % orthancVersion, tags2023b['0012,0063']) + if IsOrthancVersionAbove(_REMOTE, 1, 12, 7): + self.assertEqual('Orthanc %s' % orthancVersion, tagsReplace['0012,0063']) + self.assertEqual(tagsDefault['0012,0063'], tags2023b['0012,0063']) self.assertEqual(len(tags2021b), len(tags2023b)) @@ -11955,7 +11992,7 @@ self.assertEqual(1, len(a)) def test_deflated_invalid_size(self): # https://discourse.orthanc-server.org/t/transcoding-to-deflated-transfer-syntax-fails/5489 - if IsOrthancVersionAbove(_REMOTE, 1, 12, 7) and HasExtendedFind(_REMOTE): + if IsOrthancVersionAbove(_REMOTE, 1, 12, 7): instanceId = '6582b1c0-292ad5ab-ba0f088f-f7a1766f-9a29a54f' r = UploadInstance(_REMOTE, 'TransferSyntaxes/1.2.840.10008.1.2.1.99.dcm') @@ -11969,5 +12006,81 @@ _REMOTE['Server'], str(_REMOTE['DicomPort']), GetDatabasePath('TransferSyntaxes/1.2.840.10008.1.2.1.99.dcm') ]) attachments = DoGet(_REMOTE, '/instances/' + instanceId + '/attachments/dicom/info/') - self.assertEqual(181071, int(attachments['UncompressedSize'])) - + self.assertLessEqual(181071, int(attachments['UncompressedSize'])) + self.assertGreaterEqual(181073, int(attachments['UncompressedSize'])) # there might be some padding added + + def test_embed_jpeg(self): + if not IsOrthancVersionAbove(_REMOTE, 1, 12, 7): + return + + with open(GetDatabasePath('Lena.jpg'), 'rb') as f: + jpeg = f.read() + + i = DoPost(_REMOTE, '/tools/create-dicom', json.dumps({ + 'Content' : 'data:image/jpeg;base64,%s' % base64.b64encode(jpeg).decode(), + 'Encapsulate' : True, + 'Tags' : { + 'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.7', + } + })) ['ID'] + + tags = DoGet(_REMOTE, '/instances/%s/tags?simplify' % i) + self.assertEqual(tags['BitsAllocated'], '8') + self.assertEqual(tags['BitsStored'], '8') + self.assertEqual(tags['Columns'], '512') + self.assertEqual(tags['HighBit'], '7') + self.assertEqual(tags['PhotometricInterpretation'], 'YBR_FULL_422') + self.assertEqual(tags['PixelData'], None) + self.assertEqual(tags['PixelRepresentation'], '0') + self.assertEqual(tags['PlanarConfiguration'], '0') + self.assertEqual(tags['Rows'], '512') + self.assertEqual(tags['SOPClassUID'], '1.2.840.10008.5.1.4.1.1.7') + self.assertEqual(tags['SamplesPerPixel'], '3') + self.assertEqual(tags['SpecificCharacterSet'], 'ISO_IR 100') + pixelData = DoGet(_REMOTE, '/instances/%s/content/7fe0,0010' % i) + self.assertEqual(len(pixelData), 2) + self.assertEqual(pixelData[0], '0') + self.assertEqual(pixelData[1], '1') + resp, embedded = DoGetRaw(_REMOTE, '/instances/%s/content/7fe0,0010/1' % i) + self.assertEqual('200', resp['status']) + self.assertEqual(len(embedded), len(jpeg)) + self.assertEqual(embedded, jpeg) + + b = io.BytesIO() + UncompressImage(jpeg).convert('L').save(b, format = 'jpeg') + + b.seek(0) + grayscale = b.read() + + if len(grayscale) % 2 != 0: + grayscale = grayscale + '\0' # Add padding to OW boundaries (2 bytes) + + i = DoPost(_REMOTE, '/tools/create-dicom', json.dumps({ + 'Content' : 'data:image/jpeg;base64,%s' % base64.b64encode(grayscale).decode(), + 'Encapsulate' : True, + 'Tags' : { + 'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.7', + } + })) ['ID'] + + tags = DoGet(_REMOTE, '/instances/%s/tags?simplify' % i) + self.assertEqual(tags['BitsAllocated'], '8') + self.assertEqual(tags['BitsStored'], '8') + self.assertEqual(tags['Columns'], '512') + self.assertEqual(tags['HighBit'], '7') + self.assertEqual(tags['PhotometricInterpretation'], 'MONOCHROME2') + self.assertEqual(tags['PixelData'], None) + self.assertEqual(tags['PixelRepresentation'], '0') + self.assertFalse('PlanarConfiguration' in tags) + self.assertEqual(tags['Rows'], '512') + self.assertEqual(tags['SOPClassUID'], '1.2.840.10008.5.1.4.1.1.7') + self.assertEqual(tags['SamplesPerPixel'], '1') + self.assertEqual(tags['SpecificCharacterSet'], 'ISO_IR 100') + pixelData = DoGet(_REMOTE, '/instances/%s/content/7fe0,0010' % i) + self.assertEqual(len(pixelData), 2) + self.assertEqual(pixelData[0], '0') + self.assertEqual(pixelData[1], '1') + resp, embedded = DoGetRaw(_REMOTE, '/instances/%s/content/7fe0,0010/1' % i) + self.assertEqual('200', resp['status']) + self.assertEqual(len(embedded), len(grayscale)) + self.assertEqual(embedded, grayscale)