Skip to content

Commit

Permalink
Increase Code Coverage (#80)
Browse files Browse the repository at this point in the history
Went through and added tests to uncovered routes according to codecov. Increased coverage from ~88% to ~95%.

Few minor syntax changes made along the way. Functionality of library is unaffected.
  • Loading branch information
addisonElliott authored Jan 27, 2019
1 parent e1c23ef commit e472ad8
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 22 deletions.
2 changes: 2 additions & 0 deletions nrrd/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ def parse_number_list(x, dtype=None):
if dtype is None:
number_list_trunc = number_list.astype(int)

# If there is no difference between the truncated number list and the number list, then that means that the
# number list was all integers and we can just return that
if np.all((number_list - number_list_trunc) == 0):
number_list = number_list_trunc
elif dtype == int:
Expand Down
12 changes: 8 additions & 4 deletions nrrd/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ def _get_field_type(field, custom_field_map):
return 'double list'
elif field in ['kinds', 'labels', 'units', 'space units', 'centerings']:
return 'string list'
elif field in []:
return 'int vector'
# No int vector fields as of now
# elif field in []:
# return 'int vector'
elif field in ['space origin']:
return 'double vector'
elif field in ['measurement frame']:
Expand Down Expand Up @@ -156,6 +157,8 @@ def _determine_datatype(fields):
np_typestring = '>' + np_typestring
elif fields['endian'] == 'little':
np_typestring = '<' + np_typestring
else:
raise NRRDError('Invalid endian value in header: "%s"' % fields['endian'])

return np.dtype(np_typestring)

Expand Down Expand Up @@ -185,7 +188,7 @@ def _validate_magic_line(line):
raise NRRDError('Unsupported NRRD file version (version: %i). This library only supports v%i and below.'
% (version, 5))
except ValueError:
raise NRRDError('Invalid NRRD magic line: %s' % (line,))
raise NRRDError('Invalid NRRD magic line: %s' % line)

return len(line)

Expand Down Expand Up @@ -331,7 +334,8 @@ def read_data(header, fh=None, filename=None):
raise NRRDError('Header is missing required field: "%s".' % field)

if header['dimension'] != len(header['sizes']):
raise NRRDError('Number of elements in sizes does not match dimension')
raise NRRDError('Number of elements in sizes does not match dimension. Dimension: %i, len(sizes): %i' % (
header['dimension'], len(header['sizes'])))

# Determine the data type from the header
dtype = _determine_datatype(header)
Expand Down
9 changes: 8 additions & 1 deletion nrrd/tests/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def test_parse_optional_matrix(self):
with self.assertRaisesRegex(nrrd.NRRDError, 'Matrix should have same number of elements in each row'):
nrrd.parse_optional_matrix('none (1,0,0,0) (0,1,0) (0,0,1)')

def test_parse_number_list(self):
def test_parse_number_list_int(self):
self.assert_equal_with_datatype(nrrd.parse_number_list('1 2 3 4'), [1, 2, 3, 4])
self.assert_equal_with_datatype(nrrd.parse_number_list('1 2 3 4', dtype=float), [1., 2., 3., 4.])
self.assert_equal_with_datatype(nrrd.parse_number_list('1 2 3 4', dtype=int), [1, 2, 3, 4])
Expand All @@ -146,6 +146,13 @@ def test_parse_number_list(self):
with self.assertRaisesRegex(nrrd.NRRDError, 'dtype should be None for automatic type detection, float or int'):
nrrd.parse_number_list('1 2 3 4', dtype=np.uint8)

def test_parse_number_list_float(self):
self.assert_equal_with_datatype(nrrd.parse_number_list('1.5 2 3 4'), [1.5, 2., 3., 4.])
self.assert_equal_with_datatype(nrrd.parse_number_list('1 2 3 4.9', dtype=float), [1., 2., 3., 4.9])
self.assert_equal_with_datatype(nrrd.parse_number_list('1 2.9 3 4', dtype=int), [1, 2, 3, 4])

self.assert_equal_with_datatype(nrrd.parse_number_list('1.3'), [1.3])

def test_parse_number_auto_dtype(self):
self.assertEqual(nrrd.parse_number_auto_dtype('25'), 25)
self.assertEqual(nrrd.parse_number_auto_dtype('25.125'), 25.125)
Expand Down
143 changes: 126 additions & 17 deletions nrrd/tests/test_reading.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from nrrd.tests.util import *
import nrrd


class TestReadingFunctions(unittest.TestCase):
def setUp(self):
self.expected_header = {u'dimension': 3,
Expand Down Expand Up @@ -67,17 +68,17 @@ def test_read_detached_header_and_data(self):

# Test that the data read is able to be edited
self.assertTrue(data.flags['WRITEABLE'])

def test_read_detached_header_and_data_with_byteskip_minus1(self):
expected_header = self.expected_header
expected_header[u'data file'] = os.path.basename(RAW_DATA_FILE_PATH)
expected_header[u'byte skip'] = -1

data, header = nrrd.read(RAW_BYTESKIP_NHDR_FILE_PATH)

np.testing.assert_equal(self.expected_header, header)
np.testing.assert_equal(data, self.expected_data)

# Test that the data read is able to be edited
self.assertTrue(data.flags['WRITEABLE'])

Expand All @@ -89,23 +90,21 @@ def test_read_detached_header_and_nifti_data_with_byteskip_minus1(self):
expected_header[u'data file'] = 'BallBinary30x30x30.nii.gz'

data, header = nrrd.read(GZ_BYTESKIP_NIFTI_NHDR_FILE_PATH)

np.testing.assert_equal(self.expected_header, header)
np.testing.assert_equal(data, self.expected_data)

# Test that the data read is able to be edited
self.assertTrue(data.flags['WRITEABLE'])

def test_read_detached_header_and_nifti_data(self):

with self.assertRaisesRegex(nrrd.NRRDError, 'Size of the data does not equal '
+ 'the product of all the dimensions: 27000-27176=-176'):
+ 'the product of all the dimensions: 27000-27176=-176'):
nrrd.read(GZ_NIFTI_NHDR_FILE_PATH)

def test_read_detached_header_and_data_with_byteskip_minus5(self):

with self.assertRaisesRegex(nrrd.NRRDError, 'Invalid byteskip, allowed values '
+'are greater than or equal to -1'):
+ 'are greater than or equal to -1'):
nrrd.read(RAW_INVALID_BYTESKIP_NHDR_FILE_PATH)

