From 74f274e57cda73b3630e61eb5a365ed04d16dee9 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sun, 25 Jan 2026 11:29:20 +0900 Subject: [PATCH 1/3] refactor: Extract time structure helpers to `time_structs_helper.py` Moved `SYSTEMTIME` structure and related `kernel32` API calls, `SystemTimeToFileTime` and `CompareFileTime` helper functions from `test/test_storage.py` to `test/time_structs_helper.py`. This improves modularity and reusability of time-related utility functions within the test suite. --- comtypes/test/test_storage.py | 63 +++++++--------------------- comtypes/test/time_structs_helper.py | 39 +++++++++++++++++ 2 files changed, 54 insertions(+), 48 deletions(-) create mode 100644 comtypes/test/time_structs_helper.py diff --git a/comtypes/test/test_storage.py b/comtypes/test/test_storage.py index 9f21aa32..bd782fb5 100644 --- a/comtypes/test/test_storage.py +++ b/comtypes/test/test_storage.py @@ -3,44 +3,23 @@ import tempfile import unittest from _ctypes import COMError -from ctypes import HRESULT, POINTER, OleDLL, Structure, WinDLL, byref, c_ubyte -from ctypes.wintypes import BOOL, DWORD, FILETIME, LONG, PWCHAR, WORD +from ctypes import HRESULT, POINTER, OleDLL, byref, c_ubyte +from ctypes.wintypes import DWORD, FILETIME, PWCHAR from pathlib import Path from typing import Optional import comtypes import comtypes.client from comtypes.malloc import CoGetMalloc +from comtypes.test.time_structs_helper import ( + SYSTEMTIME, + CompareFileTime, + SystemTimeToFileTime, +) comtypes.client.GetModule("portabledeviceapi.dll") from comtypes.gen.PortableDeviceApiLib import WSTRING, IStorage, tagSTATSTG - -class SYSTEMTIME(Structure): - _fields_ = [ - ("wYear", WORD), - ("wMonth", WORD), - ("wDayOfWeek", WORD), - ("wDay", WORD), - ("wHour", WORD), - ("wMinute", WORD), - ("wSecond", WORD), - ("wMilliseconds", WORD), - ] - - -_kernel32 = WinDLL("kernel32") - -# https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-systemtimetofiletime -_SystemTimeToFileTime = _kernel32.SystemTimeToFileTime -_SystemTimeToFileTime.argtypes = [POINTER(SYSTEMTIME), POINTER(FILETIME)] -_SystemTimeToFileTime.restype = BOOL - -# https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-comparefiletime -_CompareFileTime = _kernel32.CompareFileTime -_CompareFileTime.argtypes = [POINTER(FILETIME), POINTER(FILETIME)] -_CompareFileTime.restype = LONG - STGTY_STORAGE = 1 STATFLAG_DEFAULT = 0 @@ -65,16 +44,6 @@ class SYSTEMTIME(Structure): _StgCreateDocfile.restype = HRESULT -def _systemtime_to_filetime(st: SYSTEMTIME) -> FILETIME: - ft = FILETIME() - _SystemTimeToFileTime(byref(st), byref(ft)) - return ft - - -def _compare_filetime(ft1: FILETIME, ft2: FILETIME) -> int: - return _CompareFileTime(byref(ft1), byref(ft2)) - - def _get_pwcsname(stat: tagSTATSTG) -> WSTRING: return WSTRING.from_address(ctypes.addressof(stat) + tagSTATSTG.pwcsName.offset) @@ -91,9 +60,7 @@ def _create_docfile(self, mode: int, name: Optional[str] = None) -> IStorage: _StgCreateDocfile(name, mode, 0, byref(stg)) return stg # type: ignore - FIXED_TEST_FILETIME = _systemtime_to_filetime( - SYSTEMTIME(wYear=2000, wMonth=1, wDay=1) - ) + FIXED_TEST_FILETIME = SystemTimeToFileTime(SYSTEMTIME(wYear=2000, wMonth=1, wDay=1)) def test_CreateStream(self): storage = self._create_docfile(mode=self.CREATE_TEMP_TESTDOC) @@ -218,11 +185,11 @@ def test_SetElementTimes(self): modified_stat = storage.OpenStorage( sub_name, None, self.RW_EXCLUSIVE_TX, None, 0 ).Stat(STATFLAG_DEFAULT) - self.assertEqual(_compare_filetime(orig_stat.ctime, modified_stat.ctime), 0) - self.assertEqual(_compare_filetime(orig_stat.atime, modified_stat.atime), 0) - self.assertNotEqual(_compare_filetime(orig_stat.mtime, modified_stat.mtime), 0) + self.assertEqual(CompareFileTime(orig_stat.ctime, modified_stat.ctime), 0) + self.assertEqual(CompareFileTime(orig_stat.atime, modified_stat.atime), 0) + self.assertNotEqual(CompareFileTime(orig_stat.mtime, modified_stat.mtime), 0) self.assertEqual( - _compare_filetime(self.FIXED_TEST_FILETIME, modified_stat.mtime), 0 + CompareFileTime(self.FIXED_TEST_FILETIME, modified_stat.mtime), 0 ) with self.assertRaises(COMError) as cm: storage.SetElementTimes("NonExistent", None, None, self.FIXED_TEST_FILETIME) @@ -270,9 +237,9 @@ def test_Stat(self): # Therefore, we only verify that each timestamp is a valid `FILETIME` # (non-zero is sufficient for a newly created file). zero_ft = FILETIME() - self.assertNotEqual(_compare_filetime(stat.ctime, zero_ft), 0) - self.assertNotEqual(_compare_filetime(stat.atime, zero_ft), 0) - self.assertNotEqual(_compare_filetime(stat.mtime, zero_ft), 0) + self.assertNotEqual(CompareFileTime(stat.ctime, zero_ft), 0) + self.assertNotEqual(CompareFileTime(stat.atime, zero_ft), 0) + self.assertNotEqual(CompareFileTime(stat.mtime, zero_ft), 0) # Due to header overhead and file system allocation, the size may be # greater than 0 bytes. self.assertGreaterEqual(stat.cbSize, 0) diff --git a/comtypes/test/time_structs_helper.py b/comtypes/test/time_structs_helper.py new file mode 100644 index 00000000..a186aa2f --- /dev/null +++ b/comtypes/test/time_structs_helper.py @@ -0,0 +1,39 @@ +from ctypes import POINTER, Structure, WinDLL, byref +from ctypes.wintypes import BOOL, FILETIME, LONG, WORD +from typing import Literal + + +class SYSTEMTIME(Structure): + _fields_ = [ + ("wYear", WORD), + ("wMonth", WORD), + ("wDayOfWeek", WORD), + ("wDay", WORD), + ("wHour", WORD), + ("wMinute", WORD), + ("wSecond", WORD), + ("wMilliseconds", WORD), + ] + + +_kernel32 = WinDLL("kernel32") + +# https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-systemtimetofiletime +_SystemTimeToFileTime = _kernel32.SystemTimeToFileTime +_SystemTimeToFileTime.argtypes = [POINTER(SYSTEMTIME), POINTER(FILETIME)] +_SystemTimeToFileTime.restype = BOOL + +# https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-comparefiletime +_CompareFileTime = _kernel32.CompareFileTime +_CompareFileTime.argtypes = [POINTER(FILETIME), POINTER(FILETIME)] +_CompareFileTime.restype = LONG + + +def SystemTimeToFileTime(st: SYSTEMTIME, /) -> FILETIME: + ft = FILETIME() + assert _SystemTimeToFileTime(byref(st), byref(ft)) + return ft + + +def CompareFileTime(ft1: FILETIME, ft2: FILETIME, /) -> Literal[-1, 0, 1]: + return _CompareFileTime(byref(ft1), byref(ft2)) From 578b11272130be4873c070cfc8c746f53a532cbc Mon Sep 17 00:00:00 2001 From: junkmd Date: Sun, 25 Jan 2026 11:29:20 +0900 Subject: [PATCH 2/3] test: Add tests for `IRunningObjectTable` change time methods. This commit introduces a new test class to `test/test_rot.py`. It verifies the functionality of `NoteChangeTime` and `GetTimeOfLastChange` by setting and retrieving the last change time of a registered COM object. --- comtypes/test/test_rot.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/comtypes/test/test_rot.py b/comtypes/test/test_rot.py index e230f6b5..23a13fd7 100644 --- a/comtypes/test/test_rot.py +++ b/comtypes/test/test_rot.py @@ -12,6 +12,11 @@ _CreateItemMoniker, _GetRunningObjectTable, ) +from comtypes.test.time_structs_helper import ( + SYSTEMTIME, + CompareFileTime, + SystemTimeToFileTime, +) with contextlib.redirect_stdout(None): # supress warnings GetModule("msvidctl.dll") @@ -74,3 +79,16 @@ def test_returns_enum_moniker(self): rot = _create_rot() enum_moniker = rot.EnumRunning() self.assertIsInstance(enum_moniker, IEnumMoniker) + + +class Test_NoteChangeTime_GetTimeOfLastChange(unittest.TestCase): + def test_modified_time(self): + vidctl = CreateObject(msvidctl.MSVidCtl, interface=msvidctl.IMSVidCtl) + item_id = str(GUID.create_new()) + mon = _create_item_moniker("!", item_id) + rot = _create_rot() + dw_reg = rot.Register(ROTFLAGS_ALLOWANYCLIENT, vidctl, mon) + ft = SystemTimeToFileTime(SYSTEMTIME(wYear=2000, wMonth=1, wDay=1)) + rot.NoteChangeTime(dw_reg, ft) + self.assertEqual(CompareFileTime(rot.GetTimeOfLastChange(mon), ft), 0) + rot.Revoke(dw_reg) From 97f23093a0e5fc7fe3ab0e6eca923e4ec369fda9 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sun, 25 Jan 2026 11:29:20 +0900 Subject: [PATCH 3/3] test: Add test for `IMoniker.GetTimeOfLastChange` method. It verifies the functionality of `IMoniker.GetTimeOfLastChange` for file monikers by creating and modifying a temporary file and asserting that the `FILETIME` returned by `GetTimeOfLastChange` reflects the modification. --- comtypes/test/test_moniker.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/comtypes/test/test_moniker.py b/comtypes/test/test_moniker.py index 2709b7db..f572ca3e 100644 --- a/comtypes/test/test_moniker.py +++ b/comtypes/test/test_moniker.py @@ -2,6 +2,7 @@ import ctypes import os import tempfile +import time import unittest from _ctypes import COMError from ctypes import POINTER, WinDLL, byref @@ -27,6 +28,7 @@ _CreateItemMoniker, _GetRunningObjectTable, ) +from comtypes.test.time_structs_helper import CompareFileTime with contextlib.redirect_stdout(None): # supress warnings GetModule("msvidctl.dll") @@ -176,6 +178,24 @@ def test_item(self): self.assertEqual(mon.IsRunning(bctx, None, None), hresult.S_FALSE) +class Test_GetTimeOfLastChange(unittest.TestCase): + def test_file(self): + bctx = _create_bctx() + with tempfile.NamedTemporaryFile() as f: + tmpfile = Path(f.name) + f.write(b"test data") + # Create a File Moniker for the temporary file + file_mon = _create_file_moniker(str(tmpfile)) + # Get initial time of last change for the file + initial_ft = file_mon.GetTimeOfLastChange(bctx, None) + # Modify the file to change its last write time + time.sleep(0.01) # Ensure a different timestamp + os.write(f.fileno(), b"more data") + after_change_ft = file_mon.GetTimeOfLastChange(bctx, None) + # Verify the time has changed (after_change_ft > initial_ft) + self.assertEqual(CompareFileTime(after_change_ft, initial_ft), 1) + + class Test_CommonPrefixWith(unittest.TestCase): def test_file(self): bctx = _create_bctx()