Skip to content

Commit

Permalink
Merge pull request #35 from tomaroberts/unit-tests
Browse files Browse the repository at this point in the history
Adds unit tests, coverage badge and comments, and updates GHA
  • Loading branch information
tomaroberts authored Mar 1, 2024
2 parents 66edf6b + ee2cc62 commit 935d53a
Show file tree
Hide file tree
Showing 14 changed files with 258 additions and 12 deletions.
30 changes: 28 additions & 2 deletions .github/workflows/build_and_test_cli.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Workflow to build nii2dcm and test different command line interface (CLI) options
# Workflow to build nii2dcm, run unit tests and then execute command line interface (CLI) end-to-end

name: Build nii2dcm

Expand All @@ -7,7 +7,7 @@ on:

jobs:
build-and-test:
name: Build
name: Build, Unit Tests & E2E

runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -57,6 +57,10 @@ jobs:
nii2dcm -h
nii2dcm -v
- name: Run unit tests
run: |
pytest tests/
- name: Test DicomMRISVR creation
run: |
# run nii2dcm
Expand All @@ -65,3 +69,25 @@ jobs:
ls ./output
# assert DICOM files exist
[ -f "./output/IM_0001.dcm" ] && echo "Output DICOM file exists" || exit 1
- name: Build pytest coverage file
run: |
pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=nii2dcm tests/ | tee pytest-coverage.txt ; echo $?
- name: Pytest coverage comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml

- name: Update Coverage Badge
uses: schneegans/[email protected]
with:
auth: ${{ secrets.PYTEST_COVERAGE_COMMENT }}
gistID: 57ef8057d04f67dbe6e64df410b83079
filename: nii2dcm-pytest-coverage-comment.json
label: Coverage Report
message: ${{ steps.coverageComment.outputs.coverage }}
color: ${{ steps.coverageComment.outputs.color }}
namedLogo: python
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
pytest-coverage.txt
pytest.xml

# Sphinx documentation
docs/_build/
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
·
<a href="https://github.com/tomaroberts/nii2dcm/issues">Request Feature</a>
</p>
<p align="center">
<img src="https://github.com/tomaroberts/nii2dcm/actions/workflows/build_and_test_cli.yml/badge.svg?branch=unit-tests">
<img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tomaroberts/57ef8057d04f67dbe6e64df410b83079/raw/nii2dcm-pytest-coverage-comment.json">
</p>
</div>


Expand Down
2 changes: 1 addition & 1 deletion nii2dcm/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def cli(args=None):
parser.add_argument("input_file", type=str, help="[.nii/.nii.gz] input NIfTI file")
parser.add_argument("output_dir", type=str, help="[directory] output DICOM path")
parser.add_argument(
"-d","--dicom_type",
"-d", "--dicom_type",
type=str,
help="[string] type of DICOM. Available types: MR, SVR."
)
Expand Down
2 changes: 1 addition & 1 deletion nii2dcm/_version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import dunamai as _dunamai
__version__ = _dunamai.get_version("nii2dcm", third_choice=_dunamai.Version.from_any_vcs).serialize()
__version__ = _dunamai.get_version("nii2dcm", third_choice=_dunamai.Version.from_any_vcs).serialize()
3 changes: 1 addition & 2 deletions nii2dcm/dcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import pydicom as pyd
from pydicom.dataset import FileDataset, FileMetaDataset

from nii2dcm.utils import dcm_dictionary_update
from nii2dcm.modules.patient import Patient
from nii2dcm.modules.general_study import GeneralStudy
from nii2dcm.modules.patient_study import PatientStudy
Expand Down Expand Up @@ -67,7 +66,7 @@ def __init__(self, filename=nii2dcm_temp_filename):

