Files
hrynco-kswitcher/tests/history_host.cpp
T

170 lines
12 KiB
C++

// Run from an isolated fixture folder containing KSwitcher.exe and Screenshot.exe,
// with the normal resident stopped. Never points at the installed user's data.
#include "storage.h"
#include "ocr.h"
#include "json.hpp"
#include <commctrl.h>
#include <psapi.h>
#include <iostream>
static HWND editor{};
static bool pastedImage{};
static LRESULT CALLBACK targetProc(HWND w,UINT message,WPARAM wp,LPARAM lp) {
if(message==WM_SETFOCUS && editor) {SetFocus(editor);return 0;}
return DefWindowProcW(w,message,wp,lp);
}
static void require(bool ok,const char *what) {if(!ok)throw std::runtime_error(what);std::cout<<"PASS "<<what<<'\n';}
static void pump() {MSG m;while(PeekMessageW(&m,nullptr,0,0,PM_REMOVE)){TranslateMessage(&m);DispatchMessageW(&m);}}
template<class F> static void until(F condition,const char *what,DWORD timeout=10000) {
auto end=GetTickCount64()+timeout;
while(!condition()){if(GetTickCount64()>=end)throw std::runtime_error(what);pump();Sleep(10);}
}
static LRESULT send(HWND w,UINT message,WPARAM wp=0,LPARAM lp=0) {
DWORD_PTR result{};
if(!SendMessageTimeoutW(w,message,wp,lp,SMTO_ABORTIFHUNG,5000,&result))throw std::runtime_error("window request timed out");
return LRESULT(result);
}
static void command(HWND resident,int cmd) {
COPYDATASTRUCT data{1,sizeof(cmd),&cmd};send(resident,WM_COPYDATA,0,LPARAM(&data));
}
static size_t memory(HANDLE process) {
ks::Bytes bytes(1024*1024);auto ws=reinterpret_cast<PSAPI_WORKING_SET_INFORMATION*>(bytes.data());
if(!QueryWorkingSet(process,ws,DWORD(bytes.size())))throw std::runtime_error("memory query failed");
size_t pages=0;for(size_t i=0;i<ws->NumberOfEntries;++i)if(!ws->WorkingSetInfo[i].Shared)++pages;
return pages*4096;
}
static std::wstring editorText() {wchar_t text[256]{};GetWindowTextW(editor,text,256);return text;}
static LRESULT CALLBACK editProc(HWND w,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR) {
if(message==WM_PASTE && IsClipboardFormatAvailable(CF_DIB)) {pastedImage=true;return 0;}
return DefSubclassProc(w,message,wp,lp);
}
static ks::Clip excluded(ks::Clip clip) {
clip.formats.push_back({0,L"ExcludeClipboardContentFromMonitorProcessing",ks::Bytes{0}});return clip;
}
int main() {
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
PROCESS_INFORMATION process{};HWND resident{},target{};ks::Clip saved;bool captured=false;int result=0;
try {
auto folder=std::filesystem::path(ks::executable()).parent_path();
require(folder.filename().wstring().starts_with(L"history-host-fixture-"),"isolated fixture directory");
require(!FindWindowW(L"KSwitcher.Main",nullptr),"normal resident is stopped");
WNDCLASSW targetClass{};targetClass.lpfnWndProc=targetProc;targetClass.hInstance=GetModuleHandleW(nullptr);
targetClass.lpszClassName=L"KSwitcher.HistoryTestDestination";RegisterClassW(&targetClass);
target=CreateWindowExW(0,targetClass.lpszClassName,L"KSwitcher history test destination",WS_OVERLAPPEDWINDOW|WS_VISIBLE,
50,50,500,250,nullptr,nullptr,nullptr,nullptr);
editor=CreateWindowExW(0,L"EDIT",L"",WS_CHILD|WS_VISIBLE|ES_MULTILINE,10,10,450,180,target,nullptr,nullptr,nullptr);
SetWindowSubclass(editor,editProc,1,0);
saved=ks::readClipboard(target,128*1024*1024);captured=true;
ks::writeClipboard(target,excluded(ks::textClip(L"fixture clipboard")));
auto settings=ks::loadSettings(folder/L"missing.json",ks::readLayouts());
settings.autoCorrect=false;settings.writeLog=true;settings.history=true;settings.maxItems=500;settings.maxMiB=100;
std::filesystem::create_directories(folder/L"data");
ks::saveSettings(folder/L"data/settings.json",settings);
ks::History history(folder/L"data/history");
history.clear(); // This executable only runs in its dedicated fixture directory.
ks::Pixels pixels{1566,1068,ks::Bytes(1566*1068*4,255)};
auto image=ks::Clip{{ks::imageDib(pixels)}};
history.add(image,L"test",settings);
auto imageId=history.entries().front().id;
// Seed once rather than rewriting the encrypted index 500 times. This
// also avoids measuring fixture creation in the resident under test.
auto indexPath=folder/L"data/history/index.dat";
auto rows=nlohmann::json::parse(ks::unprotect(ks::readFile(indexPath,1024*1024)));
for(int i=0;i<499;++i) {
auto id=ks::lower(ks::newSnippetId().substr(1,36)),text=L"History fixture "+std::to_wstring(i);
auto bytes=ks::protect(ks::encodeClip(ks::textClip(text)));
ks::atomicWrite(folder/L"data/history"/(id+L".dat"),bytes);
rows.insert(rows.begin(),nlohmann::json{{"id",ks::utf8(id)},{"preview",ks::utf8(text)},
{"app","test"},{"bytes",bytes.size()},{"at",i+1},{"pinned",false},{"image",false},{"files",false}});
}
auto encoded=rows.dump();ks::atomicWrite(indexPath,ks::protect(ks::Bytes(encoded.begin(),encoded.end())));
history.reload();
auto textId=history.entries().front().id;
auto exe=folder/L"KSwitcher.exe";auto line=L"\""+exe.wstring()+L"\"";
STARTUPINFOW startup{sizeof(startup)};startup.dwFlags=STARTF_USESHOWWINDOW;startup.wShowWindow=SW_HIDE;
require(CreateProcessW(exe.c_str(),line.data(),nullptr,nullptr,FALSE,CREATE_NO_WINDOW,nullptr,folder.c_str(),&startup,&process),"start isolated resident");
CloseHandle(process.hThread);process.hThread=nullptr;
until([&]{resident=FindWindowW(L"KSwitcher.Main",nullptr);return resident!=nullptr;},"resident startup");
auto base=memory(process.hProcess);
std::string forged="{\"action\":\"clear\",\"id\":\"\",\"plain\":false}";
COPYDATASTRUCT forgedRequest{0x4b530200,DWORD(forged.size()),forged.data()};
require(send(resident,WM_COPYDATA,WPARAM(target),LPARAM(&forgedRequest))==0,
"history rejects requests from an untrusted process");
auto open=[&] {
require(ks::activateWindow(target),"destination foreground");SetFocus(editor);command(resident,5);
HWND window{};
until([&]{window=FindWindowW(L"KSwitcher.History",nullptr);return window&&IsWindowVisible(window);},"history startup");
until([&]{return send(GetDlgItem(window,102),LB_GETCOUNT)>0;},"history rows");
DWORD pid{};GetWindowThreadProcessId(window,&pid);
require(pid!=process.dwProcessId,"history belongs to a separate process");
return window;
};
auto close=[&](HWND window) {
DWORD pid{};GetWindowThreadProcessId(window,&pid);HANDLE child=OpenProcess(SYNCHRONIZE,FALSE,pid);
PostMessageW(window,WM_KEYDOWN,VK_ESCAPE,0); // Escape also works with focus on the window itself.
until([&]{return WaitForSingleObject(child,0)==WAIT_OBJECT_0;},"history helper exits after Escape");
CloseHandle(child);
require(!FindWindowW(L"KSwitcher.History",nullptr),"Escape destroys history window");
};
for(int i=0;i<3;++i){auto w=open();until([&]{return send(GetDlgItem(w,102),LB_GETCOUNT)==500;},"all history rows loaded");require(true,"all history entries visible");close(w);}
auto idle=memory(process.hProcess);
std::cout<<"MEMORY resident_before="<<base<<" after_three_closes="<<idle<<'\n';
require(idle<base+2*1024*1024,"repeated history does not retain UI heap in resident");
auto w=open();send(w,WM_COMMAND,112);
until([&]{history.reload();return history.entries().front().pinned;},"resident pin mutation");
require(history.entries().front().id==textId,"pin preserves history identity");
send(GetDlgItem(w,101),WM_SETTEXT,0,LPARAM(L"History fixture 498"));
until([&]{return send(GetDlgItem(w,102),LB_GETCOUNT)==1;},"history search");
close(w);w=open();
wchar_t query[100]{};send(GetDlgItem(w,101),WM_GETTEXT,100,LPARAM(query));
require(std::wstring(query)==L"History fixture 498","search survives helper restart");
send(w,WM_COMMAND,114);
HWND entry{};until([&]{entry=FindWindowW(L"KSwitcher.Entry",nullptr);return entry!=nullptr;},"full item opens");
DWORD childPid{};GetWindowThreadProcessId(w,&childPid);HANDLE child=OpenProcess(SYNCHRONIZE,FALSE,childPid);
send(GetDlgItem(w,101),WM_KEYDOWN,VK_ESCAPE);
require(WaitForSingleObject(child,0)==WAIT_TIMEOUT && IsWindow(entry),"full item remains usable after closing history");
send(entry,WM_CLOSE);
until([&]{return WaitForSingleObject(child,0)==WAIT_OBJECT_0;},"helper exits after last full item closes");CloseHandle(child);
w=open();send(GetDlgItem(w,101),WM_SETTEXT,0,LPARAM(L""));
until([&]{return send(GetDlgItem(w,102),LB_GETCOUNT)==500;},"clear search");close(w);
ks::writeClipboard(target,excluded(image));
w=open();send(w,WM_COMMAND,111);
until([&]{return editorText()==L"History fixture 498";},"paste plain text into native editor");
until([&]{return IsClipboardFormatAvailable(CF_DIB)&&!FindWindowW(L"KSwitcher.HistoryHost",nullptr);},"image clipboard restored after paste");
require(ks::decodeImage(ks::readClipboard(target,64*1024*1024)).width==pixels.width,"image clipboard survives history paste");
auto afterPaste=memory(process.hProcess);
std::cout<<"MEMORY after_paste_with_image_backup="<<afterPaste<<'\n';
require(afterPaste<base+2*1024*1024,"image clipboard backup stays outside resident");
ks::writeClipboard(target,excluded(ks::textClip(L"keep clipboard")));
w=open();send(GetDlgItem(w,101),WM_SETTEXT,0,LPARAM(L"Image"));
until([&]{return send(GetDlgItem(w,102),LB_GETCOUNT)==1;},"image search");
pastedImage=false;send(w,WM_COMMAND,110);
until([&]{return pastedImage;},"image paste reaches destination");
until([&]{return ks::readClipboard(target).text()==L"keep clipboard";},"clipboard restored after image paste");
std::cout<<"MEMORY after_image_paste="<<memory(process.hProcess)<<'\n';
require(memory(process.hProcess)<base+2*1024*1024,"image paste does not retain full bitmap in resident");
until([&]{return !FindWindowW(L"KSwitcher.HistoryHost",nullptr);},"image paste helper exits");
w=open();send(w,WM_COMMAND,113);
until([&]{history.reload();return history.entries().size()==499;},"resident removes selected image");
GetWindowThreadProcessId(w,&childPid);child=OpenProcess(SYNCHRONIZE,FALSE,childPid);
// Startup update checks temporarily reject exit; retry after that
// operation finishes instead of treating its guard as a host leak.
ULONGLONG nextExit{};
until([&]{
if(WaitForSingleObject(process.hProcess,0)==WAIT_OBJECT_0) return true;
if(GetTickCount64()>=nextExit) {
command(resident,99);nextExit=GetTickCount64()+500;
}
return false;
},"resident accepts exit after startup update check",30000);
until([&]{return WaitForSingleObject(child,0)==WAIT_OBJECT_0;},"resident shutdown closes history helper");
CloseHandle(child);resident=nullptr;
std::cout<<"PASS isolated history host suite\n";
} catch(const std::exception &e){std::cerr<<"FAIL "<<e.what()<<" editor="<<ks::utf8(editorText())<<" foreground="<<(GetForegroundWindow()==target)<<'\n';result=1;}
if(resident)try{command(resident,99);}catch(...){}
if(process.hProcess){if(WaitForSingleObject(process.hProcess,10000)!=WAIT_OBJECT_0){TerminateProcess(process.hProcess,1);WaitForSingleObject(process.hProcess,INFINITE);}CloseHandle(process.hProcess);}
if(captured)try{ks::writeClipboard(target,saved);}catch(...){result=1;}
if(target) DestroyWindow(target);
CoUninitialize();
return result;
}