diff --git a/.gitignore b/.gitignore index 87d0e17..78dc741 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /dist /MANIFEST /*.egg-info +/openslide/_convert*.so *.pyc diff --git a/MANIFEST.in b/MANIFEST.in index 61194a1..253894b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include *.txt recursive-include doc *.py *.rst recursive-include examples *.html *.js *.png *.py +recursive-include tests *.png *.py *.tiff diff --git a/setup.py b/setup.py index 55dbbff..7c9c9ba 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ ], ), }, + test_suite='tests', maintainer='OpenSlide project', maintainer_email='openslide-users@lists.andrew.cmu.edu', description='Python interface to OpenSlide', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d160b84 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,23 @@ +# +# openslide-python - Python bindings for the OpenSlide library +# +# Copyright (c) 2016 Benjamin Gilbert +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +import os + +def file_path(name): + return os.path.join(os.path.dirname(__file__), name) diff --git a/tests/boxes.png b/tests/boxes.png new file mode 100644 index 0000000..f71c272 Binary files /dev/null and b/tests/boxes.png differ diff --git a/tests/boxes.tiff b/tests/boxes.tiff new file mode 100644 index 0000000..acf31dd Binary files /dev/null and b/tests/boxes.tiff differ diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 0000000..4ea0418 --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,39 @@ +# +# openslide-python - Python bindings for the OpenSlide library +# +# Copyright (c) 2016 Benjamin Gilbert +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +import openslide +from openslide import open_slide, OpenSlide, ImageSlide +import sys +import unittest + +from . import file_path + +# Tests should be written to be compatible with Python 2.6 unittest. + +class TestLibrary(unittest.TestCase): + def test_version(self): + string = unicode if sys.version[0] == '2' else str + self.assertTrue(isinstance(openslide.__version__, string)) + self.assertTrue(isinstance(openslide.__library_version__, string)) + + def test_open_slide(self): + self.assertTrue(isinstance(open_slide(file_path('boxes.tiff')), + OpenSlide)) + self.assertTrue(isinstance(open_slide(file_path('boxes.png')), + ImageSlide)) diff --git a/tests/test_deepzoom.py b/tests/test_deepzoom.py new file mode 100644 index 0000000..b505764 --- /dev/null +++ b/tests/test_deepzoom.py @@ -0,0 +1,76 @@ +# +# openslide-python - Python bindings for the OpenSlide library +# +# Copyright (c) 2016 Benjamin Gilbert +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +from openslide import OpenSlide, ImageSlide +from openslide.deepzoom import DeepZoomGenerator +import unittest + +from . import file_path + +# Tests should be written to be compatible with Python 2.6 unittest. + +class _BoxesDeepZoomTest(object): + def setUp(self): + self.osr = self.CLASS(file_path(self.FILENAME)) + self.dz = DeepZoomGenerator(self.osr, 254, 1) + + def tearDown(self): + self.osr.close() + + def test_metadata(self): + self.assertEqual(self.dz.level_count, 10) + self.assertEqual(self.dz.tile_count, 11) + self.assertEqual(self.dz.level_tiles, + ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1), + (1, 1), (1, 1), (1, 1), (1, 1), (2, 1))) + self.assertEqual(self.dz.level_dimensions, + ((1, 1), (2, 1), (3, 2), (5, 4), (10, 8), + (19, 16), (38, 32), (75, 63), (150, 125), (300, 250))) + + def test_get_tile(self): + self.assertEqual(self.dz.get_tile(9, (1, 0)).size, (47, 250)) + + def test_get_tile_bad_level(self): + self.assertRaises(ValueError, lambda: self.dz.get_tile(-1, (0, 0))) + self.assertRaises(ValueError, lambda: self.dz.get_tile(10, (0, 0))) + + def test_get_tile_bad_address(self): + self.assertRaises(ValueError, lambda: self.dz.get_tile(0, (-1, 0))) + self.assertRaises(ValueError, lambda: self.dz.get_tile(0, (1, 0))) + + def test_get_tile_coordinates(self): + self.assertEqual(self.dz.get_tile_coordinates(9, (1, 0)), + ((253, 0), 0, (47, 250))) + + def test_get_tile_dimensions(self): + self.assertEqual(self.dz.get_tile_dimensions(9, (1, 0)), (47, 250)) + + def test_get_dzi(self): + self.assertTrue('http://schemas.microsoft.com/deepzoom/2008' in + self.dz.get_dzi('jpeg')) + + +class TestSlideDeepZoom(_BoxesDeepZoomTest, unittest.TestCase): + CLASS = OpenSlide + FILENAME = 'boxes.tiff' + + +class TestImageDeepZoom(_BoxesDeepZoomTest, unittest.TestCase): + CLASS = ImageSlide + FILENAME = 'boxes.png' diff --git a/tests/test_imageslide.py b/tests/test_imageslide.py new file mode 100644 index 0000000..510534c --- /dev/null +++ b/tests/test_imageslide.py @@ -0,0 +1,102 @@ +# +# openslide-python - Python bindings for the OpenSlide library +# +# Copyright (c) 2016 Benjamin Gilbert +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +from openslide import ImageSlide, OpenSlideError +from PIL import Image +import unittest + +from . import file_path + +# Tests should be written to be compatible with Python 2.6 unittest. + +class TestImageWithoutOpening(unittest.TestCase): + def test_detect_format(self): + self.assertTrue( + ImageSlide.detect_format(file_path('__missing_file')) is None) + self.assertTrue( + ImageSlide.detect_format(file_path('../setup.py')) is None) + self.assertEqual( + ImageSlide.detect_format(file_path('boxes.png')), 'PNG') + + def test_open(self): + self.assertRaises(IOError, + lambda: ImageSlide(file_path('__does_not_exist'))) + self.assertRaises(IOError, + lambda: ImageSlide(file_path('../setup.py'))) + + # passing PIL.Image to ImageSlide + self.assertEqual( + ImageSlide(Image.open(file_path('boxes.png'))).dimensions, + (300, 250)) + + def test_operations_on_closed_handle(self): + img = Image.open(file_path('boxes.png')) + osr = ImageSlide(img) + osr.close() + self.assertRaises(AttributeError, + lambda: osr.read_region((0, 0), 0, (100, 100))) + # If an Image is passed to the constructor, ImageSlide.close() + # shouldn't close it + self.assertEqual(img.getpixel((0, 0)), 3) + + def test_context_manager(self): + osr = ImageSlide(file_path('boxes.png')) + with osr: + pass + self.assertRaises(AttributeError, + lambda: osr.read_region((0, 0), 0, (100, 100))) + + +class TestImage(unittest.TestCase): + def setUp(self): + self.osr = ImageSlide(file_path('boxes.png')) + + def tearDown(self): + self.osr.close() + + def test_metadata(self): + self.assertEqual(self.osr.level_count, 1) + self.assertEqual(self.osr.level_dimensions, ((300, 250),)) + self.assertEqual(self.osr.dimensions, (300, 250)) + self.assertEqual(self.osr.level_downsamples, (1.0,)) + + self.assertEqual(self.osr.get_best_level_for_downsample(0.5), 0) + self.assertEqual(self.osr.get_best_level_for_downsample(3), 0) + + self.assertEqual(self.osr.properties, {}) + self.assertEqual(self.osr.associated_images, {}) + + def test_read_region(self): + self.assertEqual(self.osr.read_region((-10, -10), 0, (400, 400)).size, + (400, 400)) + + def test_read_region_size_dimension_zero(self): + self.assertEqual(self.osr.read_region((0, 0), 0, (400, 0)).size, + (400, 0)) + + def test_read_region_bad_level(self): + self.assertRaises(OpenSlideError, + lambda: self.osr.read_region((0, 0), 1, (100, 100))) + + def test_read_region_bad_size(self): + self.assertRaises(OpenSlideError, + lambda: self.osr.read_region((0, 0), 0, (400, -5))) + + def test_thumbnail(self): + self.assertEqual(self.osr.get_thumbnail((100, 100)).size, (100, 83)) diff --git a/tests/test_openslide.py b/tests/test_openslide.py new file mode 100644 index 0000000..715c09a --- /dev/null +++ b/tests/test_openslide.py @@ -0,0 +1,121 @@ +# +# openslide-python - Python bindings for the OpenSlide library +# +# Copyright (c) 2016 Benjamin Gilbert +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +from ctypes import ArgumentError +from openslide import (OpenSlide, OpenSlideError, + OpenSlideUnsupportedFormatError) +import unittest + +from . import file_path + +# Tests should be written to be compatible with Python 2.6 unittest. + +class TestSlideWithoutOpening(unittest.TestCase): + def test_detect_format(self): + self.assertTrue( + OpenSlide.detect_format(file_path('__missing_file')) is None) + self.assertTrue( + OpenSlide.detect_format(file_path('../setup.py')) is None) + self.assertEqual( + OpenSlide.detect_format(file_path('boxes.tiff')), + 'generic-tiff') + + def test_open(self): + self.assertRaises(OpenSlideUnsupportedFormatError, + lambda: OpenSlide('__does_not_exist')) + self.assertRaises(OpenSlideUnsupportedFormatError, + lambda: OpenSlide('setup.py')) + self.assertRaises(OpenSlideError, + lambda: OpenSlide('unopenable.tiff')) + + def test_operations_on_closed_handle(self): + osr = OpenSlide(file_path('boxes.tiff')) + props = osr.properties + associated = osr.associated_images + osr.close() + self.assertRaises(ArgumentError, + lambda: osr.read_region((0, 0), 0, (100, 100))) + self.assertRaises(ArgumentError, lambda: osr.close()) + self.assertRaises(ArgumentError, lambda: props['openslide.vendor']) + self.assertRaises(ArgumentError, lambda: associated['label']) + + def test_context_manager(self): + osr = OpenSlide(file_path('boxes.tiff')) + with osr: + self.assertEqual(osr.level_count, 4) + self.assertRaises(ArgumentError, lambda: osr.level_count) + + +class TestSlide(unittest.TestCase): + def setUp(self): + self.osr = OpenSlide(file_path('boxes.tiff')) + + def tearDown(self): + self.osr.close() + + def test_basic_metadata(self): + self.assertEqual(self.osr.level_count, 4) + self.assertEqual(self.osr.level_dimensions, + ((300, 250), (150, 125), (75, 62), (37, 31))) + self.assertEqual(self.osr.dimensions, (300, 250)) + + self.assertEqual(len(self.osr.level_downsamples), self.osr.level_count) + self.assertEqual(self.osr.level_downsamples[0:2], (1, 2)) + self.assertAlmostEqual(self.osr.level_downsamples[2], 4, places=0) + self.assertAlmostEqual(self.osr.level_downsamples[3], 8, places=0) + + self.assertEqual(self.osr.get_best_level_for_downsample(0.5), 0) + self.assertEqual(self.osr.get_best_level_for_downsample(3), 1) + self.assertEqual(self.osr.get_best_level_for_downsample(37), 3) + + def test_properties(self): + self.assertEqual(self.osr.properties['openslide.vendor'], + 'generic-tiff') + self.assertRaises(KeyError, + lambda: self.osr.properties['__does_not_exist']) + # test __len__ and __iter__ + self.assertEqual(len([v for v in self.osr.properties]), + len(self.osr.properties)) + + def test_associated_images(self): + # XXX test an associated image + self.assertRaises(KeyError, + lambda: self.osr.associated_images['__missing']) + # test __len__ and __iter__ + self.assertEqual(len([v for v in self.osr.associated_images]), + len(self.osr.associated_images)) + + def test_read_region(self): + self.assertEqual(self.osr.read_region((-10, -10), 1, (400, 400)).size, + (400, 400)) + + def test_read_region_size_dimension_zero(self): + self.assertEqual(self.osr.read_region((0, 0), 1, (400, 0)).size, + (400, 0)) + + def test_read_region_bad_level(self): + self.assertEqual(self.osr.read_region((0, 0), 4, (100, 100)).size, + (100, 100)) + + def test_read_region_bad_size(self): + self.assertRaises(OpenSlideError, + lambda: self.osr.read_region((0, 0), 1, (400, -5))) + + def test_thumbnail(self): + self.assertEqual(self.osr.get_thumbnail((100, 100)).size, (100, 83)) diff --git a/tests/unopenable.tiff b/tests/unopenable.tiff new file mode 100644 index 0000000..e3de703 Binary files /dev/null and b/tests/unopenable.tiff differ