"""
Set Dicom Date/Time
Important: doing this once sets all Instances/Series/Study creation dates and times to the same values. Whereas,
Important: doing this once sets all Instances/Series/Study creation dates and times to the same values. Whereas,
doing this within the Modules would every so slightly offset the times
"""
# TODO shift to utils.py and propagate to Modules, or, create method within this Dicom class
Expand Down
8 changes: 4 additions & 4 deletions nii2dcm/dcm_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@
import pydicom as pyd


def write_slice(dcm, img_data, instance_index, output_dir):
def write_slice(dcm, img_data, slice_index, output_dir):
"""
write a single DICOM slice
dcm – nii2dcm DICOM object
img_data - [nX, nY, nSlice] image pixel data, such as from NIfTI file
instance_indexinstance index (important: counts from 0)
slice_indexslice index in nibabel img_data array (important: counts from 0, whereas DICOM instances count from 1)
output_dir – output DICOM file save location
"""

output_filename = r'IM_%04d.dcm' % (instance_index + 1) # begin filename from 1, e.g. IM_0001.dcm
output_filename = r'IM_%04d.dcm' % (slice_index + 1) # begin filename from 1, e.g. IM_0001.dcm

img_slice = img_data[:, :, instance_index]
img_slice = img_data[:, :, slice_index]

# Instance UID – unique to current slice
dcm.ds.SOPInstanceUID = pyd.uid.generate_uid(None)
Expand Down
4 changes: 2 additions & 2 deletions nii2dcm/modules/mr_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def __init__(self):
# https://dicom.nema.org/medical/Dicom/current/output/chtml/part03/sect_C.8.3.html#sect_C.8.3.1.1.1
# For now, will omit thereby inheriting parent value
# self.ds.ImageType = ''

self.ds.SamplesPerPixel = 1

# PhotometricInterpretation
# TODO: decide MONOCHROME1 or MONOCHROME2 as default
# https://dicom.nema.org/medical/Dicom/current/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2
Expand Down
Empty file added tests/__init__.py
Empty file.
82 changes: 82 additions & 0 deletions tests/test_dcm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import pytest
import os
import datetime

from pydicom.dataset import FileMetaDataset, FileDataset

from nii2dcm.dcm import Dicom
from nii2dcm.modules.patient import Patient


TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1'
DATE = datetime.datetime.now().strftime('%Y%m%d')
PATIENT_ID = '12345678'
PATIENT_SEX = ''
IMAGE_TYPE = ['SECONDARY', 'DERIVED']
CHARACTER_SET = 'ISO_IR 100'

MIN_UID_LENGTH = 10 # arbitrary just to check UID has some characters
MAX_UID_LENGTH = 64 # DICOM standard max length

class TestDicom:

Check failure on line 21 in tests/test_dcm.py

View workflow job for this annotation

GitHub Actions / Build, Unit Tests & E2E (ubuntu-latest, 3.9)

E302 expected 2 blank lines, found 1
def setup_method(self):
self.dicom = Dicom()

def test_dicom(self):
"""
Tests some metadata in basic Dicom object
"""
assert self.dicom.file_meta.TransferSyntaxUID == TRANSFER_SYNTAX_UID
assert self.dicom.ds.ContentDate == DATE
assert self.dicom.ds.AcquisitionDate == DATE
assert self.dicom.ds.SeriesDate == DATE
assert self.dicom.ds.StudyDate == DATE

def test_add_module(self):
"""
Tests add_module() method
"""
self.dicom.add_module(Patient())
assert self.dicom.ds.PatientID == PATIENT_ID
assert self.dicom.ds.PatientSex == PATIENT_SEX

def test_add_base_modules(self):
"""
Test metadata present following bulk method invocation via add_base_modules()
"""
self.dicom.add_base_modules()
assert self.dicom.ds.SpecificCharacterSet == CHARACTER_SET
assert self.dicom.ds.ImageType[0] == 'SECONDARY'
assert self.dicom.ds.ImageType[1] == 'DERIVED'

def test_get_file_meta(self):
fm = self.dicom.get_file_meta()
assert isinstance(fm, FileMetaDataset)

