github.com/apache/beam/sdks/v2@v2.48.2/python/apache_beam/io/gcp/dicomio_test.py (about) 1 # coding=utf-8 2 # 3 # Licensed to the Apache Software Foundation (ASF) under one or more 4 # contributor license agreements. See the NOTICE file distributed with 5 # this work for additional information regarding copyright ownership. 6 # The ASF licenses this file to You under the Apache License, Version 2.0 7 # (the "License"); you may not use this file except in compliance with 8 # the License. You may obtain a copy of the License at 9 # 10 # http://www.apache.org/licenses/LICENSE-2.0 11 # 12 # Unless required by applicable law or agreed to in writing, software 13 # distributed under the License is distributed on an "AS IS" BASIS, 14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 # See the License for the specific language governing permissions and 16 # limitations under the License. 17 # 18 19 """Unit tests for Dicom IO connectors.""" 20 21 # pytype: skip-file 22 23 import json 24 import os 25 import unittest 26 27 from mock import patch 28 29 import apache_beam as beam 30 from apache_beam.io import fileio 31 from apache_beam.io.filebasedsink_test import _TestCaseWithTempDirCleanUp 32 from apache_beam.io.filesystems import FileSystems 33 from apache_beam.testing.test_pipeline import TestPipeline 34 from apache_beam.testing.util import assert_that 35 from apache_beam.testing.util import equal_to 36 37 # Protect against environments where gcp library is not available. 38 # pylint: disable=wrong-import-order, wrong-import-position 39 try: 40 from apache_beam.io.gcp.dicomio import DicomSearch 41 from apache_beam.io.gcp.dicomio import FormatToQido 42 from apache_beam.io.gcp.dicomio import UploadToDicomStore 43 except ImportError: 44 DicomSearch = None # type: ignore 45 # pylint: enable=wrong-import-order, wrong-import-position 46 47 48 class FakeHttpClient(): 49 # a fake http client that talks directly to a in-memory dicom store 50 def __init__(self): 51 # set 5 fake dicom instances 52 dicom_metadata = [] 53 dicom_metadata.append({ 54 'PatientName': 'Albert', 'Age': 21, 'TestResult': 'Positive' 55 }) 56 dicom_metadata.append({ 57 'PatientName': 'Albert', 'Age': 21, 'TestResult': 'Negative' 58 }) 59 dicom_metadata.append({ 60 'PatientName': 'Brian', 'Age': 20, 'TestResult': 'Positive' 61 }) 62 dicom_metadata.append({ 63 'PatientName': 'Colin', 'Age': 25, 'TestResult': 'Negative' 64 }) 65 dicom_metadata.append({ 66 'PatientName': 'Daniel', 'Age': 22, 'TestResult': 'Negative' 67 }) 68 dicom_metadata.append({ 69 'PatientName': 'Eric', 'Age': 50, 'TestResult': 'Negative' 70 }) 71 self.dicom_metadata = dicom_metadata 72 # ids for this dicom sotre 73 self.project_id = 'test_project' 74 self.region = 'test_region' 75 self.dataset_id = 'test_dataset_id' 76 self.dicom_store_id = 'test_dicom_store_id' 77 78 def qido_search( 79 self, 80 project_id, 81 region, 82 dataset_id, 83 dicom_store_id, 84 search_type, 85 params=None, 86 credential=None): 87 # qido search function for this fake client 88 if project_id != self.project_id or region != self.region or \ 89 dataset_id != self.dataset_id or dicom_store_id != self.dicom_store_id: 90 return [], 204 91 # only supports all instance search in this client 92 if not params: 93 return self.dicom_metadata, 200 94 # only supports patient name filter in this client 95 patient_name = params['PatientName'] 96 out = [] 97 for meta in self.dicom_metadata: 98 if meta['PatientName'] == patient_name: 99 out.append(meta) 100 return out, 200 101 102 def dicomweb_store_instance( 103 self, 104 project_id, 105 region, 106 dataset_id, 107 dicom_store_id, 108 dcm_file, 109 credential=None): 110 if project_id != self.project_id or region != self.region or \ 111 dataset_id != self.dataset_id or dicom_store_id != self.dicom_store_id: 112 return [], 204 113 # convert the bytes file back to dict 114 string_array = dcm_file.decode('utf-8') 115 metadata_dict = json.loads(string_array) 116 self.dicom_metadata.append(metadata_dict) 117 return None, 200 118 119 120 @unittest.skipIf(DicomSearch is None, 'GCP dependencies are not installed') 121 class TestFormatToQido(unittest.TestCase): 122 valid_pubsub_string = ( 123 "projects/PROJECT_ID/locations/LOCATION/datasets" 124 "/DATASET_ID/dicomStores/DICOM_STORE_ID/dicomWeb/" 125 "studies/STUDY_UID/series/SERIES_UID/instances/INSTANCE_UID") 126 expected_valid_pubsub_dict = { 127 'result': { 128 'project_id': 'PROJECT_ID', 129 'region': 'LOCATION', 130 'dataset_id': 'DATASET_ID', 131 'dicom_store_id': 'DICOM_STORE_ID', 132 'search_type': 'instances', 133 'params': { 134 'StudyInstanceUID': 'STUDY_UID', 135 'SeriesInstanceUID': 'SERIES_UID', 136 'SOPInstanceUID': 'INSTANCE_UID' 137 } 138 }, 139 'input': valid_pubsub_string, 140 'success': True 141 } 142 invalid_pubsub_string = "this is not a valid pubsub message" 143 expected_invalid_pubsub_dict = { 144 'result': {}, 145 'input': 'this is not a valid pubsub message', 146 'success': False 147 } 148 149 def test_normal_convert(self): 150 with TestPipeline() as p: 151 convert_result = ( 152 p 153 | beam.Create([self.valid_pubsub_string]) 154 | FormatToQido()) 155 assert_that(convert_result, equal_to([self.expected_valid_pubsub_dict])) 156 157 def test_failed_convert(self): 158 with TestPipeline() as p: 159 convert_result = ( 160 p 161 | beam.Create([self.invalid_pubsub_string]) 162 | FormatToQido()) 163 assert_that(convert_result, equal_to([self.expected_invalid_pubsub_dict])) 164 165 166 @unittest.skipIf(DicomSearch is None, 'GCP dependencies are not installed') 167 class TestDicomSearch(unittest.TestCase): 168 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 169 def test_successful_search(self, FakeClient): 170 input_dict = {} 171 input_dict['project_id'] = "test_project" 172 input_dict['region'] = "test_region" 173 input_dict['dataset_id'] = "test_dataset_id" 174 input_dict['dicom_store_id'] = "test_dicom_store_id" 175 input_dict['search_type'] = "instances" 176 177 fc = FakeHttpClient() 178 FakeClient.return_value = fc 179 180 expected_dict = {} 181 expected_dict['result'] = fc.dicom_metadata 182 expected_dict['status'] = 200 183 expected_dict['input'] = input_dict 184 expected_dict['success'] = True 185 186 with TestPipeline() as p: 187 results = (p | beam.Create([input_dict]) | DicomSearch()) 188 assert_that(results, equal_to([expected_dict])) 189 190 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 191 def test_Qido_search_small_buffer_flush(self, FakeClient): 192 input_dict = {} 193 input_dict['project_id'] = "test_project" 194 input_dict['region'] = "test_region" 195 input_dict['dataset_id'] = "test_dataset_id" 196 input_dict['dicom_store_id'] = "test_dicom_store_id" 197 input_dict['search_type'] = "instances" 198 199 fc = FakeHttpClient() 200 FakeClient.return_value = fc 201 202 expected_dict = {} 203 expected_dict['result'] = fc.dicom_metadata 204 expected_dict['status'] = 200 205 expected_dict['input'] = input_dict 206 expected_dict['success'] = True 207 208 with TestPipeline() as p: 209 results = (p | beam.Create([input_dict] * 5) | DicomSearch(buffer_size=1)) 210 assert_that(results, equal_to([expected_dict] * 5)) 211 212 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 213 def test_param_dict_passing(self, FakeClient): 214 input_dict = {} 215 input_dict = {} 216 input_dict['project_id'] = "test_project" 217 input_dict['region'] = "test_region" 218 input_dict['dataset_id'] = "test_dataset_id" 219 input_dict['dicom_store_id'] = "test_dicom_store_id" 220 input_dict['search_type'] = "instances" 221 input_dict['params'] = {'PatientName': 'Brian'} 222 223 expected_dict = {} 224 expected_dict['result'] = [{ 225 'PatientName': 'Brian', 'Age': 20, 'TestResult': 'Positive' 226 }] 227 expected_dict['status'] = 200 228 expected_dict['input'] = input_dict 229 expected_dict['success'] = True 230 231 fc = FakeHttpClient() 232 FakeClient.return_value = fc 233 with TestPipeline() as p: 234 results = (p | beam.Create([input_dict]) | DicomSearch()) 235 assert_that(results, equal_to([expected_dict])) 236 237 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 238 def test_wrong_input_type(self, FakeClient): 239 input_dict = {} 240 input_dict['project_id'] = "test_project" 241 input_dict['region'] = "test_region" 242 input_dict['dataset_id'] = "test_dataset_id" 243 input_dict['dicom_store_id'] = "test_dicom_store_id" 244 input_dict['search_type'] = "not exist type" 245 246 expected_invalid_dict = {} 247 expected_invalid_dict['result'] = [] 248 expected_invalid_dict[ 249 'status'] = 'Search type can only be "studies", "instances" or "series"' 250 expected_invalid_dict['input'] = input_dict 251 expected_invalid_dict['success'] = False 252 253 fc = FakeHttpClient() 254 FakeClient.return_value = fc 255 with TestPipeline() as p: 256 results = (p | beam.Create([input_dict]) | DicomSearch()) 257 assert_that(results, equal_to([expected_invalid_dict])) 258 259 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 260 def test_missing_parameters(self, FakeClient): 261 input_dict = {} 262 input_dict['project_id'] = "test_project" 263 input_dict['region'] = "test_region" 264 265 expected_invalid_dict = {} 266 expected_invalid_dict['result'] = [] 267 expected_invalid_dict['status'] = 'Must have dataset_id in the dict.' 268 expected_invalid_dict['input'] = input_dict 269 expected_invalid_dict['success'] = False 270 271 fc = FakeHttpClient() 272 FakeClient.return_value = fc 273 with TestPipeline() as p: 274 results = (p | beam.Create([input_dict]) | DicomSearch()) 275 assert_that(results, equal_to([expected_invalid_dict])) 276 277 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 278 def test_client_search_notfound(self, FakeClient): 279 input_dict = {} 280 # search instances in a not exist store 281 input_dict['project_id'] = "wrong_project" 282 input_dict['region'] = "wrong_region" 283 input_dict['dataset_id'] = "wrong_dataset_id" 284 input_dict['dicom_store_id'] = "wrong_dicom_store_id" 285 input_dict['search_type'] = "instances" 286 287 expected_invalid_dict = {} 288 expected_invalid_dict['result'] = [] 289 expected_invalid_dict['status'] = 204 290 expected_invalid_dict['input'] = input_dict 291 expected_invalid_dict['success'] = False 292 293 fc = FakeHttpClient() 294 FakeClient.return_value = fc 295 with TestPipeline() as p: 296 results = (p | beam.Create([input_dict]) | DicomSearch()) 297 assert_that(results, equal_to([expected_invalid_dict])) 298 299 300 @unittest.skipIf(DicomSearch is None, 'GCP dependencies are not installed') 301 class TestDicomStoreInstance(_TestCaseWithTempDirCleanUp): 302 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 303 def test_store_byte_file(self, FakeClient): 304 input_dict = {} 305 input_dict['project_id'] = "test_project" 306 input_dict['region'] = "test_region" 307 input_dict['dataset_id'] = "test_dataset_id" 308 input_dict['dicom_store_id'] = "test_dicom_store_id" 309 310 fc = FakeHttpClient() 311 FakeClient.return_value = fc 312 313 dict_input = {'PatientName': 'George', 'Age': 23, 'TestResult': 'Negative'} 314 str_input = json.dumps(dict_input) 315 bytes_input = bytes(str_input.encode("utf-8")) 316 with TestPipeline() as p: 317 results = ( 318 p 319 | beam.Create([bytes_input]) 320 | UploadToDicomStore(input_dict, 'bytes') 321 | beam.Map(lambda x: x['success'])) 322 assert_that(results, equal_to([True])) 323 self.assertTrue(dict_input in fc.dicom_metadata) 324 325 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 326 def test_store_byte_file_small_buffer_flush(self, FakeClient): 327 input_dict = {} 328 input_dict['project_id'] = "test_project" 329 input_dict['region'] = "test_region" 330 input_dict['dataset_id'] = "test_dataset_id" 331 input_dict['dicom_store_id'] = "test_dicom_store_id" 332 333 fc = FakeHttpClient() 334 FakeClient.return_value = fc 335 336 dict_input_1 = { 337 'PatientName': 'George', 'Age': 23, 'TestResult': 'Negative' 338 } 339 str_input_1 = json.dumps(dict_input_1) 340 bytes_input_1 = bytes(str_input_1.encode("utf-8")) 341 dict_input_2 = {'PatientName': 'Peter', 'Age': 54, 'TestResult': 'Positive'} 342 str_input_2 = json.dumps(dict_input_2) 343 bytes_input_2 = bytes(str_input_2.encode("utf-8")) 344 dict_input_3 = {'PatientName': 'Zen', 'Age': 27, 'TestResult': 'Negative'} 345 str_input_3 = json.dumps(dict_input_3) 346 bytes_input_3 = bytes(str_input_3.encode("utf-8")) 347 with TestPipeline() as p: 348 results = ( 349 p 350 | beam.Create([bytes_input_1, bytes_input_2, bytes_input_3]) 351 | UploadToDicomStore(input_dict, 'bytes', buffer_size=1) 352 | beam.Map(lambda x: x['success'])) 353 assert_that(results, equal_to([True] * 3)) 354 self.assertTrue(dict_input_1 in fc.dicom_metadata) 355 self.assertTrue(dict_input_2 in fc.dicom_metadata) 356 self.assertTrue(dict_input_3 in fc.dicom_metadata) 357 358 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 359 def test_store_fileio_file(self, FakeClient): 360 input_dict = {} 361 input_dict['project_id'] = "test_project" 362 input_dict['region'] = "test_region" 363 input_dict['dataset_id'] = "test_dataset_id" 364 input_dict['dicom_store_id'] = "test_dicom_store_id" 365 366 fc = FakeHttpClient() 367 FakeClient.return_value = fc 368 369 dict_input = {'PatientName': 'George', 'Age': 23, 'TestResult': 'Negative'} 370 str_input = json.dumps(dict_input) 371 temp_dir = '%s%s' % (self._new_tempdir(), os.sep) 372 self._create_temp_file(dir=temp_dir, content=str_input) 373 374 with TestPipeline() as p: 375 results = ( 376 p 377 | beam.Create([FileSystems.join(temp_dir, '*')]) 378 | fileio.MatchAll() 379 | fileio.ReadMatches() 380 | UploadToDicomStore(input_dict, 'fileio') 381 | beam.Map(lambda x: x['success'])) 382 assert_that(results, equal_to([True])) 383 self.assertTrue(dict_input in fc.dicom_metadata) 384 385 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 386 def test_store_fileio_file_small_buffer_flush(self, FakeClient): 387 input_dict = {} 388 input_dict['project_id'] = "test_project" 389 input_dict['region'] = "test_region" 390 input_dict['dataset_id'] = "test_dataset_id" 391 input_dict['dicom_store_id'] = "test_dicom_store_id" 392 393 fc = FakeHttpClient() 394 FakeClient.return_value = fc 395 396 temp_dir = '%s%s' % (self._new_tempdir(), os.sep) 397 dict_input_1 = { 398 'PatientName': 'George', 'Age': 23, 'TestResult': 'Negative' 399 } 400 str_input_1 = json.dumps(dict_input_1) 401 self._create_temp_file(dir=temp_dir, content=str_input_1) 402 dict_input_2 = {'PatientName': 'Peter', 'Age': 54, 'TestResult': 'Positive'} 403 str_input_2 = json.dumps(dict_input_2) 404 self._create_temp_file(dir=temp_dir, content=str_input_2) 405 dict_input_3 = {'PatientName': 'Zen', 'Age': 27, 'TestResult': 'Negative'} 406 str_input_3 = json.dumps(dict_input_3) 407 self._create_temp_file(dir=temp_dir, content=str_input_3) 408 409 with TestPipeline() as p: 410 results = ( 411 p 412 | beam.Create([FileSystems.join(temp_dir, '*')]) 413 | fileio.MatchAll() 414 | fileio.ReadMatches() 415 | UploadToDicomStore(input_dict, 'fileio', buffer_size=1) 416 | beam.Map(lambda x: x['success'])) 417 assert_that(results, equal_to([True] * 3)) 418 self.assertTrue(dict_input_1 in fc.dicom_metadata) 419 self.assertTrue(dict_input_2 in fc.dicom_metadata) 420 self.assertTrue(dict_input_3 in fc.dicom_metadata) 421 422 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 423 def test_destination_notfound(self, FakeClient): 424 input_dict = {} 425 # search instances in a not exist store 426 input_dict['project_id'] = "wrong_project" 427 input_dict['region'] = "wrong_region" 428 input_dict['dataset_id'] = "wrong_dataset_id" 429 input_dict['dicom_store_id'] = "wrong_dicom_store_id" 430 431 expected_invalid_dict = {} 432 expected_invalid_dict['status'] = 204 433 expected_invalid_dict['input'] = '' 434 expected_invalid_dict['success'] = False 435 436 fc = FakeHttpClient() 437 FakeClient.return_value = fc 438 with TestPipeline() as p: 439 results = ( 440 p | beam.Create(['']) | UploadToDicomStore(input_dict, 'bytes')) 441 assert_that(results, equal_to([expected_invalid_dict])) 442 443 @patch("apache_beam.io.gcp.dicomio.DicomApiHttpClient") 444 def test_missing_parameters(self, FakeClient): 445 input_dict = {} 446 input_dict['project_id'] = "test_project" 447 input_dict['region'] = "test_region" 448 449 expected_invalid_dict = {} 450 expected_invalid_dict['result'] = [] 451 expected_invalid_dict['status'] = 'Must have dataset_id in the dict.' 452 expected_invalid_dict['input'] = input_dict 453 expected_invalid_dict['success'] = False 454 455 fc = FakeHttpClient() 456 FakeClient.return_value = fc 457 with self.assertRaisesRegex(ValueError, 458 "Must have dataset_id in the dict."): 459 p = TestPipeline() 460 _ = (p | beam.Create(['']) | UploadToDicomStore(input_dict, 'bytes')) 461 462 463 if __name__ == '__main__': 464 unittest.main()