223 lines
17 KiB
C++
223 lines
17 KiB
C++
// Exercise the private GUI controller against real Windows common controls,
|
|
// with deterministic replies and no index service or worker thread.
|
|
#include "../src/gui.cpp"
|
|
#include <iostream>
|
|
|
|
namespace {
|
|
struct IconGate {
|
|
wl::Handle started{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
|
|
wl::Handle release{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
|
|
std::atomic<unsigned> calls = 0;
|
|
std::atomic<DWORD> thread = 0;
|
|
};
|
|
LRESULT CALLBACK host_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
auto window = reinterpret_cast<wl::Window*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
|
if (message == WM_NCCREATE) {
|
|
window = static_cast<wl::Window*>(reinterpret_cast<CREATESTRUCTW*>(lparam)->lpCreateParams);
|
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(window));
|
|
window->hwnd = hwnd;
|
|
}
|
|
if (window && (message == WM_NOTIFY || message == WM_COMMAND || message == wl::icons_completed)) return window->message(message, wparam, lparam);
|
|
return DefWindowProcW(hwnd, message, wparam, lparam);
|
|
}
|
|
std::unique_ptr<wl::Completion> reply(wl::Window& window, uint64_t count = 3, uint64_t offset = 0) {
|
|
auto result = std::make_unique<wl::Completion>();
|
|
result->token = window.token; result->offset = offset;
|
|
result->reply.total = count; result->reply.generation = 42;
|
|
for (auto i = offset; i < std::min(count, offset + wl::page_size); ++i) {
|
|
wl::Result row; row.path = "C:\\fixture\\" + std::to_string(i) + "\\Figma.exe";
|
|
result->reply.results.push_back(std::move(row));
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
try {
|
|
wl::require_utf8_process(); OleInitialize(nullptr);
|
|
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_LISTVIEW_CLASSES | ICC_BAR_CLASSES}; InitCommonControlsEx(&controls);
|
|
auto instance = GetModuleHandleW(nullptr);
|
|
bool profile = argc > 1 && std::string_view(argv[1]) == "--profile-input";
|
|
auto gate = std::make_shared<IconGate>();
|
|
wl::AsyncIcons::Loader slow_icon = [gate](const wl::IconRequest&) {
|
|
++gate->calls; gate->thread = GetCurrentThreadId(); SetEvent(gate->started.value);
|
|
WaitForSingleObject(gate->release.value, 10000);
|
|
return wl::FileIcon{nullptr, 7};
|
|
};
|
|
wl::Window window(instance, L"figma.exe", {}, profile ? wl::load_file_icon : slow_icon);
|
|
WNDCLASSW cls{}; cls.lpfnWndProc = host_proc; cls.hInstance = instance; cls.lpszClassName = L"WLocate.CacheTest"; RegisterClassW(&cls);
|
|
auto hwnd = CreateWindowW(cls.lpszClassName, L"WLocate cache regression", WS_OVERLAPPEDWINDOW, 0, 0, 800, 500, nullptr, nullptr, instance, &window);
|
|
window.search = CreateWindowW(L"EDIT", L"figma.exe", WS_CHILD, 0, 0, 0, 0, hwnd, reinterpret_cast<HMENU>(1), instance, nullptr);
|
|
window.filter = CreateWindowW(L"COMBOBOX", L"", WS_CHILD | CBS_DROPDOWNLIST, 0, 0, 0, 0, hwnd, reinterpret_cast<HMENU>(2), instance, nullptr);
|
|
window.list = CreateWindowW(WC_LISTVIEWW, L"", WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_OWNERDATA | LVS_SHOWSELALWAYS | LVS_SHAREIMAGELISTS,
|
|
0, 0, 780, 400, hwnd, reinterpret_cast<HMENU>(3), instance, nullptr);
|
|
ListView_SetExtendedListViewStyle(window.list, LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER | LVS_EX_LABELTIP);
|
|
SetWindowTheme(window.list, L"Explorer", nullptr);
|
|
LVCOLUMNW column{}; column.mask = LVCF_WIDTH; column.cx = 500; ListView_InsertColumn(window.list, 0, &column);
|
|
window.status = CreateWindowW(STATUSCLASSNAMEW, L"", WS_CHILD, 0, 0, 0, 0, hwnd, nullptr, instance, nullptr);
|
|
ShowWindow(hwnd, SW_SHOWNOACTIVATE);
|
|
if (profile) {
|
|
window.icons.start(hwnd, wl::icons_completed);
|
|
auto timed = [](auto action) {
|
|
auto start = std::chrono::steady_clock::now(); action();
|
|
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
|
};
|
|
for (auto query : {L"", L"p", L"py", L"pyt", L"pyth", L"pytho", L"python", L"python*"}) {
|
|
SetWindowTextW(window.search, query);
|
|
auto clear_ms = timed([&] { window.start_search(); });
|
|
window.tasks.clear();
|
|
auto result = std::make_unique<wl::Completion>(); result->token = window.token;
|
|
auto search_ms = timed([&] { result->reply = wl::request_service(window.request()); });
|
|
auto count = result->reply.total;
|
|
auto publish_ms = timed([&] { window.receive(std::move(result)); UpdateWindow(window.list); });
|
|
std::cout << "query=" << wl::utf8(query) << " count=" << count << " clear_ms=" << clear_ms << " search_ms=" << search_ms << " publish_ms=" << publish_ms << std::endl;
|
|
for (int i = 0; i < std::min<uint64_t>(count, 30); ++i) {
|
|
NMLVDISPINFOW info{}; info.item.iItem = i; info.item.mask = LVIF_IMAGE;
|
|
auto ms = timed([&] { window.display(&info); });
|
|
if (ms > 10) std::cout << " icon_ms=" << ms << " path=" << window.row(i)->path << std::endl;
|
|
}
|
|
}
|
|
DestroyWindow(hwnd); window.hwnd = nullptr; OleUninitialize(); return 0;
|
|
}
|
|
window.receive(reply(window)); UpdateWindow(window.list);
|
|
// Dismissing the Shell menu onto blank list space clears selection but
|
|
// leaves the last row focused. The next automatic refresh used to loop.
|
|
SetFocus(window.list);
|
|
ListView_SetItemState(window.list, 2, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
|
|
ListView_SetItemState(window.list, -1, 0, LVIS_SELECTED);
|
|
window.tasks.clear(); window.pending.clear();
|
|
unsigned failures = 0, checks = 0;
|
|
auto check = [&](bool ok, const char* what) {
|
|
++checks; std::cout << (ok ? "PASS " : "FAIL ") << what << '\n'; if (!ok) ++failures;
|
|
};
|
|
window.queue(0, true); window.tasks.clear();
|
|
window.receive(reply(window)); UpdateWindow(window.list);
|
|
check(window.tasks.empty() && window.pending.empty(), "focused last row refresh queues no duplicate query");
|
|
check(window.row(2) && window.row(2)->path == "C:\\fixture\\2\\Figma.exe", "last row remains available after refresh");
|
|
check(ListView_GetNextItem(window.list, -1, LVNI_FOCUSED) == 2, "refresh preserves focused row");
|
|
std::cout << "Unexpected queued queries: " << window.tasks.size() << '\n';
|
|
window.tasks.clear(); window.pending.clear();
|
|
for (unsigned i = 0; i < 100; ++i) {
|
|
window.receive(reply(window)); UpdateWindow(window.list);
|
|
}
|
|
check(window.tasks.empty() && window.pending.empty(), "100 refresh/repaint cycles do not trigger another query");
|
|
auto hint = [&](int from, int to) {
|
|
NMLVCACHEHINT notification{}; notification.hdr = {window.list, 3, LVN_ODCACHEHINT};
|
|
notification.iFrom = from; notification.iTo = to;
|
|
SendMessageW(hwnd, WM_NOTIFY, 3, reinterpret_cast<LPARAM>(¬ification));
|
|
};
|
|
hint(0, 2); hint(-1, -1); hint(256, 512); hint(2, 1);
|
|
check(window.tasks.empty(), "cached, negative, reversed and out-of-range cache hints queue no query");
|
|
window.receive(reply(window, 0));
|
|
hint(0, 0); hint(-1, -1);
|
|
check(window.tasks.empty() && !window.row(0) && ListView_GetItemCount(window.list) == 0, "empty results ignore cache hints and clear rows");
|
|
window.receive(reply(window, 300)); window.tasks.clear(); window.pending.clear();
|
|
hint(256, 1000); hint(256, 299);
|
|
check(window.tasks.size() == 1 && window.tasks.front().offset == 256, "last-page hints are bounded by total and deduplicated");
|
|
window.tasks.clear(); window.receive(reply(window, 300, 256));
|
|
check(window.pending.empty() && window.row(299) && !window.row(300), "partial last page loads with correct bounds");
|
|
auto stale = reply(window, 3); stale->token = window.token - 1;
|
|
window.receive(std::move(stale));
|
|
check(window.total == 300 && window.row(299), "stale query completion cannot replace current results");
|
|
// Hold the Shell provider behind a gate. Painting and real EDIT changes
|
|
// must keep working even while it cannot return an icon.
|
|
window.icons.start(hwnd, wl::icons_completed);
|
|
window.pages[0][0].path = "C:\\fixture\\python.py";
|
|
auto icon_row = window.pages[0][0];
|
|
NMLVDISPINFOW info{}; info.item.mask = LVIF_IMAGE; info.item.iItem = 0;
|
|
auto began = GetTickCount64(); window.display(&info);
|
|
check(GetTickCount64() - began < 100 && info.item.iImage == I_IMAGENONE, "painting returns a placeholder without waiting for Shell");
|
|
check(WaitForSingleObject(gate->started.value, 1000) == WAIT_OBJECT_0 && gate->thread != GetCurrentThreadId(), "Shell provider runs on a separate thread");
|
|
for (unsigned i = 0; i < 100; ++i) window.display(&info);
|
|
check(gate->calls == 1, "repaints do not enqueue duplicate icon loads");
|
|
window.queue(0, true); auto old_reply = reply(window, 3); ResetEvent(window.cancel.value);
|
|
began = GetTickCount64();
|
|
for (auto query : {L"p", L"py", L"pyt", L"pyth", L"pytho", L"python", L"python*"}) SetWindowTextW(window.search, query);
|
|
check(GetTickCount64() - began < 100 && wl::window_text(window.search) == L"python*", "typing all prefixes remains responsive while the icon provider is blocked");
|
|
check(window.search_scheduled && window.tasks.empty() && window.pending.empty() && WaitForSingleObject(window.cancel.value, 0) == WAIT_OBJECT_0, "text changes immediately cancel and retire the previous query");
|
|
window.receive(std::move(old_reply)); window.queue(256);
|
|
check(window.total == 300 && window.tasks.empty(), "old replies and page requests cannot refresh results during debounce");
|
|
window.start_search();
|
|
check(window.tasks.size() == 1 && window.tasks.front().request.patterns[0] == "python*", "debounce starts only the latest query");
|
|
SetEvent(gate->release.value);
|
|
auto deadline = GetTickCount64() + 1000;
|
|
while (GetTickCount64() < deadline && !window.icons.has_file_icon(icon_row)) { window.icons.collect(); Sleep(1); }
|
|
check(window.icons.lookup(icon_row) == 7 && window.icons.has_file_icon(icon_row), "asynchronous file icon completion is cached on the UI thread");
|
|
ResetEvent(gate->release.value); ResetEvent(gate->started.value);
|
|
window.icons.lookup(".pyc");
|
|
check(WaitForSingleObject(gate->started.value, 1000) == WAIT_OBJECT_0, "second icon provider call entered");
|
|
began = GetTickCount64(); window.icons.stop();
|
|
check(GetTickCount64() - began < 100, "closing does not wait for a blocked Shell provider");
|
|
SetEvent(gate->release.value);
|
|
window.tasks.clear(); window.pending.clear(); window.busy = false;
|
|
window.receive(reply(window, 3000000)); window.attach_accessibility();
|
|
wl::ComPtr<IRawElementProviderFragment> root;
|
|
window.accessibility.provider()->QueryInterface(IID_PPV_ARGS(&root));
|
|
wl::ComPtr<IRawElementProviderFragment> current;
|
|
root->Navigate(NavigateDirection_FirstChild, ¤t);
|
|
unsigned accessible_rows = 0; began = GetTickCount64();
|
|
while (current && accessible_rows < 100) {
|
|
++accessible_rows; wl::ComPtr<IRawElementProviderFragment> next;
|
|
current->Navigate(NavigateDirection_NextSibling, &next); current = std::move(next);
|
|
}
|
|
check(accessible_rows > 0 && accessible_rows <= static_cast<unsigned>(ListView_GetCountPerPage(window.list) + 1), "three million results expose only visible rows to accessibility traversal");
|
|
check(GetTickCount64() - began < 100, "large-result accessibility traversal stays bounded");
|
|
// Test through the real UI Automation client as well as the provider
|
|
// interface; the GUI thread must keep pumping COM/window messages.
|
|
struct AutomationResult { std::atomic<bool> done = false; std::atomic<int> stage = 0; HRESULT hr = E_FAIL; int count = 0; uint64_t ms = 0; };
|
|
auto automation_result = std::make_shared<AutomationResult>();
|
|
std::thread([target = window.list, automation_result] {
|
|
auto com = CoInitializeEx(nullptr, COINIT_MULTITHREADED); auto start = GetTickCount64();
|
|
automation_result->stage = 1;
|
|
wl::ComPtr<IUIAutomation> automation; wl::ComPtr<IUIAutomationElement> element;
|
|
wl::ComPtr<IUIAutomationCondition> condition; wl::ComPtr<IUIAutomationElementArray> rows;
|
|
auto hr = CoCreateInstance(CLSID_CUIAutomation, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&automation));
|
|
automation_result->stage = 2;
|
|
if (SUCCEEDED(hr)) hr = automation->ElementFromHandle(target, &element);
|
|
automation_result->stage = 3;
|
|
VARIANT type{}; type.vt = VT_I4; type.lVal = UIA_ListItemControlTypeId;
|
|
if (SUCCEEDED(hr)) hr = automation->CreatePropertyCondition(UIA_ControlTypePropertyId, type, &condition);
|
|
automation_result->stage = 4;
|
|
if (SUCCEEDED(hr)) hr = element->FindAll(TreeScope_Descendants, condition.Get(), &rows);
|
|
automation_result->stage = 5;
|
|
if (SUCCEEDED(hr)) hr = rows->get_Length(&automation_result->count);
|
|
automation_result->hr = hr; automation_result->ms = GetTickCount64() - start;
|
|
rows.Reset(); condition.Reset(); element.Reset(); automation.Reset();
|
|
if (SUCCEEDED(com)) CoUninitialize(); automation_result->done = true;
|
|
}).detach();
|
|
auto automation_deadline = GetTickCount64() + 10000;
|
|
while (!automation_result->done && GetTickCount64() < automation_deadline) {
|
|
MSG message;
|
|
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&message); DispatchMessageW(&message); }
|
|
MsgWaitForMultipleObjectsEx(0, nullptr, 10, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
|
|
}
|
|
check(automation_result->done && SUCCEEDED(automation_result->hr) && automation_result->count > 0 && automation_result->count <= 50,
|
|
"real UI Automation FindAll visits only visible results out of three million");
|
|
check(automation_result->done && automation_result->ms < 1000, "real UI Automation traversal completes in under a second");
|
|
if (automation_result->done) std::cout << "UI Automation visible rows: " << automation_result->count << ", elapsed: " << automation_result->ms << " ms, HRESULT: " << std::hex << automation_result->hr << std::dec << '\n';
|
|
else std::cout << "UI Automation timed out at stage " << automation_result->stage << '\n';
|
|
wl::ComPtr<IItemContainerProvider> container;
|
|
window.accessibility.provider()->QueryInterface(IID_PPV_ARGS(&container));
|
|
VARIANT id{}; id.vt = VT_BSTR; id.bstrVal = SysAllocString(L"row-2999999");
|
|
wl::ComPtr<IRawElementProviderSimple> virtual_row;
|
|
auto hr = container->FindItemByProperty(nullptr, UIA_AutomationIdPropertyId, id, &virtual_row); VariantClear(&id);
|
|
check(SUCCEEDED(hr) && virtual_row, "offscreen results remain discoverable by ItemContainer");
|
|
VARIANT name{};
|
|
check(virtual_row && virtual_row->GetPropertyValue(UIA_NamePropertyId, &name) == UIA_E_ELEMENTNOTAVAILABLE, "unrealized row exposes a virtual placeholder"); VariantClear(&name);
|
|
wl::ComPtr<IVirtualizedItemProvider> realize; virtual_row.As(&realize);
|
|
check(realize && SUCCEEDED(realize->Realize()) && ListView_GetTopIndex(window.list) > 2999900, "VirtualizedItem realizes distant rows without enumerating the whole index");
|
|
wl::ComPtr<ISelectionItemProvider> selection; virtual_row.As(&selection); BOOL selected = FALSE;
|
|
check(selection && SUCCEEDED(selection->Select()) && SUCCEEDED(selection->get_IsSelected(&selected)) && selected && ListView_GetSelectedCount(window.list) == 1, "virtualized result supports native selection");
|
|
wl::ComPtr<IRawElementProviderSimple> next;
|
|
VARIANT ignored{};
|
|
check(SUCCEEDED(container->FindItemByProperty(virtual_row.Get(), 0, ignored, &next)) && !next, "ItemContainer iteration ends after the last result");
|
|
window.accessibility.reset();
|
|
check(virtual_row->GetPropertyValue(UIA_NamePropertyId, &name) == UIA_E_ELEMENTNOTAVAILABLE, "old accessibility rows become unavailable when the query changes");
|
|
window.accessibility.detach();
|
|
check(root->Navigate(NavigateDirection_FirstChild, ¤t) == UIA_E_ELEMENTNOTAVAILABLE, "providers retained by clients are safe after window detachment");
|
|
DestroyWindow(hwnd); window.hwnd = nullptr; OleUninitialize();
|
|
std::cout << checks << " checks, " << failures << " failures\n";
|
|
return failures ? 1 : 0;
|
|
} catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; }
|
|
}
|