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