summaryrefslogtreecommitdiff
path: root/silx/gui/test
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2018-03-04 10:20:27 +0100
committerPicca Frédéric-Emmanuel <picca@debian.org>2018-03-04 10:20:27 +0100
commit270d5ddc31c26b62379e3caa9044dd75ccc71847 (patch)
tree55c5bfc851dfce7172d335cd2405b214323e3caf /silx/gui/test
parente19c96eff0c310c06c4f268c8b80cb33bd08996f (diff)
New upstream version 0.7.0+dfsg
Diffstat (limited to 'silx/gui/test')
-rw-r--r--silx/gui/test/__init__.py15
-rw-r--r--silx/gui/test/test_utils.py91
-rw-r--r--silx/gui/test/utils.py88
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)