def test_read_header_and_gz_compressed_data(self):
Expand All @@ -119,7 +118,7 @@ def test_read_header_and_gz_compressed_data(self):

# Test that the data read is able to be edited
self.assertTrue(data.flags['WRITEABLE'])

def test_read_header_and_gz_compressed_data_with_byteskip_minus1(self):
expected_header = self.expected_header
expected_header[u'encoding'] = 'gzip'
Expand All @@ -130,7 +129,7 @@ def test_read_header_and_gz_compressed_data_with_byteskip_minus1(self):

np.testing.assert_equal(self.expected_header, header)
np.testing.assert_equal(data, self.expected_data)

# Test that the data read is able to be edited
self.assertTrue(data.flags['WRITEABLE'])

Expand Down Expand Up @@ -160,8 +159,8 @@ def test_read_header_and_gz_compressed_data_with_lineskip3(self):
self.assertTrue(data.flags['WRITEABLE'])

def test_read_raw_header(self):
expected_header = {u'type': 'float', u'dimension': 3}
header = nrrd.read_header(('NRRD0005', 'type: float', 'dimension: 3'))
expected_header = {u'type': 'float', u'dimension': 3, u'min': 0, u'max': 35.4}
header = nrrd.read_header(('NRRD0005', 'type: float', 'dimension: 3', 'min: 0', 'max: 35.4'))
self.assertEqual(expected_header, header)

