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