Release v0.6.18: verify conversion results and log problems only
This commit is contained in:
@@ -39,7 +39,7 @@ $checks=0
|
||||
function Assert($ok,$label) { if (!$ok) { throw $label }; $script:checks++ }
|
||||
function Window($class) {
|
||||
for($i=0;$i -lt 100;$i++) {
|
||||
$title=switch($class){'KSwitcher.Main'{'KSwitcher'};'KSwitcher.Settings'{'KSwitcher settings'};'KSwitcher.Snippets'{'Text snippets'};'KSwitcher.Logs'{'KSwitcher log'}}
|
||||
$title=switch($class){'KSwitcher.Main'{'KSwitcher'};'KSwitcher.Settings'{'KSwitcher settings'};'KSwitcher.Snippets'{'Text snippets'};'KSwitcher.Logs'{'KSwitcher diagnostic operations'}}
|
||||
$window=[ConfigurationTestUi]::FindWindow($class,$title)
|
||||
if($window -ne [IntPtr]::Zero){return $window}
|
||||
Start-Sleep -Milliseconds 50
|
||||
|
||||
+115
-6
@@ -1,4 +1,5 @@
|
||||
#include "app.h"
|
||||
#include "diagnostics.h"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
@@ -21,9 +22,16 @@ ks::App *app{};
|
||||
HWND host{}, field{};
|
||||
ks::Clip saved;
|
||||
bool captured = false, failed = false;
|
||||
bool conversionOnly = false;
|
||||
int pasteBehavior = 0;
|
||||
int enterPresses = 0;
|
||||
LRESULT CALLBACK observeEnter(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||||
{
|
||||
if (message == WM_PASTE && pasteBehavior)
|
||||
{
|
||||
if (pasteBehavior == 2) SendMessageW(w, EM_REPLACESEL, TRUE, (LPARAM)L"other!");
|
||||
return 0;
|
||||
}
|
||||
if (message == WM_KEYDOWN && wp == VK_RETURN) ++enterPresses;
|
||||
return DefSubclassProc(w, message, wp, lp);
|
||||
}
|
||||
@@ -178,12 +186,20 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
"busy conversion attempt is logged");
|
||||
require(diagnostics.find("repeat=3") != std::string::npos,
|
||||
"three repeated conversion attempts are logged");
|
||||
require(diagnostics.find("reason=recent-verification-mismatch") != std::string::npos,
|
||||
require(diagnostics.find("\"outcome\":\"recent-verification-mismatch\"") != std::string::npos,
|
||||
"failed recent-word verification has explicit outcome");
|
||||
require(diagnostics.find("copied=\"abcdef\"") != std::string::npos &&
|
||||
diagnostics.find("physicalKeys=\"ghbdtn\"") != std::string::npos,
|
||||
require(diagnostics.find("\"original\":\"abcdef\"") != std::string::npos &&
|
||||
diagnostics.find("physicalKeys=\\\"ghbdtn\\\"") != std::string::npos,
|
||||
"conversion diagnostics contain actual text and physical input for reproduction");
|
||||
}
|
||||
if (conversionOnly)
|
||||
{
|
||||
pasteBehavior = 1;
|
||||
prepare(L"ghbdtn", true);
|
||||
app->transform(ks::Direct + ru);
|
||||
stage = 40;
|
||||
break;
|
||||
}
|
||||
app->settings.snippets = {{L";addr", L"LINE1\r\n{cursor}LINE2"}};
|
||||
prepare(L"prefix ;addr", false);
|
||||
record(L";addr");
|
||||
@@ -289,6 +305,45 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
require(text() == punctuationCases.back().first, "undo preserves custom punctuation");
|
||||
stage = 20;
|
||||
break;
|
||||
case 40:
|
||||
{
|
||||
auto rows = ks::parseOperationLog(ks::wide(diagnosticLog()));
|
||||
require(text() == L"ghbdtn" && rows.back().outcome == L"Original text remained",
|
||||
"blocked paste records original text remaining in the actual editor");
|
||||
require(ks::readClipboard(app->window).text() == sentinel, "readback preserves clipboard after blocked paste");
|
||||
pasteBehavior = 2;
|
||||
prepare(L"ghbdtn", true);
|
||||
app->transform(ks::Direct + ru);
|
||||
break;
|
||||
}
|
||||
case 41:
|
||||
{
|
||||
auto rows = ks::parseOperationLog(ks::wide(diagnosticLog()));
|
||||
require(text() == L"other!" && rows.back().outcome == L"Readback differs",
|
||||
"unexpected editor content records readback mismatch");
|
||||
pasteBehavior = 0;
|
||||
prepare(L"привет", true);
|
||||
app->transform(ks::Direct + ru);
|
||||
break;
|
||||
}
|
||||
case 42:
|
||||
{
|
||||
auto rows = ks::parseOperationLog(ks::wide(diagnosticLog()));
|
||||
require(text() == L"привет" && rows.back().outcome == L"Conversion unchanged" && rows.back().problem,
|
||||
"manual unchanged conversion is recorded as a problem");
|
||||
diagnosticStart = diagnosticLog().size();
|
||||
app->settings.writeLog = false;
|
||||
ks::setLoggingEnabled(false);
|
||||
prepare(L"ghbdtn", true);
|
||||
app->transform(ks::Direct + ru);
|
||||
break;
|
||||
}
|
||||
case 43:
|
||||
require(text() == L"привет" && diagnosticLog().size() == diagnosticStart,
|
||||
"disabled diagnostics preserves conversion and writes no records");
|
||||
ks::setLoggingEnabled(true);
|
||||
finish();
|
||||
break;
|
||||
case 30:
|
||||
case 32:
|
||||
case 34:
|
||||
@@ -448,6 +503,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
} // namespace
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
conversionOnly = argc > 1 && std::string(argv[1]) == "--conversion-only";
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
auto root = std::filesystem::temp_directory_path() /
|
||||
(L"KSwitcher-integration-" + std::to_wstring(GetCurrentProcessId()));
|
||||
@@ -463,9 +519,22 @@ int main(int argc, char **argv)
|
||||
ks::showLogs(instance);
|
||||
HWND viewer = instance.logWindow;
|
||||
HWND rows = GetDlgItem(viewer, 602);
|
||||
if (argc > 2 && std::string(argv[2]) == "--preview")
|
||||
{
|
||||
MSG message{};
|
||||
while (IsWindow(viewer) && GetMessageW(&message, nullptr, 0, 0) > 0)
|
||||
{
|
||||
TranslateMessage(&message); DispatchMessageW(&message);
|
||||
}
|
||||
CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
require(rows && (GetWindowLongPtrW(rows, GWL_STYLE) & LVS_TYPEMASK) == LVS_REPORT,
|
||||
"log viewer uses report ListView");
|
||||
require(Header_GetItemCount(ListView_GetHeader(rows)) == 4, "log viewer has four columns");
|
||||
require(Header_GetItemCount(ListView_GetHeader(rows)) == 6, "log viewer has six sortable columns");
|
||||
require(GetDlgItem(viewer, 607) && GetDlgItem(viewer, 608) && GetDlgItem(viewer, 609) &&
|
||||
GetDlgItem(viewer, 610) && GetDlgItem(viewer, 611),
|
||||
"log viewer exposes program, command, result, problems filter and detail panel");
|
||||
require(GetDlgItem(viewer, IDCANCEL) != rows, "log rows and Close have different IDs");
|
||||
SetFocus(rows);
|
||||
SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(IDCANCEL, EN_SETFOCUS), (LPARAM)rows);
|
||||
@@ -475,10 +544,11 @@ int main(int argc, char **argv)
|
||||
ks::showLogs(instance);
|
||||
require(instance.logWindow == viewer, "opening logs reuses the existing window");
|
||||
SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(603, BN_CLICKED), (LPARAM)GetDlgItem(viewer, 603));
|
||||
require(IsWindow(viewer) && ListView_GetItemCount(rows) > 0, "refresh populates log rows");
|
||||
require(IsWindow(viewer), "refresh keeps log viewer open");
|
||||
SendDlgItemMessageW(viewer, 601, CB_SETCURSEL, 1, 0);
|
||||
SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(601, CBN_SELCHANGE), (LPARAM)GetDlgItem(viewer, 601));
|
||||
require(IsWindow(viewer) && ListView_GetItemCount(rows) > 0, "previous log or empty state is displayed");
|
||||
require(IsWindow(viewer) && GetWindowTextLengthW(GetDlgItem(viewer, 611)) > 0,
|
||||
"previous log or explicit empty state is displayed");
|
||||
if (argc > 2 && std::string(argv[2]) == "--clear-test")
|
||||
{
|
||||
require(std::filesystem::path(ks::executable()).parent_path().filename() == L"log-clear-test",
|
||||
@@ -503,7 +573,46 @@ int main(int argc, char **argv)
|
||||
require(newLog.find(L"new-problem") != std::wstring::npos &&
|
||||
newLog.find(L"buffered-before-clear") == std::wstring::npos,
|
||||
"logging resumes after clear without stale context");
|
||||
ks::DiagnosticJson operation{{"attempt", "1"}, {"process", "alpha.exe"},
|
||||
{"command", "Convert / cycle"}, {"source", "English"}, {"target", "Ukrainian"},
|
||||
{"outcome", "Paste verified"}, {"problem", false}, {"original", "ghbdsn"},
|
||||
{"expected", ks::utf8(L"привіт")}, {"actual", ks::utf8(L"привіт")}};
|
||||
ks::clearDiagnosticLog(false);
|
||||
const auto successfulOperation = operation;
|
||||
for (int attempt = 0; attempt < 1000; ++attempt)
|
||||
ks::diagnosticOperation(operation.dump());
|
||||
require(!std::filesystem::exists(logRoot / L"kswitcher.log"),
|
||||
"one thousand successful operations create no log file");
|
||||
operation["attempt"] = "2"; operation["process"] = "beta.exe";
|
||||
operation["outcome"] = "Original text remained"; operation["problem"] = true;
|
||||
operation["actual"] = operation["original"];
|
||||
ks::diagnosticOperation(operation.dump());
|
||||
auto problems = ks::parseOperationLog(ks::readDiagnosticLog(false));
|
||||
require(problems.size() == 1 && problems[0].process == L"beta.exe" &&
|
||||
problems[0].details.find(L"Completed operation") != std::wstring::npos &&
|
||||
problems[0].details.find(L"Paste verified") != std::wstring::npos,
|
||||
"failure writes one row with bounded preceding successful context");
|
||||
// Pre-existing successful records remain viewable and filterable.
|
||||
ks::log(L"Operation " + ks::wide(successfulOperation.dump()));
|
||||
SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(603, BN_CLICKED), 0);
|
||||
require(ListView_GetItemCount(rows) == 2, "one visible row per recorded operation");
|
||||
CheckDlgButton(viewer, 610, BST_CHECKED);
|
||||
SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(610, BN_CLICKED), 0);
|
||||
require(ListView_GetItemCount(rows) == 1, "only problems hides verified operations");
|
||||
SendDlgItemMessageW(viewer, 607, CB_SETCURSEL, 1, 0);
|
||||
SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(607, CBN_SELCHANGE), 0);
|
||||
require(ListView_GetItemCount(rows) == 0, "program filter combines with problems filter");
|
||||
SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(612, BN_CLICKED), 0);
|
||||
require(ListView_GetItemCount(rows) == 2, "reset restores all operation rows");
|
||||
NMLISTVIEW sort{}; sort.hdr = {rows, 602, LVN_COLUMNCLICK}; sort.iSubItem = 1;
|
||||
SendMessageW(viewer, WM_NOTIFY, 602, (LPARAM)&sort);
|
||||
wchar_t firstProgram[64]{};
|
||||
ListView_GetItemText(rows, 0, 1, firstProgram, 64);
|
||||
require(std::wstring(firstProgram) == L"alpha.exe", "clicking program column sorts rows");
|
||||
require(GetWindowTextLengthW(GetDlgItem(viewer, 611)) > 0, "selected operation populates detail panel");
|
||||
}
|
||||
SendDlgItemMessageW(viewer, 601, CB_SETCURSEL, 0, 0);
|
||||
SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(603, BN_CLICKED), 0);
|
||||
auto savedClipboard = ks::readClipboard(viewer);
|
||||
instance.window = viewer;
|
||||
try
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ocr.h"
|
||||
#include "richtext.h"
|
||||
#include "storage.h"
|
||||
#include "diagnostics.h"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
@@ -44,6 +45,34 @@ int main(int argc, char **argv)
|
||||
}
|
||||
try
|
||||
{
|
||||
{
|
||||
ks::DiagnosticJson record{{"attempt", "42"}, {"process", "editor.exe"}, {"command", "Convert / cycle"},
|
||||
{"source", "English"}, {"target", "Ukrainian"}, {"outcome", "Original text remained"},
|
||||
{"problem", true}, {"verification", "read-back-via-accessibility"},
|
||||
{"original", "hello\n\"quoted\""}, {"expected", ks::utf8(L"привіт")}, {"actual", "hello"},
|
||||
{"context", "copy\npaste"}};
|
||||
auto encoded = L"2026-10-31 12:00:00 Operation " + ks::wide(record.dump());
|
||||
auto rows = ks::parseOperationLog(encoded);
|
||||
require(rows.size() == 1 && rows[0].process == L"editor.exe" && rows[0].problem,
|
||||
"structured operation is one filterable row");
|
||||
require(rows[0].details.find(L"привіт") != std::wstring::npos &&
|
||||
rows[0].details.find(L"copy\r\npaste") != std::wstring::npos,
|
||||
"operation details preserve Unicode and normalize multiline text");
|
||||
record["problem"] = false;
|
||||
record["outcome"] = "Paste verified";
|
||||
rows = ks::parseOperationLog(encoded + L"\n2026-10-31 12:00:01 Operation " + ks::wide(record.dump()));
|
||||
require(rows.size() == 2 && !rows[1].problem, "verified operations remain distinguishable from problems");
|
||||
rows = ks::parseOperationLog(L"old incident\n first stage\n second stage\n2026-10-31 Operation {broken");
|
||||
require(rows.size() == 2 && rows[0].details.find(L"second stage") != std::wstring::npos &&
|
||||
rows[1].details.find(L"{broken") != std::wstring::npos,
|
||||
"legacy context is grouped and malformed records remain inspectable");
|
||||
require(ks::conversionReadbackOutcome(L"new", L"old", L"new") == L"Paste verified",
|
||||
"expected readback verifies paste");
|
||||
require(ks::conversionReadbackOutcome(L"old", L"old", L"new") == L"Original text remained",
|
||||
"original readback identifies unchanged editor text");
|
||||
require(ks::conversionReadbackOutcome(L"other", L"old", L"new") == L"Readback differs",
|
||||
"unexpected readback is never reported as success");
|
||||
}
|
||||
require(ks::diagnosticText(L"word") == L"\"word\" units=4 truncated=0",
|
||||
"diagnostic word preserved");
|
||||
require(ks::diagnosticText(L"\"\\\n\r\t") ==
|
||||
|
||||
Reference in New Issue
Block a user