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()