Reactos
1/*
2 * PROJECT: ReactOS api tests
3 * LICENSE: LGPL-2.0-or-later (https://spdx.org/licenses/LGPL-2.0-or-later)
4 * PURPOSE: Test for SHChangeNotify
5 * COPYRIGHT: Copyright 2020-2024 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
6 */
7
8// NOTE: This testcase requires shell32_apitest_sub.exe.
9
10#include "shelltest.h"
11#include "shell32_apitest_sub.h"
12#include <assert.h>
13
14#define NUM_STEP 8
15#define NUM_CHECKS 12
16#define INTERVAL 0
17#define MAX_EVENT_TYPE 6
18
19static HWND s_hMainWnd = NULL, s_hSubWnd = NULL;
20static WCHAR s_szSubProgram[MAX_PATH]; // shell32_apitest_sub.exe
21static HANDLE s_hThread = NULL;
22static INT s_iStage = -1, s_iStep = -1;
23static BYTE s_abChecks[NUM_CHECKS] = { 0 }; // Flags for testing
24static BOOL s_bGotUpdateDir = FALSE; // Got SHCNE_UPDATEDIR?
25
26static BOOL DoCreateFile(LPCWSTR pszFileName)
27{
28 HANDLE hFile = ::CreateFileW(pszFileName, GENERIC_WRITE, FILE_SHARE_READ, NULL,
29 CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
30 ::CloseHandle(hFile);
31 return hFile != INVALID_HANDLE_VALUE;
32}
33
34static void DoDeleteDirectory(LPCWSTR pszDir)
35{
36 WCHAR szPath[MAX_PATH];
37 ZeroMemory(szPath, sizeof(szPath));
38 StringCchCopyW(szPath, _countof(szPath), pszDir); // Double-NULL terminated
39 SHFILEOPSTRUCTW FileOp = { NULL, FO_DELETE, szPath, NULL, FOF_NOCONFIRMATION | FOF_SILENT };
40 SHFileOperation(&FileOp);
41}
42
43static INT GetEventType(LONG lEvent)
44{
45 switch (lEvent)
46 {
47 case SHCNE_CREATE: return 0;
48 case SHCNE_DELETE: return 1;
49 case SHCNE_RENAMEITEM: return 2;
50 case SHCNE_MKDIR: return 3;
51 case SHCNE_RMDIR: return 4;
52 case SHCNE_RENAMEFOLDER: return 5;
53 C_ASSERT(5 + 1 == MAX_EVENT_TYPE);
54 default: return -1;
55 }
56}
57
58#define FILE_1 L"_TESTFILE_1_.txt"
59#define FILE_2 L"_TESTFILE_2_.txt"
60#define DIR_1 L"_TESTDIR_1_"
61#define DIR_2 L"_TESTDIR_2_"
62
63static WCHAR s_szDir1[MAX_PATH];
64static WCHAR s_szDir1InDir1[MAX_PATH];
65static WCHAR s_szDir2InDir1[MAX_PATH];
66static WCHAR s_szFile1InDir1InDir1[MAX_PATH];
67static WCHAR s_szFile1InDir1[MAX_PATH];
68static WCHAR s_szFile2InDir1[MAX_PATH];
69
70static void DoDeleteFilesAndDirs(void)
71{
72 ::DeleteFileW(s_szFile1InDir1);
73 ::DeleteFileW(s_szFile2InDir1);
74 ::DeleteFileW(s_szFile1InDir1InDir1);
75 DoDeleteDirectory(s_szDir1InDir1);
76 DoDeleteDirectory(s_szDir2InDir1);
77 DoDeleteDirectory(s_szDir1);
78}
79
80static void TEST_Quit(void)
81{
82 CloseHandle(s_hThread);
83 s_hThread = NULL;
84
85 PostMessageW(s_hSubWnd, WM_COMMAND, IDNO, 0); // Finish
86 DoWaitForWindow(SUB_CLASSNAME, SUB_CLASSNAME, TRUE, TRUE); // Close sub-windows
87
88 DoDeleteFilesAndDirs();
89}
90
91static void DoBuildFilesAndDirs(void)
92{
93 WCHAR szPath1[MAX_PATH];
94 SHGetSpecialFolderPathW(NULL, szPath1, CSIDL_PERSONAL, FALSE); // My Documents
95 PathAppendW(szPath1, DIR_1);
96 StringCchCopyW(s_szDir1, _countof(s_szDir1), szPath1);
97
98 PathAppendW(szPath1, DIR_1);
99 StringCchCopyW(s_szDir1InDir1, _countof(s_szDir1InDir1), szPath1);
100 PathRemoveFileSpecW(szPath1);
101
102 PathAppendW(szPath1, DIR_2);
103 StringCchCopyW(s_szDir2InDir1, _countof(s_szDir2InDir1), szPath1);
104 PathRemoveFileSpecW(szPath1);
105
106 PathAppendW(szPath1, DIR_1);
107 PathAppendW(szPath1, FILE_1);
108 StringCchCopyW(s_szFile1InDir1InDir1, _countof(s_szFile1InDir1InDir1), szPath1);
109 PathRemoveFileSpecW(szPath1);
110 PathRemoveFileSpecW(szPath1);
111
112 PathAppendW(szPath1, FILE_1);
113 StringCchCopyW(s_szFile1InDir1, _countof(s_szFile1InDir1), szPath1);
114 PathRemoveFileSpecW(szPath1);
115
116 PathAppendW(szPath1, FILE_2);
117 StringCchCopyW(s_szFile2InDir1, _countof(s_szFile2InDir1), szPath1);
118 PathRemoveFileSpecW(szPath1);
119
120#define TRACE_PATH(path) trace(#path ": %ls\n", path)
121 TRACE_PATH(s_szDir1);
122 TRACE_PATH(s_szDir1InDir1);
123 TRACE_PATH(s_szFile1InDir1);
124 TRACE_PATH(s_szFile1InDir1InDir1);
125 TRACE_PATH(s_szFile2InDir1);
126#undef TRACE_PATH
127
128 DoDeleteFilesAndDirs();
129
130 ::CreateDirectoryW(s_szDir1, NULL);
131 ok_int(!!PathIsDirectoryW(s_szDir1), TRUE);
132
133 DoDeleteDirectory(s_szDir1InDir1);
134 ok_int(!PathIsDirectoryW(s_szDir1InDir1), TRUE);
135}
136
137static void DoTestEntry(LONG lEvent, LPCITEMIDLIST pidl1, LPCITEMIDLIST pidl2)
138{
139 WCHAR szPath1[MAX_PATH], szPath2[MAX_PATH];
140
141 szPath1[0] = szPath2[0] = 0;
142 SHGetPathFromIDListW(pidl1, szPath1);
143 SHGetPathFromIDListW(pidl2, szPath2);
144
145 trace("(0x%lX, '%ls', '%ls')\n", lEvent, szPath1, szPath2);
146
147 if (lEvent == SHCNE_UPDATEDIR)
148 {
149 trace("Got SHCNE_UPDATEDIR\n");
150 s_bGotUpdateDir = TRUE;
151 return;
152 }
153
154 INT iEventType = GetEventType(lEvent);
155 if (iEventType < 0)
156 return;
157
158 assert(iEventType < MAX_EVENT_TYPE);
159
160 INT i = 0;
161 s_abChecks[i++] |= (lstrcmpiW(szPath1, L"") == 0) << iEventType;
162 s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szDir1) == 0) << iEventType;
163 s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szDir2InDir1) == 0) << iEventType;
164 s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szFile1InDir1) == 0) << iEventType;
165 s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szFile2InDir1) == 0) << iEventType;
166 s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szFile1InDir1InDir1) == 0) << iEventType;
167 s_abChecks[i++] |= (lstrcmpiW(szPath2, L"") == 0) << iEventType;
168 s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szDir1) == 0) << iEventType;
169 s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szDir2InDir1) == 0) << iEventType;
170 s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szFile1InDir1) == 0) << iEventType;
171 s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szFile2InDir1) == 0) << iEventType;
172 s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szFile1InDir1InDir1) == 0) << iEventType;
173 assert(i == NUM_CHECKS);
174}
175
176static LPCSTR StringFromChecks(void)
177{
178 static char s_sz[2 * NUM_CHECKS + 1];
179
180 char *pch = s_sz;
181 for (INT i = 0; i < NUM_CHECKS; ++i)
182 {
183 WCHAR sz[3];
184 StringCchPrintfW(sz, _countof(sz), L"%02X", s_abChecks[i]);
185 *pch++ = sz[0];
186 *pch++ = sz[1];
187 }
188
189 assert((pch - s_sz) + 1 == sizeof(s_sz));
190
191 *pch = 0;
192 return s_sz;
193}
194
195struct TEST_ANSWER
196{
197 INT lineno;
198 LPCSTR answer;
199};
200
201static void DoStepCheck(INT iStage, INT iStep, LPCSTR checks)
202{
203 assert(0 <= iStep);
204 assert(iStep < NUM_STEP);
205
206 assert(0 <= iStage);
207 assert(iStage < NUM_STAGE);
208
209 if (s_bGotUpdateDir)
210 {
211 ok_int(TRUE, TRUE);
212 return;
213 }
214
215 LPCSTR answer = NULL;
216 INT lineno = 0;
217 switch (iStage)
218 {
219 case 0:
220 case 1:
221 case 3:
222 case 6:
223 case 9:
224 {
225 static const TEST_ANSWER c_answers[] =
226 {
227 { __LINE__, "000000010000010000000000" }, // 0
228 { __LINE__, "000000040000000000000400" }, // 1
229 { __LINE__, "000000000200020000000000" }, // 2
230 { __LINE__, "000000000000080000000000" }, // 3
231 { __LINE__, "000000000001010000000000" }, // 4
232 { __LINE__, "000000000002020000000000" }, // 5
233 { __LINE__, "000000000000000020000000" }, // 6
234 { __LINE__, "000010000000100000000000" }, // 7
235 };
236 C_ASSERT(_countof(c_answers) == NUM_STEP);
237 lineno = c_answers[iStep].lineno;
238 answer = c_answers[iStep].answer;
239 break;
240 }
241 case 2:
242 case 4:
243 case 5:
244 case 7:
245 {
246 static const TEST_ANSWER c_answers[] =
247 {
248 { __LINE__, "000000000000000000000000" }, // 0
249 { __LINE__, "000000000000000000000000" }, // 1
250 { __LINE__, "000000000000000000000000" }, // 2
251 { __LINE__, "000000000000000000000000" }, // 3
252 { __LINE__, "000000000000000000000000" }, // 4
253 { __LINE__, "000000000000000000000000" }, // 5
254 { __LINE__, "000000000000000000000000" }, // 6
255 { __LINE__, "000000000000000000000000" }, // 7
256 };
257 C_ASSERT(_countof(c_answers) == NUM_STEP);
258 lineno = c_answers[iStep].lineno;
259 answer = c_answers[iStep].answer;
260 break;
261 }
262 case 8:
263 {
264 static const TEST_ANSWER c_answers[] =
265 {
266 { __LINE__, "000000010000010000000000" }, // 0
267 { __LINE__, "000000040000000000000400" }, // 1
268 { __LINE__, "000000000200020000000000" }, // 2
269 { __LINE__, "000000000000080000000000" }, // 3
270 { __LINE__, "000000000001010000000000" }, // 4 // Recursive case
271 { __LINE__, "000000000002020000000000" }, // 5 // Recursive case
272 { __LINE__, "000000000000000020000000" }, // 6
273 { __LINE__, "000010000000100000000000" }, // 7
274 };
275 C_ASSERT(_countof(c_answers) == NUM_STEP);
276 lineno = c_answers[iStep].lineno;
277 answer = c_answers[iStep].answer;
278 if (iStep == 4 || iStep == 5) // Recursive cases
279 {
280 if (lstrcmpA(checks, "000000000000000000000000") == 0)
281 {
282 trace("Warning! Recursive cases...\n");
283 answer = "000000000000000000000000";
284 }
285 }
286 break;
287 }
288 DEFAULT_UNREACHABLE;
289 }
290
291 ok(lstrcmpA(checks, answer) == 0,
292 "Line %d: '%s' vs '%s' at Stage %d, Step %d\n", lineno, checks, answer, iStage, iStep);
293}
294
295static DWORD WINAPI StageThreadFunc(LPVOID arg)
296{
297 BOOL ret;
298
299 trace("Stage %d\n", s_iStage);
300
301 // 0: Create file1 in dir1
302 s_iStep = 0;
303 trace("Step %d\n", s_iStep);
304 SHChangeNotify(0, SHCNF_PATHW | SHCNF_FLUSH, NULL, NULL);
305 ZeroMemory(s_abChecks, sizeof(s_abChecks));
306 ret = DoCreateFile(s_szFile1InDir1);
307 ok_int(ret, TRUE);
308 SHChangeNotify(SHCNE_CREATE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1, 0);
309 ::Sleep(INTERVAL);
310 DoStepCheck(s_iStage, s_iStep, StringFromChecks());
311
312 // 1: Rename file1 as file2 in dir1
313 ++s_iStep;
314 trace("Step %d\n", s_iStep);
315 ZeroMemory(s_abChecks, sizeof(s_abChecks));
316 ret = MoveFileW(s_szFile1InDir1, s_szFile2InDir1);
317 ok_int(ret, TRUE);
318 SHChangeNotify(SHCNE_RENAMEITEM, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1, s_szFile2InDir1);
319 ::Sleep(INTERVAL);
320 DoStepCheck(s_iStage, s_iStep, StringFromChecks());
321
322 // 2: Delete file2 in dir1
323 ++s_iStep;
324 trace("Step %d\n", s_iStep);
325 ZeroMemory(s_abChecks, sizeof(s_abChecks));
326 ret = DeleteFileW(s_szFile2InDir1);
327 ok_int(ret, TRUE);
328 SHChangeNotify(SHCNE_DELETE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile2InDir1, NULL);
329 ::Sleep(INTERVAL);
330 DoStepCheck(s_iStage, s_iStep, StringFromChecks());
331
332 // 3: Create dir1 in dir1
333 ++s_iStep;
334 trace("Step %d\n", s_iStep);
335 ZeroMemory(s_abChecks, sizeof(s_abChecks));
336 ret = CreateDirectoryExW(s_szDir1, s_szDir1InDir1, NULL);
337 ok_int(ret, TRUE);
338 SHChangeNotify(SHCNE_MKDIR, SHCNF_PATHW | SHCNF_FLUSH, s_szDir1InDir1, NULL);
339 ::Sleep(INTERVAL);
340 DoStepCheck(s_iStage, s_iStep, StringFromChecks());
341
342 // 4: Create file1 in dir1 in dir1
343 ++s_iStep;
344 trace("Step %d\n", s_iStep);
345 ZeroMemory(s_abChecks, sizeof(s_abChecks));
346 ret = DoCreateFile(s_szFile1InDir1InDir1);
347 ok_int(ret, TRUE);
348 SHChangeNotify(SHCNE_CREATE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1InDir1, NULL);
349 ::Sleep(INTERVAL);
350 DoStepCheck(s_iStage, s_iStep, StringFromChecks());
351
352 // 5: Delete file1 in dir1 in dir1
353 ++s_iStep;
354 trace("Step %d\n", s_iStep);
355 ZeroMemory(s_abChecks, sizeof(s_abChecks));
356 ret = DeleteFileW(s_szFile1InDir1InDir1);
357 ok_int(ret, TRUE);
358 SHChangeNotify(SHCNE_DELETE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1InDir1, NULL);
359 ::Sleep(INTERVAL);
360 DoStepCheck(s_iStage, s_iStep, StringFromChecks());
361
362 // 6: Rename dir1 as dir2 in dir1
363 ++s_iStep;
364 trace("Step %d\n", s_iStep);
365 ZeroMemory(s_abChecks, sizeof(s_abChecks));
366 ret = ::MoveFileW(s_szDir1InDir1, s_szDir2InDir1);
367 ok_int(ret, TRUE);
368 SHChangeNotify(SHCNE_RENAMEFOLDER, SHCNF_PATHW | SHCNF_FLUSH, s_szDir1InDir1, s_szDir2InDir1);
369 ::Sleep(INTERVAL);
370 DoStepCheck(s_iStage, s_iStep, StringFromChecks());
371
372 // 7: Remove dir2 in dir1
373 ++s_iStep;
374 trace("Step %d\n", s_iStep);
375 ZeroMemory(s_abChecks, sizeof(s_abChecks));
376 ret = RemoveDirectoryW(s_szDir2InDir1);
377 ok_int(ret, TRUE);
378 SHChangeNotify(SHCNE_RMDIR, SHCNF_PATHW | SHCNF_FLUSH, s_szDir2InDir1, NULL);
379 ::Sleep(INTERVAL);
380 DoStepCheck(s_iStage, s_iStep, StringFromChecks());
381
382 // 8: Finish
383 ++s_iStep;
384 assert(s_iStep == NUM_STEP);
385 C_ASSERT(NUM_STEP == 8);
386 if (s_iStage + 1 < NUM_STAGE)
387 {
388 ::PostMessage(s_hSubWnd, WM_COMMAND, IDRETRY, 0); // Next stage
389 }
390 else
391 {
392 // Finish
393 ::PostMessage(s_hSubWnd, WM_COMMAND, IDNO, 0);
394 ::PostMessage(s_hMainWnd, WM_COMMAND, IDNO, 0);
395 }
396
397 s_iStep = -1;
398
399 return 0;
400}
401
402// WM_COPYDATA
403static BOOL OnCopyData(HWND hwnd, HWND hwndSender, COPYDATASTRUCT *pCopyData)
404{
405 if (pCopyData->dwData != 0xBEEFCAFE)
406 return FALSE;
407
408 LPBYTE pbData = (LPBYTE)pCopyData->lpData;
409 LPBYTE pb = pbData;
410
411 LONG cbTotal = pCopyData->cbData;
412 assert(cbTotal >= LONG(sizeof(LONG) + sizeof(DWORD) + sizeof(DWORD)));
413
414 LONG lEvent = *(LONG*)pb;
415 pb += sizeof(lEvent);
416
417 DWORD cbPidl1 = *(DWORD*)pb;
418 pb += sizeof(cbPidl1);
419
420 DWORD cbPidl2 = *(DWORD*)pb;
421 pb += sizeof(cbPidl2);
422
423 LPITEMIDLIST pidl1 = NULL;
424 if (cbPidl1)
425 {
426 pidl1 = (LPITEMIDLIST)CoTaskMemAlloc(cbPidl1);
427 CopyMemory(pidl1, pb, cbPidl1);
428 pb += cbPidl1;
429 }
430
431 LPITEMIDLIST pidl2 = NULL;
432 if (cbPidl2)
433 {
434 pidl2 = (LPITEMIDLIST)CoTaskMemAlloc(cbPidl2);
435 CopyMemory(pidl2, pb, cbPidl2);
436 pb += cbPidl2;
437 }
438
439 assert((pb - pbData) == cbTotal);
440
441 DoTestEntry(lEvent, pidl1, pidl2);
442
443 CoTaskMemFree(pidl1);
444 CoTaskMemFree(pidl2);
445
446 return TRUE;
447}
448
449static LRESULT CALLBACK
450MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
451{
452 switch (uMsg)
453 {
454 case WM_CREATE:
455 s_hMainWnd = hwnd;
456 return 0;
457
458 case WM_COMMAND:
459 switch (LOWORD(wParam))
460 {
461 case IDYES: // Start testing
462 {
463 s_iStage = 0;
464 s_bGotUpdateDir = FALSE;
465 s_hThread = ::CreateThread(NULL, 0, StageThreadFunc, hwnd, 0, NULL);
466 if (!s_hThread)
467 {
468 skip("!s_hThread\n");
469 DestroyWindow(hwnd);
470 }
471 break;
472 }
473 case IDRETRY: // New stage
474 {
475 ::CloseHandle(s_hThread);
476 ++s_iStage;
477 s_bGotUpdateDir = FALSE;
478 s_hThread = ::CreateThread(NULL, 0, StageThreadFunc, hwnd, 0, NULL);
479 if (!s_hThread)
480 {
481 skip("!s_hThread\n");
482 DestroyWindow(hwnd);
483 }
484 break;
485 }
486 case IDNO: // Quit
487 {
488 s_iStage = -1;
489 DestroyWindow(hwnd);
490 break;
491 }
492 }
493 break;
494
495 case WM_COPYDATA:
496 if (s_iStage < 0 || s_iStep < 0)
497 break;
498
499 OnCopyData(hwnd, (HWND)wParam, (COPYDATASTRUCT*)lParam);
500 break;
501
502 case WM_DESTROY:
503 ::PostQuitMessage(0);
504 break;
505
506 default:
507 return ::DefWindowProcW(hwnd, uMsg, wParam, lParam);
508 }
509 return 0;
510}
511
512static BOOL TEST_Init(void)
513{
514 if (!FindSubProgram(s_szSubProgram, _countof(s_szSubProgram)))
515 {
516 skip("shell32_apitest_sub.exe not found\n");
517 return FALSE;
518 }
519
520 // close the SUB_CLASSNAME windows
521 DoWaitForWindow(SUB_CLASSNAME, SUB_CLASSNAME, TRUE, TRUE);
522
523 // Execute sub program
524 HINSTANCE hinst = ShellExecuteW(NULL, NULL, s_szSubProgram, L"---", NULL, SW_SHOWNORMAL);
525 if ((INT_PTR)hinst <= 32)
526 {
527 skip("Unable to run shell32_apitest_sub.exe.\n");
528 return FALSE;
529 }
530
531 // prepare for files and dirs
532 DoBuildFilesAndDirs();
533
534 // Register main window
535 WNDCLASSW wc = { 0, MainWndProc };
536 wc.hInstance = GetModuleHandleW(NULL);
537 wc.hIcon = LoadIconW(NULL, IDI_APPLICATION);
538 wc.hCursor = LoadCursorW(NULL, IDC_ARROW);
539 wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
540 wc.lpszClassName = MAIN_CLASSNAME;
541 if (!RegisterClassW(&wc))
542 {
543 skip("RegisterClassW failed\n");
544 return FALSE;
545 }
546
547 // Create main window
548 HWND hwnd = CreateWindowW(MAIN_CLASSNAME, MAIN_CLASSNAME, WS_OVERLAPPEDWINDOW,
549 CW_USEDEFAULT, CW_USEDEFAULT, 400, 100,
550 NULL, NULL, GetModuleHandleW(NULL), NULL);
551 if (!hwnd)
552 {
553 skip("CreateWindowW failed\n");
554 return FALSE;
555 }
556 ::ShowWindow(hwnd, SW_SHOWNORMAL);
557 ::UpdateWindow(hwnd);
558
559 // Find sub-window
560 s_hSubWnd = DoWaitForWindow(SUB_CLASSNAME, SUB_CLASSNAME, FALSE, FALSE);
561 if (!s_hSubWnd)
562 {
563 skip("Unable to find sub-program window.\n");
564 return FALSE;
565 }
566
567 // Start testing
568 SendMessageW(s_hSubWnd, WM_COMMAND, IDYES, 0);
569
570 return TRUE;
571}
572
573static void TEST_Main(void)
574{
575 if (!TEST_Init())
576 {
577 skip("Unable to start testing.\n");
578 TEST_Quit();
579 return;
580 }
581
582 // Message loop
583 MSG msg;
584 while (GetMessageW(&msg, NULL, 0, 0))
585 {
586 ::TranslateMessage(&msg);
587 ::DispatchMessage(&msg);
588 }
589
590 TEST_Quit();
591}
592
593START_TEST(SHChangeNotify)
594{
595 trace("Please close all Explorer windows before testing.\n");
596 trace("Please don't operate your PC while testing.\n");
597
598 DWORD dwOldTick = GetTickCount();
599 TEST_Main();
600 DWORD dwNewTick = GetTickCount();
601
602 DWORD dwTick = dwNewTick - dwOldTick;
603 trace("SHChangeNotify: Total %lu.%lu sec\n", (dwTick / 1000), (dwTick / 100 % 10));
604}