Files
hrynco-kswitcher/tests/far_snippets.cpp
T

130 lines
8.3 KiB
C++

// Explicit Windows integration test. Opens an isolated Far profile and never
// types into an existing Far window. Requires Far Manager at its default path.
#include "app.h"
#include <cstdlib>
#include <cwctype>
#include <fstream>
#include <future>
namespace ks {
struct InputRegressionAccess {
static void runQueued(App &app) {
std::function<void()> job;
{ std::lock_guard lock(app.jobsMutex);
if (app.jobs.empty()) throw std::runtime_error("snippet did not queue");
job=std::move(app.jobs.front());app.jobs.pop_front(); }
auto future=std::async(std::launch::async,[job=std::move(job)] {
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);job();CoUninitialize();
});
while(future.wait_for(std::chrono::milliseconds(5))!=std::future_status::ready) {
MSG message{};while(PeekMessageW(&message,nullptr,0,0,PM_REMOVE)) {
TranslateMessage(&message);DispatchMessageW(&message);
}
}
future.get();app.replayTyping();
}
};
}
int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int) {
auto base=std::filesystem::path(ks::executable()).parent_path();
std::ofstream log(base/L"far-snippets.log");
int checks=0;
auto require=[&](bool ok,const char *message) {
if(!ok)throw std::runtime_error(message);
++checks;log<<"PASS "<<message<<'\n';log.flush();
};
auto folder=std::filesystem::temp_directory_path()/(L"ks-far-snippets-"+ks::newSnippetId());
std::filesystem::create_directories(folder);
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
ks::Clip saved;bool captured=false;PROCESS_INFORMATION child{};
HANDLE input=INVALID_HANDLE_VALUE,output=INVALID_HANDLE_VALUE;
HWND farWindow{};
HWND owner=CreateWindowExW(0,L"STATIC",L"Far snippet fixture clipboard",0,0,0,0,0,HWND_MESSAGE,nullptr,instance,nullptr);
auto originalForeground=GetForegroundWindow();
int result=1;
try {
saved=ks::readClipboard(owner,128*1024*1024);captured=true;
auto sentinel=ks::textClip(L"KSwitcher Far fixture clipboard");
sentinel.formats.push_back({0,L"ExcludeClipboardContentFromMonitorProcessing",ks::Bytes{0}});
ks::writeClipboard(owner,sentinel);
const auto programFiles=_wgetenv(L"ProgramFiles");
if(!programFiles)throw std::runtime_error("ProgramFiles is unavailable");
auto farExecutable=std::filesystem::path(programFiles)/L"Far Manager/Far.exe";
std::wstring command=L"\""+farExecutable.wstring()+L"\" -m -p -s \""+folder.wstring()+L"\\profile\" \""+folder.wstring()+L"\"";
STARTUPINFOW startup{sizeof(startup)};startup.dwFlags=STARTF_USESHOWWINDOW;startup.wShowWindow=SW_HIDE;
require(CreateProcessW(nullptr,command.data(),nullptr,nullptr,FALSE,CREATE_NEW_CONSOLE,nullptr,folder.c_str(),&startup,&child),"start isolated Far without plugins/macros");
CloseHandle(child.hThread);Sleep(1800);
require(AttachConsole(child.dwProcessId),"attach isolated console for test-only input and observation");
input=CreateFileW(L"CONIN$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,nullptr,OPEN_EXISTING,0,nullptr);
output=CreateFileW(L"CONOUT$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,nullptr,OPEN_EXISTING,0,nullptr);
require(input!=INVALID_HANDLE_VALUE && output!=INVALID_HANDLE_VALUE,"open isolated console buffers");
auto target=GetConsoleWindow();farWindow=target;ShowWindow(target,SW_SHOW);
require(ks::activateWindow(target),"activate only the isolated Far window");
auto key=[&](WORD vk,wchar_t c=0,DWORD mods=0) {
INPUT_RECORD records[2]{};
for(int i=0;i<2;++i) {
auto &r=records[i];r.EventType=KEY_EVENT;
r.Event.KeyEvent={BOOL(i==0),1,vk,WORD(MapVirtualKeyW(vk,MAPVK_VK_TO_VSC)),{c},mods};
}
DWORD written{};
if(!WriteConsoleInputW(input,records,2,&written)||written!=2)throw std::runtime_error("test input failed");
Sleep(60);
};
auto position=[&] {CONSOLE_SCREEN_BUFFER_INFO info{};if(!GetConsoleScreenBufferInfo(output,&info))throw std::runtime_error("console position unavailable");return info.dwCursorPosition;};
auto row=[&] {
CONSOLE_SCREEN_BUFFER_INFO info{};GetConsoleScreenBufferInfo(output,&info);
std::wstring text(info.dwSize.X,L' ');DWORD count{};
if(!ReadConsoleOutputCharacterW(output,text.data(),DWORD(text.size()),{0,info.dwCursorPosition.Y},&count))throw std::runtime_error("console row unavailable");
return text;
};
{
ks::App app(instance,false,folder/L"data");app.window=owner;
app.settings.history=false;app.settings.autoCorrect=false;app.settings.writeLog=true;
auto prepare=[&](wchar_t last=L'r') {
if(!ks::activateWindow(target))throw std::runtime_error("isolated Far lost foreground");
key(VK_ESCAPE);key('Y',0,LEFT_CTRL_PRESSED);key(VK_OEM_1,L':',SHIFT_PRESSED);key(WORD(towupper(last)),last);
app.run.reset();app.run.window=target;app.run.layout=ks::windowLayout(target);
app.run.at=GetTickCount64();app.run.keys=L":r";
};
auto expand=[&](const std::wstring &replacement) {
app.settings.snippets={{L":r",replacement}};
app.transform(0,0);ks::InputRegressionAccess::runQueued(app);Sleep(120);
};
for(int panels=0;panels<2;++panels) {
prepare();auto before=position();auto original=row();expand(L"rider .");
auto expected=original;expected.replace(before.X-2,2,L"rider .");expected.resize(original.size());
// Far's rightmost scroll indicator is redrawn rather than shifted.
require(row().substr(0,before.X+5)==expected.substr(0,before.X+5),"actual snippet replaces :r with rider .");
require(position().X==before.X+5,"successful expansion leaves cursor after replacement");
require(ks::readClipboard(owner).text()==sentinel.text(),"snippet restores original clipboard");
prepare(L'x');before=position();original=row();expand(L"rider .{enter}");
require(row()==original && position().X==before.X,"mismatched trigger restores original caret and never executes Enter");
prepare();before=position();original=row();expand(L"left{cursor}right");
require(row().substr(before.X-2,9)==L"leftright" && position().X==before.X+2,"cursor marker uses Far selection semantics");
prepare();before=position();original=row();expand(L"first\nsecond{enter}");
require(row()==original && position().X==before.X,"multiline console template rejected before changing text or caret");
prepare();before=position();original=row();expand(L"echo KSWITCHER_FAR_FIXTURE{enter}");
for(int wait=0;wait<30 && position().X!=before.X-2;++wait)Sleep(100);
require(position().X==before.X-2 && row().substr(before.X-2,2)==L" ",
"verified Enter runs only the harmless fixture echo and returns to an empty command line");
key('O',0,LEFT_CTRL_PRESSED);
}
app.window=nullptr;
}
log<<"Passed "<<checks<<" Far snippet integration checks\n";result=0;
}catch(const std::exception &e){log<<"FAIL "<<e.what()<<" win32="<<GetLastError()<<'\n';}
// A real user or clipboard synchronizer can copy during this explicit test.
// Never replace that newer content with the fixture's original snapshot.
const auto restoreSequence=GetClipboardSequenceNumber();
const bool restoreClipboard=GetClipboardOwner()==owner || (farWindow && ks::ownsClipboard(farWindow));
if(input!=INVALID_HANDLE_VALUE)CloseHandle(input);
if(output!=INVALID_HANDLE_VALUE)CloseHandle(output);
FreeConsole();
if(child.hProcess){TerminateProcess(child.hProcess,0);WaitForSingleObject(child.hProcess,5000);CloseHandle(child.hProcess);}
if(captured && restoreClipboard && GetClipboardSequenceNumber()==restoreSequence)
try{ks::writeClipboard(owner,saved);}catch(...){result=1;}
DestroyWindow(owner);
if(IsWindow(originalForeground))ks::activateWindow(originalForeground);
CoUninitialize();return result;
}