Skip to content

Commit da3634c

Browse files
authored
Enhance COM Moniker, Bind Context and Running Object Table Test Coverage (#912)
* test: Add `Test_IsEqual` for moniker comparison in `test_moniker.py` Introduces `Test_IsEqual` class to `test/test_moniker.py` to verify the correct functionality of `IsEqual` method for monikers. This test ensures that monikers with identical item IDs are correctly identified as equal, while those with different item IDs are not. * test: Add `Test_Hash` for moniker hash comparison in `test_moniker.py` Introduces `Test_Hash` class to `test/test_moniker.py` to verify the correct functionality of the `Hash` method for monikers. This test ensures that monikers with identical item IDs produce the same hash value, while those with different item IDs produce different hash values. * test: Add `Test_ComposeWith` for moniker composition and error handling Introduces `Test_ComposeWith` to `test/test_moniker.py` to verify the `ComposeWith` method. This test ensures that composing item monikers results in a `CLSID_CompositeMoniker`. It also validates that when `fOnlyIfNotGeneric=True`, `ComposeWith` correctly raises a `COMError` with `MK_E_NEEDGENERIC`, indicating that the moniker cannot be composed generically. Constants `CLSID_CompositeMoniker` and `MK_E_NEEDGENERIC` were added to `test/monikers_helper.py` to support this test. * test: Add `Test_Enum` for composite moniker enumeration Introduces `Test_Enum` class with `test_generic_composite` method to `test/test_moniker.py`. This test verifies the `Enum` method of composite monikers, ensuring it correctly returns an `IEnumMoniker` instance. It uses the newly added `_CreateGenericComposite` helper in `test/monikers_helper.py`. * test: Add `test_generic_composite` to `test_moniker.py` This test verifies the correct behavior of `IsSystemMoniker`, `GetDisplayName`, `GetClassID`, and `Inverse` methods for generic composite monikers. The `MKSYS_GENERICCOMPOSITE` constant was added to `monikers_helper.py` to support this test. * test: Add `Test_RemoteBindToObject` for file moniker binding Introduces `Test_RemoteBindToObject` class with `test_file` method to `test/test_moniker.py`. This test verifies the `RemoteBindToObject` method for file monikers, ensuring correct binding to `IPersistFile`. The `_CreateFileMoniker` helper function was added to `monikers_helper.py` to support this test. * test: Add `test_file` for `IsSystemMoniker` to `test_moniker.py` Introduces `test_file` method to `Test_IsSystemMoniker_GetDisplayName_Inverse` in `test/test_moniker.py`. This test verifies the correct behavior of `IsSystemMoniker`, `GetDisplayName`, `GetClassID`, and `Inverse` methods for file monikers. The `MKSYS_FILEMONIKER` and `CLSID_FileMoniker` constants were added to `monikers_helper.py` to support this test. * test: Add `Test_CommonPrefixWith` for moniker common prefix detection Introduces `Test_CommonPrefixWith` class with `test_file` method to `test/test_moniker.py`. This test verifies the `CommonPrefixWith` method for file monikers, ensuring it correctly identifies the common directory path between different file monikers. * test: Add `Test_RelativePathTo` for moniker relative path calculation Introduces `Test_RelativePathTo` class with `test_file` method to `/test/test_moniker.py`. This test verifies the `RelativePathTo` method for file monikers, ensuring it correctly calculates the relative path between two file monikers, mirroring `pathlib.Path.relative_to`. * test: Add tests for `IBindCtx.RemoteSetBindOptions` and `RemoteGetBindOptions`. This commit introduces a new test class `Test_Set_Get_BindOptions` to `test/test_bctx.py`. It verifies the correct functionality of `SetBindOptions` and `GetBindOptions` by setting and retrieving `tagBIND_OPTS2` values and asserting their correctness. * test: Add tests for `IBindCtx` object parameter (`...ObjectParam`) methods. This commit introduces a new test class `Test_Get_Register_Revoke_ObjectParam` to `test/test_bctx.py`. It verifies the functionality of `GetObjectParam`, `RegisterObjectParam`, and `RevokeObjectParam` by registering and revoking a COM object and asserting its presence or absence. * test: Add tests for `IBindCtx` bound object (`...ObjectBound`) methods. This commit introduces a new test class to `test/test_bctx.py`. It verifies the functionality of `RegisterObjectBound`, `RevokeObjectBound`, and `ReleaseBoundObjects` by registering and unregistering a COM object within the bind context. * test: Add test for `IRunningObjectTable.EnumRunning` method. * fix: Resolve path comparison issues in `test_moniker.py` Add `_get_long_path_name` to `test_moniker.py`. This change ensures consistent path normalization across test environments, preventing failures due to short path vs. long path discrepancies. * fix: Simplify relative path comparison in `test_moniker.py` Replaced the dynamic calculation of relative paths using `Path.relative_to` with a literal string in `Test_RelativePathTo.test_file`. This simplifies the test logic and resolves the `TypeError` caused by `walk_up=True` in older Python environments, aligning with the goal of reducing complexity.
1 parent 526af9a commit da3634c

4 files changed

Lines changed: 312 additions & 5 deletions

File tree

comtypes/test/monikers_helper.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
from comtypes import GUID, IUnknown
55

66
# https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-mksys
7+
MKSYS_GENERICCOMPOSITE = 1
8+
MKSYS_FILEMONIKER = 2
79
MKSYS_ITEMMONIKER = 4
810

11+
CLSID_CompositeMoniker = GUID("{00000309-0000-0000-c000-000000000046}")
12+
CLSID_FileMoniker = GUID("{00000303-0000-0000-C000-000000000046}")
913
CLSID_AntiMoniker = GUID("{00000305-0000-0000-c000-000000000046}")
1014
CLSID_ItemMoniker = GUID("{00000304-0000-0000-c000-000000000046}")
1115

@@ -15,6 +19,18 @@
1519

1620
_ole32 = OleDLL("ole32")
1721

22+
_CreateGenericComposite = _ole32.CreateGenericComposite
23+
_CreateGenericComposite.argtypes = [
24+
POINTER(IUnknown), # pmkFirst
25+
POINTER(IUnknown), # pmkRest
26+
POINTER(POINTER(IUnknown)), # ppmkComposite
27+
]
28+
_CreateGenericComposite.restype = HRESULT
29+
30+
_CreateFileMoniker = _ole32.CreateFileMoniker
31+
_CreateFileMoniker.argtypes = [LPCOLESTR, POINTER(POINTER(IUnknown))]
32+
_CreateFileMoniker.restype = HRESULT
33+
1834
_CreateItemMoniker = _ole32.CreateItemMoniker
1935
_CreateItemMoniker.argtypes = [LPCOLESTR, LPCOLESTR, POINTER(POINTER(IUnknown))]
2036
_CreateItemMoniker.restype = HRESULT
@@ -28,4 +44,5 @@
2844
_GetRunningObjectTable.restype = HRESULT
2945

3046
# Common COM Errors from Moniker/Binding Context operations
47+
MK_E_NEEDGENERIC = -2147221022 # 0x800401E2
3148
MK_E_UNAVAILABLE = -2147221021 # 0x800401E3

comtypes/test/test_bctx.py

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import contextlib
22
import unittest
33
from _ctypes import COMError
4-
from ctypes import POINTER, byref
4+
from ctypes import POINTER, byref, sizeof
55

6-
from comtypes import GUID, hresult
6+
from comtypes import GUID, hresult, tagBIND_OPTS2
77
from comtypes.client import CreateObject, GetModule
88
from comtypes.test.monikers_helper import (
99
ROTFLAGS_ALLOWANYCLIENT,
@@ -64,3 +64,80 @@ def test_returns_rot(self):
6464
rot_from_func.Revoke(dw_reg)
6565
# After revoking: should NOT be running again
6666
self.assertEqual(rot_from_bctx, rot_from_func)
67+
68+
69+
class Test_Register_Revoke_Release_ObjectBound(unittest.TestCase):
70+
def test_register_and_revoke(self):
71+
bctx = _create_bctx()
72+
vidctl = CreateObject(msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl)
73+
# Binds the object to the bind context, ensuring it stays alive during
74+
# the binding operation.
75+
hr = bctx.RegisterObjectBound(vidctl)
76+
self.assertEqual(hr, hresult.S_OK)
77+
# At this point, `bctx` holds a reference to `vidctl`.
78+
# Unlike `RegisterObjectParam`, there is no public API to retrieve
79+
# objects registered via `RegisterObjectBound` from `IBindCtx`.
80+
# Therefore, direct testing of `vidctl`'s accessibility via `bctx`
81+
# after binding (similar to `GetObjectParam`) is not possible.
82+
# Releases the reference to the object previously registered.
83+
hr = bctx.RevokeObjectBound(vidctl)
84+
self.assertEqual(hr, hresult.S_OK)
85+
# `bctx` holds a reference to `vidctl` again.
86+
# Releases all object references currently held by the bind context.
87+
bctx.RegisterObjectBound(vidctl)
88+
hr = bctx.ReleaseBoundObjects()
89+
self.assertEqual(hr, hresult.S_OK)
90+
91+
92+
class Test_Get_Register_Revoke_ObjectParam(unittest.TestCase):
93+
def test_get_and_register_and_revoke(self):
94+
bctx = _create_bctx()
95+
key = str(GUID.create_new())
96+
vidctl = CreateObject(msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl)
97+
# `GetObjectParam` should fail as it's NOT registered yet
98+
with self.assertRaises(COMError) as cm:
99+
bctx.GetObjectParam(key)
100+
self.assertEqual(cm.exception.hresult, hresult.E_FAIL)
101+
# Register object
102+
hr = bctx.RegisterObjectParam(key, vidctl)
103+
self.assertEqual(hr, hresult.S_OK)
104+
# `GetObjectParam` should succeed now
105+
ret_obj = bctx.GetObjectParam(key)
106+
self.assertEqual(ret_obj.QueryInterface(msvidctl.IMSVidCtl), vidctl)
107+
# Revoke object
108+
hr = bctx.RevokeObjectParam(key)
109+
self.assertEqual(hr, hresult.S_OK)
110+
# `GetObjectParam` should fail again after revoke
111+
with self.assertRaises(COMError) as cm:
112+
bctx.GetObjectParam(key)
113+
self.assertEqual(cm.exception.hresult, hresult.E_FAIL)
114+
115+
116+
class Test_Set_Get_BindOptions(unittest.TestCase):
117+
def test_set_get_bind_options(self):
118+
bctx = _create_bctx()
119+
# Create an instance of `BIND_OPTS2` and set some values.
120+
# In comtypes, instances of Structure subclasses like `tagBIND_OPTS2`
121+
# can be passed directly as arguments where COM methods expect a
122+
# pointer to the structure.
123+
hr = bctx.RemoteSetBindOptions(
124+
tagBIND_OPTS2(
125+
cbStruct=sizeof(tagBIND_OPTS2),
126+
grfFlags=0x11223344,
127+
grfMode=0x55667788,
128+
dwTickCountDeadline=12345,
129+
)
130+
)
131+
self.assertEqual(hr, hresult.S_OK)
132+
# Create a new instance for retrieval.
133+
# The `cbStruct` field is crucial in COM as it indicates the size of
134+
# the structure to the COM component, allowing it to handle different
135+
# versions of the structure (for backward and forward compatibility).
136+
# https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ibindctx-getbindoptions#notes-to-callers
137+
bind_opts = tagBIND_OPTS2(cbStruct=sizeof(tagBIND_OPTS2))
138+
ret = bctx.RemoteGetBindOptions(bind_opts)
139+
self.assertIsInstance(ret, tagBIND_OPTS2)
140+
self.assertEqual(bind_opts.cbStruct, sizeof(tagBIND_OPTS2))
141+
self.assertEqual(bind_opts.grfFlags, 0x11223344)
142+
self.assertEqual(bind_opts.grfMode, 0x55667788)
143+
self.assertEqual(bind_opts.dwTickCountDeadline, 12345)

comtypes/test/test_moniker.py

Lines changed: 203 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,67 @@
11
import contextlib
2+
import ctypes
3+
import os
4+
import tempfile
25
import unittest
3-
from ctypes import POINTER, byref
6+
from _ctypes import COMError
7+
from ctypes import POINTER, WinDLL, byref
8+
from ctypes.wintypes import DWORD, LPCWSTR, LPWSTR, MAX_PATH
9+
from pathlib import Path
410

511
from comtypes import GUID, hresult
612
from comtypes.client import CreateObject, GetModule
13+
from comtypes.persist import IPersistFile
714
from comtypes.test.monikers_helper import (
15+
MK_E_NEEDGENERIC,
16+
MKSYS_FILEMONIKER,
17+
MKSYS_GENERICCOMPOSITE,
818
MKSYS_ITEMMONIKER,
919
ROTFLAGS_ALLOWANYCLIENT,
1020
CLSID_AntiMoniker,
21+
CLSID_CompositeMoniker,
22+
CLSID_FileMoniker,
1123
CLSID_ItemMoniker,
1224
_CreateBindCtx,
25+
_CreateFileMoniker,
26+
_CreateGenericComposite,
1327
_CreateItemMoniker,
1428
_GetRunningObjectTable,
1529
)
1630

1731
with contextlib.redirect_stdout(None): # supress warnings
1832
GetModule("msvidctl.dll")
1933
from comtypes.gen import MSVidCtlLib as msvidctl
20-
from comtypes.gen.MSVidCtlLib import IBindCtx, IMoniker, IRunningObjectTable
34+
from comtypes.gen.MSVidCtlLib import (
35+
IBindCtx,
36+
IEnumMoniker,
37+
IMoniker,
38+
IRunningObjectTable,
39+
)
40+
41+
_kernel32 = WinDLL("kernel32")
42+
43+
_GetLongPathNameW = _kernel32.GetLongPathNameW
44+
_GetLongPathNameW.argtypes = [LPCWSTR, LPWSTR, DWORD]
45+
_GetLongPathNameW.restype = DWORD
46+
47+
48+
def _get_long_path_name(path: str) -> str:
49+
"""Converts a path to its long form using GetLongPathNameW."""
50+
buffer = ctypes.create_unicode_buffer(MAX_PATH)
51+
length = _GetLongPathNameW(path, buffer, MAX_PATH)
52+
return buffer.value[:length]
53+
54+
55+
def _create_generic_composite(mk_first: IMoniker, mk_rest: IMoniker) -> IMoniker:
56+
mon = POINTER(IMoniker)()
57+
_CreateGenericComposite(mk_first, mk_rest, byref(mon))
58+
return mon # type: ignore
59+
60+
61+
def _create_file_moniker(path: str) -> IMoniker:
62+
mon = POINTER(IMoniker)()
63+
_CreateFileMoniker(path, byref(mon))
64+
return mon # type: ignore
2165

2266

2367
def _create_item_moniker(delim: str, item: str) -> IMoniker:
@@ -41,6 +85,35 @@ def _create_rot() -> IRunningObjectTable:
4185

4286

4387
class Test_IsSystemMoniker_GetDisplayName_Inverse(unittest.TestCase):
88+
def test_generic_composite(self):
89+
item_id1 = str(GUID.create_new())
90+
item_id2 = str(GUID.create_new())
91+
mon = _create_generic_composite(
92+
_create_item_moniker("!", item_id1),
93+
_create_item_moniker("!", item_id2),
94+
)
95+
self.assertEqual(mon.IsSystemMoniker(), MKSYS_GENERICCOMPOSITE)
96+
bctx = _create_bctx()
97+
self.assertEqual(mon.GetDisplayName(bctx, None), f"!{item_id1}!{item_id2}")
98+
self.assertEqual(mon.GetClassID(), CLSID_CompositeMoniker)
99+
self.assertEqual(mon.Inverse().GetClassID(), CLSID_CompositeMoniker)
100+
101+
def test_file(self):
102+
with tempfile.NamedTemporaryFile() as f:
103+
mon = _create_file_moniker(f.name)
104+
self.assertEqual(mon.IsSystemMoniker(), MKSYS_FILEMONIKER)
105+
bctx = _create_bctx()
106+
self.assertEqual(
107+
os.path.normcase(
108+
os.path.normpath(
109+
_get_long_path_name(mon.GetDisplayName(bctx, None))
110+
)
111+
),
112+
os.path.normcase(os.path.normpath(_get_long_path_name(f.name))),
113+
)
114+
self.assertEqual(mon.GetClassID(), CLSID_FileMoniker)
115+
self.assertEqual(mon.Inverse().GetClassID(), CLSID_AntiMoniker)
116+
44117
def test_item(self):
45118
item_id = str(GUID.create_new())
46119
mon = _create_item_moniker("!", item_id)
@@ -51,6 +124,41 @@ def test_item(self):
51124
self.assertEqual(mon.Inverse().GetClassID(), CLSID_AntiMoniker)
52125

53126

127+
class Test_ComposeWith(unittest.TestCase):
128+
def test_item(self):
129+
item_id = str(GUID.create_new())
130+
mon = _create_item_moniker("!", item_id)
131+
item_mon2 = _create_item_moniker("!", str(GUID.create_new()))
132+
self.assertEqual(
133+
mon.ComposeWith(item_mon2, False).GetClassID(),
134+
CLSID_CompositeMoniker,
135+
)
136+
with self.assertRaises(COMError) as cm:
137+
mon.ComposeWith(item_mon2, True)
138+
self.assertEqual(cm.exception.hresult, MK_E_NEEDGENERIC)
139+
140+
141+
class Test_IsEqual(unittest.TestCase):
142+
def test_item(self):
143+
item_id = str(GUID.create_new())
144+
mon1 = _create_item_moniker("!", item_id)
145+
mon2 = _create_item_moniker("!", item_id) # Should be equal
146+
mon3 = _create_item_moniker("!", str(GUID.create_new())) # Should not be equal
147+
self.assertEqual(mon1.IsEqual(mon2), hresult.S_OK)
148+
self.assertEqual(mon1.IsEqual(mon3), hresult.S_FALSE)
149+
150+
151+
class Test_Hash(unittest.TestCase):
152+
def test_item(self):
153+
item_id = str(GUID.create_new())
154+
mon1 = _create_item_moniker("!", item_id)
155+
mon2 = _create_item_moniker("!", item_id) # Should be equal
156+
mon3 = _create_item_moniker("!", str(GUID.create_new())) # Should not be equal
157+
self.assertEqual(mon1.Hash(), mon2.Hash())
158+
self.assertNotEqual(mon1.Hash(), mon3.Hash())
159+
self.assertNotEqual(mon2.Hash(), mon3.Hash())
160+
161+
54162
class Test_IsRunning(unittest.TestCase):
55163
def test_item(self):
56164
vidctl = CreateObject(msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl)
@@ -66,3 +174,96 @@ def test_item(self):
66174
rot.Revoke(dw_reg)
67175
# After revoking: should NOT be running again
68176
self.assertEqual(mon.IsRunning(bctx, None, None), hresult.S_FALSE)
177+
178+
179+
class Test_CommonPrefixWith(unittest.TestCase):
180+
def test_file(self):
181+
bctx = _create_bctx()
182+
# Create temporary directories and files for realistic File Monikers
183+
with tempfile.TemporaryDirectory() as t:
184+
tmpdir = Path(t)
185+
dir_a = tmpdir / "dir_a"
186+
dir_b = tmpdir / "dir_a" / "dir_b"
187+
dir_b.mkdir(parents=True)
188+
file1 = dir_a / "file1.txt"
189+
file2 = dir_b / "file2.txt"
190+
file3 = tmpdir / "file3.txt"
191+
mon1 = _create_file_moniker(str(file1)) # tmpdir/dir_a/file1.txt
192+
mon2 = _create_file_moniker(str(file2)) # tmpdir/dir_a/dir_b/file2.txt
193+
mon3 = _create_file_moniker(str(file3)) # tmpdir/file3.txt
194+
# Common prefix between mon1 and mon2 (tmpdir/dir_a)
195+
self.assertEqual(
196+
os.path.normcase(
197+
os.path.normpath(
198+
mon1.CommonPrefixWith(mon2).GetDisplayName(bctx, None)
199+
)
200+
),
201+
os.path.normcase(os.path.normpath(dir_a)),
202+
)
203+
# Common prefix between mon1 and mon3 (tmpdir)
204+
self.assertEqual(
205+
os.path.normcase(
206+
os.path.normpath(
207+
mon1.CommonPrefixWith(mon3).GetDisplayName(bctx, None)
208+
)
209+
),
210+
os.path.normcase(os.path.normpath(tmpdir)),
211+
)
212+
213+
214+
class Test_RelativePathTo(unittest.TestCase):
215+
def test_file(self):
216+
bctx = _create_bctx()
217+
with tempfile.TemporaryDirectory() as t:
218+
tmpdir = Path(t)
219+
dir_a = tmpdir / "dir_a"
220+
dir_b = tmpdir / "dir_b"
221+
dir_a.mkdir()
222+
dir_b.mkdir()
223+
file1 = dir_a / "file1.txt"
224+
file2 = dir_b / "file2.txt"
225+
mon_from = _create_file_moniker(str(file1)) # tmpdir/dir_a/file1.txt
226+
mon_to = _create_file_moniker(str(file2)) # tmpdir/dir_b/file2.txt
227+
# The COM API returns paths with backslashes on Windows, so we normalize.
228+
self.assertEqual(
229+
# Check the display name of the relative moniker
230+
# The moniker's `RelativePathTo` method calculates the path from
231+
# the base of the `mon_from` to the target `mon_to`.
232+
os.path.normcase(
233+
os.path.normpath(
234+
mon_from.RelativePathTo(mon_to).GetDisplayName(bctx, None)
235+
)
236+
),
237+
# Calculate the relative path from the directory of file1 to file2
238+
os.path.normcase(os.path.normpath("..\\..\\dir_b\\file2.txt")),
239+
)
240+
241+
242+
class Test_Enum(unittest.TestCase):
243+
def test_generic_composite(self):
244+
item_id1 = str(GUID.create_new())
245+
item_id2 = str(GUID.create_new())
246+
item_mon1 = _create_item_moniker("!", item_id1)
247+
item_mon2 = _create_item_moniker("!", item_id2)
248+
# Create a composite moniker to ensure multiple elements for enumeration
249+
comp_mon = _create_generic_composite(item_mon1, item_mon2)
250+
enum_moniker = comp_mon.Enum(True) # True for forward enumeration
251+
self.assertIsInstance(enum_moniker, IEnumMoniker)
252+
253+
254+
class Test_RemoteBindToObject(unittest.TestCase):
255+
def test_file(self):
256+
bctx = _create_bctx()
257+
with tempfile.TemporaryDirectory() as t:
258+
tmpdir = Path(t)
259+
tmpfile = tmpdir / "tmp.lnk"
260+
tmpfile.touch()
261+
mon = _create_file_moniker(str(tmpfile))
262+
bound_obj = mon.RemoteBindToObject(bctx, None, IPersistFile._iid_)
263+
pf = bound_obj.QueryInterface(IPersistFile)
264+
self.assertEqual(
265+
os.path.normcase(os.path.normpath(_get_long_path_name(str(tmpfile)))),
266+
os.path.normcase(
267+
os.path.normpath(_get_long_path_name(pf.GetCurFile()))
268+
),
269+
)

0 commit comments

Comments
 (0)