Mercurial > hg > orthanc-tests
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') |