Skip to content

Commit 5b1aaaf

Browse files
authored
Decompose IStorage tests and enhance coverage. (#930)
* test: Add test for `IStorage.SetStateBits`. * refactor: Decompose `test_IStorage` into smaller, focused test classes. - Extract `IStorage` constants and helper functions to global scope. - Break down `Test_IStorage` into test classes for each `IStorage` method. * refactor: Clarify commented-out sections in `test_storage.py` - Convert `TODO` comments to proper Python comments for better readability. * refactor: Rename test methods in `test_storage.py` for clarity. * test: Add test for `IStorage.Stat` with `STATFLAG_NONAME`. - Add `STATFLAG_NONAME` constant. - Add test method to verify that `pwcsName` is `None` when `STATFLAG_NONAME` is used. * test: Add test for `IStorage.RenameElement` failure when destination exists. - Add `E_ACCESSDENIED` constant. - Add test method to verify that `IStorage.RenameElement` raises `E_ACCESSDENIED` when the destination name already exists. * test: Add test for `IStorage.DestroyElement` failure on non-existent element. * test: Add test for `IStorage.RenameElement` failure when renaming to same name. * refactor: Rename test methods in `test_storage.py` for brevity. * fix: Rename a constant.
1 parent 258da0e commit 5b1aaaf

1 file changed

Lines changed: 153 additions & 80 deletions

File tree

comtypes/test/test_storage.py

Lines changed: 153 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
STGTY_STORAGE = 1
2424

2525
STATFLAG_DEFAULT = 0
26+
STATFLAG_NONAME = 1
27+
2628
STGC_DEFAULT = 0
2729
STGM_CREATE = 0x00001000
2830
STGM_DIRECT = 0x00000000
@@ -36,6 +38,7 @@
3638

3739
STG_E_PATHNOTFOUND = -2147287038
3840
STG_E_INVALIDFLAG = -2147286785
41+
STG_E_ACCESSDENIED = -2147287035 # 0x80030005
3942

4043
_ole32 = OleDLL("ole32")
4144

@@ -48,29 +51,32 @@ def _get_pwcsname(stat: tagSTATSTG) -> WSTRING:
4851
return WSTRING.from_address(ctypes.addressof(stat) + tagSTATSTG.pwcsName.offset)
4952

5053

51-
class Test_IStorage(unittest.TestCase):
52-
RW_EXCLUSIVE = STGM_READWRITE | STGM_SHARE_EXCLUSIVE
53-
RW_EXCLUSIVE_TX = RW_EXCLUSIVE | STGM_TRANSACTED
54-
RW_EXCLUSIVE_CREATE = RW_EXCLUSIVE | STGM_CREATE
55-
CREATE_TESTDOC = STGM_DIRECT | STGM_CREATE | RW_EXCLUSIVE
56-
CREATE_TEMP_TESTDOC = CREATE_TESTDOC | STGM_DELETEONRELEASE
54+
RW_EXCLUSIVE = STGM_READWRITE | STGM_SHARE_EXCLUSIVE
55+
RW_EXCLUSIVE_TX = RW_EXCLUSIVE | STGM_TRANSACTED
56+
RW_EXCLUSIVE_CREATE = RW_EXCLUSIVE | STGM_CREATE
57+
CREATE_TESTDOC = STGM_DIRECT | STGM_CREATE | RW_EXCLUSIVE
58+
CREATE_TEMP_TESTDOC = CREATE_TESTDOC | STGM_DELETEONRELEASE
59+
60+
61+
def _create_docfile(mode: int, name: Optional[str] = None) -> IStorage:
62+
stg = POINTER(IStorage)()
63+
_StgCreateDocfile(name, mode, 0, byref(stg))
64+
return stg # type: ignore
65+
5766

58-
def _create_docfile(self, mode: int, name: Optional[str] = None) -> IStorage:
59-
stg = POINTER(IStorage)()
60-
_StgCreateDocfile(name, mode, 0, byref(stg))
61-
return stg # type: ignore
67+
FIXED_TEST_FILETIME = SystemTimeToFileTime(SYSTEMTIME(wYear=2000, wMonth=1, wDay=1))
6268

63-
FIXED_TEST_FILETIME = SystemTimeToFileTime(SYSTEMTIME(wYear=2000, wMonth=1, wDay=1))
6469

65-
def test_CreateStream(self):
66-
storage = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
70+
class Test_CreateStream(unittest.TestCase):
71+
def test_creates_and_writes_to_stream_in_docfile(self):
72+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
6773
# When created with `StgCreateDocfile(NULL, ...)`, `pwcsName` is a
6874
# temporary filename. The file really exists on disk because Windows
6975
# creates an actual temporary file for the compound storage.
7076
stat = storage.Stat(STATFLAG_DEFAULT)
7177
filepath = Path(stat.pwcsName)
7278
self.assertTrue(filepath.exists())
73-
stream = storage.CreateStream("example", self.RW_EXCLUSIVE_CREATE, 0, 0)
79+
stream = storage.CreateStream("example", RW_EXCLUSIVE_CREATE, 0, 0)
7480
test_data = b"Some data"
7581
pv = (c_ubyte * len(test_data)).from_buffer(bytearray(test_data))
7682
stream.RemoteWrite(pv, len(test_data))
@@ -89,114 +95,157 @@ def test_CreateStream(self):
8995
del stat # `pwcsName` is expected to be freed here.
9096
# `DidAlloc` checks are skipped to avoid using a dangling pointer.
9197

92-
# TODO: Auto-generated methods based on type info are remote-side and hard
93-
# to call from the client.
94-
# If a proper invocation method or workaround is found, testing
95-
# becomes possible.
96-
# See: https://github.com/enthought/comtypes/issues/607
97-
# def test_RemoteOpenStream(self):
98-
# pass
99-
100-
def test_CreateStorage(self):
101-
parent = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
102-
child = parent.CreateStorage("child", self.RW_EXCLUSIVE_TX, 0, 0)
98+
99+
# TODO: Auto-generated methods based on type info are remote-side and hard
100+
# to call from the client.
101+
# If a proper invocation method or workaround is found, testing
102+
# becomes possible.
103+
# See: https://github.com/enthought/comtypes/issues/607
104+
# class Test_RemoteOpenStream(unittest.TestCase):
105+
# def test_RemoteOpenStream(self):
106+
# pass
107+
108+
109+
class Test_CreateStorage(unittest.TestCase):
110+
def test_creates_child_storage_in_parent(self):
111+
parent = _create_docfile(mode=CREATE_TEMP_TESTDOC)
112+
child = parent.CreateStorage("child", RW_EXCLUSIVE_TX, 0, 0)
103113
self.assertEqual("child", child.Stat(STATFLAG_DEFAULT).pwcsName)
104114

105-
def test_OpenStorage(self):
106-
parent = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
115+
116+
class Test_OpenStorage(unittest.TestCase):
117+
def test_opens_existing_child_storage(self):
118+
parent = _create_docfile(mode=CREATE_TEMP_TESTDOC)
107119
with self.assertRaises(COMError) as cm:
108-
parent.OpenStorage("child", None, self.RW_EXCLUSIVE_TX, None, 0)
120+
parent.OpenStorage("child", None, RW_EXCLUSIVE_TX, None, 0)
109121
self.assertEqual(cm.exception.hresult, STG_E_PATHNOTFOUND)
110-
parent.CreateStorage("child", self.RW_EXCLUSIVE_TX, 0, 0)
111-
child = parent.OpenStorage("child", None, self.RW_EXCLUSIVE_TX, None, 0)
122+
parent.CreateStorage("child", RW_EXCLUSIVE_TX, 0, 0)
123+
child = parent.OpenStorage("child", None, RW_EXCLUSIVE_TX, None, 0)
112124
self.assertEqual("child", child.Stat(STATFLAG_DEFAULT).pwcsName)
113125

114-
def test_RemoteCopyTo(self):
115-
src_stg = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
116-
src_stg.CreateStorage("child", self.RW_EXCLUSIVE_TX, 0, 0)
117-
dst_stg = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
126+
127+
class Test_RemoteCopyTo(unittest.TestCase):
128+
def test_copies_storage_content_to_destination(self):
129+
src_stg = _create_docfile(mode=CREATE_TEMP_TESTDOC)
130+
src_stg.CreateStorage("child", RW_EXCLUSIVE_TX, 0, 0)
131+
dst_stg = _create_docfile(mode=CREATE_TEMP_TESTDOC)
118132
src_stg.RemoteCopyTo(0, None, None, dst_stg)
119133
src_stg.Commit(STGC_DEFAULT)
120134
del src_stg
121-
opened_stg = dst_stg.OpenStorage("child", None, self.RW_EXCLUSIVE_TX, None, 0)
135+
opened_stg = dst_stg.OpenStorage("child", None, RW_EXCLUSIVE_TX, None, 0)
122136
self.assertEqual("child", opened_stg.Stat(STATFLAG_DEFAULT).pwcsName)
123137

124-
def test_MoveElementTo(self):
125-
src_stg = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
126-
src_stg.CreateStorage("foo", self.RW_EXCLUSIVE_TX, 0, 0)
127-
dst_stg = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
138+
139+
class Test_MoveElementTo(unittest.TestCase):
140+
def test_moves_element_to_new_location_and_renames(self):
141+
src_stg = _create_docfile(mode=CREATE_TEMP_TESTDOC)
142+
src_stg.CreateStorage("foo", RW_EXCLUSIVE_TX, 0, 0)
143+
dst_stg = _create_docfile(mode=CREATE_TEMP_TESTDOC)
128144
src_stg.MoveElementTo("foo", dst_stg, "bar", STGMOVE_MOVE)
129-
opened_stg = dst_stg.OpenStorage("bar", None, self.RW_EXCLUSIVE_TX, None, 0)
145+
opened_stg = dst_stg.OpenStorage("bar", None, RW_EXCLUSIVE_TX, None, 0)
130146
self.assertEqual("bar", opened_stg.Stat(STATFLAG_DEFAULT).pwcsName)
131147
with self.assertRaises(COMError) as cm:
132-
src_stg.OpenStorage("foo", None, self.RW_EXCLUSIVE_TX, None, 0)
148+
src_stg.OpenStorage("foo", None, RW_EXCLUSIVE_TX, None, 0)
133149
self.assertEqual(cm.exception.hresult, STG_E_PATHNOTFOUND)
134150

135-
def test_Revert(self):
136-
storage = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
137-
foo = storage.CreateStorage("foo", self.RW_EXCLUSIVE_TX, 0, 0)
138-
foo.CreateStorage("bar", self.RW_EXCLUSIVE_TX, 0, 0)
139-
bar = foo.OpenStorage("bar", None, self.RW_EXCLUSIVE_TX, None, 0)
151+
152+
class Test_Revert(unittest.TestCase):
153+
def test_reverts_pending_changes_to_storage(self):
154+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
155+
foo = storage.CreateStorage("foo", RW_EXCLUSIVE_TX, 0, 0)
156+
foo.CreateStorage("bar", RW_EXCLUSIVE_TX, 0, 0)
157+
bar = foo.OpenStorage("bar", None, RW_EXCLUSIVE_TX, None, 0)
140158
self.assertEqual("bar", bar.Stat(STATFLAG_DEFAULT).pwcsName)
141159
foo.Revert()
142160
with self.assertRaises(COMError) as cm:
143-
foo.OpenStorage("bar", None, self.RW_EXCLUSIVE_TX, None, 0)
161+
foo.OpenStorage("bar", None, RW_EXCLUSIVE_TX, None, 0)
144162
self.assertEqual(cm.exception.hresult, STG_E_PATHNOTFOUND)
145163

146-
# TODO: Auto-generated methods based on type info are remote-side and hard
147-
# to call from the client.
148-
# If a proper invocation method or workaround is found, testing
149-
# becomes possible.
150-
# See: https://github.com/enthought/comtypes/issues/607
151-
# def test_RemoteEnumElements(self):
152-
# pass
153-
154-
def test_DestroyElement(self):
155-
storage = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
156-
storage.CreateStorage("example", self.RW_EXCLUSIVE_TX, 0, 0)
164+
165+
# TODO: Auto-generated methods based on type info are remote-side and hard
166+
# to call from the client.
167+
# If a proper invocation method or workaround is found, testing
168+
# becomes possible.
169+
# See: https://github.com/enthought/comtypes/issues/607
170+
# class Test_RemoteEnumElements(unittest.TestCase):
171+
# def test_RemoteEnumElements(self):
172+
# pass
173+
174+
175+
class Test_DestroyElement(unittest.TestCase):
176+
def test_destroys_existing_element_in_storage(self):
177+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
178+
storage.CreateStorage("example", RW_EXCLUSIVE_TX, 0, 0)
157179
storage.DestroyElement("example")
158180
with self.assertRaises(COMError) as cm:
159-
storage.OpenStorage("example", None, self.RW_EXCLUSIVE_TX, None, 0)
181+
storage.OpenStorage("example", None, RW_EXCLUSIVE_TX, None, 0)
160182
self.assertEqual(cm.exception.hresult, STG_E_PATHNOTFOUND)
161183

162-
def test_RenameElement(self):
163-
storage = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
164-
storage.CreateStorage("example", self.RW_EXCLUSIVE_TX, 0, 0)
184+
def test_fails_to_destroy_non_existent_element(self):
185+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
186+
with self.assertRaises(COMError) as cm:
187+
storage.DestroyElement("non_existent")
188+
self.assertEqual(cm.exception.hresult, STG_E_PATHNOTFOUND)
189+
190+
191+
class Test_RenameElement(unittest.TestCase):
192+
def test_renames_element_in_storage(self):
193+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
194+
storage.CreateStorage("example", RW_EXCLUSIVE_TX, 0, 0)
165195
storage.RenameElement("example", "sample")
166-
sample = storage.OpenStorage("sample", None, self.RW_EXCLUSIVE_TX, None, 0)
196+
sample = storage.OpenStorage("sample", None, RW_EXCLUSIVE_TX, None, 0)
167197
self.assertEqual("sample", sample.Stat(STATFLAG_DEFAULT).pwcsName)
168198
with self.assertRaises(COMError) as cm:
169-
storage.OpenStorage("example", None, self.RW_EXCLUSIVE_TX, None, 0)
199+
storage.OpenStorage("example", None, RW_EXCLUSIVE_TX, None, 0)
170200
self.assertEqual(cm.exception.hresult, STG_E_PATHNOTFOUND)
171201

172-
def test_SetElementTimes(self):
173-
storage = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
202+
def test_fails_if_destination_exists(self):
203+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
204+
storage.CreateStorage("foo", RW_EXCLUSIVE_TX, 0, 0)
205+
storage.CreateStorage("bar", RW_EXCLUSIVE_TX, 0, 0)
206+
# Rename "foo" to "bar" (which already exists)
207+
with self.assertRaises(COMError) as cm:
208+
storage.RenameElement("foo", "bar")
209+
self.assertEqual(cm.exception.hresult, STG_E_ACCESSDENIED)
210+
211+
def test_fails_if_takes_same_name(self):
212+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
213+
storage.CreateStorage("foo", RW_EXCLUSIVE_TX, 0, 0)
214+
# Rename "foo" to "foo" (same name)
215+
with self.assertRaises(COMError) as cm:
216+
storage.RenameElement("foo", "foo")
217+
self.assertEqual(cm.exception.hresult, STG_E_ACCESSDENIED)
218+
219+
220+
class Test_SetElementTimes(unittest.TestCase):
221+
def test_sets_modification_time_for_element(self):
222+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
174223
sub_name = "SubStorageElement"
175-
orig_stat = storage.CreateStorage(sub_name, self.CREATE_TESTDOC, 0, 0).Stat(
224+
orig_stat = storage.CreateStorage(sub_name, CREATE_TESTDOC, 0, 0).Stat(
176225
STATFLAG_DEFAULT
177226
)
178227
storage.SetElementTimes(
179228
sub_name,
180229
None, # pctime (creation time)
181230
None, # patime (access time)
182-
self.FIXED_TEST_FILETIME, # pmtime (modification time)
231+
FIXED_TEST_FILETIME, # pmtime (modification time)
183232
)
184233
storage.Commit(STGC_DEFAULT)
185234
modified_stat = storage.OpenStorage(
186-
sub_name, None, self.RW_EXCLUSIVE_TX, None, 0
235+
sub_name, None, RW_EXCLUSIVE_TX, None, 0
187236
).Stat(STATFLAG_DEFAULT)
188237
self.assertEqual(CompareFileTime(orig_stat.ctime, modified_stat.ctime), 0)
189238
self.assertEqual(CompareFileTime(orig_stat.atime, modified_stat.atime), 0)
190239
self.assertNotEqual(CompareFileTime(orig_stat.mtime, modified_stat.mtime), 0)
191-
self.assertEqual(
192-
CompareFileTime(self.FIXED_TEST_FILETIME, modified_stat.mtime), 0
193-
)
240+
self.assertEqual(CompareFileTime(FIXED_TEST_FILETIME, modified_stat.mtime), 0)
194241
with self.assertRaises(COMError) as cm:
195-
storage.SetElementTimes("NonExistent", None, None, self.FIXED_TEST_FILETIME)
242+
storage.SetElementTimes("NonExistent", None, None, FIXED_TEST_FILETIME)
196243
self.assertEqual(cm.exception.hresult, STG_E_PATHNOTFOUND)
197244

198-
def test_SetClass(self):
199-
storage = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC)
245+
246+
class Test_SetClass(unittest.TestCase):
247+
def test_sets_clsid(self):
248+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
200249
# Initial value is CLSID_NULL.
201250
self.assertEqual(storage.Stat(STATFLAG_DEFAULT).clsid, comtypes.GUID())
202251
new_clsid = comtypes.GUID.create_new()
@@ -206,16 +255,32 @@ def test_SetClass(self):
206255
storage.SetClass(comtypes.GUID())
207256
self.assertEqual(storage.Stat(STATFLAG_DEFAULT).clsid, comtypes.GUID())
208257

209-
def test_Stat(self):
258+
259+
class Test_SetStateBits(unittest.TestCase):
260+
def test_sets_and_updates_storage_state_bits(self):
261+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
262+
# Initial state bits should be 0
263+
self.assertEqual(storage.Stat(STATFLAG_DEFAULT).grfStateBits, 0)
264+
# 1. Set all bits
265+
bits1, mask1 = 0xABCD1234, 0xFFFFFFFF
266+
storage.SetStateBits(bits1, mask1)
267+
self.assertEqual(storage.Stat(STATFLAG_DEFAULT).grfStateBits, bits1)
268+
# 2. Partial update using mask (only lower 16 bits)
269+
bits2, mask2 = 0x00005678, 0x0000FFFF
270+
storage.SetStateBits(bits2, mask2)
271+
# Expected: 0xABCD (original upper) + 0x5678 (new lower) = 0xABCD5678
272+
self.assertEqual(storage.Stat(STATFLAG_DEFAULT).grfStateBits, 0xABCD5678)
273+
274+
275+
class Test_Stat(unittest.TestCase):
276+
def test_returns_correct_stat_information_for_docfile(self):
210277
with tempfile.TemporaryDirectory() as t:
211278
tmpdir = Path(t)
212279
tmpfile = tmpdir / "test_docfile.cfs"
213280
self.assertFalse(tmpfile.exists())
214281
# When created with `StgCreateDocfile(filepath_string, ...)`, the
215282
# compound file is created at that location.
216-
storage = self._create_docfile(
217-
name=str(tmpfile), mode=self.CREATE_TEMP_TESTDOC
218-
)
283+
storage = _create_docfile(name=str(tmpfile), mode=CREATE_TEMP_TESTDOC)
219284
self.assertTrue(tmpfile.exists())
220285
with self.assertRaises(COMError) as cm:
221286
storage.Stat(0xFFFFFFFF) # Invalid flag
@@ -244,7 +309,7 @@ def test_Stat(self):
244309
# greater than 0 bytes.
245310
self.assertGreaterEqual(stat.cbSize, 0)
246311
# `grfMode` should reflect the access mode flags from creation.
247-
self.assertEqual(stat.grfMode, self.RW_EXCLUSIVE | STGM_DIRECT)
312+
self.assertEqual(stat.grfMode, RW_EXCLUSIVE | STGM_DIRECT)
248313
self.assertEqual(stat.grfLocksSupported, 0)
249314
self.assertEqual(stat.clsid, comtypes.GUID()) # CLSID_NULL for new creation.
250315
self.assertEqual(stat.grfStateBits, 0)
@@ -254,3 +319,11 @@ def test_Stat(self):
254319
self.assertEqual(malloc.DidAlloc(name_ptr), 1)
255320
del stat # `pwcsName` is expected to be freed here.
256321
# `DidAlloc` checks are skipped to avoid using a dangling pointer.
322+
323+
def test_stat_returns_none_for_pwcsname_with_noname_flag(self):
324+
storage = _create_docfile(mode=CREATE_TEMP_TESTDOC)
325+
# Using `STATFLAG_NONAME` should return `None` for `pwcsName`.
326+
stat = storage.Stat(STATFLAG_NONAME)
327+
self.assertIsNone(stat.pwcsName)
328+
# Verify other fields are still present.
329+
self.assertEqual(stat.type, STGTY_STORAGE)

0 commit comments

Comments
 (0)