summaryrefslogtreecommitdiff
path: root/silx/gui/test
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/test')
-rw-r--r--silx/gui/test/__init__.py108
-rw-r--r--silx/gui/test/test_console.py91
-rw-r--r--silx/gui/test/test_icons.py116
-rw-r--r--silx/gui/test/test_qt.py144
-rw-r--r--silx/gui/test/test_utils.py77
-rw-r--r--silx/gui/test/utils.py428
6 files changed, 964 insertions, 0 deletions
diff --git a/silx/gui/test/__init__.py b/silx/gui/test/__init__.py
new file mode 100644
index 0000000..7449860
--- /dev/null
+++ b/silx/gui/test/__init__.py
@@ -0,0 +1,108 @@
+# 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/01/2017"
+
+
+import logging
+import os
+import sys
+import unittest
+
+
+_logger = logging.getLogger(__name__)
+
+
+def suite():
+
+ test_suite = unittest.TestSuite()
+
+ 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.gui tests disabled (DISPLAY env. variable not set)')
+
+ class SkipGUITest(unittest.TestCase):
+ def runTest(self):
+ self.skipTest(
+ 'silx.gui tests disabled (DISPLAY env. variable not set)')
+
+ test_suite.addTest(SkipGUITest())
+ return test_suite
+
+ elif os.environ.get('WITH_QT_TEST', 'True') == 'False':
+ # Explicitly disabled tests
+ _logger.warning(
+ "silx.gui tests disabled (env. variable WITH_QT_TEST=False)")
+
+ class SkipGUITest(unittest.TestCase):
+ def runTest(self):
+ self.skipTest(
+ "silx.gui tests disabled (env. variable WITH_QT_TEST=False)")
+
+ test_suite.addTest(SkipGUITest())
+ return test_suite
+
+ # Import here to avoid loading QT if tests are disabled
+
+ from ..plot import test as test_plot
+ from ..fit import test as test_fit
+ from ..hdf5 import test as test_hdf5
+ from ..widgets import test as test_widgets
+ from ..data import test as test_data
+ from . import test_qt
+ # Console tests disabled due to corruption of python environment
+ # (see issue #538 on github)
+ # from . import test_console
+ from . import test_icons
+ from . import test_utils
+
+ try:
+ from ..plot3d.test import suite as test_plot3d_suite
+
+ except ImportError:
+ _logger.warning(
+ 'silx.gui.plot3d tests disabled '
+ '(PyOpenGL or QtOpenGL not installed)')
+
+ class SkipPlot3DTest(unittest.TestCase):
+ def runTest(self):
+ self.skipTest('silx.gui.plot3d tests disabled '
+ '(PyOpenGL or QtOpenGL not installed)')
+
+ test_plot3d_suite = SkipPlot3DTest
+
+
+ test_suite.addTest(test_qt.suite())
+ test_suite.addTest(test_plot.suite())
+ test_suite.addTest(test_fit.suite())
+ test_suite.addTest(test_hdf5.suite())
+ test_suite.addTest(test_widgets.suite())
+ # test_suite.addTest(test_console.suite()) # see issue #538 on github
+ test_suite.addTest(test_icons.suite())
+ test_suite.addTest(test_data.suite())
+ test_suite.addTest(test_utils.suite())
+ test_suite.addTest(test_plot3d_suite())
+ return test_suite
diff --git a/silx/gui/test/test_console.py b/silx/gui/test/test_console.py
new file mode 100644
index 0000000..7c25372
--- /dev/null
+++ b/silx/gui/test/test_console.py
@@ -0,0 +1,91 @@
+# 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.
+#
+# ###########################################################################*/
+"""Basic tests for IPython console widget"""
+
+from __future__ import print_function
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "05/12/2016"
+
+
+import unittest
+
+from silx.gui.test.utils import TestCaseQt
+
+from silx.gui import qt
+try:
+ from silx.gui.console import IPythonDockWidget
+except ImportError:
+ console_missing = True
+else:
+ console_missing = False
+
+
+# dummy objects to test pushing variables to the interactive namespace
+_a = 1
+
+
+def _f():
+ print("Hello World!")
+
+
+@unittest.skipIf(console_missing, "Could not import Ipython and/or qtconsole")
+class TestConsole(TestCaseQt):
+ """Basic test for ``module.IPythonDockWidget``"""
+
+ def setUp(self):
+ super(TestConsole, self).setUp()
+ self.console = IPythonDockWidget(
+ available_vars={"a": _a, "f": _f},
+ custom_banner="Welcome!\n")
+ self.console.show()
+ self.qWaitForWindowExposed(self.console)
+
+ def tearDown(self):
+ self.console.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.console.close()
+ del self.console
+ super(TestConsole, self).tearDown()
+
+ def testShow(self):
+ pass
+
+ def testInteract(self):
+ self.mouseClick(self.console, qt.Qt.LeftButton)
+ self.keyClicks(self.console, 'import silx')
+ self.keyClick(self.console, qt.Qt.Key_Enter)
+ self.qapp.processEvents()
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(TestConsole))
+ return test_suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/silx/gui/test/test_icons.py b/silx/gui/test/test_icons.py
new file mode 100644
index 0000000..f363c43
--- /dev/null
+++ b/silx/gui/test/test_icons.py
@@ -0,0 +1,116 @@
+# 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.
+#
+# ###########################################################################*/
+"""Basic test of Qt icons module."""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "26/04/2017"
+
+
+import gc
+import unittest
+import weakref
+
+from silx.gui import qt
+from silx.gui.test.utils import TestCaseQt
+from silx.gui import icons
+
+
+class TestIcons(TestCaseQt):
+ """Test to check that icons module."""
+
+ def testSvgIcon(self):
+ if "svg" not in qt.supportedImageFormats():
+ self.skipTest("SVG not supported")
+ icon = icons.getQIcon("test-svg")
+ self.assertIsNotNone(icon)
+
+ def testPngIcon(self):
+ icon = icons.getQIcon("test-png")
+ self.assertIsNotNone(icon)
+
+ def testUnexistingIcon(self):
+ self.assertRaises(ValueError, icons.getQIcon, "not-exists")
+
+ def testExistingQPixmap(self):
+ icon = icons.getQPixmap("crop")
+ self.assertIsNotNone(icon)
+
+ def testUnexistingQPixmap(self):
+ self.assertRaises(ValueError, icons.getQPixmap, "not-exists")
+
+ def testCache(self):
+ icon1 = icons.getQIcon("crop")
+ icon2 = icons.getQIcon("crop")
+ self.assertIs(icon1, icon2)
+
+ def testCacheReleased(self):
+ icon = icons.getQIcon("crop")
+ icon_ref = weakref.ref(icon)
+ del icon
+ gc.collect()
+ self.assertIsNone(icon_ref())
+
+
+class TestAnimatedIcons(TestCaseQt):
+ """Test to check that icons module."""
+
+ def testProcessWorking(self):
+ icon = icons.getWaitIcon()
+ self.assertIsNotNone(icon)
+
+ def testProcessWorkingCache(self):
+ icon1 = icons.getWaitIcon()
+ icon2 = icons.getWaitIcon()
+ self.assertIs(icon1, icon2)
+
+ def testMovieIconExists(self):
+ if "mng" not in qt.supportedImageFormats():
+ self.skipTest("MNG not supported")
+ icon = icons.MovieAnimatedIcon("process-working")
+ self.assertIsNotNone(icon)
+
+ def testMovieIconNotExists(self):
+ self.assertRaises(ValueError, icons.MovieAnimatedIcon, "not-exists")
+
+ def testMultiImageIconExists(self):
+ icon = icons.MultiImageAnimatedIcon("process-working")
+ self.assertIsNotNone(icon)
+
+ def testMultiImageIconNotExists(self):
+ self.assertRaises(ValueError, icons.MultiImageAnimatedIcon, "not-exists")
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(TestIcons))
+ test_suite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(TestAnimatedIcons))
+ return test_suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/silx/gui/test/test_qt.py b/silx/gui/test/test_qt.py
new file mode 100644
index 0000000..3a89a33
--- /dev/null
+++ b/silx/gui/test/test_qt.py
@@ -0,0 +1,144 @@
+# 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.
+#
+# ###########################################################################*/
+"""Basic test of Qt bindings wrapper."""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "05/12/2016"
+
+
+import os.path
+import unittest
+
+from silx.test.utils import temp_dir
+from silx.gui.test.utils import TestCaseQt
+
+from silx.gui import qt
+
+
+class TestQtWrapper(unittest.TestCase):
+ """Minimalistic test to check that Qt has been loaded."""
+
+ def testQObject(self):
+ """Test that QObject is there."""
+ obj = qt.QObject()
+ self.assertTrue(obj is not None)
+
+
+class TestLoadUi(TestCaseQt):
+ """Test loadUi function"""
+
+ TEST_UI = """<?xml version="1.0" encoding="UTF-8"?>
+ <ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>293</width>
+ <height>296</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Test loadUi</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <widget class="QPushButton" name="pushButton">
+ <property name="geometry">
+ <rect>
+ <x>10</x>
+ <y>10</y>
+ <width>89</width>
+ <height>27</height>
+ </rect>
+ </property>
+ <property name="text">
+ <string>Button 1</string>
+ </property>
+ </widget>
+ <widget class="QPushButton" name="pushButton_2">
+ <property name="geometry">
+ <rect>
+ <x>10</x>
+ <y>50</y>
+ <width>89</width>
+ <height>27</height>
+ </rect>
+ </property>
+ <property name="text">
+ <string>Button 2</string>
+ </property>
+ </widget>
+ </widget>
+ <widget class="QMenuBar" name="menubar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>293</width>
+ <height>25</height>
+ </rect>
+ </property>
+ </widget>
+ <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <resources/>
+ <connections/>
+ </ui>
+ """
+
+ def testLoadUi(self):
+ """Create a QMainWindow from an ui file"""
+ with temp_dir() as tmp:
+ uifile = os.path.join(tmp, "test.ui")
+
+ # write file
+ with open(uifile, mode='w') as f:
+ f.write(self.TEST_UI)
+
+ class TestMainWindow(qt.QMainWindow):
+ def __init__(self, parent=None):
+ super(TestMainWindow, self).__init__(parent)
+ qt.loadUi(uifile, self)
+
+ testMainWindow = TestMainWindow()
+ testMainWindow.show()
+ self.qWaitForWindowExposed(testMainWindow)
+
+ testMainWindow.setAttribute(qt.Qt.WA_DeleteOnClose)
+ testMainWindow.close()
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ for TestCaseCls in (TestQtWrapper, TestLoadUi):
+ test_suite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(TestCaseCls))
+ return test_suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/silx/gui/test/test_utils.py b/silx/gui/test/test_utils.py
new file mode 100644
index 0000000..4625969
--- /dev/null
+++ b/silx/gui/test/test_utils.py
@@ -0,0 +1,77 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 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.
+#
+# ###########################################################################*/
+"""Test of utils module."""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "16/01/2017"
+
+
+import unittest
+
+import numpy
+
+from silx.gui import qt
+from silx.gui.test.utils import TestCaseQt
+
+from silx.gui import _utils
+
+
+class TestQImageConversion(TestCaseQt):
+ """Tests conversion of QImage to/from numpy array."""
+
+ def testConvertArrayToQImage(self):
+ """Test conversion of numpy array to QImage"""
+ image = numpy.ones((3, 3, 3), dtype=numpy.uint8)
+ qimage = _utils.convertArrayToQImage(image)
+
+ self.assertEqual(qimage.height(), image.shape[0])
+ self.assertEqual(qimage.width(), image.shape[1])
+ self.assertEqual(qimage.format(), qt.QImage.Format_RGB888)
+
+ color = qt.QColor(1, 1, 1).rgb()
+ self.assertEqual(qimage.pixel(1, 1), color)
+
+ def testConvertQImageToArray(self):
+ """Test conversion of QImage to numpy array"""
+ qimage = qt.QImage(3, 3, qt.QImage.Format_RGB888)
+ qimage.fill(0x010101)
+ image = _utils.convertQImageToArray(qimage)
+
+ self.assertEqual(qimage.height(), image.shape[0])
+ self.assertEqual(qimage.width(), image.shape[1])
+ self.assertEqual(image.shape[2], 3)
+ self.assertTrue(numpy.all(numpy.equal(image, 1)))
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(
+ TestQImageConversion))
+ return test_suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/silx/gui/test/utils.py b/silx/gui/test/utils.py
new file mode 100644
index 0000000..50cf7bf
--- /dev/null
+++ b/silx/gui/test/utils.py
@@ -0,0 +1,428 @@
+# 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.
+#
+# ###########################################################################*/
+"""Helper class to write Qt widget unittests."""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "11/04/2017"
+
+
+import gc
+import logging
+import unittest
+import time
+import functools
+import sys
+
+logging.basicConfig()
+_logger = logging.getLogger(__name__)
+
+from silx.gui import qt
+
+if qt.BINDING == 'PySide':
+ from PySide.QtTest import QTest
+elif qt.BINDING == 'PyQt5':
+ from PyQt5.QtTest import QTest
+elif qt.BINDING == 'PyQt4':
+ from PyQt4.QtTest import QTest
+else:
+ raise ImportError('Unsupported Qt bindings')
+
+# Qt4/Qt5 compatibility wrapper
+if qt.BINDING in ('PySide', 'PyQt4'):
+ _logger.info("QTest.qWaitForWindowExposed not available," +
+ "using QTest.qWaitForWindowShown instead.")
+
+ def qWaitForWindowExposed(window, timeout=None):
+ """Mimic QTest.qWaitForWindowExposed for Qt4."""
+ QTest.qWaitForWindowShown(window)
+ return True
+else:
+ qWaitForWindowExposed = QTest.qWaitForWindowExposed
+
+
+def qWaitForWindowExposedAndActivate(window, timeout=None):
+ """Waits until the window is shown in the screen.
+
+ It also activates the window and raises it.
+
+ See QTest.qWaitForWindowExposed for details.
+ """
+ if timeout is None:
+ result = qWaitForWindowExposed(window)
+ else:
+ result = qWaitForWindowExposed(window, timeout)
+
+ if result:
+ # Makes sure window is active and on top
+ window.activateWindow()
+ window.raise_()
+
+ return result
+
+
+# Placeholder for QApplication
+_qapp = None
+
+
+class TestCaseQt(unittest.TestCase):
+ """Base class to write test for Qt stuff.
+
+ It creates a QApplication before running the tests.
+ WARNING: The QApplication is shared by all tests, which might have side
+ effects.
+
+ After each test, this class is checking for widgets remaining alive.
+ To allow some widgets to remain alive at the end of a test, set the
+ allowedLeakingWidgets attribute to the number of widgets that can remain
+ alive at the end of the test.
+ With PySide, this test is not run for now as it seems PySide
+ is leaking widgets internally.
+
+ All keyboard and mouse event simulation methods call qWait(20) after
+ simulating the event (as QTest does on Mac OSX).
+ This was introduced to fix issues with continuous integration tests
+ running with Xvfb on Linux.
+ """
+
+ DEFAULT_TIMEOUT_WAIT = 100
+ """Default timeout for qWait"""
+
+ TIMEOUT_WAIT = 0
+ """Extra timeout in millisecond to add to qSleep, qWait and
+ qWaitForWindowExposed.
+
+ Intended purpose is for debugging, to add extra time to waits in order to
+ allow to view the tested widgets.
+ """
+
+ @classmethod
+ def exceptionHandler(cls, exceptionClass, exception, stack):
+ import traceback
+ message = (''.join(traceback.format_tb(stack)))
+ template = 'Traceback (most recent call last):\n{2}{0}: {1}'
+ message = template.format(exceptionClass.__name__, exception, message)
+ cls._exceptions.append(message)
+
+ @classmethod
+ def setUpClass(cls):
+ """Makes sure Qt is inited"""
+ cls._oldExceptionHook = sys.excepthook
+ sys.excepthook = cls.exceptionHandler
+
+ global _qapp
+ if _qapp is None:
+ # 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()
+ _qapp.processEvents()
+
+ @classmethod
+ def tearDownClass(cls):
+ sys.excepthook = cls._oldExceptionHook
+
+ def setUp(self):
+ """Get the list of existing widgets."""
+ self.allowedLeakingWidgets = 0
+ self.__previousWidgets = self.qapp.allWidgets()
+ self.__class__._exceptions = []
+
+ def _currentTestSucceeded(self):
+ if hasattr(self, '_outcome'):
+ # For Python >= 3.4
+ result = self.defaultTestResult() # these 2 methods have no side effects
+ self._feedErrorsToResult(result, self._outcome.errors)
+ else:
+ # For Python < 3.4
+ result = getattr(self, '_outcomeForDoCleanups', self._resultForDoCleanups)
+
+ 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
+
+ def _checkForUnreleasedWidgets(self):
+ """Test fixture checking that no more widgets exists."""
+ gc.collect()
+
+ widgets = [widget for widget in self.qapp.allWidgets()
+ if widget not in self.__previousWidgets]
+ del self.__previousWidgets
+
+ if qt.BINDING == 'PySide':
+ return # Do not test for leaking widgets with PySide
+
+ allowedLeakingWidgets = self.allowedLeakingWidgets
+ self.allowedLeakingWidgets = 0
+
+ if widgets and len(widgets) <= allowedLeakingWidgets:
+ _logger.info(
+ '%s: %d remaining widgets after test' % (self.id(),
+ len(widgets)))
+
+ if len(widgets) > allowedLeakingWidgets:
+ raise RuntimeError(
+ "Test ended with widgets alive: %s" % str(widgets))
+
+ def tearDown(self):
+ if len(self.__class__._exceptions) > 0:
+ messages = "\n".join(self.__class__._exceptions)
+ raise AssertionError("Exception occured in Qt thread:\n" + messages)
+
+ if self._currentTestSucceeded():
+ self._checkForUnreleasedWidgets()
+
+ @property
+ def qapp(self):
+ """The QApplication currently running."""
+ return qt.QApplication.instance()
+
+ # Proxy to QTest
+
+ Press = QTest.Press
+ """Key press action code"""
+
+ Release = QTest.Release
+ """Key release action code"""
+
+ Click = QTest.Click
+ """Key click action code"""
+
+ QTest = property(lambda self: QTest,
+ doc="""The Qt QTest class from the used Qt binding.""")
+
+ def keyClick(self, widget, key, modifier=qt.Qt.NoModifier, delay=-1):
+ """Simulate clicking a key.
+
+ See QTest.keyClick for details.
+ """
+ QTest.keyClick(widget, key, modifier, delay)
+ self.qWait(20)
+
+ def keyClicks(self, widget, sequence, modifier=qt.Qt.NoModifier, delay=-1):
+ """Simulate clicking a sequence of keys.
+
+ See QTest.keyClick for details.
+ """
+ QTest.keyClicks(widget, sequence, modifier, delay)
+ self.qWait(20)
+
+ def keyEvent(self, action, widget, key,
+ modifier=qt.Qt.NoModifier, delay=-1):
+ """Sends a Qt key event.
+
+ See QTest.keyEvent for details.
+ """
+ QTest.keyEvent(action, widget, key, modifier, delay)
+ self.qWait(20)
+
+ def keyPress(self, widget, key, modifier=qt.Qt.NoModifier, delay=-1):
+ """Sends a Qt key press event.
+
+ See QTest.keyPress for details.
+ """
+ QTest.keyPress(widget, key, modifier, delay)
+ self.qWait(20)
+
+ def keyRelease(self, widget, key, modifier=qt.Qt.NoModifier, delay=-1):
+ """Sends a Qt key release event.
+
+ See QTest.keyRelease for details.
+ """
+ QTest.keyRelease(widget, key, modifier, delay)
+ self.qWait(20)
+
+ def mouseClick(self, widget, button, modifier=None, pos=None, delay=-1):
+ """Simulate clicking a mouse button.
+
+ See QTest.mouseClick for details.
+ """
+ if modifier is None:
+ modifier = qt.Qt.KeyboardModifiers()
+ pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint()
+ QTest.mouseClick(widget, button, modifier, pos, delay)
+ self.qWait(20)
+
+ def mouseDClick(self, widget, button, modifier=None, pos=None, delay=-1):
+ """Simulate double clicking a mouse button.
+
+ See QTest.mouseDClick for details.
+ """
+ if modifier is None:
+ modifier = qt.Qt.KeyboardModifiers()
+ pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint()
+ QTest.mouseDClick(widget, button, modifier, pos, delay)
+ self.qWait(20)
+
+ def mouseMove(self, widget, pos=None, delay=-1):
+ """Simulate moving the mouse.
+
+ See QTest.mouseMove for details.
+ """
+ pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint()
+ QTest.mouseMove(widget, pos, delay)
+ self.qWait(20)
+
+ def mousePress(self, widget, button, modifier=None, pos=None, delay=-1):
+ """Simulate pressing a mouse button.
+
+ See QTest.mousePress for details.
+ """
+ if modifier is None:
+ modifier = qt.Qt.KeyboardModifiers()
+ pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint()
+ QTest.mousePress(widget, button, modifier, pos, delay)
+ self.qWait(20)
+
+ def mouseRelease(self, widget, button, modifier=None, pos=None, delay=-1):
+ """Simulate releasing a mouse button.
+
+ See QTest.mouseRelease for details.
+ """
+ if modifier is None:
+ modifier = qt.Qt.KeyboardModifiers()
+ pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint()
+ QTest.mouseRelease(widget, button, modifier, pos, delay)
+ self.qWait(20)
+
+ def qSleep(self, ms):
+ """Sleep for ms milliseconds, blocking the execution of the test.
+
+ See QTest.qSleep for details.
+ """
+ QTest.qSleep(ms + self.TIMEOUT_WAIT)
+
+ def qWait(self, ms=None):
+ """Waits for ms milliseconds, events will be processed.
+
+ See QTest.qWait for details.
+ """
+ if ms is None:
+ ms = self.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,
+ maxtime=timeout)
+ timeout = endTimeMS - int(time.time() * 1000)
+ else:
+ QTest.qWait(ms + self.TIMEOUT_WAIT)
+
+ def qWaitForWindowExposed(self, window, timeout=None):
+ """Waits until the window is shown in the screen.
+
+ See QTest.qWaitForWindowExposed for details.
+ """
+ result = qWaitForWindowExposedAndActivate(window, timeout)
+
+ if self.TIMEOUT_WAIT:
+ QTest.qWait(self.TIMEOUT_WAIT)
+
+ return result
+
+
+class SignalListener():
+ """Util to listen a Qt event and store parameters
+ """
+
+ def __init__(self):
+ self.__calls = []
+
+ def __call__(self, *args, **kargs):
+ self.__calls.append((args, kargs))
+
+ def clear(self):
+ """Clear stored data"""
+ self.__calls = []
+
+ def callCount(self):
+ """
+ Returns how many times the listener was called.
+
+ :rtype: int
+ """
+ return len(self.__calls)
+
+ def arguments(self, callIndex=None, argumentIndex=None):
+ """Returns positional arguments optionally filtered by call count id
+ or argument index.
+
+ :param int callIndex: Index of the called data
+ :param int argumentIndex: Index of the positional argument.
+ """
+ if callIndex is not None:
+ result = self.__calls[callIndex][0]
+ if argumentIndex is not None:
+ result = result[argumentIndex]
+ else:
+ result = [x[0] for x in self.__calls]
+ if argumentIndex is not None:
+ result = [x[argumentIndex] for x in result]
+ return result
+
+ def karguments(self, callIndex=None, argumentName=None):
+ """Returns positional arguments optionally filtered by call count id
+ or name of the keyword argument.
+
+ :param int callIndex: Index of the called data
+ :param int argumentName: Name of the keyword argument.
+ """
+ if callIndex is not None:
+ result = self.__calls[callIndex][1]
+ if argumentName is not None:
+ result = result[argumentName]
+ else:
+ result = [x[1] for x in self.__calls]
+ if argumentName is not None:
+ result = [x[argumentName] for x in result]
+ return result
+
+ def partial(self, *args, **kargs):
+ """Returns a new partial object which when called will behave like this
+ listener called with the positional arguments args and keyword
+ arguments keywords. If more arguments are supplied to the call, they
+ are appended to args. If additional keyword arguments are supplied,
+ they extend and override keywords.
+ """
+ return functools.partial(self, *args, **kargs)
+
+
+def getQToolButtonFromAction(action):
+ """Return a QToolButton corresponding to a QAction.
+
+ :param QAction action: The QAction from which to get QToolButton.
+ :return: A QToolButton associated to action or None.
+ """
+ for widget in action.associatedWidgets():
+ if isinstance(widget, qt.QToolButton):
+ return widget
+ return None