Release v0.6.28: typo correction, partial sync and selection diagnostics
This commit is contained in:
+48
-5
@@ -599,6 +599,9 @@ int main(int argc, char **argv) {
|
||||
require(!app.handleKeyboard(f12,true,MOD_CONTROL|MOD_SHIFT),"exact modifiers only");
|
||||
app.resetInput();
|
||||
app.handleKeyboard(event('A',0x1e),true,0);
|
||||
if (app.run.keys!=L"a" && app.run.keys!=L"A")
|
||||
std::cerr << "Input fixture foreground=" << (uintptr_t)GetForegroundWindow()
|
||||
<< " recorded=" << ks::utf8(app.run.keys) << '\n';
|
||||
require(app.run.keys==L"a"||app.run.keys==L"A","external scan recorded with current CapsLock");
|
||||
auto keys=app.run.keys; auto version=app.inputVersion.load();
|
||||
for(WORD key:{WORD('V'),WORD(VK_SHIFT),WORD(VK_LEFT),WORD(VK_F15),WORD(0xE8)})
|
||||
@@ -656,6 +659,23 @@ int main(int argc, char **argv) {
|
||||
require((GetWindowLongPtrW(GetDlgItem(syncWindow,604),GWL_STYLE)&ES_PASSWORD)!=0,
|
||||
"sync password is masked");
|
||||
require(!IsWindowEnabled(app.settingsWindow),"sync dialog owns settings interaction");
|
||||
for(int id:{602,603,604,605,606,607,610,611,612,613,IDOK})
|
||||
require(!IsWindowEnabled(GetDlgItem(syncWindow,id)),"disabled synchronization disables dependent controls");
|
||||
require(IsWindowEnabled(GetDlgItem(syncWindow,601)) && IsWindowEnabled(GetDlgItem(syncWindow,609)) &&
|
||||
IsWindowEnabled(GetDlgItem(syncWindow,IDCANCEL)),"enable, forget password and close remain available");
|
||||
SendMessageW(syncWindow,WM_COMMAND,612,0);
|
||||
require(!FindWindowW(L"KSwitcher.SyncConflicts",nullptr),"disabled conflict action is ignored");
|
||||
SendDlgItemMessageW(syncWindow,601,BM_SETCHECK,BST_CHECKED,0);
|
||||
SendMessageW(syncWindow,WM_COMMAND,MAKEWPARAM(601,BN_CLICKED),0);
|
||||
for(int id:{602,603,604,605,606,607,610,611,613,IDOK})
|
||||
require(IsWindowEnabled(GetDlgItem(syncWindow,id)),"enabling synchronization immediately enables connection controls");
|
||||
require(!ks::loadSyncConfig(app.data).enabled,"checking enable alone does not connect or persist");
|
||||
auto enabledSync=ks::loadSyncConfig(app.data);
|
||||
enabledSync.enabled=true;
|
||||
enabledSync.server=L"https://sync.example.test";
|
||||
enabledSync.username=L"test";
|
||||
ks::saveSyncConfig(app.data,enabledSync);
|
||||
SendMessageW(syncWindow,WM_COMMAND,MAKEWPARAM(601,BN_CLICKED),0);
|
||||
ks::InputRegressionAccess::sampleConflicts(app);
|
||||
SendMessageW(syncWindow,WM_COMMAND,612,0);
|
||||
auto conflictsWindow=FindWindowW(L"KSwitcher.SyncConflicts",nullptr);
|
||||
@@ -678,7 +698,7 @@ int main(int argc, char **argv) {
|
||||
require(!IsWindowEnabled(syncWindow),"conflict dialog owns sync interaction");
|
||||
SendMessageW(conflictsWindow,WM_COMMAND,IDCANCEL,0);
|
||||
require(IsWindowEnabled(syncWindow),"closing conflict dialog restores sync window");
|
||||
for(int id:{601,602,603,604,605,606,607,608,609,610,611,612,IDOK,IDCANCEL}) {
|
||||
for(int id:{601,602,603,604,605,606,607,608,609,610,611,612,613,IDOK,IDCANCEL}) {
|
||||
RECT control{},client{};
|
||||
GetWindowRect(GetDlgItem(syncWindow,id),&control);
|
||||
MapWindowPoints(nullptr,syncWindow,reinterpret_cast<POINT*>(&control),2);
|
||||
@@ -686,8 +706,12 @@ int main(int argc, char **argv) {
|
||||
require(control.left>=0&&control.top>=0&&control.right<=client.right&&control.bottom<=client.bottom,
|
||||
"sync controls fit dialog");
|
||||
}
|
||||
SendDlgItemMessageW(syncWindow,601,BM_SETCHECK,BST_UNCHECKED,0);
|
||||
SendMessageW(syncWindow,WM_COMMAND,MAKEWPARAM(601,BN_CLICKED),0);
|
||||
require(IsWindowEnabled(GetDlgItem(syncWindow,IDOK)),"Save remains available to persist disabling sync");
|
||||
SendMessageW(syncWindow,WM_COMMAND,IDOK,0);
|
||||
require(!ks::loadSyncConfig(app.data).enabled,"saving disabled sync does not enable network access");
|
||||
require(!IsWindowEnabled(GetDlgItem(syncWindow,IDOK)),"Save disables after disabled state is persisted");
|
||||
SendMessageW(syncWindow,WM_COMMAND,IDCANCEL,0);
|
||||
require(IsWindowEnabled(app.settingsWindow),"closing sync restores parent");
|
||||
require(ks::InputRegressionAccess::staleSync(app,false),"in-flight sync cannot overwrite newer local edits");
|
||||
@@ -704,21 +728,31 @@ int main(int argc, char **argv) {
|
||||
GetWindowRect(autoOptions,&original);
|
||||
// Use a client size above the scrolling baseline at every DPI.
|
||||
UINT dpi=GetDpiForWindow(autoOptions);
|
||||
SetWindowPos(autoOptions,nullptr,0,0,MulDiv(800,dpi,96),MulDiv(900,dpi,96),
|
||||
SetWindowPos(autoOptions,nullptr,0,0,MulDiv(800,dpi,96),MulDiv(1100,dpi,96),
|
||||
SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||
RECT baseline{}; GetWindowRect(autoOptions,&baseline);
|
||||
RECT clientBefore{}; GetClientRect(autoOptions,&clientBefore);
|
||||
for(int i=0;i<3;++i) GetWindowRect(GetDlgItem(autoOptions,411+i),&before[i]);
|
||||
GetWindowRect(GetDlgItem(autoOptions,IDOK),&buttonBefore);
|
||||
SetWindowPos(autoOptions,nullptr,0,0,baseline.right-baseline.left+240,
|
||||
baseline.bottom-baseline.top+120,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||
RECT clientAfter{}; GetClientRect(autoOptions,&clientAfter);
|
||||
// Windows can constrain oversized windows to the current desktop.
|
||||
// Check layout against the actual available client space.
|
||||
int extraBeforeX=std::max(0,int(clientBefore.right)-MulDiv(560,dpi,96));
|
||||
int extraAfterX=std::max(0,int(clientAfter.right)-MulDiv(560,dpi,96));
|
||||
int extraY=std::max(0,int(clientAfter.bottom)-MulDiv(880,dpi,96))-
|
||||
std::max(0,int(clientBefore.bottom)-MulDiv(880,dpi,96));
|
||||
for(int i=0;i<3;++i) {
|
||||
GetWindowRect(GetDlgItem(autoOptions,411+i),&after[i]);
|
||||
require(after[i].right-after[i].left==before[i].right-before[i].left+80 &&
|
||||
after[i].bottom-after[i].top==before[i].bottom-before[i].top+120,
|
||||
int extraX=(extraAfterX*(i+1)/3-extraAfterX*i/3)-
|
||||
(extraBeforeX*(i+1)/3-extraBeforeX*i/3);
|
||||
require(after[i].right-after[i].left==before[i].right-before[i].left+extraX &&
|
||||
after[i].bottom-after[i].top==before[i].bottom-before[i].top+extraY,
|
||||
"dictionary columns share extra width and receive extra height");
|
||||
}
|
||||
GetWindowRect(GetDlgItem(autoOptions,IDOK),&buttonAfter);
|
||||
require(buttonAfter.left-buttonBefore.left==240 && buttonAfter.top-buttonBefore.top==120,
|
||||
require(buttonAfter.left-buttonBefore.left==extraAfterX-extraBeforeX && buttonAfter.top-buttonBefore.top==extraY,
|
||||
"auto-correction buttons follow bottom-right corner");
|
||||
require(after[0].right<after[1].left && after[1].right<after[2].left &&
|
||||
after[2].bottom<buttonAfter.top,
|
||||
@@ -735,6 +769,9 @@ int main(int argc, char **argv) {
|
||||
"personal dictionary field fits options window");
|
||||
}
|
||||
SetDlgItemTextW(autoOptions,412,L"Релізик\r\nрелізик, словечко");
|
||||
SetDlgItemTextW(autoOptions,416,L" Првиіт = Привіт\r\nhelo=hello");
|
||||
SendDlgItemMessageW(autoOptions,414,BM_SETCHECK,BST_CHECKED,0);
|
||||
SendDlgItemMessageW(autoOptions,415,BM_SETCHECK,BST_CHECKED,0);
|
||||
SendMessageW(autoOptions,WM_COMMAND,IDOK,0);
|
||||
require(!IsWindow(autoOptions),"personal words accepted without file editing");
|
||||
SendMessageW(app.settingsWindow,WM_COMMAND,255,0);
|
||||
@@ -744,6 +781,12 @@ int main(int argc, char **argv) {
|
||||
GetDlgItemTextW(autoOptions,412,personalWords,128);
|
||||
require(std::wstring(personalWords)==L"релізик, словечко",
|
||||
"personal dictionary normalizes case, newlines and duplicate words");
|
||||
wchar_t pairs[128]{};
|
||||
GetDlgItemTextW(autoOptions,416,pairs,128);
|
||||
require(std::wstring(pairs)==L"helo=hello\r\nпрвиіт=привіт\r\n" &&
|
||||
SendDlgItemMessageW(autoOptions,414,BM_GETCHECK,0,0)==BST_CHECKED &&
|
||||
SendDlgItemMessageW(autoOptions,415,BM_GETCHECK,0,0)==BST_CHECKED,
|
||||
"typo options and normalized replacement pairs survive reopening");
|
||||
SendMessageW(autoOptions,WM_COMMAND,IDCANCEL,0);
|
||||
if (!app.history) app.history=std::make_unique<ks::History>(temp/L"history-layout");
|
||||
ks::showHistory(app,false);
|
||||
|
||||
+80
-1
@@ -23,6 +23,7 @@ HWND host{}, field{};
|
||||
ks::Clip saved;
|
||||
bool captured = false, failed = false;
|
||||
bool conversionOnly = false;
|
||||
bool typosOnly = false;
|
||||
int pasteBehavior = 0;
|
||||
int enterPresses = 0;
|
||||
int copyFailuresRemaining = 0, copyRequests = 0;
|
||||
@@ -427,8 +428,30 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
case 47:
|
||||
require(text() == L"OLDsnipping ", "recent Cyrillic suffix recovery preserves older prefix");
|
||||
require(droppedSelectionKeys == 0, "recent suffix regression dropped four selection keys");
|
||||
app->settings.writeLog = true;
|
||||
prepare(L"prefix wrong ", false);
|
||||
record(L"other ");
|
||||
app->transform(ks::Direct + ru);
|
||||
break;
|
||||
case 48:
|
||||
{
|
||||
auto rows = ks::parseOperationLog(ks::wide(diagnosticLog()));
|
||||
require(text() == L"prefix wrong " && !rows.empty() &&
|
||||
rows.back().outcome == L"recent-verification-mismatch",
|
||||
"diagnostic mismatch does not replace unverified text");
|
||||
auto details = rows.back().details;
|
||||
require(details.find(L"selection-mismatch") != details.npos &&
|
||||
details.find(L"Expected selection:\r\nother ") != details.npos &&
|
||||
details.find(L"Read back:\r\nwrong ") != details.npos,
|
||||
"failed verification includes expected and copied text in exported details");
|
||||
require(details.find(L"Copy begin number=") != details.npos &&
|
||||
details.find(L"Copy result number=") != details.npos &&
|
||||
details.find(L"Selection initial match=0") != details.npos &&
|
||||
details.find(L"SelectPrevious") != details.npos,
|
||||
"incident includes clipboard and selection stages");
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
case 30:
|
||||
case 32:
|
||||
case 34:
|
||||
@@ -597,6 +620,61 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
case 22:
|
||||
require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma");
|
||||
app->settings.autoReplace = true;
|
||||
app->settings.autoReplacements = {{L"helo", L"hello"}};
|
||||
ActivateKeyboardLayout(app->layouts[en].handle, 0);
|
||||
prepare(L"(Helo)! ", false);
|
||||
record(L"(Helo)! ");
|
||||
app->transform(ks::AutoCorrect);
|
||||
break;
|
||||
case 23:
|
||||
require(text() == L"(Hello)! ", "explicit typo pair replaces verified text with punctuation and capital");
|
||||
app->transform(ks::Undo);
|
||||
break;
|
||||
case 24:
|
||||
require(text() == L"(Helo)! ", "Undo restores explicit typo replacement");
|
||||
app->settings.autoReplace = false;
|
||||
app->settings.autoTypos = true;
|
||||
app->settings.autoWords[L"en"].push_back(L"kswitcher");
|
||||
// Use a personal target to avoid bundled dictionary ambiguity.
|
||||
prepare(L"kswithcer ", false);
|
||||
record(L"kswithcer ");
|
||||
app->transform(ks::AutoCorrect);
|
||||
break;
|
||||
case 25:
|
||||
require(text() == L"kswitcher ", "dictionary typo correction replaces text in the current layout");
|
||||
require(ks::readClipboard(app->window).text() == sentinel, "typo correction restores clipboard");
|
||||
app->transform(ks::Undo);
|
||||
break;
|
||||
case 26:
|
||||
require(text() == L"kswithcer ", "Undo restores dictionary typo correction");
|
||||
record(L"kswithcer ");
|
||||
app->transform(ks::AutoCorrect);
|
||||
break;
|
||||
case 27:
|
||||
require(text() == L"kswithcer ", "Undo suppresses the rejected spelling correction");
|
||||
app->settings.autoReplace = true;
|
||||
app->settings.autoReplacements = {{L"tehh", L"the"}};
|
||||
app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages);
|
||||
prepare(L"", false);
|
||||
{
|
||||
std::vector<INPUT> inputs;
|
||||
for (WORD key : std::wstring(L"TEHH X"))
|
||||
for (DWORD flags : {DWORD(0), DWORD(KEYEVENTF_KEYUP)})
|
||||
{
|
||||
auto input = ks::keyboardInput(key == L' ' ? VK_SPACE : key, flags);
|
||||
input.ki.dwExtraInfo = 123;
|
||||
inputs.push_back(input);
|
||||
}
|
||||
require(SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(),
|
||||
"custom typo and following word submitted without a pause");
|
||||
}
|
||||
break;
|
||||
case 28:
|
||||
require(text() == L"the x", "custom replacement through keyboard hook preserves subsequent typing");
|
||||
app->settings.autoReplace = false;
|
||||
app->settings.autoTypos = false;
|
||||
if (typosOnly) { finish(); break; }
|
||||
stage = 30;
|
||||
break;
|
||||
}
|
||||
@@ -621,6 +699,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
conversionOnly = argc > 1 && std::string(argv[1]) == "--conversion-only";
|
||||
typosOnly = argc > 1 && std::string(argv[1]) == "--typos-only";
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
auto root = std::filesystem::temp_directory_path() /
|
||||
(L"KSwitcher-integration-" + std::to_wstring(GetCurrentProcessId()));
|
||||
@@ -776,7 +855,7 @@ int main(int argc, char **argv)
|
||||
20, 20, 590, 230, host, nullptr, cls.hInstance, nullptr);
|
||||
deadline = GetTickCount64() + 30000;
|
||||
SetWindowSubclass(field, observeEnter, 1, 0);
|
||||
if (argc > 1 && std::string(argv[1]) == "--typing-only")
|
||||
if (typosOnly || (argc > 1 && std::string(argv[1]) == "--typing-only"))
|
||||
stage = 20;
|
||||
if (argc > 1 && std::string(argv[1]) == "--snippets-only")
|
||||
stage = 30;
|
||||
|
||||
+79
-1
@@ -54,6 +54,10 @@ int main()
|
||||
rejects([&] { validateSyncConfig(invalid); }, "reject unsupported provider");
|
||||
saveSyncConfig(root, config);
|
||||
require(syncVisible(root) && loadSyncConfig(root) == config, "connection round trip");
|
||||
auto partialConfig = config; partialConfig.keepLocalLayouts = true;
|
||||
saveSyncConfig(root, partialConfig);
|
||||
require(loadSyncConfig(root).keepLocalLayouts, "partial sync mode survives restart");
|
||||
saveSyncConfig(root, config);
|
||||
auto rawConfig = parse(readFile(root / L"sync.json", 16384));
|
||||
require(!rawConfig.contains("password") && !rawConfig.contains("token"), "no secrets in config");
|
||||
Settings local;
|
||||
@@ -84,9 +88,49 @@ int main()
|
||||
left = right = j; left["snippets"].push_back({{"trigger", "aaa"}, {"text", "one"}});
|
||||
right["snippets"].push_back({{"trigger", "bbb"}, {"text", "two"}});
|
||||
require(parse(mergeSyncSettings(base, bytes(left), bytes(right)))["snippets"].size() == 3, "independent snippet additions merge");
|
||||
rejects([&] { mergeSyncSettings({}, bytes(left), base); }, "first sync needs explicit choice");
|
||||
require(parse(mergeSyncSettings({}, bytes(left), base))["snippets"].size() == 2,
|
||||
"first sync merges independent additions without choosing a whole profile");
|
||||
require(parse(mergeSyncSettings({}, base, base)) == j, "identical first sync safe");
|
||||
{
|
||||
auto l = j, r = j;
|
||||
l["autoCorrectMinLength"] = 4; r["autoCorrectMinLength"] = 5;
|
||||
l["autoCorrectNever"].push_back("localword");
|
||||
r["autoCorrectNever"].push_back("remoteword");
|
||||
auto plan = mergeSyncProfile({}, bytes(l), bytes(r));
|
||||
require(plan.conflicts.size() == 1 && parse(plan.local)["autoCorrectNever"].size() == 2 &&
|
||||
parse(plan.remote)["autoCorrectNever"].size() == 2,
|
||||
"first sync merges lists while preserving scalar conflicts");
|
||||
require(parse(plan.local)["autoCorrectMinLength"] == 4 && parse(plan.remote)["autoCorrectMinLength"] == 5,
|
||||
"first sync never guesses between different scalar values");
|
||||
auto again = mergeSyncProfile(plan.base, plan.local, plan.remote);
|
||||
require(again.conflicts.size() == 1, "first-sync conflicts survive saving the ancestor");
|
||||
auto resolved = mergeSyncProfile(plan.base, plan.local, plan.remote, {{plan.conflicts[0], false}});
|
||||
require(resolved.conflicts.empty() && parse(resolved.local)["autoCorrectMinLength"] == 5,
|
||||
"first-sync conflict can be resolved independently");
|
||||
}
|
||||
require(SyncIntervalMs == 600000, "background interval is ten minutes");
|
||||
{
|
||||
auto l = j, r = j;
|
||||
l["autoCorrectReplacements"]["helo"] = "hello";
|
||||
r["autoCorrectReplacements"]["wrold"] = "world";
|
||||
auto paired = mergeSyncSettings(base, bytes(l), bytes(r));
|
||||
require(parse(paired)["autoCorrectReplacements"].size() == 2, "independent replacement pairs merge");
|
||||
l = r = parse(paired);
|
||||
l["autoCorrectReplacements"].erase("helo");
|
||||
r["autoCorrectReplacements"]["wrold"] = "worlds";
|
||||
auto removed = mergeSyncSettings(paired, bytes(l), bytes(r));
|
||||
auto restored = restoreSyncSettings(removed, local, {});
|
||||
require(restored.autoReplacements.size() == 1 && restored.autoReplacements.at(L"wrold") == L"worlds",
|
||||
"replacement deletion merges with an independent edit and remains loadable");
|
||||
r["autoCorrectReplacements"]["helo"] = "help";
|
||||
require(mergeSyncProfile(paired, bytes(l), bytes(r)).conflicts.size() == 1,
|
||||
"replacement deletion versus edit is a conflict");
|
||||
auto legacy = j;
|
||||
for (auto key : {"autoCorrectReplace", "autoCorrectTypos", "autoCorrectReplacements"}) legacy.erase(key);
|
||||
auto migrated = restoreSyncSettings(bytes(legacy), local, {});
|
||||
require(!migrated.autoReplace && !migrated.autoTypos && migrated.autoReplacements.empty(),
|
||||
"older synchronized profiles default new typo modes off");
|
||||
}
|
||||
{
|
||||
auto l = j, r = j;
|
||||
l["autoCorrectMinLength"] = 4; r["autoCorrectMinLength"] = 5;
|
||||
@@ -173,6 +217,40 @@ int main()
|
||||
rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload); }, "racing writer protected");
|
||||
require(provider.writes == 1 && provider.remote.settings == base, "CAS failure preserves remote");
|
||||
provider.race = false;
|
||||
{
|
||||
auto remoteProfile = j;
|
||||
remoteProfile["layouts"] = Json::array({"win:00000422"});
|
||||
remoteProfile["layoutHotkeys"] = Json::array({{{"layoutId", "win:00000422"}, {"hotkey", "Ctrl+Alt+9"}}});
|
||||
remoteProfile["autoCorrectNever"].push_back("remoteword");
|
||||
FakeProvider partial;
|
||||
partial.remote = {3, bytes(remoteProfile)};
|
||||
auto validate = [&](const Bytes &b) { restoreSyncSettings(b, local, {}, true); };
|
||||
auto result = synchronize(partial, {}, base, SyncAction::Automatic, validate, {}, true);
|
||||
auto restored = restoreSyncSettings(result.settings, local, {}, true);
|
||||
require(restored.cycle == local.cycle && restored.direct == local.direct &&
|
||||
restored.autoNever == std::vector<std::wstring>{L"remoteword"},
|
||||
"partial sync restores other settings and keeps local layout configuration");
|
||||
auto next = sharedSyncSettings(restored);
|
||||
result = synchronize(partial, result.base, next, SyncAction::Automatic, validate, {}, true);
|
||||
require(parse(partial.remote.settings)["layouts"] == remoteProfile["layouts"] &&
|
||||
parse(partial.remote.settings)["layoutHotkeys"] == remoteProfile["layoutHotkeys"],
|
||||
"repeated partial sync never deletes unavailable server layouts or shortcuts");
|
||||
auto changed = parse(next); changed["autoCorrectMinLength"] = 7;
|
||||
result = synchronize(partial, result.base, bytes(changed), SyncAction::Upload, validate, {}, true);
|
||||
require(parse(partial.remote.settings)["autoCorrectMinLength"] == 7 &&
|
||||
parse(partial.remote.settings)["layoutHotkeys"] == remoteProfile["layoutHotkeys"],
|
||||
"explicit partial upload preserves server layouts too");
|
||||
auto downloaded = synchronize(partial, result.base, base, SyncAction::Download, validate, {}, true);
|
||||
auto downloadedSettings = restoreSyncSettings(downloaded.settings, local, {}, true);
|
||||
require(downloadedSettings.autoMinLength == 7 && downloadedSettings.cycle == local.cycle &&
|
||||
downloadedSettings.direct == local.direct, "partial Download applies other settings without replacing local layouts");
|
||||
try { restoreSyncSettings(bytes(remoteProfile), local, {}); require(false, "full sync must identify missing layouts"); }
|
||||
catch (const std::runtime_error &e) {
|
||||
std::string message = e.what();
|
||||
require(message.find("win:00000422") != message.npos && message.find("Keep this PC's layouts") != message.npos,
|
||||
"missing-layout error identifies IDs and offers partial sync");
|
||||
}
|
||||
}
|
||||
rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload,
|
||||
[](const Bytes &) { throw std::runtime_error("invalid"); }); }, "validate before upload");
|
||||
require(provider.writes == 1, "invalid data not uploaded");
|
||||
|
||||
+58
-2
@@ -111,8 +111,8 @@ int main(int argc, char **argv)
|
||||
require(diagnostics.record(L"later-failure", true, 30500).find(L"expired-context") == std::wstring::npos,
|
||||
"context expires after thirty seconds");
|
||||
diagnostics.record(L"oldest-context", false, 30600);
|
||||
for (int i = 0; i < 40; ++i)
|
||||
diagnostics.record(L"bounded-context", false, 30600 + i);
|
||||
for (int i = 0; i < 128; ++i)
|
||||
diagnostics.record(L"bounded-context", false, 30600);
|
||||
require(diagnostics.record(L"bounded-failure", true, 30700).find(L"oldest-context") == std::wstring::npos,
|
||||
"context buffer has a fixed size");
|
||||
diagnostics.record(L"disabled-context", false, 30800);
|
||||
@@ -217,6 +217,13 @@ int main(int argc, char **argv)
|
||||
require(!autoSettings.autoCorrect, "auto-correction defaults off");
|
||||
autoSettings.cycle = {L"en", L"uk"};
|
||||
corrector.configure(L"dictionaries", autoSettings.autoLanguages);
|
||||
{
|
||||
auto options = autoSettings;
|
||||
options.autoTypos = true;
|
||||
auto typo = corrector.suggest(L"првиіт ", 2, layouts, options);
|
||||
require(typo && typo->text == L"привіт " && typo->target == 2,
|
||||
"bundled Ukrainian dictionary corrects a common transposition");
|
||||
}
|
||||
auto suggested = corrector.suggest(L"ghbdsn ", 0, layouts, autoSettings);
|
||||
require(suggested && suggested->text == L"привіт " && suggested->target == 2,
|
||||
"dictionary corrects Ukrainian and retains trailing space");
|
||||
@@ -497,6 +504,45 @@ int main(int argc, char **argv)
|
||||
auto correction = edited.suggest(L"руддщ ", 2, layouts, options);
|
||||
require(correction && correction->text == L"hello ",
|
||||
"unsorted dictionaries and duplicates load using lowest duplicate rank");
|
||||
require(!edited.suggest(L"hlelo ", 0, layouts, options), "spelling correction defaults off");
|
||||
options.autoTypos = true;
|
||||
for (auto typo : {L"hlelo ", L"helo ", L"helllo ", L"hezlo "})
|
||||
{
|
||||
correction = edited.suggest(typo, 0, layouts, options);
|
||||
require(correction && correction->text == L"hello " && correction->target == 0,
|
||||
"single transposition, deletion, insertion and substitution retain the source layout");
|
||||
}
|
||||
correction = edited.suggest(L"(Hlelo), ", 0, layouts, options);
|
||||
require(correction && correction->text == L"(Hello), ", "typos retain case and punctuation");
|
||||
for (auto text : {L"hello ", L"HLELO ", L"hLelo ", L"h3llo ", L"h.llo ", L"hlo ", L"hello@example.com "})
|
||||
require(!edited.suggest(text, 0, layouts, options), "known or unsafe typo input is unchanged");
|
||||
options.autoWords[L"en"] = {L"hella"};
|
||||
require(!edited.suggest(L"hell ", 0, layouts, options), "personal dictionary alternatives create ambiguity");
|
||||
options.autoWords.clear();
|
||||
options.autoReplacements = {{L"hlelo", L"greeting"}, {L"hello", L"greetings"}, {L"првиіт", L"привіт"}};
|
||||
options.autoReplace = true;
|
||||
correction = edited.suggest(L"Hlelo! ", 0, layouts, options);
|
||||
require(correction && correction->text == L"Greeting! ", "explicit replacements override spelling guesses");
|
||||
correction = edited.suggest(L"hello ", 0, layouts, options);
|
||||
require(correction && correction->text == L"greetings ", "explicit replacements can override known words");
|
||||
correction = edited.suggest(L"Првиіт ", 2, layouts, options);
|
||||
require(correction && correction->text == L"Привіт " && correction->target == 2,
|
||||
"Ukrainian replacements work without dictionary membership");
|
||||
options.autoNever = {L"hlelo"};
|
||||
require(!edited.suggest(L"hlelo ", 0, layouts, options), "never-correct overrides explicit pairs");
|
||||
options.autoNever.clear();
|
||||
edited.reject(L"(Hlelo)! ", options);
|
||||
require(!edited.suggest(L"hlelo ", 0, layouts, options), "undo rejection suppresses custom replacements");
|
||||
options.autoReplace = false;
|
||||
options.autoWords[L"uk"] = {L"привіт"};
|
||||
correction = edited.suggest(L"првиіт ", 2, layouts, options);
|
||||
require(correction && correction->text == L"привіт ", "Ukrainian personal words are typo targets");
|
||||
options.cycle = {L"en"};
|
||||
require(!edited.suggest(L"првиіт ", 2, layouts, options), "disabled source layouts do not correct typos");
|
||||
put(L"en.txt", L"hello\t1\nhella\t99999\n");
|
||||
ks::AutoCorrector ambiguous;
|
||||
ambiguous.configure(folder, {L"en", L"uk"});
|
||||
require(!ambiguous.suggest(L"hell ", 0, layouts, options), "frequency filter does not conceal ambiguous typo targets");
|
||||
}
|
||||
ks::Pixels pixels{2, 1, {10, 20, 30, 255, 40, 50, 60, 255}};
|
||||
ks::Clip firstImage{{ks::imageDib(pixels), {0, L"Ole Private Data", {1, 2, 3}}}};
|
||||
@@ -542,6 +588,8 @@ int main(int argc, char **argv)
|
||||
settings.writeLog = false;
|
||||
settings.elevatedAutostart = true;
|
||||
settings.autoCorrect = true;
|
||||
settings.autoReplace = settings.autoTypos = true;
|
||||
settings.autoReplacements = {{L"првиіт", L"привіт"}, {L"helo", L"hello"}};
|
||||
settings.autoMaxRank = 10000;
|
||||
settings.autoMinLength = 5;
|
||||
settings.autoPrefixChars = L"(#";
|
||||
@@ -554,6 +602,14 @@ int main(int argc, char **argv)
|
||||
ks::saveSettings(temp / L"settings.json", settings);
|
||||
auto restored = ks::loadSettings(temp / L"settings.json", {});
|
||||
require(restored.autoWords == settings.autoWords, "personal dictionaries survive settings roundtrip");
|
||||
require(restored.autoReplace && restored.autoTypos && restored.autoReplacements == settings.autoReplacements,
|
||||
"typo options and Unicode pairs survive settings roundtrip");
|
||||
for (const auto &pair : {std::pair{L"hello", L"привіт"}, {L"hello", L"hello"}, {L"hi!", L"hello"}})
|
||||
{
|
||||
auto invalid = settings;
|
||||
invalid.autoReplacements = {{pair.first, pair.second}};
|
||||
rejects([&] { ks::validateSettings(invalid); }, "invalid replacement pairs rejected");
|
||||
}
|
||||
auto invalidWords = settings;
|
||||
invalidWords.autoWords[L"uk"] = {L"hello"};
|
||||
rejects([&] { ks::validateSettings(invalidWords); }, "personal dictionary rejects wrong language letters");
|
||||
|
||||
Reference in New Issue
Block a user