2323STGTY_STORAGE = 1
2424
2525STATFLAG_DEFAULT = 0
26+ STATFLAG_NONAME = 1
27+
2628STGC_DEFAULT = 0
2729STGM_CREATE = 0x00001000
2830STGM_DIRECT = 0x00000000
3638
3739STG_E_PATHNOTFOUND = - 2147287038
3840STG_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