diff options
| author | Brett Stottlemyer <bstottle@ford.com> | 2024-12-18 10:33:56 -0500 |
|---|---|---|
| committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2025-03-13 16:28:42 +0100 |
| commit | 19abd816e73bebdd489408d0a3b7676822bff39c (patch) | |
| tree | 8459ae9401f5e190995b3e24b6ae6968cf457baf /sources/pyside6/tests/QtRemoteObjects/integration_test.py | |
| parent | 3c66c456aeab597b7cb046f81c7f015433bb57a4 (diff) | |
Make Remote Objects usable beyond Models
While present, the Qt Remote Objects bindings to Python have not been
very useful. The only usable components were those based on
QAbstractItemModel, due to the lack of a way to interpret .rep files
from Python. This addresses that limitation.
Fixes: PYSIDE-862
Change-Id: Ice57c0c64f11c3c7e74d50ce3c48617bd9b422a3
Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io>
Reviewed-by: Brett Stottlemyer <brett.stottlemyer@gmail.com>
Diffstat (limited to 'sources/pyside6/tests/QtRemoteObjects/integration_test.py')
| -rw-r--r-- | sources/pyside6/tests/QtRemoteObjects/integration_test.py | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/sources/pyside6/tests/QtRemoteObjects/integration_test.py b/sources/pyside6/tests/QtRemoteObjects/integration_test.py new file mode 100644 index 000000000..69b4930da --- /dev/null +++ b/sources/pyside6/tests/QtRemoteObjects/integration_test.py @@ -0,0 +1,369 @@ +#!/usr/bin/python +# Copyright (C) 2025 Ford Motor Company +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +from __future__ import annotations + +'''Test cases for basic Source/Replica communication''' + +import os +import sys +import textwrap +import enum +import gc + +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 QUrl, qWarning +from PySide6.QtRemoteObjects import (QRemoteObjectHost, QRemoteObjectNode, QRemoteObjectReplica, + QRemoteObjectPendingCall, RepFile, getCapsuleCount) +from PySide6.QtTest import QSignalSpy, QTest + +from test_shared import wrap_tests_for_cleanup +from helper.usesqapplication import UsesQApplication + +contents = """ +class Simple +{ + PROP(int i = 2); + PROP(float f = -1. READWRITE); + SIGNAL(random(int i)); + SLOT(void reset()); + SLOT(int add(int i)); +}; +""" + + +class QBasicTest(UsesQApplication): + '''Test case for basic source/replica communication''' + def setUp(self): + # Separate output to make debugging easier + qWarning(f"\nSet up {self.__class__.__qualname__}") + super().setUp() + '''Set up test environment''' + if hasattr(self.__class__, "contents"): + qWarning(f"Using class contents >{self.__class__.contents}<") + self.rep = RepFile(self.__class__.contents) + else: + self.rep = RepFile(contents) + self.host = QRemoteObjectHost(QUrl("tcp://127.0.0.1:0")) + self.host.setObjectName("host") + self.node = QRemoteObjectNode() + self.node.setObjectName("node") + self.node.connectToNode(self.host.hostUrl()) # pick up the url with the assigned port + + def compare_properties(self, instance, values): + '''Compare properties of instance with values''' + self.assertEqual(instance.i, values[0]) + self.assertAlmostEqual(instance.f, values[1], places=5) + + def default_setup(self): + '''Set up default test environment''' + replica = self.node.acquire(self.rep.replica["Simple"]) + # Make sure the replica is initialized with default values + self.compare_properties(replica, [2, -1]) + self.assertEqual(replica.isInitialized(), False) + source = self.rep.source["Simple"]() + # Make sure the source is initialized with default values + self.compare_properties(source, [2, -1]) + return replica, source + + def tearDown(self): + self.assertEqual(getCapsuleCount(), 0) + self.app.processEvents() + super().tearDown() + # Separate output to make debugging easier + qWarning(f"Tore down {self.__class__.__qualname__}\n") + + +@wrap_tests_for_cleanup(extra=['rep', 'host', 'node']) +class ReplicaInitialization(QBasicTest): + def test_ReplicaInitialization(self): + replica, source = self.default_setup() + source.i = -1 + source.f = 3.14 + self.compare_properties(source, [-1, 3.14]) + init_spy = QSignalSpy(replica.initialized) + self.host.enableRemoting(source) + self.assertEqual(replica.waitForSource(1000), True) + self.assertEqual(replica.state(), QRemoteObjectReplica.State.Valid) + # Make sure the replica values are updated to the source values + self.compare_properties(replica, [-1, 3.14]) + self.assertEqual(init_spy.count(), 1) + self.assertEqual(replica.isInitialized(), True) + + +@wrap_tests_for_cleanup(extra=['rep', 'host', 'node']) +class SourcePropertyChange(QBasicTest): + def test_SourcePropertyChange(self): + replica, source = self.default_setup() + self.host.enableRemoting(source) + self.assertEqual(replica.waitForSource(1000), True) + # Make sure the replica values are unchanged since the source had the same values + self.compare_properties(replica, [2, -1]) + source_spy = QSignalSpy(source.iChanged) + replica_spy = QSignalSpy(replica.iChanged) + source.i = 42 + self.assertEqual(source_spy.count(), 1) + # Make sure the source value is updated + self.compare_properties(source, [42, source.f]) + self.assertTrue(replica_spy.wait(1000)) + self.assertEqual(replica_spy.count(), 1) + # Make sure the replica value is updated + self.compare_properties(replica, [42, replica.f]) + + +@wrap_tests_for_cleanup(extra=['rep', 'host', 'node']) +class ReplicaPropertyChange(QBasicTest): + def test_ReplicaPropertyChange(self): + replica, source = self.default_setup() + self.host.enableRemoting(source) + self.assertEqual(replica.waitForSource(1000), True) + # Make sure push methods are working + source_spy = QSignalSpy(source.iChanged) + replica_spy = QSignalSpy(replica.iChanged) + replica.pushI(11) + # # Let eventloop run to update the source and verify the values + self.assertTrue(source_spy.wait(1000)) + self.assertEqual(source_spy.count(), 1) + self.compare_properties(source, [11, source.f]) + # Let eventloop run to update the replica and verify the values + self.assertTrue(replica_spy.wait(1000)) + self.assertEqual(replica_spy.count(), 1) + self.compare_properties(replica, [11, replica.f]) + + # Test setter on replica + source_spy = QSignalSpy(source.fChanged) + replica_spy = QSignalSpy(replica.fChanged) + replica.f = 4.2 + # Make sure the replica values are ** NOT CHANGED ** since the eventloop hasn't run + self.compare_properties(replica, [11, -1]) + # Let eventloop run to update the source and verify the values + self.assertTrue(source_spy.wait(1000)) + self.assertEqual(source_spy.count(), 1) + self.compare_properties(source, [source.i, 4.2]) + # Let eventloop run to update the replica and verify the values + self.assertTrue(replica_spy.wait(1000)) + self.assertEqual(replica_spy.count(), 1) + self.compare_properties(replica, [replica.i, 4.2]) + + +@wrap_tests_for_cleanup(extra=['rep', 'host', 'node']) +class DerivedReplicaPropertyChange(QBasicTest): + def test_DerivedReplicaPropertyChange(self): + # Don't use default_setup(), instead create a derived replica + Replica = self.rep.replica["Simple"] + + class DerivedReplica(Replica): + pass + + replica = self.node.acquire(DerivedReplica) + # Make sure the replica is initialized with default values + self.compare_properties(replica, [2, -1]) + self.assertEqual(replica.isInitialized(), False) + source = self.rep.source["Simple"]() + self.host.enableRemoting(source) + self.assertEqual(replica.waitForSource(1000), True) + + +@wrap_tests_for_cleanup(extra=['rep', 'host', 'node']) +class ReplicaSlotNotImplementedChange(QBasicTest): + def test_ReplicaSlotNotImplementedChange(self): + replica, source = self.default_setup() + self.host.enableRemoting(source) + self.assertEqual(replica.waitForSource(1000), True) + # Ideally this would fail as the slot is not implemented on the source + res = replica.reset() + self.assertEqual(type(res), type(None)) + QTest.qWait(100) # Wait for 100 ms for async i/o. There isn't a signal to wait on + res = replica.add(5) + self.assertEqual(type(res), QRemoteObjectPendingCall) + + +@wrap_tests_for_cleanup(extra=['rep', 'host', 'node']) +class ReplicaSlotImplementedChange(QBasicTest): + def test_ReplicaSlotImplementedChange(self): + replica = self.node.acquire(self.rep.replica["Simple"]) + replica.setObjectName("replica") + + class Source(self.rep.source["Simple"]): + def __init__(self): + super().__init__() + self.i = 6 + self.f = 3.14 + + def reset(self): + self.i = 0 + self.f = 0 + + def add(self, i): + return self.i + i + + source = Source() + source.setObjectName("source") + self.host.enableRemoting(source) + self.assertEqual(replica.waitForSource(1000), True) + self.compare_properties(source, [6, 3.14]) + self.compare_properties(replica, [6, 3.14]) + replica_spy = QSignalSpy(replica.iChanged) + res = replica.reset() + self.assertEqual(type(res), type(None)) + self.assertEqual(replica_spy.wait(1000), True) + self.compare_properties(source, [0, 0]) + self.compare_properties(replica, [0, 0]) + res = replica.add(5) + self.assertEqual(type(res), QRemoteObjectPendingCall) + self.assertEqual(res.waitForFinished(1000), True) + self.assertEqual(res.returnValue(), 5) + + +@wrap_tests_for_cleanup(extra=['rep', 'host', 'node']) +class RefCountTest(QBasicTest): + contents = textwrap.dedent("""\ + POD MyPOD{ + ENUM class Position : unsigned short {position1=1, position2=2, position3=4} + Position pos, + QString name + } + class Simple + { + ENUM Position {Left, Right, Top, Bottom} + PROP(MyPOD myPod); + PROP(Position pos); + } + """) + + def test_RefCount(self): + # Once the rep file is loaded, we should be tracking 4 converter capsules + # - 1 for the POD itself + # - 1 for the enum in the POD + # - 1 for the enum in the Source + # - 1 for the enum in the Replica + # We should be tracking 3 qobject capsules (POD, Replica, Source) + # Note: Source and Replica are distinct types, so Source::EPosition and + # Replica::EPosition are distinct as well. + # Note 2: The name of the enum ("Position") can be reused for different + # types in different classes as shown above. + self.assertEqual(getCapsuleCount(), 7) + MyPod = self.rep.pod["MyPOD"] + self.assertTrue(isinstance(MyPod, type)) + self.assertTrue(issubclass(MyPod, tuple)) + MyEnum = MyPod.get_enum("Position") + self.assertTrue(isinstance(MyEnum, type)) + self.assertTrue(issubclass(MyEnum, enum.Enum)) + e = MyEnum(4) # noqa: F841 + Source = self.rep.source["Simple"] + source = Source() # noqa: F841 + source = None # noqa: F841 + Source = None + Replica = self.rep.replica["Simple"] + replica = self.node.acquire(Replica) # noqa: F841 + replica = None # noqa: F841 + Replica = None + MyEnum = None + MyPod = None + self.rep = None + e = None # noqa: F841 + gc.collect() + # The enum and POD capsules will only be deleted (garbage collected) if + # the types storing them (RepFile, Replica and Source) are garbage + # collected first. + self.assertEqual(getCapsuleCount(), 0) + + +@wrap_tests_for_cleanup(extra=['rep', 'host', 'node']) +class EnumTest(QBasicTest): + contents = textwrap.dedent("""\ + POD MyPOD{ + ENUM class Position : unsigned short {position1=1, position2=2, position3=4} + Position pos, + QString name + } + class Simple + { + ENUM Position {Left, Right, Top, Bottom} + PROP(MyPOD myPod); + PROP(Position pos); + } + """) + + def test_Enum(self): + MyPod = self.rep.pod["MyPOD"] + self.assertTrue(isinstance(MyPod, type)) + self.assertTrue(issubclass(MyPod, tuple)) + PodEnum = MyPod.get_enum("Position") + self.assertTrue(isinstance(PodEnum, type)) + self.assertTrue(issubclass(PodEnum, enum.Enum)) + t = (PodEnum(4), "test") + myPod = MyPod(*t) + with self.assertRaises(ValueError): + myPod = MyPod(PodEnum(0), "thing") # 0 isn't a valid enum value + myPod = MyPod(PodEnum(2), "thing") + self.assertEqual(myPod.pos, PodEnum.position2) + replica = self.node.acquire(self.rep.replica["Simple"]) + replica.setObjectName("replica") + source = self.rep.source["Simple"]() + source.setObjectName("source") + source.myPod = (PodEnum.position2, "Hello") + SourceEnum = source.get_enum("Position") + self.assertTrue(isinstance(SourceEnum, type)) + self.assertTrue(issubclass(SourceEnum, enum.Enum)) + source.pos = SourceEnum.Top + self.assertEqual(source.myPod, (PodEnum.position2, "Hello")) + self.assertNotEqual(source.pos, 2) + self.host.enableRemoting(source) + self.assertEqual(replica.waitForSource(1000), True) + self.assertEqual(replica.myPod, (PodEnum.position2, "Hello")) + ReplicaEnum = replica.get_enum("Position") + # Test invalid comparisons + self.assertNotEqual(replica.pos, 2) + self.assertNotEqual(replica.pos, SourceEnum.Top) + self.assertNotEqual(replica.myPod, (SourceEnum(2), "Hello")) + self.assertNotEqual(replica.myPod, (ReplicaEnum(2), "Hello")) + self.assertNotEqual(replica.myPod, (2, "Hello")) + # Test valid comparisons to Replica enum + self.assertEqual(replica.pos, ReplicaEnum.Top) + self.assertEqual(replica.myPod, (PodEnum(2), "Hello")) + + +@wrap_tests_for_cleanup(extra=['rep', 'host', 'node']) +class PodTest(QBasicTest): + contents = textwrap.dedent("""\ + POD MyPod(int i, QString s) + + class Simple + { + PROP(MyPod pod); + } + """) + + def test_Pod(self): + MyPod = self.rep.pod["MyPod"] + self.assertTrue(isinstance(MyPod, type)) + self.assertTrue(issubclass(MyPod, tuple)) + source = self.rep.source["Simple"]() + t = (42, "Hello") + pod = MyPod(*t) + source.pod = t + self.assertEqual(source.pod, t) + self.assertEqual(source.pod, pod) + source.pod = pod + self.assertEqual(source.pod, t) + self.assertEqual(source.pod, pod) + with self.assertRaises(ValueError): + source.pod = (11, "World", "!") + with self.assertRaises(TypeError): + source.pod = MyPod("Hello", "World") + self.assertEqual(source.pod, pod) + self.assertTrue(isinstance(pod, MyPod)) + self.assertEqual(pod.i, 42) + self.assertEqual(pod.s, "Hello") + self.assertTrue(isinstance(source.pod, MyPod)) + + +if __name__ == '__main__': + unittest.main() |
