diff options
Diffstat (limited to 'sources/pyside6/tests/signals')
47 files changed, 3158 insertions, 0 deletions
diff --git a/sources/pyside6/tests/signals/CMakeLists.txt b/sources/pyside6/tests/signals/CMakeLists.txt new file mode 100644 index 000000000..ff342adc7 --- /dev/null +++ b/sources/pyside6/tests/signals/CMakeLists.txt @@ -0,0 +1,47 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +PYSIDE_TEST(args_dont_match_test.py) +PYSIDE_TEST(bug_79.py) +PYSIDE_TEST(bug_189.py) +PYSIDE_TEST(bug_311.py) +PYSIDE_TEST(bug_312.py) +PYSIDE_TEST(bug_319.py) +PYSIDE_TEST(decorators_test.py) +PYSIDE_TEST(disconnect_test.py) +PYSIDE_TEST(invalid_callback_test.py) +PYSIDE_TEST(lambda_gui_test.py) +PYSIDE_TEST(lambda_test.py) +PYSIDE_TEST(leaking_signal_test.py) +PYSIDE_TEST(multiple_connections_gui_test.py) +PYSIDE_TEST(multiple_connections_test.py) +PYSIDE_TEST(pysignal_test.py) +PYSIDE_TEST(qobject_callable_connect_test.py) +PYSIDE_TEST(qobject_destroyed_test.py) +PYSIDE_TEST(qobject_receivers_test.py) +PYSIDE_TEST(qobject_sender_test.py) +PYSIDE_TEST(ref01_test.py) +PYSIDE_TEST(ref02_test.py) +PYSIDE_TEST(ref03_test.py) +PYSIDE_TEST(ref04_test.py) +PYSIDE_TEST(ref05_test.py) +PYSIDE_TEST(ref06_test.py) +PYSIDE_TEST(segfault_proxyparent_test.py) +PYSIDE_TEST(self_connect_test.py) +PYSIDE_TEST(short_circuit_test.py) +PYSIDE_TEST(signal2signal_connect_test.py) +PYSIDE_TEST(signal_across_threads.py) +PYSIDE_TEST(signal_autoconnect_test.py) +PYSIDE_TEST(signal_connectiontype_support_test.py) +PYSIDE_TEST(signal_emission_gui_test.py) +PYSIDE_TEST(signal_emission_test.py) +PYSIDE_TEST(signal_enum_test.py) +PYSIDE_TEST(signal_func_test.py) +PYSIDE_TEST(signal_manager_refcount_test.py) +PYSIDE_TEST(signal_newenum_test.py) +PYSIDE_TEST(signal_number_limit_test.py) +PYSIDE_TEST(signal_object_test.py) +PYSIDE_TEST(signal_signature_test.py) +PYSIDE_TEST(signal_with_primitive_type_test.py) +PYSIDE_TEST(slot_reference_count_test.py) +PYSIDE_TEST(static_metaobject_test.py) diff --git a/sources/pyside6/tests/signals/anonymous_slot_leak_test.py b/sources/pyside6/tests/signals/anonymous_slot_leak_test.py new file mode 100644 index 000000000..560a08659 --- /dev/null +++ b/sources/pyside6/tests/signals/anonymous_slot_leak_test.py @@ -0,0 +1,55 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from functools import partial +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtWidgets import QWidget +from helper.usesqapplication import UsesQApplication + + +have_debug = hasattr(sys, "gettotalrefcount") + + +class LeakerLambda(): + def __init__(self, widget): + widget.windowIconChanged.connect(lambda *args: None) + + +class LeakerFunctoolsPartial(): + def __init__(self, widget): + widget.windowIconChanged.connect(partial(int, 0)) + + +class TestBugPYSIDE2299(UsesQApplication): + def leak(self, leaker): + widget = QWidget() + + # Warm-up + leaker(widget) + + refs_before = sys.gettotalrefcount() + for _ in range(1000): + leaker(widget) + refs_after = sys.gettotalrefcount() + + self.assertAlmostEqual(refs_after - refs_before, 0, delta=10) + + @unittest.skipUnless(have_debug, "You need a debug build") + def test_lambda(self): + self.leak(LeakerLambda) + + @unittest.skipUnless(have_debug, "You need a debug build") + def test_functools_partial(self): + self.leak(LeakerFunctoolsPartial) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/args_dont_match_test.py b/sources/pyside6/tests/signals/args_dont_match_test.py new file mode 100644 index 000000000..4f56be348 --- /dev/null +++ b/sources/pyside6/tests/signals/args_dont_match_test.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + + +class Sender(QObject): + the_signal = Signal(int, int, int) + + +class ArgsDontMatch(unittest.TestCase): + + def callback(self, arg1): + self.ok = True + + def testConnectSignalToSlotWithLessArgs(self): + self.ok = False + obj1 = Sender() + obj1.the_signal.connect(self.callback) + obj1.the_signal.emit(1, 2, 3) + + self.assertTrue(self.ok) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/bug_189.py b/sources/pyside6/tests/signals/bug_189.py new file mode 100644 index 000000000..1c013ddea --- /dev/null +++ b/sources/pyside6/tests/signals/bug_189.py @@ -0,0 +1,38 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QTimer +from PySide6.QtWidgets import QSlider +from helper.usesqapplication import UsesQApplication + + +class TestBugPYSIDE189(UsesQApplication): + + def testDisconnect(self): + # Disconnecting from a signal owned by a destroyed object + # should raise an exception, not segfault. + def onValueChanged(self, value): + pass + + sld = QSlider() + sld.valueChanged.connect(onValueChanged) + + sld.deleteLater() + + QTimer.singleShot(0, self.app.quit) + self.app.exec() + + self.assertRaises(RuntimeError, sld.valueChanged.disconnect, onValueChanged) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/bug_311.py b/sources/pyside6/tests/signals/bug_311.py new file mode 100644 index 000000000..e27476172 --- /dev/null +++ b/sources/pyside6/tests/signals/bug_311.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QDate, QObject, Signal +from helper.usesqapplication import UsesQApplication + + +class DerivedDate(QDate): + def __init__(self, y, m, d): + super().__init__(y, m, d) + + +class Emitter(QObject): + dateSignal1 = Signal(QDate) + dateSignal2 = Signal(DerivedDate) + tupleSignal = Signal(tuple) + + +class SignaltoSignalTest(UsesQApplication): + def myCb(self, dt): + self._dt = dt + + def testBug(self): + e = Emitter() + d = DerivedDate(2010, 8, 24) + self._dt = None + e.dateSignal1.connect(self.myCb) + e.dateSignal1.emit(d) + self.assertEqual(self._dt, d) + + self._dt = None + e.dateSignal2.connect(self.myCb) + e.dateSignal2.emit(d) + self.assertEqual(self._dt, d) + + myTuple = (5, 6, 7) + self._dt = None + e.tupleSignal.connect(self.myCb) + e.tupleSignal.emit(myTuple) + self.assertEqual(myTuple, self._dt) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/bug_312.py b/sources/pyside6/tests/signals/bug_312.py new file mode 100644 index 000000000..80d56a020 --- /dev/null +++ b/sources/pyside6/tests/signals/bug_312.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + +MAX_LOOPS = 5 +MAX_OBJECTS = 200 + + +class Sender(QObject): + fire = Signal() + + +class MultipleSlots(unittest.TestCase): + def myCB(self): + self._count += 1 + + def testDisconnectCleanup(self): + for c in range(MAX_LOOPS): + self._count = 0 + self._senders = [] + for i in range(MAX_OBJECTS): + o = Sender() + o.fire.connect(lambda: self.myCB()) + self._senders.append(o) + o.fire.emit() + + self.assertEqual(self._count, MAX_OBJECTS) + + # delete all senders will disconnect the signals + self._senders = [] + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/bug_319.py b/sources/pyside6/tests/signals/bug_319.py new file mode 100644 index 000000000..657733afb --- /dev/null +++ b/sources/pyside6/tests/signals/bug_319.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal, Slot +from helper.usesqapplication import UsesQApplication + + +class Listener(QObject): + def __init__(self): + super().__init__(None) + self._phrase = [] + + @Slot(tuple) + def listen(self, words): + for w in words: + self._phrase.append(w) + + +class Communicate(QObject): + # create a new signal on the fly and name it 'speak' + speak = Signal(tuple) + + +class SignaltoSignalTest(UsesQApplication): + def testBug(self): + someone = Communicate() + someone2 = Listener() + # connect signal and slot + someone.speak.connect(someone2.listen) + # emit 'speak' signal + talk = ("one", "two", "three") + someone.speak.emit(talk) + self.assertEqual(someone2._phrase, list(talk)) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/bug_79.py b/sources/pyside6/tests/signals/bug_79.py new file mode 100644 index 000000000..77ac621d5 --- /dev/null +++ b/sources/pyside6/tests/signals/bug_79.py @@ -0,0 +1,59 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import gc +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtGui import QStandardItemModel +from PySide6.QtWidgets import QApplication, QTreeView + + +try: + from sys import gettotalrefcount + skiptest = False +except ImportError: + skiptest = True + + +class ConnectTest(unittest.TestCase): + + def callback(self, o): + print("callback") + self._called = o + + def testNoLeaks_ConnectAndDisconnect(self): + self._called = None + app = QApplication([]) # noqa: F841 + o = QTreeView() + o.setModel(QStandardItemModel()) + o.selectionModel().destroyed.connect(self.callback) + o.selectionModel().destroyed.disconnect(self.callback) + gc.collect() + # if this is no debug build, then we check at least that + # we do not crash any longer. + for idx in range(200): + # PYSIDE-2230: Warm-up is necessary before measuring, because + # the code changes the constant parts after some time. + o.selectionModel().destroyed.connect(self.callback) + o.selectionModel().destroyed.disconnect(self.callback) + if not skiptest: + total = gettotalrefcount() + for idx in range(1000): + o.selectionModel().destroyed.connect(self.callback) + o.selectionModel().destroyed.disconnect(self.callback) + gc.collect() + if not skiptest: + delta = gettotalrefcount() - total + print("delta total refcount =", delta) + self.assertTrue(abs(delta) < 10) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/decorators_test.py b/sources/pyside6/tests/signals/decorators_test.py new file mode 100644 index 000000000..b29339ee4 --- /dev/null +++ b/sources/pyside6/tests/signals/decorators_test.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Slot, Signal + + +class Sender(QObject): + mySignal = Signal() + + +class MyObject(QObject): + def __init__(self, parent=None): + QObject.__init__(self, parent) + self._slotCalledCount = 0 + + @Slot() + def mySlot(self): + self._slotCalledCount = self._slotCalledCount + 1 + + @Slot(int) + @Slot('QString') + def mySlot2(self, arg0): + self._slotCalledCount = self._slotCalledCount + 1 + + @Slot(name='mySlot3') + def foo(self): + self._slotCalledCount = self._slotCalledCount + 1 + + @Slot(str, int) + def mySlot4(self, a, b): + self._slotCalledCount = self._slotCalledCount + 1 + + @Slot(result=int) + def mySlot5(self): + self._slotCalledCount = self._slotCalledCount + 1 + + @Slot(result=QObject) + def mySlot6(self): + self._slotCalledCount = self._slotCalledCount + 1 + + +class StaticMetaObjectTest(unittest.TestCase): + + def testSignalPropagation(self): + o = MyObject() + m = o.metaObject() + self.assertTrue(m.indexOfSlot('mySlot()') > 0) + self.assertTrue(m.indexOfSlot('mySlot2(int)') > 0) + self.assertTrue(m.indexOfSlot('mySlot2(QString)') > 0) + self.assertTrue(m.indexOfSlot('mySlot3()') > 0) + self.assertTrue(m.indexOfSlot('mySlot4(QString,int)') > 0) + + def testEmission(self): + sender = Sender() + o = MyObject() + sender.mySignal.connect(o.mySlot) + sender.mySignal.emit() + self.assertTrue(o._slotCalledCount == 1) + + def testResult(self): + o = MyObject() + mo = o.metaObject() + i = mo.indexOfSlot('mySlot5()') + m = mo.method(i) + self.assertEqual(m.typeName(), "int") + + def testResultObject(self): + o = MyObject() + mo = o.metaObject() + i = mo.indexOfSlot('mySlot6()') + m = mo.method(i) + self.assertEqual(m.typeName(), "QObject*") + + +class SlotWithoutArgs(unittest.TestCase): + + def testError(self): + # It should be an error to call the slot without the + # arguments, as just @Slot would end up in a slot + # accepting argument functions + self.assertRaises(TypeError, Slot, lambda: 3) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/disconnect_test.py b/sources/pyside6/tests/signals/disconnect_test.py new file mode 100644 index 000000000..ea3782a91 --- /dev/null +++ b/sources/pyside6/tests/signals/disconnect_test.py @@ -0,0 +1,70 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(True) + +from PySide6.QtCore import QObject, Signal +from testbinding import TestObject + + +class Foo(QObject): + bar = Signal() + + +class TestDisconnect(unittest.TestCase): + def theSlot1(self): + self.called1 = True + + def theSlot2(self): + self.called2 = True + + def testIt(self): + self.called1 = False + self.called2 = False + f = Foo() + f.bar.connect(self.theSlot1) + f.bar.connect(self.theSlot2) + f.bar.emit() + self.assertTrue(self.called1) + self.assertTrue(self.called2) + + self.called1 = False + self.called2 = False + f.bar.disconnect() + f.bar.emit() + self.assertFalse(self.called1) + self.assertFalse(self.called2) + + def testDuringCallback(self): + """ Test to see if the C++ object for a connection is accessed after the + method returns. This causes a segfault if the memory that was used by the + C++ object has been reused. """ + + self.called = False + obj = TestObject(0) + + def callback(): + obj.signalWithDefaultValue.disconnect(callback) + + # Connect more callbacks to try to overwrite memory + for i in range(1000): + obj.signalWithDefaultValue.connect(lambda: None) + + self.called = True + + # A non-None return value is needed + return True + obj.signalWithDefaultValue.connect(callback) + obj.signalWithDefaultValue.emit() + self.assertTrue(self.called) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/invalid_callback_test.py b/sources/pyside6/tests/signals/invalid_callback_test.py new file mode 100644 index 000000000..2788c1d1a --- /dev/null +++ b/sources/pyside6/tests/signals/invalid_callback_test.py @@ -0,0 +1,41 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +'''Test cases for passing invalid callbacks to QObject.connect''' + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject + + +class InvalidCallback(unittest.TestCase): + '''Test case for passing an invalid callback to QObject.connect''' + + def setUp(self): + # Acquire resources + self.obj = QObject() + + def tearDown(self): + # Release resources + try: + del self.obj + except AttributeError: + pass + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def testIntegerCb(self): + # Test passing an int as callback to QObject.connect + self.assertRaises(TypeError, self.obj.destroyed.connect, 42) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/lambda_gui_test.py b/sources/pyside6/tests/signals/lambda_gui_test.py new file mode 100644 index 000000000..2123e7206 --- /dev/null +++ b/sources/pyside6/tests/signals/lambda_gui_test.py @@ -0,0 +1,50 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +'''Connecting lambda to gui signals''' + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtWidgets import QSpinBox, QPushButton + +from helper.usesqapplication import UsesQApplication + + +class Control: + def __init__(self): + self.arg = False + + +class QtWidgetsSigLambda(UsesQApplication): + + def testButton(self): + # Connecting a lambda to a QPushButton.clicked() + obj = QPushButton('label') + ctr = Control() + func = lambda: setattr(ctr, 'arg', True) # noqa: E731 + obj.clicked.connect(func) + obj.click() + self.assertTrue(ctr.arg) + self.assertTrue(obj.clicked.disconnect(func)) + + def testSpinButton(self): + # Connecting a lambda to a QPushButton.clicked() + obj = QSpinBox() + ctr = Control() + arg = 444 + func = lambda x: setattr(ctr, 'arg', 444) # noqa: E731 + obj.valueChanged.connect(func) + obj.setValue(444) + self.assertEqual(ctr.arg, arg) + self.assertTrue(obj.valueChanged.disconnect(func)) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/lambda_test.py b/sources/pyside6/tests/signals/lambda_test.py new file mode 100644 index 000000000..23fcdf5fa --- /dev/null +++ b/sources/pyside6/tests/signals/lambda_test.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +'''Connecting lambda to signals''' + +import os +import sys +import unittest +import weakref + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QCoreApplication, QObject, Signal, SIGNAL, QProcess + +from helper.usesqapplication import UsesQApplication + + +class Sender(QObject): + void_signal = Signal() + int_signal = Signal(int) + + def __init__(self, parent=None): + super().__init__(parent) + self._delayed_int = 0 + + def emit_void(self): + self.void_signal.emit() + + def emit_int(self, v): + self.int_signal.emit(v) + + +class Receiver(QObject): + + def __init__(self, *args): + super().__init__(*args) + + +class BasicCase(unittest.TestCase): + + def testSimplePythonSignalNoArgs(self): + # Connecting a lambda to a simple python signal without arguments + receiver = Receiver() + sender = Sender() + sender.void_signal.connect(lambda: setattr(receiver, 'called', True)) + sender.emit_void() + self.assertTrue(receiver.called) + + def testSimplePythonSignal(self): + # Connecting a lambda to a simple python signal witharguments + receiver = Receiver() + sender = Sender() + arg = 42 + sender.int_signal.connect(lambda x: setattr(receiver, 'arg', arg)) + sender.emit_int(arg) + self.assertEqual(receiver.arg, arg) + + def testSimplePythonSignalNoArgsString(self): + # Connecting a lambda to a simple python signal without arguments + receiver = Receiver() + sender = Sender() + QObject.connect(sender, SIGNAL('void_signal()'), + lambda: setattr(receiver, 'called', True)) + sender.emit_void() + self.assertTrue(receiver.called) + + def testSimplePythonSignalString(self): + # Connecting a lambda to a simple python signal witharguments + receiver = Receiver() + sender = Sender() + arg = 42 + QObject.connect(sender, SIGNAL('int_signal(int)'), + lambda x: setattr(receiver, 'arg', arg)) + sender.emit_int(arg) + self.assertEqual(receiver.arg, arg) + + +class QtSigLambda(UsesQApplication): + + qapplication = True + + def testWithArgs(self): + '''Connecting a lambda to a signal with and without arguments''' + proc = QProcess() + dummy = Receiver() + proc.started.connect(lambda: setattr(dummy, 'called', True)) + proc.finished.connect(lambda x: setattr(dummy, 'exit_code', x)) + + proc.start(sys.executable, ['-c', '""']) + self.assertTrue(proc.waitForStarted()) + self.assertTrue(proc.waitForFinished()) + + self.assertTrue(dummy.called) + self.assertEqual(dummy.exit_code, proc.exitCode()) + + def testRelease(self): + """PYSIDE-2646: Test whether main thread target slot lambda/methods + (and their captured objects) are released by the signal manager + after a while.""" + + def do_connect(sender): + receiver = Receiver() + sender.void_signal.connect(lambda: setattr(receiver, 'called', True)) + return receiver + + sender = Sender() + receiver = weakref.ref(do_connect(sender)) + sender.emit_void() + self.assertTrue(receiver().called) + del sender + for i in range(3): + if not receiver(): + break + QCoreApplication.processEvents() + self.assertFalse(receiver()) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/leaking_signal_test.py b/sources/pyside6/tests/signals/leaking_signal_test.py new file mode 100644 index 000000000..666ae7a13 --- /dev/null +++ b/sources/pyside6/tests/signals/leaking_signal_test.py @@ -0,0 +1,27 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + + +class LeakingSignal(unittest.TestCase): + + def testLeakingSignal(self): + # Was segfaulting when the signal was garbage collected. + class Emitter(QObject): + my_signal = Signal(object) + + emitter = Emitter() # noqa: F841 + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/multiple_connections_gui_test.py b/sources/pyside6/tests/signals/multiple_connections_gui_test.py new file mode 100644 index 000000000..295369b7d --- /dev/null +++ b/sources/pyside6/tests/signals/multiple_connections_gui_test.py @@ -0,0 +1,60 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtWidgets import QPushButton, QSpinBox + +from helper.basicpyslotcase import BasicPySlotCase +from helper.usesqapplication import UsesQApplication + + +class QtGuiMultipleSlots(UsesQApplication): + '''Multiple connections to QtGui signals''' + + def run_many(self, signal, emitter, receivers, args=None): + """Utility method to connect a list of receivers to a signal. + sender - QObject that will emit the signal + signal - string with the signal signature + emitter - the callable that will trigger the signal + receivers - list of BasicPySlotCase instances + args - tuple with the arguments to be sent. + """ + + if args is None: + args = tuple() + + for rec in receivers: + rec.setUp() + signal.connect(rec.cb) + rec.args = tuple(args) + + emitter(*args) + + for rec in receivers: + self.assertTrue(rec.called) + + def testButtonClick(self): + """Multiple connections to QPushButton.clicked()""" + sender = QPushButton('button') + receivers = [BasicPySlotCase() for x in range(30)] + self.run_many(sender.clicked, sender.click, receivers) + + def testSpinBoxValueChanged(self): + """Multiple connections to QSpinBox.valueChanged(int)""" + sender = QSpinBox() + # FIXME if number of receivers if higher than 50, segfaults + receivers = [BasicPySlotCase() for x in range(10)] + self.run_many(sender.valueChanged, sender.setValue, + receivers, (1,)) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/multiple_connections_test.py b/sources/pyside6/tests/signals/multiple_connections_test.py new file mode 100644 index 000000000..233851797 --- /dev/null +++ b/sources/pyside6/tests/signals/multiple_connections_test.py @@ -0,0 +1,90 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +from functools import partial +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal, QProcess + +from helper.basicpyslotcase import BasicPySlotCase +from helper.usesqapplication import UsesQApplication + + +class MultipleSignalConnections(unittest.TestCase): + '''Base class for multiple signal connection testing''' + + def run_many(self, signal, emitter, receivers, args=None): + """Utility method to connect a list of receivers to a signal. + sender - QObject that will emit the signal + signal - string with the signal signature + emitter - the callable that will trigger the signal + receivers - list of BasicPySlotCase instances + args - tuple with the arguments to be sent. + """ + + if args is None: + args = tuple() + for rec in receivers: + rec.setUp() + self.assertTrue(signal.connect(rec.cb)) + rec.args = tuple(args) + + emitter(*args) + + for rec in receivers: + self.assertTrue(rec.called) + + +class PythonMultipleSlots(UsesQApplication, MultipleSignalConnections): + '''Multiple connections to python signals''' + + def testPythonSignal(self): + """Multiple connections to a python signal (short-circuit)""" + + class Sender(QObject): + + foobar = Signal(int) + + sender = Sender() + receivers = [BasicPySlotCase() for x in range(10)] + self.run_many(sender.foobar, partial(sender.foobar.emit), + receivers, (0, )) + + +class QProcessMultipleSlots(UsesQApplication, MultipleSignalConnections): + '''Multiple connections to QProcess signals''' + + def testQProcessStarted(self): + '''Multiple connections to QProcess.started()''' + sender = QProcess() + receivers = [BasicPySlotCase() for x in range(10)] + + def start_proc(*args): + sender.start(sys.executable, ['-c', '""']) + self.assertTrue(sender.waitForStarted()) + self.assertTrue(sender.waitForFinished()) + + self.run_many(sender.started, start_proc, receivers) + + def testQProcessFinished(self): + '''Multiple connections to QProcess.finished(int)''' + sender = QProcess() + receivers = [BasicPySlotCase() for x in range(10)] + + def start_proc(*args): + sender.start(sys.executable, ['-c', '""']) + self.assertTrue(sender.waitForStarted()) + self.assertTrue(sender.waitForFinished()) + + self.run_many(sender.finished, start_proc, receivers, (0, QProcess.ExitStatus.NormalExit)) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/pysignal_test.py b/sources/pyside6/tests/signals/pysignal_test.py new file mode 100644 index 000000000..d6f44edf8 --- /dev/null +++ b/sources/pyside6/tests/signals/pysignal_test.py @@ -0,0 +1,203 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal, Qt +from PySide6.QtWidgets import QSpinBox, QApplication, QWidget # noqa: F401 + +from helper.usesqapplication import UsesQApplication + + +TEST_LIST = ["item1", "item2", "item3"] + + +class Sender(QObject): + """Sender class used in this test.""" + + foo = Signal() + foo_int = Signal(int) + dummy = Signal(str) + dummy2 = Signal(str, list) + + def __init__(self, parent=None): + super().__init__(parent) + + def callDummy(self): + self.dummy.emit("PyObject") + + def callDummy2(self): + self.dummy2.emit("PyObject0", TEST_LIST) + + +class PyObjectType(UsesQApplication): + def mySlot(self, arg): + self.assertEqual(arg, "PyObject") + self.called = True + self.callCount += 1 + + def mySlot2(self, arg0, arg1): + self.assertEqual(arg0, "PyObject0") + self.assertEqual(arg1, TEST_LIST) + self.callCount += 1 + if self.running: + self.app.quit() + + def setUp(self): + super().setUp() + self.callCount = 0 + self.running = False + + def testWithOneArg(self): + o = Sender() + o.dummy.connect(self.mySlot) + o.callDummy() + self.assertEqual(self.callCount, 1) + + def testWithTwoArg(self): + o = Sender() + o.dummy2.connect(self.mySlot2) + o.callDummy2() + self.assertEqual(self.callCount, 1) + + def testAsyncSignal(self): + self.called = False + self.running = True + o = Sender() + o.dummy2.connect(self.mySlot2, Qt.QueuedConnection) + o.callDummy2() + self.app.exec() + self.assertEqual(self.callCount, 1) + + def testTwice(self): + self.called = False + self.running = True + o = Sender() + o.dummy2.connect(self.mySlot2, Qt.QueuedConnection) + o.callDummy2() + o.callDummy2() + self.app.exec() + self.assertEqual(self.callCount, 2) + + +class PythonSigSlot(unittest.TestCase): + def setUp(self): + self.called = False + + def tearDown(self): + try: + del self.args + except: # noqa: E722 + pass + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def callback(self, *args): + if tuple(self.args) == args: + self.called = True + + def testNoArgs(self): + """Python signal and slots without arguments""" + obj1 = Sender() + + obj1.foo.connect(self.callback) + self.args = tuple() + obj1.foo.emit(*self.args) + + self.assertTrue(self.called) + + def testWithArgs(self): + """Python signal and slots with integer arguments""" + obj1 = Sender() + + obj1.foo_int.connect(self.callback) + self.args = (42,) + obj1.foo_int.emit(*self.args) + + self.assertTrue(self.called) + + def testDisconnect(self): + obj1 = Sender() + + obj1.foo_int.connect(self.callback) + self.assertTrue(obj1.foo_int.disconnect(self.callback)) + + self.args = (42, ) + obj1.foo_int.emit(*self.args) + + self.assertTrue(not self.called) + + +class SpinBoxPySignal(UsesQApplication): + """Tests the connection of python signals to QSpinBox qt slots.""" + + def setUp(self): + super().setUp() + self.obj = Sender() + self.spin = QSpinBox() + self.spin.setValue(0) + + def tearDown(self): + super().tearDown() + del self.obj + del self.spin + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def testValueChanged(self): + """Emission of a python signal to QSpinBox setValue(int)""" + + self.obj.foo_int.connect(self.spin.setValue) + self.assertEqual(self.spin.value(), 0) + + self.obj.foo_int.emit(4) + self.assertEqual(self.spin.value(), 4) + + def testValueChangedMultiple(self): + """Multiple emissions of a python signal to QSpinBox setValue(int)""" + self.obj.foo_int.connect(self.spin.setValue) + self.assertEqual(self.spin.value(), 0) + + self.obj.foo_int.emit(4) + self.assertEqual(self.spin.value(), 4) + + self.obj.foo_int.emit(77) + self.assertEqual(self.spin.value(), 77) + + +class WidgetPySignal(UsesQApplication): + """Tests the connection of python signals to QWidget qt slots.""" + + def setUp(self): + super(WidgetPySignal, self).setUp() + self.obj = Sender() + self.widget = QWidget() + + def tearDown(self): + super(WidgetPySignal, self).tearDown() + del self.obj + del self.widget + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def testShow(self): + """Emission of a python signal to QWidget slot show()""" + self.widget.hide() + + self.obj.foo.connect(self.widget.show) + self.assertTrue(not self.widget.isVisible()) + + self.obj.foo.emit() + self.assertTrue(self.widget.isVisible()) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/qobject_callable_connect_test.py b/sources/pyside6/tests/signals/qobject_callable_connect_test.py new file mode 100644 index 000000000..a7a26d6f5 --- /dev/null +++ b/sources/pyside6/tests/signals/qobject_callable_connect_test.py @@ -0,0 +1,45 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + + +class Emitter(QObject): + sig = Signal(int) + + +class CallableObject(QObject): + called = False + x = 0 + + def __call__(self, x: int): + self.called = True + self.x = x + + +class QObjectCallableConnectTest(unittest.TestCase): + '''Test case for QObject.connect() when the callable is also a QObject.''' + + def testCallableConnect(self): + emitter = Emitter() + obj = CallableObject() + x = 1 + + emitter.sig.connect(obj) + emitter.sig.emit(x) + + self.assertTrue(obj.called) + self.assertEqual(obj.x, x) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/qobject_destroyed_test.py b/sources/pyside6/tests/signals/qobject_destroyed_test.py new file mode 100644 index 000000000..a21762b41 --- /dev/null +++ b/sources/pyside6/tests/signals/qobject_destroyed_test.py @@ -0,0 +1,39 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject + + +class QObjectDestroyed(unittest.TestCase): + """Very simple test case for the destroyed() signal of QObject""" + + def setUp(self): + self.called = False + + def destroyed_cb(self): + self.called = True + + def testDestroyed(self): + """Emission of QObject.destroyed() to a python slot""" + obj = QObject() + obj.destroyed.connect(self.destroyed_cb) + del obj + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + # PYSIDE-535: Why do I need to do it twice, here? + gc.collect() + self.assertTrue(self.called) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/qobject_receivers_test.py b/sources/pyside6/tests/signals/qobject_receivers_test.py new file mode 100644 index 000000000..9839255ac --- /dev/null +++ b/sources/pyside6/tests/signals/qobject_receivers_test.py @@ -0,0 +1,65 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +''' Test case for QObject.receivers()''' + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, SIGNAL, SLOT + + +def cute_slot(): + pass + + +class TestQObjectReceivers(unittest.TestCase): + '''Test case for QObject::receivers''' + + def testBasic(self): + sender = QObject() + receiver1 = QObject() + receiver2 = QObject() + self.assertEqual(sender.receivers(SIGNAL("")), 0) + sender.destroyed.connect(receiver1.deleteLater) + self.assertEqual(sender.receivers(SIGNAL("destroyed()")), 1) + sender.destroyed.connect(receiver2.deleteLater) + self.assertEqual(sender.receivers(SIGNAL("destroyed()")), 2) + sender.disconnect(sender, SIGNAL("destroyed()"), receiver2, SLOT("deleteLater()")) + self.assertEqual(sender.receivers(SIGNAL("destroyed()")), 1) + del receiver2 + del receiver1 + del sender + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def testPySlots(self): + sender = QObject() + receiver = QObject() + sender.destroyed.connect(cute_slot) + self.assertEqual(sender.receivers(SIGNAL("destroyed( )")), 1) + sender.destroyed.connect(receiver.deleteLater) + self.assertEqual(sender.receivers(SIGNAL("destroyed()")), 2) + del sender + del receiver + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def testPySignals(self): + sender = QObject() + receiver = QObject() + sender.connect(sender, SIGNAL("some_dynamic_signal()"), cute_slot) + self.assertEqual(sender.receivers(SIGNAL("some_dynamic_signal( )")), 1) + sender.connect(sender, SIGNAL("some_dynamic_signal()"), receiver, SLOT("deleteLater()")) + self.assertEqual(sender.receivers(SIGNAL("some_dynamic_signal( )")), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/qobject_sender_test.py b/sources/pyside6/tests/signals/qobject_sender_test.py new file mode 100644 index 000000000..9c1121eb8 --- /dev/null +++ b/sources/pyside6/tests/signals/qobject_sender_test.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +'''Test cases for QObject.sender()''' + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QCoreApplication, QObject, QTimer, Signal +from helper.usesqapplication import UsesQApplication + + +class ExtQTimer(QTimer): + def __init__(self): + super().__init__() + + +class Sender(QObject): + foo = Signal() + + +class Receiver(QObject): + def __init__(self): + super().__init__() + self.the_sender = None + + def callback(self): + self.the_sender = self.sender() + if QCoreApplication.instance(): + QCoreApplication.instance().exit() + + +class ObjectSenderTest(unittest.TestCase): + '''Test case for QObject.sender() method.''' + + def testSenderPythonSignal(self): + sender = Sender() + recv = Receiver() + sender.foo.connect(recv.callback) + sender.foo.emit() + self.assertEqual(sender, recv.the_sender) + + +class ObjectSenderCheckOnReceiverTest(unittest.TestCase): + '''Test case for QObject.sender() method, this one tests the equality on the Receiver object.''' + + def testSenderPythonSignal(self): + sender = Sender() + recv = Receiver() + sender.foo.connect(recv.callback) + sender.foo.emit() + self.assertEqual(sender, recv.the_sender) + + +class ObjectSenderWithQAppTest(UsesQApplication): + '''Test case for QObject.sender() method with QApplication.''' + + def testSenderCppSignal(self): + sender = QTimer() + sender.setObjectName('foo') + recv = Receiver() + sender.timeout.connect(recv.callback) + sender.start(10) + self.app.exec() + self.assertEqual(sender, recv.the_sender) + + def testSenderCppSignalSingleShotTimer(self): + recv = Receiver() + QTimer.singleShot(10, recv.callback) + self.app.exec() + self.assertTrue(isinstance(recv.the_sender, QObject)) + + def testSenderCppSignalSingleShotTimerWithContext(self): + recv = Receiver() + QTimer.singleShot(10, recv, recv.callback) + self.app.exec() + self.assertTrue(isinstance(recv.the_sender, QObject)) + + def testSenderCppSignalWithPythonExtendedClass(self): + sender = ExtQTimer() + recv = Receiver() + sender.timeout.connect(recv.callback) + sender.start(10) + self.app.exec() + self.assertEqual(sender, recv.the_sender) + + +class ObjectSenderWithQAppCheckOnReceiverTest(UsesQApplication): + '''Test case for QObject.sender() method with QApplication.''' + + def testSenderCppSignal(self): + sender = QTimer() + sender.setObjectName('foo') + recv = Receiver() + sender.timeout.connect(recv.callback) + sender.start(10) + self.app.exec() + self.assertEqual(sender, recv.the_sender) + + def testSenderCppSignalWithPythonExtendedClass(self): + sender = ExtQTimer() + recv = Receiver() + sender.timeout.connect(recv.callback) + sender.start(10) + self.app.exec() + self.assertEqual(sender, recv.the_sender) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/ref01_test.py b/sources/pyside6/tests/signals/ref01_test.py new file mode 100644 index 000000000..1a62b2218 --- /dev/null +++ b/sources/pyside6/tests/signals/ref01_test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + + +class BoundAndUnboundSignalsTest(unittest.TestCase): + + def setUp(self): + self.methods = set(('connect', 'disconnect', 'emit')) + + def tearDown(self): + del self.methods + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def testUnboundSignal(self): + self.assertEqual(type(QObject.destroyed), Signal) + self.assertFalse(self.methods.issubset(dir(QObject.destroyed))) + + def testBoundSignal(self): + obj = QObject() + self.assertNotEqual(type(obj.destroyed), Signal) + self.assertTrue(self.methods.issubset(dir(obj.destroyed))) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/ref02_test.py b/sources/pyside6/tests/signals/ref02_test.py new file mode 100644 index 000000000..54b6f4a52 --- /dev/null +++ b/sources/pyside6/tests/signals/ref02_test.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QTimeLine +from helper.usesqapplication import UsesQApplication + + +class NativeSignalsTest(UsesQApplication): + + def setUp(self): + UsesQApplication.setUp(self) + self.called = False + self.timeline = QTimeLine(100) + + def tearDown(self): + del self.called + del self.timeline + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + UsesQApplication.tearDown(self) + + def testSignalWithIntArgument(self): + + def valueChangedSlot(value): + self.called = True + self.assertEqual(type(value), float) + self.app.quit() + + self.timeline.valueChanged.connect(valueChangedSlot) + self.timeline.start() + + self.app.exec() + self.assertTrue(self.called) + + def testSignalWithoutArguments(self): + + def finishedSlot(): + self.called = True + self.app.quit() + + self.timeline.finished.connect(finishedSlot) + self.timeline.start() + + self.app.exec() + self.assertTrue(self.called) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/ref03_test.py b/sources/pyside6/tests/signals/ref03_test.py new file mode 100644 index 000000000..c43c2e549 --- /dev/null +++ b/sources/pyside6/tests/signals/ref03_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject + + +class DisconnectSignalsTest(unittest.TestCase): + + def setUp(self): + self.emitter = QObject() + + def tearDown(self): + del self.emitter + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") + def testConnectionRefCount(self): + + def destroyedSlot(): + pass + + self.assertEqual(sys.getrefcount(destroyedSlot), 2) + self.emitter.destroyed.connect(destroyedSlot) + self.assertEqual(sys.getrefcount(destroyedSlot), 3) + self.emitter.destroyed.disconnect(destroyedSlot) + self.assertEqual(sys.getrefcount(destroyedSlot), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/ref04_test.py b/sources/pyside6/tests/signals/ref04_test.py new file mode 100644 index 000000000..fce801456 --- /dev/null +++ b/sources/pyside6/tests/signals/ref04_test.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + + +class ExtQObject(QObject): + + mySignal = Signal() + + def __init__(self): + super().__init__() + + +class UserSignalTest(unittest.TestCase): + + def setUp(self): + self.emitter = ExtQObject() + self.counter = 0 + + def tearDown(self): + del self.emitter + del self.counter + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def testConnectEmitDisconnect(self): + + def slot(): + self.counter += 1 + + self.emitter.mySignal.connect(slot) + + self.assertEqual(self.counter, 0) + self.emitter.mySignal.emit() + self.assertEqual(self.counter, 1) + self.emitter.mySignal.emit() + self.assertEqual(self.counter, 2) + + self.emitter.mySignal.disconnect(slot) + + self.emitter.mySignal.emit() + self.assertEqual(self.counter, 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/ref05_test.py b/sources/pyside6/tests/signals/ref05_test.py new file mode 100644 index 000000000..fb9debf39 --- /dev/null +++ b/sources/pyside6/tests/signals/ref05_test.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, QTimeLine, Slot +from helper.usesqapplication import UsesQApplication + + +class ExtQObject(QObject): + + def __init__(self): + super().__init__() + self.counter = 0 + + @Slot('qreal') + def foo(self, value): + self.counter += 1 + + +class UserSlotTest(UsesQApplication): + + def setUp(self): + UsesQApplication.setUp(self) + self.receiver = ExtQObject() + self.timeline = QTimeLine(100) + + def tearDown(self): + del self.timeline + del self.receiver + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + UsesQApplication.tearDown(self) + + def testUserSlot(self): + self.timeline.setUpdateInterval(10) + + self.timeline.finished.connect(self.app.quit) + + self.timeline.valueChanged.connect(self.receiver.foo) + self.timeline.start() + + self.app.exec() + + self.assertTrue(self.receiver.counter > 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/ref06_test.py b/sources/pyside6/tests/signals/ref06_test.py new file mode 100644 index 000000000..a827131db --- /dev/null +++ b/sources/pyside6/tests/signals/ref06_test.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, QTimeLine, Signal, Slot +from helper.usesqapplication import UsesQApplication + + +class ExtQObject(QObject): + signalbetween = Signal('qreal') + + def __init__(self): + super().__init__() + self.counter = 0 + + @Slot('qreal') + def foo(self, value): + self.counter += 1 + + +class SignaltoSignalTest(UsesQApplication): + + def setUp(self): + UsesQApplication.setUp(self) + self.receiver = ExtQObject() + self.timeline = QTimeLine(100) + + def tearDown(self): + del self.timeline + del self.receiver + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + UsesQApplication.tearDown(self) + + def testSignaltoSignal(self): + self.timeline.setUpdateInterval(10) + + self.timeline.finished.connect(self.app.quit) + + self.timeline.valueChanged.connect(self.receiver.signalbetween) + self.receiver.signalbetween.connect(self.receiver.foo) + + self.timeline.start() + + self.app.exec() + + self.assertTrue(self.receiver.counter > 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/segfault_proxyparent_test.py b/sources/pyside6/tests/signals/segfault_proxyparent_test.py new file mode 100644 index 000000000..cb0df0978 --- /dev/null +++ b/sources/pyside6/tests/signals/segfault_proxyparent_test.py @@ -0,0 +1,78 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + +# Description of the problem +# After creating an PyObject that inherits from QObject, connecting it, +# deleting it and later creating another Python QObject-based object, this +# new object will point to the same memory position as the first one. + +# Somehow the underlying QObject also points to the same position. + + +class Sender(QObject): + + bar = Signal(int) + + def __init__(self, parent=None): + QObject.__init__(self, parent) + + +class Joe(QObject): + + bar = Signal(int) + + def __init__(self, parent=None): + QObject.__init__(self, parent) + + +class SegfaultCase(unittest.TestCase): + """Test case for the segfault happening when parent() is called inside + ProxyObject""" + + def setUp(self): + self.called = False + + def tearDown(self): + try: + del self.args + except: # noqa: E722 + pass + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def callback(self, *args): + if tuple(self.args) == args: + self.called = True + + def testSegfault(self): + """Regression: Segfault for qobjects in the same memory position.""" + obj = Sender() + obj.bar.connect(self.callback) + self.args = (33,) + obj.bar.emit(self.args[0]) + self.assertTrue(self.called) + del obj + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + obj = Joe() + obj.bar.connect(self.callback) + self.args = (33,) + obj.bar.emit(self.args[0]) + self.assertTrue(self.called) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/self_connect_test.py b/sources/pyside6/tests/signals/self_connect_test.py new file mode 100644 index 000000000..08ca725f8 --- /dev/null +++ b/sources/pyside6/tests/signals/self_connect_test.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +'''Using self.connect(signal, method)''' + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Slot +from PySide6.QtWidgets import QPushButton, QWidget + +from helper.usesqapplication import UsesQApplication + + +class Receiver(QObject): + def __init__(self, p=None): + super().__init__(p) + self.triggered = False + + @Slot(bool, int) + def default_parameter_slot(self, bool_value, int_value=0): + self.triggered = True + + +class SelfConnect(UsesQApplication): + + def testButtonClickClose(self): + button = QPushButton() + button.clicked.connect(button.close) + + button.show() + self.assertTrue(button.isVisible()) + button.click() + self.assertTrue(not button.isVisible()) + + def testWindowButtonClickClose(self): + button = QPushButton() + window = QWidget() + button.clicked.connect(window.close) + + window.show() + self.assertTrue(window.isVisible()) + button.click() + self.assertTrue(not window.isVisible()) + + def testDefaultParameters(self): + button = QPushButton() + receiver = Receiver(button) + button.clicked.connect(receiver.default_parameter_slot) + button.clicked.connect(button.close) + button.show() + button.click() + self.assertTrue(receiver.triggered) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/short_circuit_test.py b/sources/pyside6/tests/signals/short_circuit_test.py new file mode 100644 index 000000000..1ad4bc24c --- /dev/null +++ b/sources/pyside6/tests/signals/short_circuit_test.py @@ -0,0 +1,85 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + + +class Sender(QObject): + """Sender class used in this test.""" + + foo = Signal() + foo_int = Signal(int) + foo_int_int_string = Signal(int, int, str) + foo_int_qobject = Signal(int, QObject) + + def __init__(self, parent=None): + QObject.__init__(self, parent) + + +class ShortCircuitSignals(unittest.TestCase): + def setUp(self): + self.called = False + + def tearDown(self): + try: + del self.args + except: # noqa: E722 + pass + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def callback(self, *args): + if tuple(self.args) == args: + self.called = True + + def testNoArgs(self): + """Short circuit signal without arguments""" + obj1 = Sender() + obj1.foo.connect(self.callback) + self.args = tuple() + obj1.foo.emit(*self.args) + self.assertTrue(self.called) + + def testWithArgs(self): + """Short circuit signal with integer arguments""" + obj1 = Sender() + + obj1.foo_int.connect(self.callback) + self.args = (42,) + obj1.foo_int.emit(*self.args) + + self.assertTrue(self.called) + + def testMultipleArgs(self): + """Short circuit signal with multiple arguments""" + obj1 = Sender() + + obj1.foo_int_int_string.connect(self.callback) + self.args = (42, 33, 'char') + obj1.foo_int_int_string.emit(*self.args) + + self.assertTrue(self.called) + + def testComplexArgs(self): + """Short circuit signal with complex arguments""" + obj1 = Sender() + + obj1.foo_int_qobject.connect(self.callback) + self.args = (42, obj1) + + obj1.foo_int_qobject.emit(*self.args) + self.assertTrue(self.called) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal2signal_connect_test.py b/sources/pyside6/tests/signals/signal2signal_connect_test.py new file mode 100644 index 000000000..31129f7a1 --- /dev/null +++ b/sources/pyside6/tests/signals/signal2signal_connect_test.py @@ -0,0 +1,121 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +''' Test case for signal to signal connections.''' + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + + +class Sender(QObject): + + mysignal_int = Signal(int) + mysignal_int_int = Signal(int, int) + mysignal_string = Signal(str) + + +class Forwarder(Sender): + + forward = Signal() + forward_qobject = Signal(QObject) + + +def cute_slot(): + pass + + +class TestSignal2SignalConnect(unittest.TestCase): + '''Test case for signal to signal connections''' + + def setUp(self): + # Set up the basic resources needed + self.sender = Sender() + self.forwarder = Forwarder() + self.args = None + self.called = False + + def tearDown(self): + # Delete used resources + try: + del self.sender + except: # noqa: E722 + pass + try: + del self.forwarder + except: # noqa: E722 + pass + del self.args + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + + def callback_noargs(self): + # Default callback without arguments + self.called = True + + def callback_args(self, *args): + # Default callback with arguments + if args == self.args: + self.called = True + else: + raise TypeError("Invalid arguments") + + def callback_qobject(self, *args): + # Default callback for QObject as argument + if args[0].objectName() == self.args[0]: + self.called = True + else: + raise TypeError("Invalid arguments") + + def testSignalWithoutArguments(self): + self.sender.destroyed.connect(self.forwarder.forward) + self.forwarder.forward.connect(self.callback_noargs) + del self.sender + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + self.assertTrue(self.called) + + def testSignalWithOnePrimitiveTypeArgument(self): + self.sender.mysignal_int.connect(self.forwarder.mysignal_int) + self.forwarder.mysignal_int.connect(self.callback_args) + self.args = (19,) + self.sender.mysignal_int.emit(*self.args) + self.assertTrue(self.called) + + def testSignalWithMultiplePrimitiveTypeArguments(self): + self.sender.mysignal_int_int.connect(self.forwarder.mysignal_int_int) + self.forwarder.mysignal_int_int.connect(self.callback_args) + self.args = (23, 29) + self.sender.mysignal_int_int.emit(*self.args) + self.assertTrue(self.called) + + def testSignalWithOneStringArgument(self): + self.sender.mysignal_string.connect(self.forwarder.mysignal_string) + self.forwarder.mysignal_string.connect(self.callback_args) + self.args = ('myargument',) + self.sender.mysignal_string.emit(*self.args) + self.assertTrue(self.called) + + def testSignalWithOneQObjectArgument(self): + self.sender.destroyed.connect(self.forwarder.forward_qobject) + self.forwarder.forward_qobject.connect(self.callback_qobject) + + obj_name = 'sender' + self.sender.setObjectName(obj_name) + self.args = (obj_name, ) + del self.sender + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + self.assertTrue(self.called) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_across_threads.py b/sources/pyside6/tests/signals/signal_across_threads.py new file mode 100644 index 000000000..91b1ca986 --- /dev/null +++ b/sources/pyside6/tests/signals/signal_across_threads.py @@ -0,0 +1,82 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +'''Test case for PYSIDE-1354: Ensure that slots are invoked from the receiver's +thread context when using derived classes (and thus, a global receiver).''' + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, QThread, QTimer, Slot +from helper.usesqapplication import UsesQApplication + + +class ReceiverBase(QObject): + def __init__(self, parent=None): + super().__init__(parent) + self.senderThread = None + + @Slot() + def slot_function(self): + self.senderThread = QThread.currentThread() + + +class Receiver(ReceiverBase): + pass + + +class TestThread(QThread): + def __init__(self, parent=None): + super().__init__(parent) + + def run(self): + pass + + +class SignalAcrossThreads(UsesQApplication): + def setUp(self): + UsesQApplication.setUp(self) + self._timer_tick = 0 + self._timer = QTimer() + self._timer.setInterval(20) + self._timer.timeout.connect(self._control_test) + self._worker_thread = TestThread() + + def tearDown(self): + UsesQApplication.tearDown(self) + + @Slot() + def _control_test(self): + if self._timer_tick == 0: + self._worker_thread.start() + elif self._timer_tick == 1: + self._worker_thread.wait() + else: + self._timer.stop() + self.app.quit() + self._timer_tick += 1 + + def test(self): + worker_thread_receiver = Receiver() + worker_thread_receiver.moveToThread(self._worker_thread) + self._worker_thread.started.connect(worker_thread_receiver.slot_function) + + main_thread = QThread.currentThread() + main_thread_receiver = Receiver() + self._worker_thread.started.connect(main_thread_receiver.slot_function) + + self._timer.start() + self.app.exec() + + self.assertEqual(worker_thread_receiver.senderThread, self._worker_thread) + self.assertEqual(main_thread_receiver.senderThread, main_thread) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_autoconnect_test.py b/sources/pyside6/tests/signals/signal_autoconnect_test.py new file mode 100644 index 000000000..51d1cea3a --- /dev/null +++ b/sources/pyside6/tests/signals/signal_autoconnect_test.py @@ -0,0 +1,41 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QMetaObject, Slot +from PySide6.QtWidgets import QApplication, QPushButton, QWidget + + +class MyObject(QWidget): + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self._method_called = False + + @Slot() + def on_button_clicked(self): + self._method_called = True + + +class AutoConnectionTest(unittest.TestCase): + + def testConnection(self): + app = QApplication([]) # noqa: F841 + + win = MyObject() + btn = QPushButton("click", win) + btn.setObjectName("button") + QMetaObject.connectSlotsByName(win) + btn.click() + self.assertTrue(win._method_called) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_connectiontype_support_test.py b/sources/pyside6/tests/signals/signal_connectiontype_support_test.py new file mode 100644 index 000000000..0a69c1e02 --- /dev/null +++ b/sources/pyside6/tests/signals/signal_connectiontype_support_test.py @@ -0,0 +1,42 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal, Qt + + +class Sender(QObject): + """Dummy class used in this test.""" + + foo = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + + +class TestConnectionTypeSupport(unittest.TestCase): + def callback(self, *args): + if tuple(self.args) == args: + self.called = True + + def testNoArgs(self): + """Connect signal using a Qt.ConnectionType as argument""" + obj1 = Sender() + + obj1.foo.connect(self.callback, Qt.DirectConnection) + self.args = tuple() + obj1.foo.emit(*self.args) + + self.assertTrue(self.called) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_emission_gui_test.py b/sources/pyside6/tests/signals/signal_emission_gui_test.py new file mode 100644 index 000000000..5a49b9d12 --- /dev/null +++ b/sources/pyside6/tests/signals/signal_emission_gui_test.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +"""Tests covering signal emission and receiving to python slots""" + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtWidgets import QSpinBox, QPushButton + +from helper.basicpyslotcase import BasicPySlotCase +from helper.usesqapplication import UsesQApplication + + +class ButtonPySlot(UsesQApplication, BasicPySlotCase): + """Tests the connection of python slots to QPushButton signals""" + + def testButtonClicked(self): + """Connection of a python slot to QPushButton.clicked()""" + button = QPushButton('Mylabel') + button.clicked.connect(self.cb) + self.args = tuple() + button.clicked.emit() + self.assertTrue(self.called) + + def testButtonClick(self): + """Indirect qt signal emission using the QPushButton.click() method """ + button = QPushButton('label') + button.clicked.connect(self.cb) + self.args = tuple() + button.click() + self.assertTrue(self.called) + + +class SpinBoxPySlot(UsesQApplication, BasicPySlotCase): + """Tests the connection of python slots to QSpinBox signals""" + + def setUp(self): + super(SpinBoxPySlot, self).setUp() + self.spin = QSpinBox() + + def tearDown(self): + del self.spin + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + super(SpinBoxPySlot, self).tearDown() + + def testSpinBoxValueChanged(self): + """Connection of a python slot to QSpinBox.valueChanged(int)""" + self.spin.valueChanged.connect(self.cb) + self.args = [3] + self.spin.valueChanged.emit(*self.args) + self.assertTrue(self.called) + + def testSpinBoxValueChangedImplicit(self): + """Indirect qt signal emission using QSpinBox.setValue(int)""" + self.spin.valueChanged.connect(self.cb) + self.args = [42] + self.spin.setValue(self.args[0]) + self.assertTrue(self.called) + + def atestSpinBoxValueChangedFewArgs(self): + """Emission of signals with fewer arguments than needed""" + self.spin.valueChanged.connect(self.cb) + self.args = (554,) + self.assertRaises(TypeError, self.spin.valueChanged.emit) + + +class QSpinBoxQtSlots(UsesQApplication): + """Tests the connection to QSpinBox qt slots""" + + qapplication = True + + def testSetValueIndirect(self): + """Indirect signal emission: QSpinBox using valueChanged(int)/setValue(int)""" + spinSend = QSpinBox() + spinRec = QSpinBox() + + spinRec.setValue(5) + + spinSend.valueChanged.connect(spinRec.setValue) + self.assertEqual(spinRec.value(), 5) + spinSend.setValue(3) + self.assertEqual(spinRec.value(), 3) + self.assertEqual(spinSend.value(), 3) + + def testSetValue(self): + """Direct signal emission: QSpinBox using valueChanged(int)/setValue(int)""" + spinSend = QSpinBox() + spinRec = QSpinBox() + + spinRec.setValue(5) + spinSend.setValue(42) + + spinSend.valueChanged.connect(spinRec.setValue) + self.assertEqual(spinRec.value(), 5) + self.assertEqual(spinSend.value(), 42) + spinSend.valueChanged.emit(3) + + self.assertEqual(spinRec.value(), 3) + # Direct emission shouldn't change the value of the emitter + self.assertEqual(spinSend.value(), 42) + + spinSend.valueChanged.emit(66) + self.assertEqual(spinRec.value(), 66) + self.assertEqual(spinSend.value(), 42) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_emission_test.py b/sources/pyside6/tests/signals/signal_emission_test.py new file mode 100644 index 000000000..b31d89c2f --- /dev/null +++ b/sources/pyside6/tests/signals/signal_emission_test.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +"""Tests covering signal emission and receiving to python slots""" + +import functools +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal, SIGNAL, QProcess, QTimeLine + +from helper.usesqapplication import UsesQApplication + + +class ArgsOnEmptySignal(UsesQApplication): + '''Trying to emit a signal without arguments passing some arguments''' + + def testArgsToNoArgsSignal(self): + '''Passing arguments to a signal without arguments''' + process = QProcess() + self.assertRaises(TypeError, process.started.emit, 42) + + +class MoreArgsOnEmit(UsesQApplication): + '''Trying to pass more args than needed to emit (signals with args)''' + + def testMoreArgs(self): + '''Passing more arguments than needed''' + process = QProcess() + self.assertRaises(TypeError, process.finished.emit, 55, QProcess.ExitStatus.NormalExit, 42) + + +class Sender(QObject): + '''Sender class''' + + dummy = Signal() + dummy_int = Signal(int) + + +class PythonSignalToCppSlots(UsesQApplication): + '''Connect python signals to C++ slots''' + + def testWithoutArgs(self): + '''Connect python signal to QTimeLine.toggleDirection()''' + timeline = QTimeLine() + sender = Sender() + sender.dummy.connect(timeline.toggleDirection) + + orig_dir = timeline.direction() + sender.dummy.emit() + new_dir = timeline.direction() + + if orig_dir == QTimeLine.Forward: + self.assertEqual(new_dir, QTimeLine.Backward) + else: + self.assertEqual(new_dir, QTimeLine.Forward) + + def testWithArgs(self): + '''Connect python signals to QTimeLine.setCurrentTime(int)''' + timeline = QTimeLine() + sender = Sender() + + sender.dummy_int.connect(timeline.setCurrentTime) + + current = timeline.currentTime() + sender.dummy_int.emit(current + 42) + self.assertEqual(timeline.currentTime(), current + 42) + + +class CppSignalsToCppSlots(UsesQApplication): + '''Connection between C++ slots and signals''' + + def testWithoutArgs(self): + '''Connect QProcess.started() to QTimeLine.togglePaused()''' + process = QProcess() + timeline = QTimeLine() + + process.finished.connect(timeline.toggleDirection) + + orig_dir = timeline.direction() + + process.start(sys.executable, ['-c', '"print 42"']) + self.assertTrue(process.waitForStarted()) + self.assertTrue(process.waitForFinished()) + + new_dir = timeline.direction() + + if orig_dir == QTimeLine.Forward: + self.assertEqual(new_dir, QTimeLine.Backward) + else: + self.assertEqual(new_dir, QTimeLine.Forward) + + +called = False + + +def someSlot(args=None): + global called + called = True + + +class DynamicSignalsToFuncPartial(UsesQApplication): + + def testIt(self): + global called + called = False + o = Sender() + o.dummy.connect(functools.partial(someSlot, "partial ..")) + o.dummy.emit() + self.assertTrue(called) + + +class EmitUnknownType(UsesQApplication): + def testIt(self): + a = QObject() + a.connect(SIGNAL('foobar(Dummy)'), lambda x: 42) # Just connect with an unknown type + self.assertRaises(TypeError, a.emit, SIGNAL('foobar(Dummy)'), 22) + + +class EmitEnum(UsesQApplication): + """Test emission of enum arguments""" + + def slot(self, arg): + self.arg = arg + + def testIt(self): + self.arg = None + p = QProcess() + p.stateChanged.connect(self.slot) + p.stateChanged.emit(QProcess.NotRunning) + self.assertEqual(self.arg, QProcess.NotRunning) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_enum_test.py b/sources/pyside6/tests/signals/signal_enum_test.py new file mode 100644 index 000000000..a792e9b0c --- /dev/null +++ b/sources/pyside6/tests/signals/signal_enum_test.py @@ -0,0 +1,57 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +from enum import Enum +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) + +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal, Slot + + +class Colors(Enum): + red = 1 + green = 2 + blue = 3 + + +class Obj(QObject): + enum_signal = Signal(Colors) + object_signal = Signal(object) + + def __init__(self, parent=None): + QObject.__init__(self, parent) + self.enum_signal.connect(self.get_result) + self.object_signal.connect(self.get_result) + self.value = -1 + + @Slot() + def get_result(self, i): + self.value = i + + +class SignalEnumTests(unittest.TestCase): + '''Test Signal with enum.Enum''' + + def testSignal(self): + o = Obj() + # Default value + self.assertEqual(o.value, -1) + + # Enum Signal + o.enum_signal.emit(Colors.green) + self.assertEqual(o.value, Colors.green) + + # object Signal + o.object_signal.emit(Colors.red) + self.assertEqual(o.value, Colors.red) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_func_test.py b/sources/pyside6/tests/signals/signal_func_test.py new file mode 100644 index 000000000..d441d4de9 --- /dev/null +++ b/sources/pyside6/tests/signals/signal_func_test.py @@ -0,0 +1,31 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import SIGNAL, SLOT + + +class SIGNALSLOTTests(unittest.TestCase): + '''Test the output of SIGNAL and SLOT.''' + + def testSIGNAL(self): + # SIGNAL function + a = "foobar" + self.assertEqual(str(SIGNAL(a)), "2foobar") + + def testSLOT(self): + # SLOT function + a = "foobar" + self.assertEqual(str(SLOT(a)), "1foobar") + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_manager_refcount_test.py b/sources/pyside6/tests/signals/signal_manager_refcount_test.py new file mode 100644 index 000000000..955d5b65b --- /dev/null +++ b/sources/pyside6/tests/signals/signal_manager_refcount_test.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject + + +class SignalManagerRefCount(unittest.TestCase): + """Simple test case to check if the signal_manager is erroneously incrementing the + object refcounter.""" + + @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") + def testObjectRefcount(self): + """Emission of QObject.destroyed() to a python slot""" + def callback(): + pass + obj = QObject() + refcount = sys.getrefcount(obj) + obj.destroyed.connect(callback) + self.assertEqual(refcount, sys.getrefcount(obj)) + obj.destroyed.disconnect(callback) + self.assertEqual(refcount, sys.getrefcount(obj)) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_newenum_test.py b/sources/pyside6/tests/signals/signal_newenum_test.py new file mode 100644 index 000000000..5fbb875af --- /dev/null +++ b/sources/pyside6/tests/signals/signal_newenum_test.py @@ -0,0 +1,50 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Qt, Slot, Signal + + +class Receiver(QObject): + def __init__(self): + super().__init__() + self.result = 0 + + @Slot(Qt.Alignment, str) + def handler(self, e, s): + print('handler', e, "type=", type(e).__name__, s) + self.result += 1 + + +class Sender(QObject): + test_sig = Signal(Qt.AlignmentFlag, str) + + def __init__(self): + super().__init__() + + def emit_test_sig(self): + self.test_sig.emit(Qt.AlignLeft, "bla") + + +class TestSignalNewEnum(unittest.TestCase): + """Test for PYSIDE-2095, signals with new enums in Python 3.11.""" + + def testIt(self): + sender = Sender() + receiver = Receiver() + sender.test_sig.connect(receiver.handler) + + sender.emit_test_sig() + self.assertEqual(receiver.result, 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_number_limit_test.py b/sources/pyside6/tests/signals/signal_number_limit_test.py new file mode 100644 index 000000000..29825fe50 --- /dev/null +++ b/sources/pyside6/tests/signals/signal_number_limit_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + + +class Emitter(QObject): + s1 = Signal() + s2 = Signal() + s3 = Signal() + s4 = Signal() + s5 = Signal() + s6 = Signal() + s7 = Signal() + s8 = Signal() + s9 = Signal() + s10 = Signal() + s11 = Signal() + s12 = Signal() + s13 = Signal() + s14 = Signal() + + +class SignalNumberLimitTest(unittest.TestCase): + def myCb(self): + self._count += 1 + + def testBug(self): + e = Emitter() + e.s1.connect(self.myCb) + e.s2.connect(self.myCb) + e.s3.connect(self.myCb) + e.s4.connect(self.myCb) + e.s5.connect(self.myCb) + e.s6.connect(self.myCb) + e.s7.connect(self.myCb) + e.s8.connect(self.myCb) + e.s9.connect(self.myCb) + e.s10.connect(self.myCb) + e.s11.connect(self.myCb) + e.s12.connect(self.myCb) + e.s13.connect(self.myCb) + e.s14.connect(self.myCb) + + self._count = 0 + e.s1.emit() + e.s2.emit() + e.s3.emit() + e.s4.emit() + e.s5.emit() + e.s6.emit() + e.s7.emit() + e.s8.emit() + e.s9.emit() + e.s10.emit() + e.s11.emit() + e.s12.emit() + e.s13.emit() + e.s14.emit() + self.assertEqual(self._count, 14) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_object_test.py b/sources/pyside6/tests/signals/signal_object_test.py new file mode 100644 index 000000000..607f51813 --- /dev/null +++ b/sources/pyside6/tests/signals/signal_object_test.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QTimer, Signal, QObject, Slot, Qt +from helper.usesqapplication import UsesQApplication + + +class MyObject(QTimer): + sig1 = Signal() + sig2 = Signal(int, name='rangeChanged') + sig3 = Signal(int) + sig4 = Signal((int,), (str,)) + sig5 = Signal((str,), (int,)) + sig6 = Signal(QObject) + + @Slot(int) + def myRange(self, r): + self._range = r + + def slot1(self): + self._called = True + + def slotString(self, s): + self._s = s + + def slotObject(self, o): + self._o = o + + +class SignalObjectTest(UsesQApplication): + def cb(self): + self._cb_called = True + self.app.exit() + + def testsingleConnect(self): + o = MyObject() + o.sig1.connect(o.slot1) + o.sig1.emit() + self.assertTrue(o._called) + + def testSignalWithArgs(self): + o = MyObject() + o.sig3.connect(o.myRange) + o.sig3.emit(10) + self.assertEqual(o._range, 10) + + def testSignatureParse(self): + o = MyObject() + o.sig2.connect(o.myRange) + o.sig2.emit(10) + + def testDictOperator(self): + o = MyObject() + o.sig4[str].connect(o.slotString) + o.sig4[str].emit("PySide") + self.assertEqual(o._s, "PySide") + + def testGeneretedSignal(self): + o = MyObject() + o.timeout.connect(self.cb) + o.start(100) + self.app.exec() + self.assertTrue(self._cb_called) + + def testConnectionType(self): + o = MyObject() + o.timeout.connect(self.cb, type=Qt.DirectConnection) + o.start(100) + self.app.exec() + self.assertTrue(self._cb_called) + + def testSignalWithSignal(self): + o = MyObject() + o.sig2.connect(o.myRange) + o.sig5.connect(o.sig2) + o.sig5[int].emit(10) + self.assertEqual(o._range, 10) + + def testSignalWithObject(self): + o = MyObject() + o.sig6.connect(o.slotObject) + arg = QObject() + o.sig6.emit(arg) + self.assertEqual(arg, o._o) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_signature_test.py b/sources/pyside6/tests/signals/signal_signature_test.py new file mode 100644 index 000000000..e8f08b2d9 --- /dev/null +++ b/sources/pyside6/tests/signals/signal_signature_test.py @@ -0,0 +1,105 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +'''Test case for signal signature received by QObject::connectNotify().''' + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal, SIGNAL, SLOT +from helper.usesqapplication import UsesQApplication + + +called = False +name = "Old" + + +class Sender(QObject): + dummySignal = Signal() + + +class Obj(QObject): + dummySignalArgs = Signal(str) + numberSignal = Signal(int) + + def __init__(self): + super().__init__() + self.signal = '' + + def connectNotify(self, signal): + self.signal = signal + + @staticmethod + def static_method(): + global called + called = True + + @staticmethod + def static_method_args(arg="default"): + global name + name = arg + + +def callback(arg=None): + pass + + +def callback_empty(): + pass + + +class TestConnectNotifyWithNewStyleSignals(UsesQApplication): + '''Test case for signal signature received by QObject::connectNotify().''' + + def testOldStyle(self): + sender = Obj() + receiver = QObject() + sender.connect(SIGNAL('destroyed()'), receiver, SLOT('deleteLater()')) + # When connecting to a regular slot, and not a python callback function, QObject::connect + # will use the non-cloned method signature, so connectinc to destroyed() will actually + # connect to destroyed(QObject*). + self.assertEqual(sender.signal.methodSignature(), 'destroyed(QObject*)') + + def testOldStyleWithPythonCallback(self): + sender = Obj() + sender.connect(SIGNAL('destroyed()'), callback) + self.assertEqual(sender.signal.methodSignature(), 'destroyed()') + + def testNewStyle(self): + sender = Obj() + + sender.destroyed.connect(callback_empty) + self.assertEqual(sender.signal.methodSignature(), 'destroyed()') + + sender.destroyed[QObject].connect(callback) + self.assertEqual(sender.signal.methodSignature(), 'destroyed(QObject*)') + + def testStaticSlot(self): + global called + sender = Sender() + sender.dummySignal.connect(Obj.static_method) + sender.dummySignal.emit() + self.assertTrue(called) + + def testStaticSlotArgs(self): + global name + sender = Obj() + sender.dummySignalArgs.connect(Obj.static_method_args) + sender.dummySignalArgs[str].emit("New") + self.assertEqual(name, "New") + + def testLambdaSlot(self): + sender = Obj() + sender.numberSignal[int].connect(lambda x: 42) + with self.assertRaises(IndexError): + sender.numberSignal[str].emit("test") + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signal_with_primitive_type_test.py b/sources/pyside6/tests/signals/signal_with_primitive_type_test.py new file mode 100644 index 000000000..01492b333 --- /dev/null +++ b/sources/pyside6/tests/signals/signal_with_primitive_type_test.py @@ -0,0 +1,38 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QCoreApplication, QTimeLine + + +class SignalPrimitiveTypeTest(unittest.TestCase): + + def signalValueChanged(self, v): + self.called = True + self._app.quit() + + def createTimeLine(self): + self.called = False + tl = QTimeLine(10000) + tl.valueChanged.connect(self.signalValueChanged) + return tl + + def testTimeLine(self): + self._valueChangedCount = 0 + self._app = QCoreApplication([]) + tl = self.createTimeLine() + tl.start() + self._app.exec() + self.assertTrue(self.called) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/signals.pyproject b/sources/pyside6/tests/signals/signals.pyproject new file mode 100644 index 000000000..b63724eaf --- /dev/null +++ b/sources/pyside6/tests/signals/signals.pyproject @@ -0,0 +1,19 @@ +{ + "files": ["anonymous_slot_leak_test.py", "args_dont_match_test.py", + "bug_189.py", "bug_311.py", "bug_312.py", "bug_319.py", "bug_79.py", + "decorators_test.py", "disconnect_test.py", "invalid_callback_test.py", + "lambda_gui_test.py", "lambda_test.py", "leaking_signal_test.py", + "multiple_connections_gui_test.py", "multiple_connections_test.py", + "pysignal_test.py", "qobject_callable_connect_test.py", "qobject_destroyed_test.py", + "qobject_receivers_test.py", "qobject_sender_test.py", "ref01_test.py", + "ref02_test.py", "ref03_test.py", "ref04_test.py", "ref05_test.py", + "ref06_test.py", "segfault_proxyparent_test.py", + "self_connect_test.py", "short_circuit_test.py", + "signal2signal_connect_test.py", "signal_across_threads.py", + "signal_autoconnect_test.py", "signal_connectiontype_support_test.py", + "signal_emission_gui_test.py", "signal_emission_test.py", + "signal_enum_test.py", "signal_func_test.py", "signal_manager_refcount_test.py", + "signal_newenum_test.py", "signal_number_limit_test.py", + "signal_object_test.py", "signal_signature_test.py", "signal_with_primitive_type_test.py", + "slot_reference_count_test.py", "static_metaobject_test.py"] +} diff --git a/sources/pyside6/tests/signals/slot_reference_count_test.py b/sources/pyside6/tests/signals/slot_reference_count_test.py new file mode 100644 index 000000000..9d5c73652 --- /dev/null +++ b/sources/pyside6/tests/signals/slot_reference_count_test.py @@ -0,0 +1,70 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +''' Forced disconnection: Delete one end of the signal connection''' + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal + + +class Dummy(QObject): + foo = Signal() + + def dispatch(self): + self.foo.emit() + + +class PythonSignalRefCount(unittest.TestCase): + + def setUp(self): + self.emitter = Dummy() + + def tearDown(self): + self.emitter + + @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") + def testRefCount(self): + def cb(*args): + pass + + self.assertEqual(sys.getrefcount(cb), 2) + + self.emitter.foo.connect(cb) + self.assertEqual(sys.getrefcount(cb), 3) + + self.emitter.foo.disconnect(cb) + self.assertEqual(sys.getrefcount(cb), 2) + + +class CppSignalRefCount(unittest.TestCase): + + def setUp(self): + self.emitter = QObject() + + def tearDown(self): + self.emitter + + @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") + def testRefCount(self): + def cb(*args): + pass + + self.assertEqual(sys.getrefcount(cb), 2) + + self.emitter.destroyed.connect(cb) + self.assertEqual(sys.getrefcount(cb), 3) + + self.emitter.destroyed.disconnect(cb) + self.assertEqual(sys.getrefcount(cb), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/signals/static_metaobject_test.py b/sources/pyside6/tests/signals/static_metaobject_test.py new file mode 100644 index 000000000..d7bf73e44 --- /dev/null +++ b/sources/pyside6/tests/signals/static_metaobject_test.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +"""Tests covering signal emission and receiving to python slots""" + +import gc +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QObject, Signal, Slot, SIGNAL +from helper.usesqapplication import UsesQApplication + + +class Sender(QObject): + + foo = Signal() + foo2 = Signal() + + +class MyObject(QObject): + + foo2 = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self._slotCalledCount = 0 + + # this '@Slot()' is needed to get the right sort order in testSharedSignalEmission. + # For some reason, it also makes the tests actually work! + @Slot() + def mySlot(self): + self._slotCalledCount = self._slotCalledCount + 1 + + +class StaticMetaObjectTest(UsesQApplication): + + def testSignalPropagation(self): + """Old style, dynamic signal creation.""" + o = QObject() + o2 = MyObject() + + # SIGNAL foo not created yet + self.assertEqual(o.metaObject().indexOfSignal("foo()"), -1) + + o.connect(SIGNAL("foo()"), o2.mySlot) + # SIGNAL foo create after connect + self.assertTrue(o.metaObject().indexOfSignal("foo()") > 0) + + # SIGNAL does not propagate to others objects of the same type + self.assertEqual(o2.metaObject().indexOfSignal("foo()"), -1) + + del o + del o2 + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + o = MyObject() + # The SIGNAL was destroyed with old objects + self.assertEqual(o.metaObject().indexOfSignal("foo()"), -1) + + def testSharedSignalEmission(self): + o = Sender() + m = MyObject() + + o.foo2.connect(m.mySlot) + m.foo2.connect(m.mySlot) + o.foo2.emit() + self.assertEqual(m._slotCalledCount, 1) + del o + # PYSIDE-535: Need to collect garbage in PyPy to trigger deletion + gc.collect() + m.foo2.emit() + self.assertEqual(m._slotCalledCount, 2) + + +if __name__ == '__main__': + unittest.main() |