Skip to content

Commit 925c5cb

Browse files
authored
Revive and enhance test_outparam. (#897)
* test: Unskip `test_c_char` in `test_outparam` as it now passes. The `test_c_char` in `test_outparam.py` was previously skipped due to "failing for mysterious reasons". It now passes without modifications. This suggests that underlying issues might have been resolved by this package's community or within CPython over the past few years. * test: Remove redundant `str` conversion from `comstring` in `test_outparam`. Removed the `text_type = str` assignment and its usage in `comstring` function. This change eliminates a redundant `str` conversion that remained from Python 2/3 version bridging. * refactor: Use `logging` module for debug output in `test_outparam`. Replaced `print` statements with `logger.debug` in `test_outparam.py` to allow for more granular control and suppression of debug messages. * test: Replace `ValueError` with `assert` for memory allocation check in `test_outparam`. Changed the memory allocation check in `test_outparam.py` from raising a `ValueError` to using an `assert` statement. In test code, avoiding `if` branches is preferred. * test: Verify `c_wchar_p` constructor does not allocate memory via COM allocator. Added an assertion to `test_outparam.py` to verify that `c_wchar_p`'s constructor does not allocate memory using `CoTaskMemAlloc`. This ensures the memory is not identified as COM-allocated. This check is vital to prevent the patched `__ctypes_from_outparam__` from attempting to free unmanaged memory, which it expects to be COM-allocated. * test: Remove irrelevant `BSTR` comment from `test_outparam`. Removed a commented-out `BSTR` instantiation from `test_outparam.py`. While its presence might have been intended to prevent some type definition regression, it felt largely unrelated to the core purpose of this specific test and was therefore removed for clarity. * test: Refactor `test_c_char` to use `subTest` and verify memory deallocation. Refactored `test_c_char` in `test_outparam.py` to utilize `self.subTest` for improved test granularity. The test now explicitly verifies memory allocation status using `malloc.DidAlloc` before and after calling `__ctypes_from_outparam__`, ensuring correct memory deallocation behavior. Old commented-out debug statements were also removed. * docs: Remove outdated TODO comment from `test_outparam`. Removed the `TODO` comment in `comtypes/test/test_outparam.py` regarding "untested changes" and "global effects on other tests". This comment is no longer relevant due to recent improvements and the unskipping of `test_c_char`. * fix: Rename `from_outparm` to `from_outparam` in `test_outparam.py`. Corrected a typo in the function name `from_outparm` to `from_outparam` within `test_outparam.py`.
1 parent 5932fe9 commit 925c5cb

File tree

1 file changed

+30
-21
lines changed

1 file changed

+30
-21
lines changed

comtypes/test/test_outparam.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import unittest
23
from ctypes import (
34
HRESULT,
@@ -22,7 +23,7 @@
2223
from comtypes import COMMETHOD, GUID, IUnknown
2324
from comtypes.GUID import _CoTaskMemFree
2425

25-
text_type = str
26+
logger = logging.getLogger(__name__)
2627

2728

2829
class IMalloc(IUnknown):
@@ -55,48 +56,56 @@ class IMalloc(IUnknown):
5556
assert bool(malloc)
5657

5758

58-
def from_outparm(self):
59+
def from_outparam(self):
5960
if not self:
6061
return None
6162
result = wstring_at(self)
62-
if not malloc.DidAlloc(self):
63-
raise ValueError("memory was NOT allocated by CoTaskMemAlloc")
63+
# `DidAlloc` method returns;
64+
# * 1 (allocated)
65+
# * 0 (not allocated)
66+
# * -1 (cannot determine or NULL)
67+
# https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-imalloc-didalloc
68+
assert malloc.DidAlloc(self), "memory was NOT allocated by CoTaskMemAlloc"
6469
_CoTaskMemFree(self)
6570
return result
6671

6772

6873
def comstring(text, typ=c_wchar_p):
69-
text = text_type(text)
7074
size = (len(text) + 1) * sizeof(c_wchar)
7175
mem = _CoTaskMemAlloc(size)
72-
print("malloc'd 0x%x, %d bytes" % (mem, size))
76+
logger.debug("malloc'd 0x%x, %d bytes" % (mem, size))
7377
ptr = cast(mem, typ)
7478
memmove(mem, text, size)
7579
return ptr
7680

7781

7882
class Test(unittest.TestCase):
79-
@unittest.skip("This fails for reasons I don't understand yet")
80-
# TODO untested changes; this was modified because it had global effects on other tests
81-
@patch.object(c_wchar_p, "__ctypes_from_outparam__", from_outparm)
83+
@patch.object(c_wchar_p, "__ctypes_from_outparam__", from_outparam)
8284
def test_c_char(self):
83-
# ptr = c_wchar_p("abc")
84-
# self.failUnlessEqual(ptr.__ctypes_from_outparam__(),
85-
# "abc")
86-
87-
# p = BSTR("foo bar spam")
85+
ptr = c_wchar_p("abc")
86+
# The normal constructor does not allocate memory using `CoTaskMemAlloc`.
87+
# Therefore, calling the patched `ptr.__ctypes_from_outparam__()` would
88+
# attempt to free invalid memory, potentially leading to a crash.
89+
self.assertEqual(malloc.DidAlloc(ptr), 0)
8890

8991
x = comstring("Hello, World")
9092
y = comstring("foo bar")
9193
z = comstring("spam, spam, and spam")
9294

93-
# (x.__ctypes_from_outparam__(), x.__ctypes_from_outparam__())
94-
print((x.__ctypes_from_outparam__(), None)) # x.__ctypes_from_outparam__())
95-
96-
# print comstring("Hello, World", c_wchar_p).__ctypes_from_outparam__()
97-
# print comstring("Hello, World", c_wchar_p).__ctypes_from_outparam__()
98-
# print comstring("Hello, World", c_wchar_p).__ctypes_from_outparam__()
99-
# print comstring("Hello, World", c_wchar_p).__ctypes_from_outparam__()
95+
# The `__ctypes_from_outparam__` method is called to convert an output
96+
# parameter into a Python object. In this test, the custom
97+
# `from_outparam` function not only converts the `c_wchar_p` to a
98+
# Python string but also frees the associated memory. Therefore, it can
99+
# only be called once for each allocated memory block.
100+
for wchar_ptr, expected in [
101+
(x, "Hello, World"),
102+
(y, "foo bar"),
103+
(z, "spam, spam, and spam"),
104+
]:
105+
with self.subTest(wchar_ptr=wchar_ptr, expected=expected):
106+
self.assertEqual(malloc.DidAlloc(wchar_ptr), 1)
107+
self.assertEqual(wchar_ptr.__ctypes_from_outparam__(), expected)
108+
self.assertEqual(malloc.DidAlloc(wchar_ptr), 0)
100109

101110

102111
if __name__ == "__main__":

0 commit comments

Comments
 (0)