Files
hrynco-kswitcher/tests/settings_merge.cpp
T
2026-11-12 01:18:21 +02:00

138 lines
10 KiB
C++

#include "settings_merge.h"
#include "mouse_buttons.h"
#include <iostream>
using namespace ks;
int main(){try{
Settings base;base.autoCorrect=false;base.toggle=L"Ctrl+Alt+F11";
auto draft=base,current=base;draft.toggle=L"Ctrl+Alt+F12";current.autoCorrect=true;
auto merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current)),{});
if(merged.toggle!=draft.toggle||!merged.autoCorrect)throw std::runtime_error("independent changes lost");
current.toggle=L"Ctrl+Alt+F10";bool rejected=false;
try{mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current));}
catch(const std::exception &e){rejected=std::string(e.what()).starts_with("Settings conflict: ");}
if(!rejected)throw std::runtime_error("same-field conflict silently overwritten");
merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current),true),{});
if(merged.toggle!=draft.toggle||!merged.autoCorrect)throw std::runtime_error("confirmed edits discarded unrelated current changes");
merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(base),encodeSettings(current)),{});
if(merged.toggle!=current.toggle||!merged.autoCorrect)throw std::runtime_error("unchanged draft reverted current settings");
draft=base;current=base;draft.chatCycles[L"telegram\nfirst"]={L"win:04090409"};current.chatCycles[L"telegram\nsecond"]={L"win:04220422"};
merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current)),{});
if(merged.chatCycles.size()!=2)throw std::runtime_error("independent nested changes lost");
draft = base;
current = base;
draft.mouseBindings[L"chrome"][L"XButton1"] = {MouseActionType::Shortcut, {MOD_CONTROL, VK_TAB}};
current.mouseBindings[L"chrome"][L"XButton2"] = {MouseActionType::Shortcut, {MOD_CONTROL | MOD_SHIFT, VK_TAB}};
merged = decodeSettings(mergeConfigurationSettings(encodeSettings(base), encodeSettings(draft),
encodeSettings(current)), {});
if (merged.mouseBindings.at(L"chrome").size() != 2)
throw std::runtime_error("independent mouse bindings lost");
auto shared = sharedSyncSettings(merged);
if (shared == sharedSyncSettings(base) ||
restoreSyncSettings(shared, Settings{}, {}, false).mouseBindings != merged.mouseBindings)
throw std::runtime_error("mouse profiles must restore from a synchronized backup");
auto withButtons = merged;
draft = current = withButtons;
draft.mouseBindings.at(L"chrome").erase(L"XButton1");
current.mouseBindings.at(L"chrome")[L"XButton2"] = {MouseActionType::Shortcut, {MOD_CONTROL, VK_F4}};
merged = decodeSettings(mergeConfigurationSettings(encodeSettings(withButtons), encodeSettings(draft),
encodeSettings(current)), {});
const auto &buttons = merged.mouseBindings.at(L"chrome");
if (buttons.size() != 1 || buttons.at(L"XButton2") != MouseAction{MouseActionType::Shortcut, {MOD_CONTROL, VK_F4}})
throw std::runtime_error("mouse button removal lost during merge");
merged.mouseBindings[GlobalMouseProfile][L"Double middle"] = {MouseActionType::Mute};
merged.mouseBindings[L"chrome"][L"Double middle"] = {MouseActionType::Standard};
merged.mouseBindings[L"edge"][L"XButton1"] = {MouseActionType::Launch, {}, L"C:\\Tools\\app.exe"};
merged.mouseBindings[L"edge"][L"XButton2"] = {MouseActionType::Launch, {}, L"calc.exe"};
merged.mouseBindings[GlobalMouseProfile][L"Left"] = {MouseActionType::Shortcut, {MOD_CONTROL, 'W'}, {}, {L"chrome", L"msedge"}};
merged.mouseBindings[GlobalMouseProfile][L"Middle"] = {MouseActionType::Back, {}, {}, {L"chrome"}};
auto restored = decodeSettings(encodeSettings(merged), {});
if (restored.mouseBindings != merged.mouseBindings ||
effectiveMouseAction(restored, L"chrome", 7) ||
!effectiveMouseAction(restored, L"edge", 7) ||
effectiveMouseAction(restored, L"edge", 7)->type != MouseActionType::Mute)
throw std::runtime_error("mouse action override or inheritance failed");
if (!effectiveMouseAction(restored, L"chrome", 0) || !effectiveMouseAction(restored, L"msedge", 0) ||
effectiveMouseAction(restored, L"explorer", 0) || !effectiveMouseAction(restored, L"chrome", 2) ||
effectiveMouseAction(restored, L"msedge", 2) || !effectiveMouseAction(restored, L"explorer", 7))
throw std::runtime_error("application restrictions or unrestricted fallback failed");
draft = current = restored;
draft.mouseBindings[GlobalMouseProfile][L"Middle"].applications.clear();
current.mouseBindings[GlobalMouseProfile][L"Middle"].type = MouseActionType::Mute;
const auto competing = mergeSyncProfile(encodeSettings(restored), encodeSettings(draft), encodeSettings(current));
if (competing.conflicts.size() != 1)
throw std::runtime_error("competing edits to a mouse action and its scope must conflict");
Settings profiles;
profiles.mouseBindings[L"name:Browsing"][L"XButton1"] = {MouseActionType::Forward};
profiles.mouseBindings[L"name:Browsing"][L"XButton2"] = {MouseActionType::Back};
profiles.mouseProfileApplications[L"name:Browsing"] = {L"chrome", L"msedge"};
profiles.mouseBindings[L"name:Empty"];
profiles.mouseBindings[GlobalMouseProfile][L"XButton1"] = {MouseActionType::VolumeUp};
profiles.mouseBindings[GlobalMouseProfile][L"XButton2"] = {MouseActionType::VolumeDown};
profiles.mouseBindings[GlobalMouseProfile][L"XButton2"].volumeStep = 7;
auto resolve = [&](const wchar_t *app, int event, bool foreground = true)
{
const auto *action = effectiveMouseAction(profiles, app, event, foreground);
return action ? action->type : MouseActionType::Standard;
};
profiles = decodeSettings(mergeConfigurationSettings(encodeSettings(base), encodeSettings(profiles), encodeSettings(base)), {});
if (profiles.mouseBindings.at(GlobalMouseProfile).at(L"XButton1").volumeStep != DefaultMouseVolumeStep ||
profiles.mouseBindings.at(GlobalMouseProfile).at(L"XButton2").volumeStep != 7)
throw std::runtime_error("default and custom volume steps must round-trip independently");
auto invalidVolume = profiles;
invalidVolume.mouseBindings[GlobalMouseProfile][L"XButton1"].volumeStep = 0;
rejected = false;
try { validateSettings(invalidVolume); }
catch (const std::runtime_error &) { rejected = true; }
if (!rejected) throw std::runtime_error("invalid volume steps must be rejected");
if (!profiles.mouseBindings.contains(L"name:Empty") ||
resolve(L"chrome", 3) != MouseActionType::Forward || resolve(L"msedge", 4) != MouseActionType::Back ||
resolve(L"chrome", 3, false) != MouseActionType::VolumeUp ||
resolve(L"explorer", 4) != MouseActionType::VolumeDown)
throw std::runtime_error("named profiles must persist and override fallback only for the matching foreground app");
profiles.mouseBindings[L"name:Alternative"][L"XButton1"] = {MouseActionType::Back};
profiles.mouseProfileApplications[L"name:Alternative"] = {L"chrome"};
profiles.disabledMouseProfiles.insert(L"name:Browsing");
profiles = decodeSettings(encodeSettings(profiles), {});
if (resolve(L"chrome", 3) != MouseActionType::Back)
throw std::runtime_error("disabled profiles must not resolve actions");
profiles.disabledMouseProfiles.clear();
rejected = false;
try { validateSettings(profiles); }
catch (const std::runtime_error &e) { rejected = std::string(e.what()).starts_with("Mouse action conflict:"); }
if (!rejected) throw std::runtime_error("overlapping active profiles must be rejected");
Settings wheel;
const int down = mouseWheelEvent(WM_MOUSEWHEEL, -WHEEL_DELTA);
RAWMOUSE raw{};
raw.usButtonFlags = RI_MOUSE_WHEEL;
raw.usButtonData = static_cast<USHORT>(-WHEEL_DELTA);
if (rawMouseSignal(raw) != down || mouseEventEnabled(wheel, down))
throw std::runtime_error("wheel capture must match the runtime signal and be opt-in");
raw.usButtonFlags = RI_MOUSE_HWHEEL;
if (rawMouseSignal(raw) != mouseWheelEvent(WM_MOUSEHWHEEL, -WHEEL_DELTA))
throw std::runtime_error("horizontal wheel capture must match the runtime signal");
int remainder{};
if (mouseWheelSteps(remainder, -30) != 0 || mouseWheelSteps(remainder, -90) != 1 ||
mouseWheelSteps(remainder, 240) != 2 || remainder != 0)
throw std::runtime_error("high-resolution wheel deltas must accumulate by detent");
wheel.customMouseEvents.emplace(MouseEvents[down], L"Lower volume");
wheel.mouseBindings[GlobalMouseProfile][MouseEvents[down]] = {MouseActionType::VolumeDown};
wheel.mouseBindings[L"name:Browser"][L"XButton1"] = {MouseActionType::Back};
wheel.mouseProfileApplications[L"name:Browser"] = {L"chrome"};
wheel.disabledMouseProfiles.insert(L"name:Browser");
wheel.appLanguageSets[L"Browser"] = {{L"win:00000409"}, {L"chrome"}};
const auto restoredWheel = decodeSettings(encodeSettings(wheel), {});
const auto syncedWheel = restoreSyncSettings(sharedSyncSettings(wheel), Settings{}, {});
if (mouseEventName(restoredWheel, down) != L"Lower volume" ||
!effectiveMouseAction(restoredWheel, L"explorer", down) ||
sharedSyncSettings(syncedWheel) != sharedSyncSettings(wheel))
throw std::runtime_error("custom wheel event and action must restore from a synchronized backup");
if (!restoreSyncSettings(sharedSyncSettings(Settings{}), wheel, {}).customMouseEvents.empty())
throw std::runtime_error("an explicit empty backup must clear custom mouse events");
wheel.customMouseEvents.clear();
rejected = false;
try { validateSettings(wheel); }
catch (const std::runtime_error &) { rejected = true; }
if (!rejected) throw std::runtime_error("unregistered mouse signals cannot have bindings");
std::cout<<"Settings-save merge checks passed\n";
}catch(const std::exception &e){std::cerr<<e.what()<<'\n';return 1;}}