def test_get_dataset(self):
ds = self.dicom.get_dataset()
assert isinstance(ds, FileDataset)

def test_save_as(self):
"""
Test DICOM save (default save location: cwd)
"""
self.dicom.ds.save_as(self.dicom.filename)
assert os.path.exists(self.dicom.filename)
os.remove(self.dicom.filename)
if os.path.exists(self.dicom.filename):
raise Exception("Failed to delete temporary DICOM created during pytest process.")

def test_init_study_tags(self):
self.dicom.init_study_tags()
assert isinstance(self.dicom.ds.StudyInstanceUID, str)
assert self.dicom.ds.StudyInstanceUID.find(".")
assert MIN_UID_LENGTH < len(self.dicom.ds.StudyInstanceUID) <= MAX_UID_LENGTH

def test_init_series_tags(self):
self.dicom.init_study_tags()
assert isinstance(self.dicom.ds.SeriesInstanceUID, str)
assert self.dicom.ds.SeriesInstanceUID.find(".")
assert MIN_UID_LENGTH < len(self.dicom.ds.SeriesInstanceUID) <= MAX_UID_LENGTH

assert len(str(self.dicom.ds.SeriesNumber)) == 4
46 changes: 46 additions & 0 deletions tests/test_dcm_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os

import pytest
import nibabel as nib

from nii2dcm import dcm_writer
from nii2dcm.dcm import Dicom
from nii2dcm.nii import Nifti


NII_FILE_PATH = "tests/data/DicomMRISVR/t2-svr-atlas-35wk.nii.gz"
INSTANCE_INDEX = 10 # dcm instances count from 1
SLICE_NUMBER = INSTANCE_INDEX-1 # nibabel slice array counts from 0
OUTPUT_DIR = "tests/data"
OUTPUT_DCM_FILENAME = r'IM_%04d.dcm' % (INSTANCE_INDEX)
OUTPUT_DCM_PATH = os.path.join(os.getcwd(), OUTPUT_DIR, OUTPUT_DCM_FILENAME)

class TestDicomWriter:

Check failure on line 18 in tests/test_dcm_writer.py

View workflow job for this annotation

GitHub Actions / Build, Unit Tests & E2E (ubuntu-latest, 3.9)

E302 expected 2 blank lines, found 1
def setup_method(self):
self.dicom = Dicom()
self.nii = nib.load(NII_FILE_PATH)
self.img_data = self.nii.get_fdata().astype("uint16")
self.nii2dcm_parameters = Nifti.get_nii2dcm_parameters(self.nii)

def test_write_slice(self):
dcm_writer.write_slice(self.dicom, self.img_data, SLICE_NUMBER, OUTPUT_DIR)

assert os.path.exists(OUTPUT_DCM_PATH)
os.remove(OUTPUT_DCM_PATH)
if os.path.exists(OUTPUT_DCM_PATH):
raise Exception("Failed to delete temporary DICOM created during pytest process.")

def test_transfer_nii_hdr_series_tags(self):
dcm_writer.transfer_nii_hdr_series_tags(self.dicom, self.nii2dcm_parameters)
assert self.dicom.ds.Rows == self.nii.shape[0]
assert self.dicom.ds.Columns == self.nii.shape[1]

def test_transfer_nii_hdr_instance_tags(self):
dcm_writer.transfer_nii_hdr_instance_tags(self.dicom, self.nii2dcm_parameters, SLICE_NUMBER)
assert self.dicom.ds.InstanceNumber == INSTANCE_INDEX

def test_transfer_ref_dicom_series_tags(self):
"""
TODO: implement test
"""
pass
23 changes: 23 additions & 0 deletions tests/test_nii.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import pytest

from nii2dcm.nii import Nifti
import nibabel as nib


NII_FILE_PATH = "tests/data/DicomMRISVR/t2-svr-atlas-35wk.nii.gz"
NII_VOXEL_DIMS = (180, 221, 180)
NII_VOXEL_SPACING = (0.5, 0.5, 0.5)

