diff options
author | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
commit | f7bdc2acff3c13a6d632c28c4569690ab106eed7 (patch) | |
tree | 9d67cdb7152ee4e711379e03fe0546c7c3b97303 /silx/test |
Import Upstream version 0.5.0+dfsg
Diffstat (limited to 'silx/test')
-rw-r--r-- | silx/test/__init__.py | 80 | ||||
-rw-r--r-- | silx/test/test_resources.py | 97 | ||||
-rw-r--r-- | silx/test/test_sx.py | 174 | ||||
-rw-r--r-- | silx/test/test_version.py | 49 | ||||
-rw-r--r-- | silx/test/utils.py | 284 |
5 files changed, 684 insertions, 0 deletions
diff --git a/silx/test/__init__.py b/silx/test/__init__.py new file mode 100644 index 0000000..01ee8d1 --- /dev/null +++ b/silx/test/__init__.py @@ -0,0 +1,80 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2015-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Full silx test suite. + + +It is possible to disable tests depending on Qt by setting +:envvar:`WITH_QT_TEST` environment variable to 'False'. +It will skip all tests from :mod:`silx.test.gui`. +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "20/04/2017" + + +import logging +import os +import unittest + + +logger = logging.getLogger(__name__) + + +def suite(): + from . import test_version + from . import test_resources + from . import test_sx + from ..io import test as test_io + from ..math import test as test_math + from ..image import test as test_image + from ..gui import test as test_gui + from ..utils import test as test_utils + from ..opencl import test as test_ocl + test_suite = unittest.TestSuite() + # test sx first cause qui tests load ipython module + test_suite.addTest(test_sx.suite()) + test_suite.addTest(test_gui.suite()) + # then test no-gui tests + test_suite.addTest(test_utils.suite()) + test_suite.addTest(test_version.suite()) + test_suite.addTest(test_resources.suite()) + test_suite.addTest(test_utils.suite()) + test_suite.addTest(test_io.suite()) + test_suite.addTest(test_math.suite()) + test_suite.addTest(test_image.suite()) + test_suite.addTest(test_ocl.suite()) + return test_suite + + +def run_tests(): + """Run test complete test_suite""" + runner = unittest.TextTestRunner() + if not runner.run(suite()).wasSuccessful(): + print("Test suite failed") + return 1 + else: + print("Test suite succeeded") + return 0 diff --git a/silx/test/test_resources.py b/silx/test/test_resources.py new file mode 100644 index 0000000..9d3e277 --- /dev/null +++ b/silx/test/test_resources.py @@ -0,0 +1,97 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Test for resource files management.""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "20/04/2017" + + +import os +import unittest + +import silx.resources +from .utils import utilstest + + +class TestResources(unittest.TestCase): + def test_resource_dir(self): + """Get a resource directory""" + icons_dirname = silx.resources.resource_filename('gui/icons/') + self.assertTrue(os.path.isdir(icons_dirname)) + + def test_resource_file(self): + """Get a resource file name""" + filename = silx.resources.resource_filename('gui/icons/colormap.png') + self.assertTrue(os.path.isfile(filename)) + + def test_resource_nonexistent(self): + """Get a non existent resource""" + filename = silx.resources.resource_filename('non_existent_file.txt') + self.assertFalse(os.path.exists(filename)) + + +class TestExternalResources(unittest.TestCase): + "This is a test for the TestResources" + def test_tempdir(self): + "test the temporary directory creation" + myutilstest = silx.resources.ExternalResources("toto", "http://www.silx.org") + d = myutilstest.tempdir + self.assertTrue(os.path.isdir(d)) + self.assertEqual(d, myutilstest.tempdir, 'tmpdir is stable') + myutilstest.clean_up() + self.assertFalse(os.path.isdir(d)) + e = myutilstest.tempdir + self.assertTrue(os.path.isdir(e)) + self.assertEqual(e, myutilstest.tempdir, 'tmpdir is stable') + self.assertNotEqual(d, e, "tempdir changed") + myutilstest.clean_up() + + def test_download(self): + "test the download from silx.org" + f = utilstest.getfile("lena.png") + self.assertTrue(os.path.exists(f)) + f = utilstest.getdir("source.tar.gz") + self.assertTrue(os.path.isfile(f)) + self.assertTrue(os.path.isdir(f[:-7])) + + def test_dowload_all(self): + "test the download of all files from silx.org" + l = utilstest.download_all() + self.assertGreater(len(l), 1, "At least 2 items were downloaded") + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestResources)) + test_suite.addTest(TestExternalResources("test_tempdir")) + test_suite.addTest(TestExternalResources("test_download")) # order matters ! + test_suite.addTest(TestExternalResources("test_dowload_all")) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/test/test_sx.py b/silx/test/test_sx.py new file mode 100644 index 0000000..0de3b35 --- /dev/null +++ b/silx/test/test_sx.py @@ -0,0 +1,174 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +__authors__ = ["T. Vincent", "P. Knobel"] +__license__ = "MIT" +__date__ = "05/12/2016" + + +import logging +import os +import sys +import unittest + +import numpy + + +_logger = logging.getLogger(__name__) + + +if sys.platform.startswith('linux') and not os.environ.get('DISPLAY', ''): + # On linux and no DISPLAY available (e.g., ssh without -X) + _logger.warning('silx.sx tests disabled (DISPLAY env. variable not set)') + + class SkipSXTest(unittest.TestCase): + def runTest(self): + self.skipTest( + 'silx.sx tests disabled (DISPLAY env. variable not set)') + + def suite(): + suite = unittest.TestSuite() + suite.addTest(SkipSXTest()) + return suite + +elif os.environ.get('WITH_QT_TEST', 'True') == 'False': + # Explicitly disabled tests + _logger.warning( + "silx.sx tests disabled (env. variable WITH_QT_TEST=False)") + + class SkipSXTest(unittest.TestCase): + def runTest(self): + self.skipTest( + "silx.sx tests disabled (env. variable WITH_QT_TEST=False)") + + def suite(): + suite = unittest.TestSuite() + suite.addTest(SkipSXTest()) + return suite + +else: + # Import here to avoid loading QT if tests are disabled + + from silx.gui import qt + # load TestCaseQt before sx + from silx.gui.test.utils import TestCaseQt + from silx import sx + + class SXTest(TestCaseQt): + """Test the sx module""" + + def _expose_and_close(self, plot): + self.qWaitForWindowExposed(plot) + self.qapp.processEvents() + plot.setAttribute(qt.Qt.WA_DeleteOnClose) + plot.close() + + def test_plot(self): + """Test plot function""" + y = numpy.random.random(100) + x = numpy.arange(len(y)) * 0.5 + + # Nothing + plt = sx.plot() + self._expose_and_close(plt) + + # y + plt = sx.plot(y, title='y') + self._expose_and_close(plt) + + # y, style + plt = sx.plot(y, 'blued ', title='y, "blued "') + self._expose_and_close(plt) + + # x, y + plt = sx.plot(x, y, title='x, y') + self._expose_and_close(plt) + + # x, y, style + plt = sx.plot(x, y, 'ro-', xlabel='x', title='x, y, "ro-"') + self._expose_and_close(plt) + + # x, y, style, y + plt = sx.plot(x, y, 'ro-', y ** 2, xlabel='x', ylabel='y', + title='x, y, "ro-", y ** 2') + self._expose_and_close(plt) + + # x, y, style, y, style + plt = sx.plot(x, y, 'ro-', y ** 2, 'b--', + title='x, y, "ro-", y ** 2, "b--"') + self._expose_and_close(plt) + + # x, y, style, x, y, style + plt = sx.plot(x, y, 'ro-', x, y ** 2, 'b--', + title='x, y, "ro-", x, y ** 2, "b--"') + self._expose_and_close(plt) + + # x, y, x, y + plt = sx.plot(x, y, x, y ** 2, title='x, y, x, y ** 2') + self._expose_and_close(plt) + + def test_imshow(self): + """Test imshow function""" + img = numpy.arange(100.).reshape(10, 10) + 1 + + # Nothing + plt = sx.imshow() + self._expose_and_close(plt) + + # image + plt = sx.imshow(img) + self._expose_and_close(plt) + + # image, gray cmap + plt = sx.imshow(img, cmap='jet', title='jet cmap') + self._expose_and_close(plt) + + # image, log cmap + plt = sx.imshow(img, norm='log', title='log cmap') + self._expose_and_close(plt) + + # image, fixed range + plt = sx.imshow(img, vmin=10, vmax=20, + title='[10,20] cmap') + self._expose_and_close(plt) + + # image, keep ratio + plt = sx.imshow(img, aspect=True, + title='keep ratio') + self._expose_and_close(plt) + + # image, change origin and scale + plt = sx.imshow(img, origin=(10, 10), scale=(2, 2), + title='origin=(10, 10), scale=(2, 2)') + self._expose_and_close(plt) + + def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(SXTest)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/test/test_version.py b/silx/test/test_version.py new file mode 100644 index 0000000..bb91e4e --- /dev/null +++ b/silx/test/test_version.py @@ -0,0 +1,49 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2015-2016 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Basic test of top-level package import and existence of version info.""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "26/02/2016" + +import unittest + +import silx + + +class TestVersion(unittest.TestCase): + def test_version(self): + self.assertTrue(isinstance(silx.version, str)) + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestVersion)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/test/utils.py b/silx/test/utils.py new file mode 100644 index 0000000..ec86a2a --- /dev/null +++ b/silx/test/utils.py @@ -0,0 +1,284 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Utilities for writing tests. + +- :class:`ParametricTestCase` provides a :meth:`TestCase.subTest` replacement + for Python < 3.4 +- :class:`TestLogging` with context or the :func:`test_logging` decorator + enables testing the number of logging messages of different levels. +- :func:`temp_dir` provides a with context to create/delete a temporary + directory. +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "20/04/2017" + + +import os +import contextlib +import functools +import logging +import numpy +import shutil +import sys +import tempfile +import unittest +from ..resources import ExternalResources + +logger = logging.getLogger(__name__) + +utilstest = ExternalResources(project="silx", + url_base="http://www.silx.org/pub/silx/", + env_key="SILX_DATA", + timeout=60) +"This is the instance to be used. Singleton-like feature provided by module" + +# Parametric Test Base Class ################################################## + +if sys.hexversion >= 0x030400F0: # Python >= 3.4 + class ParametricTestCase(unittest.TestCase): + pass + +else: + class ParametricTestCase(unittest.TestCase): + """TestCase with subTest support for Python < 3.4. + + Add subTest method to support parametric tests. + API is the same, but behavior differs: + If a subTest fails, the following ones are not run. + """ + + _subtest_msg = None # Class attribute to provide a default value + + @contextlib.contextmanager + def subTest(self, msg=None, **params): + """Use as unittest.TestCase.subTest method in Python >= 3.4.""" + # Format arguments as: '[msg] (key=value, ...)' + param_str = ', '.join(['%s=%s' % (k, v) for k, v in params.items()]) + self._subtest_msg = '[%s] (%s)' % (msg or '', param_str) + yield + self._subtest_msg = None + + def shortDescription(self): + short_desc = super(ParametricTestCase, self).shortDescription() + if self._subtest_msg is not None: + # Append subTest message to shortDescription + short_desc = ' '.join( + [msg for msg in (short_desc, self._subtest_msg) if msg]) + + return short_desc if short_desc else None + + +# Test logging messages ####################################################### + +class TestLogging(logging.Handler): + """Context checking the number of logging messages from a specified Logger. + + It disables propagation of logging message while running. + + This is meant to be used as a with statement, for example: + + >>> with TestLogging(logger, error=2, warning=0): + >>> pass # Run tests here expecting 2 ERROR and no WARNING from logger + ... + + :param logger: Name or instance of the logger to test. + (Default: root logger) + :type logger: str or :class:`logging.Logger` + :param int critical: Expected number of CRITICAL messages. + Default: Do not check. + :param int error: Expected number of ERROR messages. + Default: Do not check. + :param int warning: Expected number of WARNING messages. + Default: Do not check. + :param int info: Expected number of INFO messages. + Default: Do not check. + :param int debug: Expected number of DEBUG messages. + Default: Do not check. + :param int notset: Expected number of NOTSET messages. + Default: Do not check. + :raises RuntimeError: If the message counts are the expected ones. + """ + + def __init__(self, logger=None, critical=None, error=None, + warning=None, info=None, debug=None, notset=None): + if logger is None: + logger = logging.getLogger() + elif not isinstance(logger, logging.Logger): + logger = logging.getLogger(logger) + self.logger = logger + + self.records = [] + + self.count_by_level = { + logging.CRITICAL: critical, + logging.ERROR: error, + logging.WARNING: warning, + logging.INFO: info, + logging.DEBUG: debug, + logging.NOTSET: notset + } + + super(TestLogging, self).__init__() + + def __enter__(self): + """Context (i.e., with) support""" + self.records = [] # Reset recorded LogRecords + self.logger.addHandler(self) + self.logger.propagate = False + + def __exit__(self, exc_type, exc_value, traceback): + """Context (i.e., with) support""" + self.logger.removeHandler(self) + self.logger.propagate = True + + for level, expected_count in self.count_by_level.items(): + if expected_count is None: + continue + + # Number of records for the specified level_str + count = len([r for r in self.records if r.levelno == level]) + if count != expected_count: # That's an error + # Resend record logs through logger as they where masked + # to help debug + for record in self.records: + self.logger.handle(record) + raise RuntimeError( + 'Expected %d %s logging messages, got %d' % ( + expected_count, logging.getLevelName(level), count)) + + def emit(self, record): + """Override :meth:`logging.Handler.emit`""" + self.records.append(record) + + +def test_logging(logger=None, critical=None, error=None, + warning=None, info=None, debug=None, notset=None): + """Decorator checking number of logging messages. + + Propagation of logging messages is disabled by this decorator. + + In case the expected number of logging messages is not found, it raises + a RuntimeError. + + >>> class Test(unittest.TestCase): + ... @test_logging('module_logger_name', error=2, warning=0) + ... def test(self): + ... pass # Test expecting 2 ERROR and 0 WARNING messages + + :param logger: Name or instance of the logger to test. + (Default: root logger) + :type logger: str or :class:`logging.Logger` + :param int critical: Expected number of CRITICAL messages. + Default: Do not check. + :param int error: Expected number of ERROR messages. + Default: Do not check. + :param int warning: Expected number of WARNING messages. + Default: Do not check. + :param int info: Expected number of INFO messages. + Default: Do not check. + :param int debug: Expected number of DEBUG messages. + Default: Do not check. + :param int notset: Expected number of NOTSET messages. + Default: Do not check. + """ + def decorator(func): + test_context = TestLogging(logger, critical, error, + warning, info, debug, notset) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with test_context: + result = func(*args, **kwargs) + return result + return wrapper + return decorator + + + + + + +# Temporary directory context ################################################# + +@contextlib.contextmanager +def temp_dir(): + """with context providing a temporary directory. + + >>> import os.path + >>> with temp_dir() as tmp: + ... print(os.path.isdir(tmp)) # Use tmp directory + """ + tmp_dir = tempfile.mkdtemp() + try: + yield tmp_dir + finally: + shutil.rmtree(tmp_dir) + + +# Synthetic data and random noise ############################################# +def add_gaussian_noise(y, stdev=1., mean=0.): + """Add random gaussian noise to synthetic data. + + :param ndarray y: Array of synthetic data + :param float mean: Mean of the gaussian distribution of noise. + :param float stdev: Standard deviation of the gaussian distribution of + noise. + :return: Array of data with noise added + """ + noise = numpy.random.normal(mean, stdev, size=y.size) + noise.shape = y.shape + return y + noise + + +def add_poisson_noise(y): + """Add random noise from a poisson distribution to synthetic data. + + :param ndarray y: Array of synthetic data + :return: Array of data with noise added + """ + yn = numpy.random.poisson(y) + yn.shape = y.shape + return yn + + +def add_relative_noise(y, max_noise=5.): + """Add relative random noise to synthetic data. The maximum noise level + is given in percents. + + An array of noise in the interval [-max_noise, max_noise] (continuous + uniform distribution) is generated, and applied to the data the + following way: + + :math:`yn = y * (1. + noise / 100.)` + + :param ndarray y: Array of synthetic data + :param float max_noise: Maximum percentage of noise + :return: Array of data with noise added + """ + noise = max_noise * (2 * numpy.random.random(size=y.size) - 1) + noise.shape = y.shape + return y * (1. + noise / 100.) |