Skip to content

Commit 06879ab

Browse files
authored
Add tests for COM connection point interfaces. (#919)
* test: Add `test/test_connectionpoints.py`. This new test file, `test/test_connectionpoints.py`, verifies the functionality of `connectionpoints` methods to ensure proper COM connection point management. * test: Add `IConnectionPointContainer.EnumConnectionPoints` test. Adds `test_EnumConnectionPoints` to `test_connectionpoints.py`. This test verifies the correct enumeration of connection points and ensures that each enumerated connection point properly returns its `IConnectionPointContainer`. * test: Add `IConnectionPoint` interface tests. This commit introduces `Test_IConnectionPoint` to `test_connectionpoints.py`. It verifies the `IConnectionPoint.GetConnectionInterface` method, ensuring that the correct interface ID is returned for a connection point. * test: Add `IConnectionPoint.GetConnectionPointContainer` test. This test verifies the `IConnectionPoint.GetConnectionPointContainer` method, ensuring that the connection point correctly returns its owning `IConnectionPointContainer`. * test: Add `IConnectionPoint.Advise` and `IConnectionPoint.Unadvise` tests. This commit adds `test_Advise_Unadvise` to `Test_IConnectionPoint` in `test_connectionpoints.py`. It introduces a `Sink` class as a COM event receiver and verifies the functionality of `IConnectionPoint.Advise` and `IConnectionPoint.Unadvise` methods, ensuring proper connection and disconnection of event sinks. * test: Add `IConnectionPoint.EnumConnections` test. This commit adds `test_EnumConnections` to the `Test_IConnectionPoint` class in `test_connectionpoints.py`. This test verifies the correct enumeration of active connections and ensures that the returned connection data (interface pointer and cookie) matches the advised sink. * refactor: Clarify assertions in `test_Advise_Unadvise`. Adds comments to `test_Advise_Unadvise` in `test_connectionpoints.py` to explicitly state the intent of each assertion regarding the connection status before and after `Advise` and `Unadvise` calls. * refactor: Encapsulate `Sink` creation within `Test_IConnectionPoint`. Moves the `Sink` class definition into a `create_sink` class method within `Test_IConnectionPoint` in `test_connectionpoints.py`. This change encapsulates the `Sink` object creation logic. * test: Add `Test_Sink` for `IConnectionPoint` event handling. Introduces a new test class `Test_Sink` in `test_connectionpoints.py` to verify proper event sinking mechanisms. * refactor: Use `IUnknown` for sink return type in `create_sink_and_log`. Adjusts the `create_sink_and_log` method in `test_connectionpoints.py` to return the `IUnknown` interface. This change emphasizes that COM objects are handled as pointers in the COM world, rather than being tied to specific Python types or attributes.
1 parent 338e34d commit 06879ab

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import contextlib
2+
import time
3+
import unittest as ut
4+
from collections.abc import Sequence
5+
from ctypes import byref
6+
7+
import comtypes.client
8+
from comtypes import COMObject, IUnknown
9+
from comtypes.connectionpoints import IConnectionPointContainer
10+
11+
# generating `MSVidCtlLib` also generates `stdole`.
12+
with contextlib.redirect_stdout(None): # supress warnings
13+
comtypes.client.GetModule("msvidctl.dll")
14+
from comtypes.gen import MSVidCtlLib as msvidctl
15+
from comtypes.gen import stdole
16+
17+
18+
class Test_IConnectionPointContainer(ut.TestCase):
19+
EVENT_IID = msvidctl._IMSVidCtlEvents._iid_
20+
21+
def setUp(self):
22+
self.impl = comtypes.client.CreateObject(
23+
msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl
24+
)
25+
self.cpc = self.impl.QueryInterface(IConnectionPointContainer)
26+
27+
def test_EnumConnectionPoints(self):
28+
conn_pts = list(self.cpc.EnumConnectionPoints())
29+
self.assertGreater(len(conn_pts), 0)
30+
self.assertTrue(
31+
all(pt.GetConnectionPointContainer() == self.cpc for pt in conn_pts)
32+
)
33+
34+
def test_FindConnectionPoint(self):
35+
cp = self.cpc.FindConnectionPoint(byref(self.EVENT_IID))
36+
self.assertEqual(cp.GetConnectionPointContainer(), self.cpc)
37+
38+
39+
class Test_IConnectionPoint(ut.TestCase):
40+
EVENT_IID = msvidctl._IMSVidCtlEvents._iid_
41+
OUTGOING_ITF = msvidctl._IMSVidCtlEvents
42+
43+
def setUp(self):
44+
self.impl = comtypes.client.CreateObject(
45+
msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl
46+
)
47+
self.cpc = self.impl.QueryInterface(IConnectionPointContainer)
48+
self.cp = self.cpc.FindConnectionPoint(byref(self.EVENT_IID))
49+
50+
@classmethod
51+
def create_sink(cls) -> COMObject:
52+
class Sink(COMObject):
53+
_com_interfaces_ = [cls.OUTGOING_ITF]
54+
55+
return Sink()
56+
57+
def test_GetConnectionInterface(self):
58+
self.assertEqual(self.cp.GetConnectionInterface(), self.EVENT_IID)
59+
60+
def test_GetConnectionPointContainer(self):
61+
self.assertEqual(self.cp.GetConnectionPointContainer(), self.cpc)
62+
63+
def test_Advise_Unadvise(self):
64+
# Verify the connection DOES NOT exist.
65+
self.assertEqual(len(list(self.cp.EnumConnections())), 0)
66+
sink = self.create_sink()
67+
# Since `POINTER(IUnknown).from_param`(`_compointer_base.from_param`)
68+
# can accept a `COMObject` instance, `IConnectionPoint.Advise` can
69+
# take either a COM object or a COM interface pointer.
70+
cookie = self.cp.Advise(sink)
71+
# Verify the connection exists.
72+
self.assertEqual(len(list(self.cp.EnumConnections())), 1)
73+
self.cp.Unadvise(cookie)
74+
# Verify the connection DOES NOT exist again.
75+
self.assertEqual(len(list(self.cp.EnumConnections())), 0)
76+
77+
def test_EnumConnections(self):
78+
sink = self.create_sink().QueryInterface(self.OUTGOING_ITF)
79+
cookie = self.cp.Advise(sink)
80+
conns = [
81+
(data.pUnk.QueryInterface(self.OUTGOING_ITF), data.dwCookie)
82+
for data in self.cp.EnumConnections()
83+
]
84+
self.assertEqual(len(conns), 1)
85+
((punk, ck),) = conns
86+
self.assertEqual(ck, cookie)
87+
self.assertEqual(punk, sink)
88+
self.cp.Unadvise(cookie)
89+
90+
91+
class Test_Sink(ut.TestCase):
92+
EVENT_IID = stdole.FontEvents._iid_
93+
OUTGOING_ITF = stdole.FontEvents
94+
95+
def setUp(self):
96+
self.impl = comtypes.client.CreateObject(stdole.StdFont, interface=stdole.IFont)
97+
self.cpc = self.impl.QueryInterface(IConnectionPointContainer)
98+
self.cp = self.cpc.FindConnectionPoint(byref(self.EVENT_IID))
99+
100+
@classmethod
101+
def create_sink_and_log(cls) -> tuple[IUnknown, Sequence[str]]:
102+
eventlog = []
103+
104+
class Sink(COMObject):
105+
_com_interfaces_ = [cls.OUTGOING_ITF]
106+
107+
# This method directly handles the event from the COM object.
108+
# Its name and signature must match the event definition in the
109+
# COM interface.
110+
# In a real-world scenario, event utilities in `client` module
111+
# would dynamically generate or map these methods.
112+
def FontChanged(self, PropertyName: str) -> None:
113+
eventlog.append(PropertyName)
114+
115+
return Sink().QueryInterface(IUnknown), eventlog
116+
117+
def test_sink(self):
118+
sink, fired_events = self.create_sink_and_log()
119+
cookie = self.cp.Advise(sink)
120+
self.assertFalse(fired_events)
121+
# Trigger the event by changing a property
122+
self.impl.Bold = not self.impl.Bold
123+
# We need to ensure the event has a chance to fire.
124+
# For testing, we need a small delay (or a COM message pump).
125+
time.sleep(0.05)
126+
# Assert the event was fired
127+
self.assertEqual(fired_events, ["Bold"])
128+
self.cp.Unadvise(cookie)

0 commit comments

Comments
 (0)