class TestNifti:

Check failure on line 11 in tests/test_nii.py

View workflow job for this annotation

GitHub Actions / Build, Unit Tests & E2E (ubuntu-latest, 3.9)

E302 expected 2 blank lines, found 1
def setup_method(self):
self.nii = nib.load(NII_FILE_PATH)

def test_get_nii2dcm_parameters(self):
nii_parameters = Nifti.get_nii2dcm_parameters(self.nii)
assert nii_parameters["Rows"] == NII_VOXEL_DIMS[0]
assert nii_parameters["Columns"] == NII_VOXEL_DIMS[1]
assert nii_parameters["NumberOfSlices"] == NII_VOXEL_DIMS[2]
assert nii_parameters["AcquisitionMatrix"] == [0, NII_VOXEL_DIMS[0], NII_VOXEL_DIMS[1], 0]
assert nii_parameters["dimX"] == NII_VOXEL_SPACING[0]
assert nii_parameters["dimY"] == NII_VOXEL_SPACING[1]
assert nii_parameters["SliceThickness"] == str(NII_VOXEL_SPACING[2])
55 changes: 55 additions & 0 deletions tests/test_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pytest
import os, shutil

Check failure on line 2 in tests/test_run.py

View workflow job for this annotation

GitHub Actions / Build, Unit Tests & E2E (ubuntu-latest, 3.9)

E401 multiple imports on one line
import pydicom as pyd

from nii2dcm.run import run_nii2dcm


NII_FILE_PATH = "tests/data/DicomMRISVR/t2-svr-atlas-35wk.nii.gz"
OUTPUT_DIR = "tests/data/tmp_dcm_dir"
OUTPUT_DCM_PATH = os.path.join(os.getcwd(), OUTPUT_DIR)
NUM_DICOM_FILES = 180
SINGLE_DICOM_FILENAME = "IM_0001.dcm"

class TestRun:

Check failure on line 14 in tests/test_run.py

View workflow job for this annotation

GitHub Actions / Build, Unit Tests & E2E (ubuntu-latest, 3.9)

E302 expected 2 blank lines, found 1
def setup_method(self):
os.makedirs(OUTPUT_DCM_PATH, exist_ok=True)

@pytest.mark.parametrize(
"TEST_DICOM_TYPE, TEST_DCM_MODALITY",
[
(None, ''), # basic DICOM with undefined modality
("MR", "MR"), # MRI DICOM
("SVR", "MR") # SVR DICOM hence MR modality
]
)
def test_run_dicom_types(self, TEST_DICOM_TYPE, TEST_DCM_MODALITY):
"""
Test run_nii2dcm with different dicom_types
"""
run_nii2dcm(
NII_FILE_PATH,
OUTPUT_DCM_PATH,
dicom_type=TEST_DICOM_TYPE
)
assert os.path.exists(os.path.join(OUTPUT_DCM_PATH, SINGLE_DICOM_FILENAME))
assert len(os.listdir(OUTPUT_DCM_PATH)) == NUM_DICOM_FILES

ds = pyd.dcmread(os.path.join(OUTPUT_DCM_PATH, SINGLE_DICOM_FILENAME))
assert ds.Modality == TEST_DCM_MODALITY

shutil.rmtree(OUTPUT_DCM_PATH)

def test_run_reference_dicom(self):
"""
Test run_nii2dcm with different ref_dicom option
"""
# TODO: implement - will involve adding reference DICOM test dataset
pass

def teardown_method(self):
"""
Remove output DICOM directory in event of test failure
"""
if os.path.exists(OUTPUT_DCM_PATH):
shutil.rmtree(OUTPUT_DCM_PATH)
9 changes: 9 additions & 0 deletions tests/test_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import pytest
from packaging import version

from nii2dcm._version import __version__


class TestVersion:
def test_version(self):
assert isinstance(version.parse(__version__), version.Version)

0 comments on commit 935d53a

Please sign in to comment.