reactos/modules/rostests/apitests/shell32/SHChangeNotify.cpp
Katayama Hirofumi MZ d23573beed [SHELL32_APITEST] Follow-up to #6796 (25e2f5f)
This change will massively improve speed.
JIRA issue: CORE-13950
- Set zero to INTERVAL.
- Remove SHCNF_FLUSHNOWAIT.
2024-04-24 10:22:42 +09:00

638 lines
19 KiB
C++

/*
* PROJECT: ReactOS api tests
* LICENSE: LGPL-2.0-or-later (https://spdx.org/licenses/LGPL-2.0-or-later)
* PURPOSE: Test for SHChangeNotify
* COPYRIGHT: Copyright 2020-2024 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
*/
// NOTE: This testcase requires shell32_apitest_sub.exe.
#include "shelltest.h"
#include "shell32_apitest_sub.h"
#include <assert.h>
#define NUM_STAGE 4
#define NUM_STEP 8
#define NUM_CHECKS 12
#define INTERVAL 0
#define MAX_EVENT_TYPE 6
static HWND s_hMainWnd = NULL, s_hSubWnd = NULL;
static WCHAR s_szSubProgram[MAX_PATH]; // shell32_apitest_sub.exe
static HANDLE s_hThread = NULL;
static INT s_iStage = -1, s_iStep = -1;
static BYTE s_abChecks[NUM_CHECKS] = { 0 }; // Flags for testing
static BOOL s_bGotUpdateDir = FALSE; // Got SHCNE_UPDATEDIR?
static BOOL DoCreateFile(LPCWSTR pszFileName)
{
HANDLE hFile = ::CreateFileW(pszFileName, GENERIC_WRITE, FILE_SHARE_READ, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
::CloseHandle(hFile);
return hFile != INVALID_HANDLE_VALUE;
}
static void DoDeleteDirectory(LPCWSTR pszDir)
{
WCHAR szPath[MAX_PATH];
ZeroMemory(szPath, sizeof(szPath));
StringCchCopyW(szPath, _countof(szPath), pszDir); // Double-NULL terminated
SHFILEOPSTRUCTW FileOp = { NULL, FO_DELETE, szPath, NULL, FOF_NOCONFIRMATION | FOF_SILENT };
SHFileOperation(&FileOp);
}
static INT GetEventType(LONG lEvent)
{
switch (lEvent)
{
case SHCNE_CREATE: return 0;
case SHCNE_DELETE: return 1;
case SHCNE_RENAMEITEM: return 2;
case SHCNE_MKDIR: return 3;
case SHCNE_RMDIR: return 4;
case SHCNE_RENAMEFOLDER: return 5;
C_ASSERT(5 + 1 == MAX_EVENT_TYPE);
default: return -1;
}
}
#define FILE_1 L"_TESTFILE_1_.txt"
#define FILE_2 L"_TESTFILE_2_.txt"
#define DIR_1 L"_TESTDIR_1_"
#define DIR_2 L"_TESTDIR_2_"
static WCHAR s_szDir1[MAX_PATH];
static WCHAR s_szDir1InDir1[MAX_PATH];
static WCHAR s_szDir2InDir1[MAX_PATH];
static WCHAR s_szFile1InDir1InDir1[MAX_PATH];
static WCHAR s_szFile1InDir1[MAX_PATH];
static WCHAR s_szFile2InDir1[MAX_PATH];
static void DoDeleteFilesAndDirs(void)
{
::DeleteFileW(s_szFile1InDir1);
::DeleteFileW(s_szFile2InDir1);
::DeleteFileW(s_szFile1InDir1InDir1);
DoDeleteDirectory(s_szDir1InDir1);
DoDeleteDirectory(s_szDir2InDir1);
DoDeleteDirectory(s_szDir1);
}
static void TEST_Quit(void)
{
CloseHandle(s_hThread);
s_hThread = NULL;
PostMessageW(s_hSubWnd, WM_COMMAND, IDNO, 0); // Finish
DoWaitForWindow(SUB_CLASSNAME, SUB_CLASSNAME, TRUE, TRUE); // Close sub-windows
DoDeleteFilesAndDirs();
}
static BOOL FindSubProgram(void)
{
GetModuleFileNameW(NULL, s_szSubProgram, _countof(s_szSubProgram));
PathRemoveFileSpecW(s_szSubProgram);
PathAppendW(s_szSubProgram, L"shell32_apitest_sub.exe");
if (!PathFileExistsW(s_szSubProgram))
{
PathRemoveFileSpecW(s_szSubProgram);
PathAppendW(s_szSubProgram, L"testdata\\shell32_apitest_sub.exe");
if (!PathFileExistsW(s_szSubProgram))
return FALSE;
}
return TRUE;
}
static void DoBuildFilesAndDirs(void)
{
WCHAR szPath1[MAX_PATH];
SHGetSpecialFolderPathW(NULL, szPath1, CSIDL_PERSONAL, FALSE); // My Documents
PathAppendW(szPath1, DIR_1);
StringCchCopyW(s_szDir1, _countof(s_szDir1), szPath1);
PathAppendW(szPath1, DIR_1);
StringCchCopyW(s_szDir1InDir1, _countof(s_szDir1InDir1), szPath1);
PathRemoveFileSpecW(szPath1);
PathAppendW(szPath1, DIR_2);
StringCchCopyW(s_szDir2InDir1, _countof(s_szDir2InDir1), szPath1);
PathRemoveFileSpecW(szPath1);
PathAppendW(szPath1, DIR_1);
PathAppendW(szPath1, FILE_1);
StringCchCopyW(s_szFile1InDir1InDir1, _countof(s_szFile1InDir1InDir1), szPath1);
PathRemoveFileSpecW(szPath1);
PathRemoveFileSpecW(szPath1);
PathAppendW(szPath1, FILE_1);
StringCchCopyW(s_szFile1InDir1, _countof(s_szFile1InDir1), szPath1);
PathRemoveFileSpecW(szPath1);
PathAppendW(szPath1, FILE_2);
StringCchCopyW(s_szFile2InDir1, _countof(s_szFile2InDir1), szPath1);
PathRemoveFileSpecW(szPath1);
#define TRACE_PATH(path) trace(#path ": %ls\n", path)
TRACE_PATH(s_szDir1);
TRACE_PATH(s_szDir1InDir1);
TRACE_PATH(s_szFile1InDir1);
TRACE_PATH(s_szFile1InDir1InDir1);
TRACE_PATH(s_szFile2InDir1);
#undef TRACE_PATH
DoDeleteFilesAndDirs();
::CreateDirectoryW(s_szDir1, NULL);
ok_int(!!PathIsDirectoryW(s_szDir1), TRUE);
DoDeleteDirectory(s_szDir1InDir1);
ok_int(!PathIsDirectoryW(s_szDir1InDir1), TRUE);
}
static void DoTestEntry(LONG lEvent, LPCITEMIDLIST pidl1, LPCITEMIDLIST pidl2)
{
WCHAR szPath1[MAX_PATH], szPath2[MAX_PATH];
szPath1[0] = szPath2[0] = 0;
SHGetPathFromIDListW(pidl1, szPath1);
SHGetPathFromIDListW(pidl2, szPath2);
trace("(0x%lX, '%ls', '%ls')\n", lEvent, szPath1, szPath2);
if (lEvent == SHCNE_UPDATEDIR)
{
trace("Got SHCNE_UPDATEDIR\n");
s_bGotUpdateDir = TRUE;
return;
}
INT iEventType = GetEventType(lEvent);
if (iEventType < 0)
return;
assert(iEventType < MAX_EVENT_TYPE);
INT i = 0;
s_abChecks[i++] |= (lstrcmpiW(szPath1, L"") == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szDir1) == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szDir2InDir1) == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szFile1InDir1) == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szFile2InDir1) == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath1, s_szFile1InDir1InDir1) == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath2, L"") == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szDir1) == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szDir2InDir1) == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szFile1InDir1) == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szFile2InDir1) == 0) << iEventType;
s_abChecks[i++] |= (lstrcmpiW(szPath2, s_szFile1InDir1InDir1) == 0) << iEventType;
assert(i == NUM_CHECKS);
}
static LPCSTR StringFromChecks(void)
{
static char s_sz[2 * NUM_CHECKS + 1];
char *pch = s_sz;
for (INT i = 0; i < NUM_CHECKS; ++i)
{
WCHAR sz[3];
StringCchPrintfW(sz, _countof(sz), L"%02X", s_abChecks[i]);
*pch++ = sz[0];
*pch++ = sz[1];
}
assert((pch - s_sz) + 1 == sizeof(s_sz));
*pch = 0;
return s_sz;
}
struct TEST_ANSWER
{
INT lineno;
LPCSTR answer;
};
static void DoStepCheck(INT iStage, INT iStep, LPCSTR checks)
{
assert(0 <= iStep);
assert(iStep < NUM_STEP);
assert(0 <= iStage);
assert(iStage < NUM_STAGE);
if (s_bGotUpdateDir)
{
ok_int(TRUE, TRUE);
return;
}
LPCSTR answer = NULL;
INT lineno;
switch (iStage)
{
case 0:
{
static const TEST_ANSWER c_answers[] =
{
{ __LINE__, "000000010000010000000000" }, // 0
{ __LINE__, "000000040000000000000400" }, // 1
{ __LINE__, "000000000200020000000000" }, // 2
{ __LINE__, "000000000000080000000000" }, // 3
{ __LINE__, "000000000001010000000000" }, // 4
{ __LINE__, "000000000002020000000000" }, // 5
{ __LINE__, "000000000000000020000000" }, // 6
{ __LINE__, "000010000000100000000000" }, // 7
};
C_ASSERT(_countof(c_answers) == NUM_STEP);
lineno = c_answers[iStep].lineno;
answer = c_answers[iStep].answer;
break;
}
case 1:
{
static const TEST_ANSWER c_answers[] =
{
{ __LINE__, "000000010000010000000000" }, // 0
{ __LINE__, "000000040000000000000400" }, // 1
{ __LINE__, "000000000200020000000000" }, // 2
{ __LINE__, "000000000000080000000000" }, // 3
{ __LINE__, "000000000001010000000000" }, // 4
{ __LINE__, "000000000002020000000000" }, // 5
{ __LINE__, "000000000000000020000000" }, // 6
{ __LINE__, "000010000000100000000000" }, // 7
};
C_ASSERT(_countof(c_answers) == NUM_STEP);
lineno = c_answers[iStep].lineno;
answer = c_answers[iStep].answer;
break;
}
case 2:
{
static const TEST_ANSWER c_answers[] =
{
{ __LINE__, "000000010000010000000000" }, // 0
{ __LINE__, "000000040000000000000400" }, // 1
{ __LINE__, "000000000200020000000000" }, // 2
{ __LINE__, "000000000000080000000000" }, // 3
{ __LINE__, "000000000001010000000000" }, // 4 // Recursive
{ __LINE__, "000000000002020000000000" }, // 5 // Recursive
{ __LINE__, "000000000000000020000000" }, // 6
{ __LINE__, "000010000000100000000000" }, // 7
};
C_ASSERT(_countof(c_answers) == NUM_STEP);
lineno = c_answers[iStep].lineno;
answer = c_answers[iStep].answer;
if (iStep == 4 || iStep == 5) // Recursive cases
{
if (lstrcmpA(checks, "000000000000000000000000") == 0)
answer = "000000000000000000000000";
else
trace("Warning\n");
}
break;
}
case 3:
{
static const TEST_ANSWER c_answers[] =
{
{ __LINE__, "000000010000010000000000" }, // 0
{ __LINE__, "000000040000000000000400" }, // 1
{ __LINE__, "000000000200020000000000" }, // 2
{ __LINE__, "000000000000080000000000" }, // 3
{ __LINE__, "000000000001010000000000" }, // 4 // Recursive
{ __LINE__, "000000000002020000000000" }, // 5 // Recursive
{ __LINE__, "000000000000000020000000" }, // 6
{ __LINE__, "000010000000100000000000" }, // 7
};
C_ASSERT(_countof(c_answers) == NUM_STEP);
lineno = c_answers[iStep].lineno;
answer = c_answers[iStep].answer;
break;
}
default:
{
assert(0);
break;
}
}
ok(lstrcmpA(checks, answer) == 0,
"Line %d: '%s' vs '%s' at Stage %d, Step %d\n", lineno, checks, answer, iStage, iStep);
}
static DWORD WINAPI StageThreadFunc(LPVOID arg)
{
BOOL ret;
trace("Stage %d\n", s_iStage);
// 0: Create file1 in dir1
s_iStep = 0;
trace("Step %d\n", s_iStep);
::Sleep(1000); // Extra wait
ZeroMemory(s_abChecks, sizeof(s_abChecks));
ret = DoCreateFile(s_szFile1InDir1);
ok_int(ret, TRUE);
SHChangeNotify(SHCNE_CREATE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1, 0);
::Sleep(INTERVAL);
DoStepCheck(s_iStage, s_iStep, StringFromChecks());
// 1: Rename file1 as file2 in dir1
++s_iStep;
trace("Step %d\n", s_iStep);
ZeroMemory(s_abChecks, sizeof(s_abChecks));
ret = MoveFileW(s_szFile1InDir1, s_szFile2InDir1);
ok_int(ret, TRUE);
SHChangeNotify(SHCNE_RENAMEITEM, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1, s_szFile2InDir1);
::Sleep(INTERVAL);
DoStepCheck(s_iStage, s_iStep, StringFromChecks());
// 2: Delete file2 in dir1
++s_iStep;
trace("Step %d\n", s_iStep);
ZeroMemory(s_abChecks, sizeof(s_abChecks));
ret = DeleteFileW(s_szFile2InDir1);
ok_int(ret, TRUE);
SHChangeNotify(SHCNE_DELETE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile2InDir1, NULL);
::Sleep(INTERVAL);
DoStepCheck(s_iStage, s_iStep, StringFromChecks());
// 3: Create dir1 in dir1
++s_iStep;
trace("Step %d\n", s_iStep);
ZeroMemory(s_abChecks, sizeof(s_abChecks));
ret = CreateDirectoryExW(s_szDir1, s_szDir1InDir1, NULL);
ok_int(ret, TRUE);
SHChangeNotify(SHCNE_MKDIR, SHCNF_PATHW | SHCNF_FLUSH, s_szDir1InDir1, NULL);
::Sleep(INTERVAL);
DoStepCheck(s_iStage, s_iStep, StringFromChecks());
// 4: Create file1 in dir1 in dir1
++s_iStep;
trace("Step %d\n", s_iStep);
ZeroMemory(s_abChecks, sizeof(s_abChecks));
ret = DoCreateFile(s_szFile1InDir1InDir1);
ok_int(ret, TRUE);
SHChangeNotify(SHCNE_CREATE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1InDir1, NULL);
::Sleep(INTERVAL);
DoStepCheck(s_iStage, s_iStep, StringFromChecks());
// 5: Delete file1 in dir1 in dir1
++s_iStep;
trace("Step %d\n", s_iStep);
ZeroMemory(s_abChecks, sizeof(s_abChecks));
ret = DeleteFileW(s_szFile1InDir1InDir1);
ok_int(ret, TRUE);
SHChangeNotify(SHCNE_DELETE, SHCNF_PATHW | SHCNF_FLUSH, s_szFile1InDir1InDir1, NULL);
::Sleep(INTERVAL);
DoStepCheck(s_iStage, s_iStep, StringFromChecks());
// 6: Rename dir1 as dir2 in dir1
++s_iStep;
trace("Step %d\n", s_iStep);
ZeroMemory(s_abChecks, sizeof(s_abChecks));
ret = ::MoveFileW(s_szDir1InDir1, s_szDir2InDir1);
ok_int(ret, TRUE);
SHChangeNotify(SHCNE_RENAMEFOLDER, SHCNF_PATHW | SHCNF_FLUSH, s_szDir1InDir1, s_szDir2InDir1);
::Sleep(INTERVAL);
DoStepCheck(s_iStage, s_iStep, StringFromChecks());
// 7: Remove dir2 in dir1
++s_iStep;
trace("Step %d\n", s_iStep);
ZeroMemory(s_abChecks, sizeof(s_abChecks));
ret = RemoveDirectoryW(s_szDir2InDir1);
ok_int(ret, TRUE);
SHChangeNotify(SHCNE_RMDIR, SHCNF_PATHW | SHCNF_FLUSH, s_szDir2InDir1, NULL);
::Sleep(INTERVAL);
DoStepCheck(s_iStage, s_iStep, StringFromChecks());
// 8: Finish
++s_iStep;
assert(s_iStep == NUM_STEP);
C_ASSERT(NUM_STEP == 8);
if (s_iStage + 1 < NUM_STAGE)
{
::PostMessage(s_hSubWnd, WM_COMMAND, IDRETRY, 0); // Next stage
}
else
{
// Finish
::PostMessage(s_hSubWnd, WM_COMMAND, IDNO, 0);
::PostMessage(s_hMainWnd, WM_COMMAND, IDNO, 0);
}
s_iStep = -1;
return 0;
}
// WM_COPYDATA
static BOOL OnCopyData(HWND hwnd, HWND hwndSender, COPYDATASTRUCT *pCopyData)
{
if (pCopyData->dwData != 0xBEEFCAFE)
return FALSE;
LPBYTE pbData = (LPBYTE)pCopyData->lpData;
LPBYTE pb = pbData;
LONG cbTotal = pCopyData->cbData;
assert(cbTotal >= LONG(sizeof(LONG) + sizeof(DWORD) + sizeof(DWORD)));
LONG lEvent = *(LONG*)pb;
pb += sizeof(lEvent);
DWORD cbPidl1 = *(DWORD*)pb;
pb += sizeof(cbPidl1);
DWORD cbPidl2 = *(DWORD*)pb;
pb += sizeof(cbPidl2);
LPITEMIDLIST pidl1 = NULL;
if (cbPidl1)
{
pidl1 = (LPITEMIDLIST)CoTaskMemAlloc(cbPidl1);
CopyMemory(pidl1, pb, cbPidl1);
pb += cbPidl1;
}
LPITEMIDLIST pidl2 = NULL;
if (cbPidl2)
{
pidl2 = (LPITEMIDLIST)CoTaskMemAlloc(cbPidl2);
CopyMemory(pidl2, pb, cbPidl2);
pb += cbPidl2;
}
assert((pb - pbData) == cbTotal);
DoTestEntry(lEvent, pidl1, pidl2);
CoTaskMemFree(pidl1);
CoTaskMemFree(pidl2);
return TRUE;
}
static LRESULT CALLBACK
MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
s_hMainWnd = hwnd;
return 0;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDYES: // Start testing
{
s_iStage = 0;
s_bGotUpdateDir = FALSE;
s_hThread = ::CreateThread(NULL, 0, StageThreadFunc, hwnd, 0, NULL);
if (!s_hThread)
{
skip("!s_hThread\n");
DestroyWindow(hwnd);
}
break;
}
case IDRETRY: // New stage
{
::CloseHandle(s_hThread);
++s_iStage;
s_bGotUpdateDir = FALSE;
s_hThread = ::CreateThread(NULL, 0, StageThreadFunc, hwnd, 0, NULL);
if (!s_hThread)
{
skip("!s_hThread\n");
DestroyWindow(hwnd);
}
break;
}
case IDNO: // Quit
{
s_iStage = -1;
DestroyWindow(hwnd);
break;
}
}
break;
case WM_COPYDATA:
if (s_iStage < 0 || s_iStep < 0)
break;
OnCopyData(hwnd, (HWND)wParam, (COPYDATASTRUCT*)lParam);
break;
case WM_DESTROY:
::PostQuitMessage(0);
break;
default:
return ::DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
return 0;
}
static BOOL TEST_Init(void)
{
if (!FindSubProgram())
{
skip("shell32_apitest_sub.exe not found\n");
return FALSE;
}
// close the SUB_CLASSNAME windows
DoWaitForWindow(SUB_CLASSNAME, SUB_CLASSNAME, TRUE, TRUE);
// Execute sub program
HINSTANCE hinst = ShellExecuteW(NULL, NULL, s_szSubProgram, L"---", NULL, SW_SHOWNORMAL);
if ((INT_PTR)hinst <= 32)
{
skip("Unable to run shell32_apitest_sub.exe.\n");
return FALSE;
}
// prepare for files and dirs
DoBuildFilesAndDirs();
// Register main window
WNDCLASSW wc = { 0, MainWndProc };
wc.hInstance = GetModuleHandleW(NULL);
wc.hIcon = LoadIconW(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursorW(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wc.lpszClassName = MAIN_CLASSNAME;
if (!RegisterClassW(&wc))
{
skip("RegisterClassW failed\n");
return FALSE;
}
// Create main window
HWND hwnd = CreateWindowW(MAIN_CLASSNAME, MAIN_CLASSNAME, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 400, 100,
NULL, NULL, GetModuleHandleW(NULL), NULL);
if (!hwnd)
{
skip("CreateWindowW failed\n");
return FALSE;
}
::ShowWindow(hwnd, SW_SHOWNORMAL);
::UpdateWindow(hwnd);
// Find sub-window
s_hSubWnd = DoWaitForWindow(SUB_CLASSNAME, SUB_CLASSNAME, FALSE, FALSE);
if (!s_hSubWnd)
{
skip("Unable to find sub-program window.\n");
return FALSE;
}
// Start testing
SendMessageW(s_hSubWnd, WM_COMMAND, IDYES, 0);
return TRUE;
}
static void TEST_Main(void)
{
if (!TEST_Init())
{
skip("Unable to start testing.\n");
TEST_Quit();
return;
}
// Message loop
MSG msg;
while (GetMessageW(&msg, NULL, 0, 0))
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
TEST_Quit();
}
START_TEST(SHChangeNotify)
{
trace("Please close all Explorer windows before testing.\n");
trace("Please don't operate your PC while testing.\n");
DWORD dwOldTick = GetTickCount();
TEST_Main();
DWORD dwNewTick = GetTickCount();
DWORD dwTick = dwNewTick - dwOldTick;
trace("SHChangeNotify: Total %lu.%lu sec\n", (dwTick / 1000), (dwTick / 100 % 10));
}