diff options
Diffstat (limited to 'silx/gui/test')
-rw-r--r-- | silx/gui/test/__init__.py | 108 | ||||
-rw-r--r-- | silx/gui/test/test_console.py | 91 | ||||
-rw-r--r-- | silx/gui/test/test_icons.py | 116 | ||||
-rw-r--r-- | silx/gui/test/test_qt.py | 144 | ||||
-rw-r--r-- | silx/gui/test/test_utils.py | 77 | ||||
-rw-r--r-- | silx/gui/test/utils.py | 428 |
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 |