diff options
author | Picca Frédéric-Emmanuel <picca@debian.org> | 2018-03-04 10:20:27 +0100 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@debian.org> | 2018-03-04 10:20:27 +0100 |
commit | 270d5ddc31c26b62379e3caa9044dd75ccc71847 (patch) | |
tree | 55c5bfc851dfce7172d335cd2405b214323e3caf /silx/gui/test | |
parent | e19c96eff0c310c06c4f268c8b80cb33bd08996f (diff) |
New upstream version 0.7.0+dfsg
Diffstat (limited to 'silx/gui/test')
-rw-r--r-- | silx/gui/test/__init__.py | 15 | ||||
-rw-r--r-- | silx/gui/test/test_utils.py | 91 | ||||
-rw-r--r-- | silx/gui/test/utils.py | 88 |
3 files changed, 174 insertions, 20 deletions
diff --git a/silx/gui/test/__init__.py b/silx/gui/test/__init__.py index 7449860..0d0805f 100644 --- a/silx/gui/test/__init__.py +++ b/silx/gui/test/__init__.py @@ -24,13 +24,14 @@ # ###########################################################################*/ __authors__ = ["T. Vincent", "P. Knobel"] __license__ = "MIT" -__date__ = "05/01/2017" +__date__ = "28/11/2017" import logging import os import sys import unittest +from silx.test.utils import test_options _logger = logging.getLogger(__name__) @@ -52,15 +53,14 @@ def suite(): test_suite.addTest(SkipGUITest()) return test_suite - elif os.environ.get('WITH_QT_TEST', 'True') == 'False': + elif not test_options.WITH_QT_TEST: # Explicitly disabled tests - _logger.warning( - "silx.gui tests disabled (env. variable WITH_QT_TEST=False)") + msg = "silx.gui tests disabled: %s" % test_options.WITH_QT_TEST_REASON + _logger.warning(msg) class SkipGUITest(unittest.TestCase): def runTest(self): - self.skipTest( - "silx.gui tests disabled (env. variable WITH_QT_TEST=False)") + self.skipTest(test_options.WITH_QT_TEST_REASON) test_suite.addTest(SkipGUITest()) return test_suite @@ -72,6 +72,7 @@ def suite(): from ..hdf5 import test as test_hdf5 from ..widgets import test as test_widgets from ..data import test as test_data + from ..dialog import test as test_dialog from . import test_qt # Console tests disabled due to corruption of python environment # (see issue #538 on github) @@ -94,7 +95,6 @@ def suite(): test_plot3d_suite = SkipPlot3DTest - test_suite.addTest(test_qt.suite()) test_suite.addTest(test_plot.suite()) test_suite.addTest(test_fit.suite()) @@ -105,4 +105,5 @@ def suite(): test_suite.addTest(test_data.suite()) test_suite.addTest(test_utils.suite()) test_suite.addTest(test_plot3d_suite()) + test_suite.addTest(test_dialog.suite()) return test_suite diff --git a/silx/gui/test/test_utils.py b/silx/gui/test/test_utils.py index 4625969..b1cdf0f 100644 --- a/silx/gui/test/test_utils.py +++ b/silx/gui/test/test_utils.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017 European Synchrotron Radiation Facility +# Copyright (c) 2017-2018 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 @@ -29,10 +29,12 @@ __license__ = "MIT" __date__ = "16/01/2017" +import threading import unittest import numpy +from silx.third_party.concurrent_futures import wait from silx.gui import qt from silx.gui.test.utils import TestCaseQt @@ -66,10 +68,97 @@ class TestQImageConversion(TestCaseQt): self.assertTrue(numpy.all(numpy.equal(image, 1))) +class TestSubmitToQtThread(TestCaseQt): + """Test submission of tasks to Qt main thread""" + + def setUp(self): + # Reset executor to test lazy-loading in different conditions + _utils._executor = None + super(TestSubmitToQtThread, self).setUp() + + def _task(self, value1, value2): + return value1, value2 + + def _taskWithException(self, *args, **kwargs): + raise RuntimeError('task exception') + + def testFromMainThread(self): + """Call submitToQtMainThread from the main thread""" + value1, value2 = 0, 1 + future = _utils.submitToQtMainThread(self._task, value1, value2=value2) + self.assertTrue(future.done()) + self.assertEqual(future.result(1), (value1, value2)) + self.assertIsNone(future.exception(1)) + + future = _utils.submitToQtMainThread(self._taskWithException) + self.assertTrue(future.done()) + with self.assertRaises(RuntimeError): + future.result(1) + self.assertIsInstance(future.exception(1), RuntimeError) + + def _threadedTest(self): + """Function run in a thread for the tests""" + value1, value2 = 0, 1 + future = _utils.submitToQtMainThread(self._task, value1, value2=value2) + + wait([future], 3) + + self.assertTrue(future.done()) + self.assertEqual(future.result(1), (value1, value2)) + self.assertIsNone(future.exception(1)) + + future = _utils.submitToQtMainThread(self._taskWithException) + + wait([future], 3) + + self.assertTrue(future.done()) + with self.assertRaises(RuntimeError): + future.result(1) + self.assertIsInstance(future.exception(1), RuntimeError) + + def testFromPythonThread(self): + """Call submitToQtMainThread from a Python thread""" + thread = threading.Thread(target=self._threadedTest) + thread.start() + for i in range(100): # Loop over for 10 seconds + self.qapp.processEvents() + thread.join(0.1) + if not thread.is_alive(): + break + else: + self.fail(('Thread task still running')) + + def testFromQtThread(self): + """Call submitToQtMainThread from a Qt thread pool""" + class Runner(qt.QRunnable): + def __init__(self, fn): + super(Runner, self).__init__() + self._fn = fn + + def run(self): + self._fn() + + def autoDelete(self): + return True + + threadPool = qt.silxGlobalThreadPool() + runner = Runner(self._threadedTest) + threadPool.start(runner) + for i in range(100): # Loop over for 10 seconds + self.qapp.processEvents() + done = threadPool.waitForDone(100) + if done: + break + else: + self.fail('Thread pool task still running') + + def suite(): test_suite = unittest.TestSuite() test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase( TestQImageConversion)) + test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase( + TestSubmitToQtThread)) return test_suite diff --git a/silx/gui/test/utils.py b/silx/gui/test/utils.py index 19c448a..3eee474 100644 --- a/silx/gui/test/utils.py +++ b/silx/gui/test/utils.py @@ -26,7 +26,7 @@ __authors__ = ["T. Vincent"] __license__ = "MIT" -__date__ = "01/09/2017" +__date__ = "27/02/2018" import gc @@ -43,6 +43,8 @@ from silx.gui import qt if qt.BINDING == 'PySide': from PySide.QtTest import QTest +elif qt.BINDING == 'PySide2': + from PySide2.QtTest import QTest elif qt.BINDING == 'PyQt5': from PyQt5.QtTest import QTest elif qt.BINDING == 'PyQt4': @@ -137,11 +139,9 @@ class TestCaseQt(unittest.TestCase): # Makes sure a QApplication exists and do it once for all _qapp = qt.QApplication.instance() or qt.QApplication([]) - # Create/delate a QWidget to make sure init of QDesktopWidget - _dummyWidget = qt.QWidget() - _dummyWidget.setAttribute(qt.Qt.WA_DeleteOnClose) - _dummyWidget.show() - _dummyWidget.close() + # Makes sure QDesktopWidget is init + # Otherwise it happens randomly during the tests + cls._desktopWidget = _qapp.desktop() _qapp.processEvents() @classmethod @@ -163,9 +163,10 @@ class TestCaseQt(unittest.TestCase): # For Python < 3.4 result = getattr(self, '_outcomeForDoCleanups', self._resultForDoCleanups) + skipped = self.id() in [case.id() for case, _ in result.skipped] error = self.id() in [case.id() for case, _ in result.errors] failure = self.id() in [case.id() for case, _ in result.failures] - return not error and not failure + return not error and not failure and not skipped def _checkForUnreleasedWidgets(self): """Test fixture checking that no more widgets exists.""" @@ -175,7 +176,7 @@ class TestCaseQt(unittest.TestCase): if widget not in self.__previousWidgets] del self.__previousWidgets - if qt.BINDING == 'PySide': + if qt.BINDING in ('PySide', 'PySide2'): return # Do not test for leaking widgets with PySide allowedLeakingWidgets = self.allowedLeakingWidgets @@ -318,24 +319,25 @@ class TestCaseQt(unittest.TestCase): """ QTest.qSleep(ms + self.TIMEOUT_WAIT) - def qWait(self, ms=None): + @classmethod + def qWait(cls, ms=None): """Waits for ms milliseconds, events will be processed. See QTest.qWait for details. """ if ms is None: - ms = self.DEFAULT_TIMEOUT_WAIT + ms = cls.DEFAULT_TIMEOUT_WAIT if qt.BINDING == 'PySide': # PySide has no qWait, provide a replacement timeout = int(ms) endTimeMS = int(time.time() * 1000) + timeout while timeout > 0: - self.qapp.processEvents(qt.QEventLoop.AllEvents, + _qapp.processEvents(qt.QEventLoop.AllEvents, maxtime=timeout) timeout = endTimeMS - int(time.time() * 1000) else: - QTest.qWait(ms + self.TIMEOUT_WAIT) + QTest.qWait(ms + cls.TIMEOUT_WAIT) def qWaitForWindowExposed(self, window, timeout=None): """Waits until the window is shown in the screen. @@ -349,6 +351,57 @@ class TestCaseQt(unittest.TestCase): return result + _qobject_destroyed = False + + @classmethod + def _aboutToDestroy(cls): + cls._qobject_destroyed = True + + @classmethod + def qWaitForDestroy(cls, ref): + """ + Wait for Qt object destruction. + + Use a weakref as parameter to avoid any strong references to the + object. + + It have to be used as following. Removing the reference to the object + before calling the function looks to be expected, else + :meth:`deleteLater` will not work. + + .. code-block:: python + + ref = weakref.ref(self.obj) + self.obj = None + self.qWaitForDestroy(ref) + + :param weakref ref: A weakref to an object to avoid any reference + :return: True if the object was destroyed + :rtype: bool + """ + cls._qobject_destroyed = False + if qt.BINDING == 'PyQt4': + # Without this, QWidget will be still alive on PyQt4 + # (at least on Windows Python 2.7) + # If it is not skipped on PySide, silx.gui.dialog tests will + # segfault (at least on Windows Python 2.7) + import gc + gc.collect() + qobject = ref() + if qobject is None: + return True + qobject.destroyed.connect(cls._aboutToDestroy) + qobject.deleteLater() + qobject = None + for _ in range(10): + if cls._qobject_destroyed: + break + cls.qWait(10) + else: + _logger.debug("Object was not destroyed") + + return ref() is None + def logScreenShot(self, level=logging.ERROR): """Take a screenshot and log it into the logging system if the logger is enabled for the expected level. @@ -454,3 +507,14 @@ def getQToolButtonFromAction(action): if isinstance(widget, qt.QToolButton): return widget return None + + +def findChildren(parent, kind, name=None): + if qt.BINDING == "PySide" and name is not None: + result = [] + for obj in parent.findChildren(kind): + if obj.objectName() == name: + result.append(obj) + return result + else: + return parent.findChildren(kind, name=name) |