diff --git a/.gitignore b/.gitignore index 3c9dc5b..f4858d0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,9 @@ target/ **/*.csv __pycache__ **/*.pyc +htmlcov +.coverage +coverage.xml +/tests/data/tmp +/tests/data/**/*.dcm +!/tests/data/**/no-img.dcm \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..183f02e --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,13 @@ +requests +pymongo +schedule +pydicom +pynetdicom +image +numpy +pandas +pillow +pypng +pytest +pytest-mock +pytest-cov diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..3580cfe --- /dev/null +++ b/tests/README.md @@ -0,0 +1,34 @@ +# Testing Framework for Niffler Modules + +## Setup + +Install the requirements from `/requirements-dev.txt` + +``` +pip install -r requirements-dev.txt +``` + +Add the test data in `/tests/data//input` for respective tests. + +### PNG Extraction Data Setup + +Test data in `/tests/data/png-extraction/input`. + +For unit tests, add a valid dcm file, with name `test-img.dcm`. + +## Running Tests + +Initialize the required data, and run tests from ``. + +```bash +pytest ./tests +``` + +For coverage report, run + +```bash +pytest ./tests --cov=./modules --cov-report=html +``` + +and open the `/htmlcov/index.html` + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ba961c6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +import os +import pytest +from pathlib import Path + + +def create_dirs(*args): + for dir in args: + if not os.path.exists(dir): + os.makedirs(dir) + + +def pytest_configure(): + pytest.data_dir = Path.cwd() / 'tests' / 'data' + pytest.out_dir = Path.cwd() / 'tests' / 'data' / 'tmp' / 'niffler-tests' + pytest.create_dirs = create_dirs diff --git a/tests/data/png-extraction/input/.gitkeep b/tests/data/png-extraction/input/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/png-extraction/input/no-img.dcm b/tests/data/png-extraction/input/no-img.dcm new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/png-extraction/no_input_files/.gitkeep b/tests/data/png-extraction/no_input_files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_png_extraction.py b/tests/unit/test_png_extraction.py new file mode 100644 index 0000000..efe0e25 --- /dev/null +++ b/tests/unit/test_png_extraction.py @@ -0,0 +1,217 @@ +import glob +import pytest +import sys +import time + +from pathlib import Path, PurePath +from pytest_mock import MockerFixture + +# Import Niffler Module +niffler_modules_path = Path.cwd() / 'modules' +sys.path.append(str(niffler_modules_path / 'png-extraction')) +import ImageExtractor + +import pydicom +import pandas as pd + + +@pytest.fixture +def mock_pydicom_config_data_element_callback(mocker: MockerFixture): + return mocker.patch.object(pydicom.config, 'data_element_callback') + + +@pytest.fixture +def mock_pydicom_config_data_element_callback_kwargs(mocker: MockerFixture): + return mocker.patch.object(pydicom.config, 'data_element_callback_kwargs') + + +@pytest.fixture +def mock_logger(mocker: MockerFixture): + return mocker.patch('ImageExtractor.logging') + + +class TestGetPath: + dicom_home = "/mock/path/to/dicom/home" + + def test_get_path_zero_depth(self): + depth = 0 + dcm_path = ImageExtractor.get_path(depth, self.dicom_home) + assert dcm_path == f"{self.dicom_home}/*.dcm" + + def test_get_path_some_depth(self): + depth = 3 + dcm_path = ImageExtractor.get_path(depth, self.dicom_home) + assert dcm_path == f"{self.dicom_home}{''.join(['/*']*depth)}/*.dcm" + + +class TestFixMismatch: + with_VRs = ['PN', 'DS', 'IS'] + + def test_fix_mismatch(self, mock_pydicom_config_data_element_callback, mock_pydicom_config_data_element_callback_kwargs): + ImageExtractor.fix_mismatch(with_VRs=self.with_VRs) + assert pydicom.config.data_element_callback is ImageExtractor.fix_mismatch_callback + assert pydicom.config.data_element_callback_kwargs['with_VRs'] == self.with_VRs + + +class TestExtractHeaders: + valid_test_dcm_file = 0, str( + pytest.data_dir / 'png-extraction' / 'input' / 'test-img.dcm') + invalid_test_dcm_file = 0, str( + pytest.data_dir / 'png-extraction' / 'input' / 'no-img.dcm') + + def test_no_image(self): + headers = ImageExtractor.extract_headers( + self.invalid_test_dcm_file) + assert headers['has_pix_array'] is False + + def test_valid_image(self): + headers = ImageExtractor.extract_headers(self.valid_test_dcm_file) + assert headers['has_pix_array'] is True + + # TODO large dcm files + + +class TestGetTuples: + test_dcm_file = str( + pytest.data_dir / 'png-extraction' / 'input' / 'test-img.dcm') + test_valid_plan = pydicom.dcmread(test_dcm_file, force=True) + + def test_correct_output(self): + first_key = self.test_valid_plan.dir()[0] + tuple_list = ImageExtractor.get_tuples(self.test_valid_plan) + assert tuple_list[0][0] == first_key + + # TODO hasattr error + # TODO large dcm files + + +class TestExtractImages: + + # TODO Write suitable assertions + + test_dcm_file = str( + pytest.data_dir / 'png-extraction' / 'input' / 'test-img.dcm') + invalid_test_dcm_file = str( + pytest.data_dir / 'png-extraction' / 'input' / 'no-img.dcm') + + def setup_method(self): + header_list = [ImageExtractor.extract_headers( + (0, self.test_dcm_file))] + self.file_data = pd.DataFrame(header_list) + self.index = 0 + self.invalid_file_data = pd.DataFrame([ + { + 'some_col_1': 'Dummy Col1 Value', + 'some_col_2': 'Dummy Col2 Value', + 'file': self.invalid_test_dcm_file + } + ]) + out_dir = pytest.out_dir / 'png-extraction/outputs/TestExtractImages' + self.png_destination = f"{str(out_dir)}/extracted-images/" + self.failed = f"{str(out_dir)}/failed-dicom/" + pytest.create_dirs(out_dir, self.png_destination, self.failed) + + def test_is16bit(self): + flattened_to_level = "patient" + is16Bit = "True" + out_img = ImageExtractor.extract_images( + self.file_data, + self.index, + self.png_destination, + flattened_to_level, + self.failed, + is16Bit + ) + assert out_img[0].startswith(self.test_dcm_file) + + def test_not_is16bit(self): + flattened_to_level = "patient" + is16Bit = "False" + out_img = ImageExtractor.extract_images( + self.file_data, + self.index, + self.png_destination, + flattened_to_level, + self.failed, + is16Bit + ) + assert out_img[0].startswith(self.test_dcm_file) + + def test_level_patient(self): + flattened_to_level = "patient" + is16Bit = "False" + out_img = ImageExtractor.extract_images( + self.file_data, + self.index, + self.png_destination, + flattened_to_level, + self.failed, + is16Bit + ) + assert out_img[0].startswith(self.test_dcm_file) + + def test_level_study(self): + flattened_to_level = "study" + is16Bit = "False" + out_img = ImageExtractor.extract_images( + self.file_data, + self.index, + self.png_destination, + flattened_to_level, + self.failed, + is16Bit + ) + assert out_img[0].startswith(self.test_dcm_file) + + def test_level_other(self): + flattened_to_level = "other" + is16Bit = "False" + out_img = ImageExtractor.extract_images( + self.file_data, + self.index, + self.png_destination, + flattened_to_level, + self.failed, + is16Bit + ) + assert out_img[0].startswith(self.test_dcm_file) + + def test_level_other_no_study_uuid(self): + flattened_to_level = "other" + is16Bit = "False" + out_img = ImageExtractor.extract_images( + self.file_data.drop(['StudyInstanceUID'], axis=1), + self.index, + self.png_destination, + flattened_to_level, + self.failed, + is16Bit + ) + assert out_img[0].startswith(self.test_dcm_file) + + def test_level_study_no_study_uuid(self): + flattened_to_level = "study" + is16Bit = "False" + out_img = ImageExtractor.extract_images( + self.file_data.drop(['StudyInstanceUID'], axis=1), + self.index, + self.png_destination, + flattened_to_level, + self.failed, + is16Bit + ) + assert out_img[0].startswith(self.test_dcm_file) + + def test_failed_read_attrerr(self): + flattened_to_level = "study" + is16Bit = "False" + out_img = ImageExtractor.extract_images( + self.invalid_file_data, + self.index, + self.png_destination, + flattened_to_level, + self.failed, + is16Bit + ) + assert out_img[1][0] == self.invalid_test_dcm_file + assert out_img[2] is not None