comparison Plugins/CGet/Run.py @ 347:c56eaf5928f0

integration tests for c-get using pydicom
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 26 Oct 2020 12:23:49 +0100
parents
children 79ce0f7a9714
comparison
equal deleted inserted replaced
346:a56cbcbacfde 347:c56eaf5928f0
1 #!/usr/bin/python
2
3 # Orthanc - A Lightweight, RESTful DICOM Store
4 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
5 # Department, University Hospital of Liege, Belgium
6 # Copyright (C) 2017-2020 Osimis S.A., Belgium
7 #
8 # This program is free software: you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 # General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21
22
23 import argparse
24 import os
25 import pprint
26 import re
27 import sys
28 import unittest
29
30 sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'Tests'))
31 from Toolbox import *
32
33
34 ##
35 ## Parse the command-line arguments
36 ##
37
38 parser = argparse.ArgumentParser(description = 'Run the integration tests for the patient recycling behavior.')
39
40 parser.add_argument('--server',
41 default = 'localhost',
42 help = 'Address of the Orthanc server to test')
43 parser.add_argument('--aet',
44 default = 'ORTHANC',
45 help = 'AET of the Orthanc instance to test')
46 parser.add_argument('--dicom',
47 type = int,
48 default = 4242,
49 help = 'DICOM port of the Orthanc instance to test')
50 parser.add_argument('--rest',
51 type = int,
52 default = 8042,
53 help = 'Port to the REST API')
54 parser.add_argument('--username',
55 default = 'alice',
56 help = 'Username to the REST API')
57 parser.add_argument('--password',
58 default = 'orthanctest',
59 help = 'Password to the REST API')
60 parser.add_argument('--force', help = 'Do not warn the user',
61 action = 'store_true')
62 parser.add_argument('options', metavar = 'N', nargs = '*',
63 help='Arguments to Python unittest')
64
65 args = parser.parse_args()
66
67
68 ##
69 ## Configure the testing context
70 ##
71
72 if not args.force:
73 print("""
74 WARNING: This test will remove all the content of your
75 Orthanc instance running on %s!
76
77 Are you sure ["yes" to go on]?""" % args.server)
78
79 if sys.stdin.readline().strip() != 'yes':
80 print('Aborting...')
81 exit(0)
82
83
84 ORTHANC = DefineOrthanc(server = args.server,
85 username = args.username,
86 password = args.password,
87 restPort = args.rest,
88 aet = args.aet,
89 dicomPort = args.dicom)
90
91
92 ##
93 ## pydicom toolbox
94 ##
95
96 from pydicom.dataset import Dataset
97 from pynetdicom import (
98 AE,
99 evt,
100 build_role,
101 debug_logger,
102 )
103 from pynetdicom.sop_class import *
104
105 def ExecuteCGet(orthanc, dataset, sopClass, callback):
106 handlers = [(evt.EVT_C_STORE, callback)]
107
108 ae = AE(ae_title = 'ORTHANCTEST')
109
110 ae.add_requested_context(PatientRootQueryRetrieveInformationModelGet)
111 ae.add_requested_context(sopClass)
112 role = build_role(sopClass, scp_role = True, scu_role = True)
113
114 assoc = ae.associate(orthanc['Server'], orthanc['DicomPort'],
115 ext_neg = [role], evt_handlers = handlers)
116
117 if assoc.is_established:
118 responses = assoc.send_c_get(
119 dataset,
120 PatientRootQueryRetrieveInformationModelGet,
121 msg_id = 9999,
122 )
123
124 # Only report the result of the last sub-operation
125 last = None
126
127 for (result, identifier) in responses:
128 if result:
129 last = result
130 else:
131 assoc.release()
132 raise Exception('Connection timed out, was aborted or received invalid response')
133
134 assoc.release()
135 return last
136 else:
137 raise Exception('Association rejected, aborted or never connected')
138
139
140
141
142
143 ##
144 ## The tests
145 ##
146 ## IMPORTANT RESOURCES:
147 ## http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.3.html#table_C.4-3
148 ## http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.3.3.html
149 ##
150
151
152 def DefaultCallback(event):
153 to_match = PatientRootQueryRetrieveInformationModelGet
154 cxs = [cx for cx in event.assoc.accepted_contexts if cx.abstract_syntax == to_match]
155 if len(cxs) != 1:
156 raise Exception()
157 else:
158 return 0x0000
159
160
161
162 class Orthanc(unittest.TestCase):
163 def setUp(self):
164 if (sys.version_info >= (3, 0)):
165 # Remove annoying warnings about unclosed socket in Python 3
166 import warnings
167 warnings.simplefilter('ignore', ResourceWarning)
168
169 DropOrthanc(ORTHANC)
170
171
172 def test_success(self):
173 UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0001.dcm')
174 UploadInstance(ORTHANC, 'Brainix/Flair/IM-0001-0001.dcm')
175
176 dataset = Dataset()
177 dataset.QueryRetrieveLevel = 'STUDY'
178 dataset.StudyInstanceUID = '2.16.840.1.113669.632.20.1211.10000357775'
179
180 result = ExecuteCGet(ORTHANC, dataset, MRImageStorage, DefaultCallback)
181
182 self.assertEqual(0x0000, result[0x00000900].value) # Status - Success
183 self.assertEqual(2, result[0x00001021].value) # Completed sub-operations
184 self.assertEqual(0, result[0x00001022].value) # Failed sub-operations
185 self.assertEqual(0, result[0x00001023].value) # Warning sub-operations
186
187 # "Warning, Failure, or Success shall not contain the Number
188 # of Remaining Sub-operations Attribute."
189 self.assertFalse(0x00001020 in result) # Remaining sub-operations
190
191
192 def test_some_failure(self):
193 # Failure in 1 on 2 images
194 def Callback(event):
195 Callback.count += 1
196
197 if Callback.count == 1:
198 return 0xA702 # Refused: Out of resources - Unable to perform sub-operations
199 elif Callback.count == 2:
200 return 0x0000
201 else:
202 raise Exception('')
203
204 Callback.count = 0 # Static variable of function "Callback"
205
206 UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0001.dcm')
207 UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0002.dcm')
208
209 dataset = Dataset()
210 dataset.QueryRetrieveLevel = 'STUDY'
211 dataset.StudyInstanceUID = '2.16.840.1.113669.632.20.1211.10000357775'
212
213 result = ExecuteCGet(ORTHANC, dataset, MRImageStorage, Callback)
214
215 # Fixed in Orthanc 1.8.1. "From what I read from the DICOM
216 # standard the C-GET should at least return a warning
217 # (0xB000), see C.4.3.1.4 Status as one or more sub-operations
218 # failed."
219 # https://groups.google.com/g/orthanc-users/c/tS826iEzHb0/m/KzHZk61tAgAJ
220 # https://github.com/pydicom/pynetdicom/issues/552#issuecomment-712477451
221
222 self.assertEqual(0xB000, result[0x00000900].value) # Status - One or more Failures or Warnings
223 self.assertEqual(1, result[0x00001021].value) # Completed sub-operations
224 self.assertEqual(1, result[0x00001022].value) # Failed sub-operations
225 self.assertEqual(0, result[0x00001023].value) # Warning sub-operations
226
227 # "Warning, Failure, or Success shall not contain the Number
228 # of Remaining Sub-operations Attribute."
229 self.assertFalse(0x00001020 in result) # Remaining sub-operations
230
231
232 def test_all_failure(self):
233 def Callback(event):
234 return 0xA702 # Refused: Out of resources - Unable to perform sub-operations
235
236 UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0001.dcm')
237 UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0002.dcm')
238
239 dataset = Dataset()
240 dataset.QueryRetrieveLevel = 'STUDY'
241 dataset.StudyInstanceUID = '2.16.840.1.113669.632.20.1211.10000357775'
242
243 result = ExecuteCGet(ORTHANC, dataset, MRImageStorage, Callback)
244
245 # Must return "Failure or Refused if all sub-operations were unsuccessful"
246 # http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.3.3.html
247
248 self.assertEqual(0xA702, result[0x00000900].value) # Status - Unable to perform sub-operations
249 self.assertEqual(0, result[0x00001021].value) # Completed sub-operations
250 self.assertEqual(2, result[0x00001022].value) # Failed sub-operations
251 self.assertEqual(0, result[0x00001023].value) # Warning sub-operations
252
253 # "Warning, Failure, or Success shall not contain the Number
254 # of Remaining Sub-operations Attribute."
255 self.assertFalse(0x00001020 in result) # Remaining sub-operations
256
257
258 def test_warning(self):
259 def Callback(event):
260 return 0xB000 # Sub-operations Complete - One or more Failures or Warnings
261
262 UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0001.dcm')
263
264 dataset = Dataset()
265 dataset.QueryRetrieveLevel = 'STUDY'
266 dataset.StudyInstanceUID = '2.16.840.1.113669.632.20.1211.10000357775'
267
268 result = ExecuteCGet(ORTHANC, dataset, MRImageStorage, Callback)
269
270 self.assertEqual(0xB000, result[0x00000900].value) # Status - One or more Failures or Warnings
271 self.assertEqual(0, result[0x00001021].value) # Completed sub-operations
272 self.assertEqual(0, result[0x00001022].value) # Failed sub-operations
273 self.assertEqual(1, result[0x00001023].value) # Warning sub-operations
274
275 # "Warning, Failure, or Success shall not contain the Number
276 # of Remaining Sub-operations Attribute."
277 self.assertFalse(0x00001020 in result) # Remaining sub-operations
278
279
280 def test_missing(self):
281 dataset = Dataset()
282 dataset.QueryRetrieveLevel = 'STUDY'
283 dataset.StudyInstanceUID = 'nope'
284
285 result = ExecuteCGet(ORTHANC, dataset, UltrasoundImageStorage, DefaultCallback)
286
287 self.assertEqual(0xC000, result[0x00000900].value) # Status - Failed: Unable to process
288 self.assertEqual(0, result[0x00001021].value) # Completed sub-operations
289 self.assertEqual(0, result[0x00001022].value) # Failed sub-operations
290 self.assertEqual(0, result[0x00001023].value) # Warning sub-operations
291
292 # "Warning, Failure, or Success shall not contain the Number
293 # of Remaining Sub-operations Attribute."
294 self.assertFalse(0x00001020 in result) # Remaining sub-operations
295
296
297 def test_cancel(self):
298 # Fixed in Orthanc 1.8.1.
299 # https://groups.google.com/g/orthanc-users/c/tS826iEzHb0/m/QbPw6XPZAgAJ
300 # https://github.com/pydicom/pynetdicom/issues/553#issuecomment-713164041
301
302 def Callback(event):
303 Callback.count += 1
304
305 if Callback.count == 1:
306 return 0x0000
307 elif Callback.count == 2:
308 to_match = PatientRootQueryRetrieveInformationModelGet
309 cxs = [cx for cx in event.assoc.accepted_contexts if cx.abstract_syntax == to_match]
310 cx_id = cxs[0].context_id
311 event.assoc.send_c_cancel(9999, cx_id)
312 return 0x0000 # Success
313 else:
314 raise Exception('')
315
316 Callback.count = 0 # Static variable of function "Callback"
317
318 UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0001.dcm')
319 UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0002.dcm')
320 UploadInstance(ORTHANC, 'Brainix/Flair/IM-0001-0001.dcm')
321
322 dataset = Dataset()
323 dataset.QueryRetrieveLevel = 'STUDY'
324 dataset.StudyInstanceUID = '2.16.840.1.113669.632.20.1211.10000357775'
325
326 result = ExecuteCGet(ORTHANC, dataset, MRImageStorage, Callback)
327
328 self.assertEqual(0xfe00, result[0x00000900].value) # Status - Sub-operations terminated due to Cancel Indication
329 self.assertEqual(2, result[0x00001020].value) # Remaining sub-operations
330 self.assertEqual(1, result[0x00001021].value) # Completed sub-operations
331 self.assertEqual(0, result[0x00001022].value) # Failed sub-operations
332 self.assertEqual(0, result[0x00001023].value) # Warning sub-operations
333
334
335
336 try:
337 print('\nStarting the tests...')
338 unittest.main(argv = [ sys.argv[0] ] + args.options)
339
340 finally:
341 print('\nDone')