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 
19 static HWND s_hMainWnd = NULL, s_hSubWnd = NULL;
20 static WCHAR s_szSubProgram[MAX_PATH]; // shell32_apitest_sub.exe
21 static HANDLE s_hThread = NULL;
22 static INT s_iStage = -1, s_iStep = -1;
23 static BYTE s_abChecks[NUM_CHECKS] = { 0 }; // Flags for testing
24 static BOOL s_bGotUpdateDir = FALSE; // Got SHCNE_UPDATEDIR?
25 
DoCreateFile(LPCWSTR pszFileName)26 static 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 
DoDeleteDirectory(LPCWSTR pszDir)34 static 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 
GetEventType(LONG lEvent)43 static 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 
63 static WCHAR s_szDir1[MAX_PATH];
64 static WCHAR s_szDir1InDir1[MAX_PATH];
65 static WCHAR s_szDir2InDir1[MAX_PATH];
66 static WCHAR s_szFile1InDir1InDir1[MAX_PATH];
67 static WCHAR s_szFile1InDir1[MAX_PATH];
68 static WCHAR s_szFile2InDir1[MAX_PATH];
69 
DoDeleteFilesAndDirs(void)70 static 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 
TEST_Quit(void)80 static 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 
DoBuildFilesAndDirs(void)91 static 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 
DoTestEntry(LONG lEvent,LPCITEMIDLIST pidl1,LPCITEMIDLIST pidl2)137 static 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 
StringFromChecks(void)176 static 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 
195 struct TEST_ANSWER
196 {
197     INT lineno;
198     LPCSTR answer;
199 };
200 
DoStepCheck(INT iStage,INT iStep,LPCSTR checks)201 static 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;
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:
289         {
290             assert(0);
291             break;
292         }
293     }
294 
295     ok(lstrcmpA(checks, answer) == 0,
296        "Line %d: '%s' vs '%s' at Stage %d, Step %d\n", lineno, checks, answer, iStage, iStep);
297 }
298 
StageThreadFunc(LPVOID arg)299 static DWORD WINAPI StageThreadFunc(LPVOID arg)
300 {
301     BOOL ret;
302 
303     trace("Stage %d\n", s_iStage);
304 
305     // 0: Create file1 in dir1
306     s_iStep = 0;
307     trace("Step %d\n", s_iStep);
308     SHChangeNotify(0, SHCNF_PATHW | SHCNF_FLUSH, NULL, NULL);
309     ZeroMemory(s_abChecks, sizeof(s_abChecks));
310     ret = DoCreateFile(s_szFile1InDir1);
311     ok_int(ret, TRUE);
312     SHChangeNotify(SHCNE_CREATE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1, 0);
313     ::Sleep(INTERVAL);
314     DoStepCheck(s_iStage, s_iStep, StringFromChecks());
315 
316     // 1: Rename file1 as file2 in dir1
317     ++s_iStep;
318     trace("Step %d\n", s_iStep);
319     ZeroMemory(s_abChecks, sizeof(s_abChecks));
320     ret = MoveFileW(s_szFile1InDir1, s_szFile2InDir1);
321     ok_int(ret, TRUE);
322     SHChangeNotify(SHCNE_RENAMEITEM, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1, s_szFile2InDir1);
323     ::Sleep(INTERVAL);
324     DoStepCheck(s_iStage, s_iStep, StringFromChecks());
325 
326     // 2: Delete file2 in dir1
327     ++s_iStep;
328     trace("Step %d\n", s_iStep);
329     ZeroMemory(s_abChecks, sizeof(s_abChecks));
330     ret = DeleteFileW(s_szFile2InDir1);
331     ok_int(ret, TRUE);
332     SHChangeNotify(SHCNE_DELETE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile2InDir1, NULL);
333     ::Sleep(INTERVAL);
334     DoStepCheck(s_iStage, s_iStep, StringFromChecks());
335 
336     // 3: Create dir1 in dir1
337     ++s_iStep;
338     trace("Step %d\n", s_iStep);
339     ZeroMemory(s_abChecks, sizeof(s_abChecks));
340     ret = CreateDirectoryExW(s_szDir1, s_szDir1InDir1, NULL);
341     ok_int(ret, TRUE);
342     SHChangeNotify(SHCNE_MKDIR, SHCNF_PATHW | SHCNF_FLUSH, s_szDir1InDir1, NULL);
343     ::Sleep(INTERVAL);
344     DoStepCheck(s_iStage, s_iStep, StringFromChecks());
345 
346     // 4: Create file1 in dir1 in dir1
347     ++s_iStep;
348     trace("Step %d\n", s_iStep);
349     ZeroMemory(s_abChecks, sizeof(s_abChecks));
350     ret = DoCreateFile(s_szFile1InDir1InDir1);
351     ok_int(ret, TRUE);
352     SHChangeNotify(SHCNE_CREATE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1InDir1, NULL);
353     ::Sleep(INTERVAL);
354     DoStepCheck(s_iStage, s_iStep, StringFromChecks());
355 
356     // 5: Delete file1 in dir1 in dir1
357     ++s_iStep;
358     trace("Step %d\n", s_iStep);
359     ZeroMemory(s_abChecks, sizeof(s_abChecks));
360     ret = DeleteFileW(s_szFile1InDir1InDir1);
361     ok_int(ret, TRUE);
362     SHChangeNotify(SHCNE_DELETE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1InDir1, NULL);
363     ::Sleep(INTERVAL);
364     DoStepCheck(s_iStage, s_iStep, StringFromChecks());
365 
366     // 6: Rename dir1 as dir2 in dir1
367     ++s_iStep;
368     trace("Step %d\n", s_iStep);
369     ZeroMemory(s_abChecks, sizeof(s_abChecks));
370     ret = ::MoveFileW(s_szDir1InDir1, s_szDir2InDir1);
371     ok_int(ret, TRUE);
372     SHChangeNotify(SHCNE_RENAMEFOLDER, SHCNF_PATHW | SHCNF_FLUSH, s_szDir1InDir1, s_szDir2InDir1);
373     ::Sleep(INTERVAL);
374     DoStepCheck(s_iStage, s_iStep, StringFromChecks());
375 
376     // 7: Remove dir2 in dir1
377     ++s_iStep;
378     trace("Step %d\n", s_iStep);
379     ZeroMemory(s_abChecks, sizeof(s_abChecks));
380     ret = RemoveDirectoryW(s_szDir2InDir1);
381     ok_int(ret, TRUE);
382     SHChangeNotify(SHCNE_RMDIR, SHCNF_PATHW | SHCNF_FLUSH, s_szDir2InDir1, NULL);
383     ::Sleep(INTERVAL);
384     DoStepCheck(s_iStage, s_iStep, StringFromChecks());
385 
386     // 8: Finish
387     ++s_iStep;
388     assert(s_iStep == NUM_STEP);
389     C_ASSERT(NUM_STEP == 8);
390     if (s_iStage + 1 < NUM_STAGE)
391     {
392         ::PostMessage(s_hSubWnd, WM_COMMAND, IDRETRY, 0); // Next stage
393     }
394     else
395     {
396         // Finish
397         ::PostMessage(s_hSubWnd, WM_COMMAND, IDNO, 0);
398         ::PostMessage(s_hMainWnd, WM_COMMAND, IDNO, 0);
399     }
400 
401     s_iStep = -1;
402 
403     return 0;
404 }
405 
406 // WM_COPYDATA
OnCopyData(HWND hwnd,HWND hwndSender,COPYDATASTRUCT * pCopyData)407 static BOOL OnCopyData(HWND hwnd, HWND hwndSender, COPYDATASTRUCT *pCopyData)
408 {
409     if (pCopyData->dwData != 0xBEEFCAFE)
410         return FALSE;
411 
412     LPBYTE pbData = (LPBYTE)pCopyData->lpData;
413     LPBYTE pb = pbData;
414 
415     LONG cbTotal = pCopyData->cbData;
416     assert(cbTotal >= LONG(sizeof(LONG) + sizeof(DWORD) + sizeof(DWORD)));
417 
418     LONG lEvent = *(LONG*)pb;
419     pb += sizeof(lEvent);
420 
421     DWORD cbPidl1 = *(DWORD*)pb;
422     pb += sizeof(cbPidl1);
423 
424     DWORD cbPidl2 = *(DWORD*)pb;
425     pb += sizeof(cbPidl2);
426 
427     LPITEMIDLIST pidl1 = NULL;
428     if (cbPidl1)
429     {
430         pidl1 = (LPITEMIDLIST)CoTaskMemAlloc(cbPidl1);
431         CopyMemory(pidl1, pb, cbPidl1);
432         pb += cbPidl1;
433     }
434 
435     LPITEMIDLIST pidl2 = NULL;
436     if (cbPidl2)
437     {
438         pidl2 = (LPITEMIDLIST)CoTaskMemAlloc(cbPidl2);
439         CopyMemory(pidl2, pb, cbPidl2);
440         pb += cbPidl2;
441     }
442 
443     assert((pb - pbData) == cbTotal);
444 
445     DoTestEntry(lEvent, pidl1, pidl2);
446 
447     CoTaskMemFree(pidl1);
448     CoTaskMemFree(pidl2);
449 
450     return TRUE;
451 }
452 
453 static LRESULT CALLBACK
MainWndProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)454 MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
455 {
456     switch (uMsg)
457     {
458         case WM_CREATE:
459             s_hMainWnd = hwnd;
460             return 0;
461 
462         case WM_COMMAND:
463             switch (LOWORD(wParam))
464             {
465                 case IDYES: // Start testing
466                 {
467                     s_iStage = 0;
468                     s_bGotUpdateDir = FALSE;
469                     s_hThread = ::CreateThread(NULL, 0, StageThreadFunc, hwnd, 0, NULL);
470                     if (!s_hThread)
471                     {
472                         skip("!s_hThread\n");
473                         DestroyWindow(hwnd);
474                     }
475                     break;
476                 }
477                 case IDRETRY: // New stage
478                 {
479                     ::CloseHandle(s_hThread);
480                     ++s_iStage;
481                     s_bGotUpdateDir = FALSE;
482                     s_hThread = ::CreateThread(NULL, 0, StageThreadFunc, hwnd, 0, NULL);
483                     if (!s_hThread)
484                     {
485                         skip("!s_hThread\n");
486                         DestroyWindow(hwnd);
487                     }
488                     break;
489                 }
490                 case IDNO: // Quit
491                 {
492                     s_iStage = -1;
493                     DestroyWindow(hwnd);
494                     break;
495                 }
496             }
497             break;
498 
499         case WM_COPYDATA:
500             if (s_iStage < 0 || s_iStep < 0)
501                 break;
502 
503             OnCopyData(hwnd, (HWND)wParam, (COPYDATASTRUCT*)lParam);
504             break;
505 
506         case WM_DESTROY:
507             ::PostQuitMessage(0);
508             break;
509 
510         default:
511             return ::DefWindowProcW(hwnd, uMsg, wParam, lParam);
512     }
513     return 0;
514 }
515 
TEST_Init(void)516 static BOOL TEST_Init(void)
517 {
518     if (!FindSubProgram(s_szSubProgram, _countof(s_szSubProgram)))
519     {
520         skip("shell32_apitest_sub.exe not found\n");
521         return FALSE;
522     }
523 
524     // close the SUB_CLASSNAME windows
525     DoWaitForWindow(SUB_CLASSNAME, SUB_CLASSNAME, TRUE, TRUE);
526 
527     // Execute sub program
528     HINSTANCE hinst = ShellExecuteW(NULL, NULL, s_szSubProgram, L"---", NULL, SW_SHOWNORMAL);
529     if ((INT_PTR)hinst <= 32)
530     {
531         skip("Unable to run shell32_apitest_sub.exe.\n");
532         return FALSE;
533     }
534 
535     // prepare for files and dirs
536     DoBuildFilesAndDirs();
537 
538     // Register main window
539     WNDCLASSW wc = { 0, MainWndProc };
540     wc.hInstance = GetModuleHandleW(NULL);
541     wc.hIcon = LoadIconW(NULL, IDI_APPLICATION);
542     wc.hCursor = LoadCursorW(NULL, IDC_ARROW);
543     wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
544     wc.lpszClassName = MAIN_CLASSNAME;
545     if (!RegisterClassW(&wc))
546     {
547         skip("RegisterClassW failed\n");
548         return FALSE;
549     }
550 
551     // Create main window
552     HWND hwnd = CreateWindowW(MAIN_CLASSNAME, MAIN_CLASSNAME, WS_OVERLAPPEDWINDOW,
553                               CW_USEDEFAULT, CW_USEDEFAULT, 400, 100,
554                               NULL, NULL, GetModuleHandleW(NULL), NULL);
555     if (!hwnd)
556     {
557         skip("CreateWindowW failed\n");
558         return FALSE;
559     }
560     ::ShowWindow(hwnd, SW_SHOWNORMAL);
561     ::UpdateWindow(hwnd);
562 
563     // Find sub-window
564     s_hSubWnd = DoWaitForWindow(SUB_CLASSNAME, SUB_CLASSNAME, FALSE, FALSE);
565     if (!s_hSubWnd)
566     {
567         skip("Unable to find sub-program window.\n");
568         return FALSE;
569     }
570 
571     // Start testing
572     SendMessageW(s_hSubWnd, WM_COMMAND, IDYES, 0);
573 
574     return TRUE;
575 }
576 
TEST_Main(void)577 static void TEST_Main(void)
578 {
579     if (!TEST_Init())
580     {
581         skip("Unable to start testing.\n");
582         TEST_Quit();
583         return;
584     }
585 
586     // Message loop
587     MSG msg;
588     while (GetMessageW(&msg, NULL, 0, 0))
589     {
590         ::TranslateMessage(&msg);
591         ::DispatchMessage(&msg);
592     }
593 
594     TEST_Quit();
595 }
596 
START_TEST(SHChangeNotify)597 START_TEST(SHChangeNotify)
598 {
599     trace("Please close all Explorer windows before testing.\n");
600     trace("Please don't operate your PC while testing.\n");
601 
602     DWORD dwOldTick = GetTickCount();
603     TEST_Main();
604     DWORD dwNewTick = GetTickCount();
605 
606     DWORD dwTick = dwNewTick - dwOldTick;
607     trace("SHChangeNotify: Total %lu.%lu sec\n", (dwTick / 1000), (dwTick / 100 % 10));
608 }
609