expected_header = {u'my extra info': u'my : colon-separated : values'}
Expand Down Expand Up @@ -230,11 +229,10 @@ def test_read_simple_4d_nrrd(self):
[np.NaN, np.NaN, np.NaN]]),
'endian': 'little',
'encoding': 'raw',
'measurement frame': np.array([[1.0001, 0., 0.],
[0., 1.0000000006, 0.],
'measurement frame': np.array([[1.0001, 0., 0.],
[0., 1.0000000006, 0.],
[0., 0., 1.000000000000009]])}


data, header = nrrd.read(RAW_4D_NRRD_FILE_PATH)

np.testing.assert_equal(header, expected_header)
Expand Down Expand Up @@ -299,6 +297,117 @@ def test_custom_fields_with_field_map(self):

np.testing.assert_equal(header, expected_header)

def test_invalid_custom_field(self):
custom_field_map = {'int': 'fake'}

with self.assertRaisesRegex(nrrd.NRRDError, 'Invalid field type given: fake'):
nrrd.read_header(ASCII_1D_CUSTOM_FIELDS_FILE_PATH, custom_field_map)

def test_invalid_magic_line(self):
with self.assertRaisesRegex(nrrd.NRRDError, 'Invalid NRRD magic line. Is this an NRRD file?'):
nrrd.read_header(('invalid magic line', 'my extra info:=my : colon-separated : values'))

def test_invalid_magic_line2(self):
with self.assertRaisesRegex(nrrd.NRRDError, 'Unsupported NRRD file version \\(version: 2000\\). This library '
'only supports v5 and below.'):
nrrd.read_header(('NRRD2000', 'my extra info:=my : colon-separated : values'))

def test_invalid_magic_line3(self):
with self.assertRaisesRegex(nrrd.NRRDError, 'Invalid NRRD magic line: NRRDnono'):
nrrd.read_header(('NRRDnono', 'my extra info:=my : colon-separated : values'))

def test_missing_required_field(self):
with open(RAW_NRRD_FILE_PATH, 'rb') as fh:
header = nrrd.read_header(fh)
np.testing.assert_equal(self.expected_header, header)

# Delete required field
del header['type']

with self.assertRaisesRegex(nrrd.NRRDError, 'Header is missing required field: "type".'):
nrrd.read_data(header, fh, RAW_NRRD_FILE_PATH)

def test_wrong_sizes(self):
with open(RAW_NRRD_FILE_PATH, 'rb') as fh:
header = nrrd.read_header(fh)
np.testing.assert_equal(self.expected_header, header)

# Make the number of dimensions wrong
header['dimension'] = 2

with self.assertRaisesRegex(nrrd.NRRDError, 'Number of elements in sizes does not match dimension. '
'Dimension: 2, len\\(sizes\\): 3'):
nrrd.read_data(header, fh, RAW_NRRD_FILE_PATH)

def test_invalid_encoding(self):
with open(RAW_NRRD_FILE_PATH, 'rb') as fh:
header = nrrd.read_header(fh)
np.testing.assert_equal(self.expected_header, header)

# Set the encoding to be incorrect
header['encoding'] = 'fake'

with self.assertRaisesRegex(nrrd.NRRDError, 'Unsupported encoding: "fake"'):
nrrd.read_data(header, fh, RAW_NRRD_FILE_PATH)

def test_detached_header_no_filename(self):
self.expected_header[u'data file'] = os.path.basename(RAW_DATA_FILE_PATH)

with open(RAW_NHDR_FILE_PATH, 'rb') as fh:
header = nrrd.read_header(fh)
np.testing.assert_equal(self.expected_header, header)

# No filename is specified for read_data
with self.assertRaisesRegex(nrrd.NRRDError, 'Filename parameter must be specified when a relative data file'
' path is given'):
nrrd.read_data(header, fh)

def test_invalid_lineskip(self):
with open(RAW_NRRD_FILE_PATH, 'rb') as fh:
header = nrrd.read_header(fh)
np.testing.assert_equal(self.expected_header, header)

# Set the line skip to be incorrect
header['line skip'] = -1

with self.assertRaisesRegex(nrrd.NRRDError, 'Invalid lineskip, allowed values are greater than or equal to'
' 0'):
nrrd.read_data(header, fh, RAW_NRRD_FILE_PATH)

def test_missing_endianness(self):
with open(RAW_NRRD_FILE_PATH, 'rb') as fh:
header = nrrd.read_header(fh)
np.testing.assert_equal(self.expected_header, header)

# Delete the endian field from header
# Since our data is short (itemsize = 2), we should receive an error
del header['endian']

with self.assertRaisesRegex(nrrd.NRRDError, 'Header is missing required field: "endian".'):
nrrd.read_data(header, fh, RAW_NRRD_FILE_PATH)

def test_big_endian(self):
with open(RAW_NRRD_FILE_PATH, 'rb') as fh:
header = nrrd.read_header(fh)
np.testing.assert_equal(self.expected_header, header)

# Set endianness to big to verify it is doing correctly
header['endian'] = 'big'

data = nrrd.read_data(header, fh, RAW_NRRD_FILE_PATH)
np.testing.assert_equal(data, self.expected_data.byteswap())

def test_invalid_endian(self):
with open(RAW_NRRD_FILE_PATH, 'rb') as fh:
header = nrrd.read_header(fh)
np.testing.assert_equal(self.expected_header, header)

# Set endianness to fake value
header['endian'] = 'fake'

with self.assertRaisesRegex(nrrd.NRRDError, 'Invalid endian value in header: "fake"'):
nrrd.read_data(header, fh, RAW_NRRD_FILE_PATH)


if __name__ == '__main__':
unittest.main()
44 changes: 44 additions & 0 deletions nrrd/tests/test_writing.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,18 @@ def test_write_fake_encoding(self):
with self.assertRaisesRegex(nrrd.NRRDError, 'Invalid encoding specification while writing NRRD file: fake'):
nrrd.write(output_filename, self.data_input, {u'encoding': 'fake'})

def test_write_detached_raw(self):
output_filename = os.path.join(self.temp_write_dir, 'testfile_detached_raw.nhdr')
output_data_filename = os.path.join(self.temp_write_dir, 'testfile_detached_raw.raw')

nrrd.write(output_filename, self.data_input, {u'encoding': 'raw'}, detached_header=False)

# Read back the same file
data, header = nrrd.read(output_filename)
self.assertEqual(self.expected_data, data.tostring(order='F'))
self.assertEqual(header['encoding'], 'raw')
self.assertEqual(header['data file'], output_data_filename)

def test_write_detached_gz(self):
output_filename = os.path.join(self.temp_write_dir, 'testfile_detached_raw.nhdr')
output_data_filename = os.path.join(self.temp_write_dir, 'testfile_detached_raw.raw.gz')
Expand Down Expand Up @@ -220,6 +232,38 @@ def test_write_detached_ascii(self):
self.assertEqual(header['encoding'], 'txt')
self.assertEqual(header['data file'], 'testfile_detached_raw.txt')

def test_invalid_custom_field(self):
output_filename = os.path.join(self.temp_write_dir, 'testfile_invalid_custom_field.nrrd')
header = {'int': 12}
custom_field_map = {'int': 'fake'}

with self.assertRaisesRegex(nrrd.NRRDError, 'Invalid field type given: fake'):
nrrd.write(output_filename, np.zeros((3, 9)), header, custom_field_map=custom_field_map)

def test_remove_endianness(self):
output_filename = os.path.join(self.temp_write_dir, 'testfile_remove_endianness.nrrd')

x = np.arange(1, 28)
nrrd.write(output_filename, x, {u'encoding': 'ascii', u'endian': 'little', 'space': 'right-anterior-superior',
'space dimension': 3})

# Read back the same file
data, header = nrrd.read(output_filename)
self.assertEqual(header['encoding'], 'ascii')

# Check for endian and space dimension, both of these should have been removed from the header
# Endian because it is an ASCII encoded file and space dimension because space is specified
self.assertFalse('endian' in header)
self.assertFalse('space dimension' in header)
np.testing.assert_equal(data, x)

def test_unsupported_encoding(self):
output_filename = os.path.join(self.temp_write_dir, 'testfile_unsupported_encoding.nrrd')
header = {'encoding': 'fake'}

with self.assertRaisesRegex(nrrd.NRRDError, 'Unsupported encoding: "fake"'):
nrrd.write(output_filename, np.zeros((3, 9)), header)


if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions nrrd/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def write(filename, data, header={}, detached_header=False, relative_data_path=T
header['data file'] = os.path.basename(data_filename) \
if relative_data_path else os.path.abspath(data_filename)
else:
# TODO This will cause issues for relative data files because it will not save in the correct spot
data_filename = header['data file']
elif filename.endswith('.nrrd') and detached_header:
data_filename = filename
Expand Down

0 comments on commit e472ad8

Please sign in to comment.