Files
strans/tests/engine_test.c
Hojun-Cho 8ebe6b32df engine: a mode mark does not outlive its owner
The mark a mode switch leaves, from 6a6749e, is cleared at the top of
transition(), so the next key takes it away.  Nothing takes it away when
there is no next key.  reset() clears the pending text, the raw state, the okurigana split
and the search, and leaves modemark alone -- so a release or a lifecycle
reset flushes everything the popup was showing except the one thing that
is still drawn.

Then samedraw() sees a picture identical to the last one and redraw()
sends nothing, and win.c only ever unmaps from a Drawcmd (winshow, the
sole caller of both map and unmap).  So the popup stays.  Press the 한/영
key, type nothing, and alt-tab: 한 sits above every window, override
redirect and typed as a tooltip, until the next keystroke in any
strans-aware field -- which may be minutes.  Counted on a private X
server, viewable override-redirect windows:

			before	after
	at rest		0	0
	after Ctrl+S	1	1
	after letting go	1	0

reset() is where it belongs rather than the release arm, because the mark
is one more thing that is pending: it is drawn only when nothing else is
(snapshot), and it means the switch has not been typed on yet.  Clearing
it there is safe for the switch itself only because transition() zeroes it
at the top and setlang() runs after flush() -- reset() is reached through
that flush, before the new mark is set.  That ordering is not obvious, and
it is exactly what engine/direct-language-modes already asserts at
engine_test.c:584; breaking setlang so a switch marks nothing fails that
line and the new ones together.

Not a Wayland defect: wl.c's leave() calls hidepopup() unconditionally
after the release, so its popup was already coming down.  X11 was the
frontend that trusted the picture.

The new case goes in engine/direct-language-modes beside the mark
assertions that were already there, and drives imhandlekey rather than
transition, since the defect is on the owner-release path.  Without the
fix:

	engine_test.c:619: check failed: draindraw(&dc) > 0

that is, the engine published no picture at all.  Its second line needs
its own reason to exist, so it was checked by removing the fix and making
samedraw() answer 0 for everything: the picture then arrives and still
carries the mark,

	engine_test.c:620: want 0, got 1  (dc.pre.n)

which is the assertion that would otherwise have been decoration.  The two
setup lines are load-bearing too -- with setlang marking nothing they fail
rather than passing vacuously, since an empty picture equal to the last
one is never sent.

90 unit, check-live, check-stress and valgrind clean; the count is
unchanged because the case joined a test that already existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 07:48:26 +09:00

2223 lines
57 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "strans.c"
#include "test.h"
static int typekeys(struct ct*, char*, Str*);
static int draindraw(Drawcmd*);
/* One key from the owner, as imhandlekey drives the engine. */
static int
keystroke(u32int ks, u32int mod, Str *com)
{
int eaten;
eaten = keymeaningful(ks) && transition(ks, mod, com);
redraw();
return eaten;
}
void
testengineinit(int lang)
{
init();
im.l = getlang(lang);
}
void
testenginehandle(Keyreq *req)
{
imhandlekey(req);
}
void*
testengineowner(void)
{
return activeowner;
}
void
testenginepreedit(Str *preedit)
{
impre(&im, preedit);
}
void
testenginecaret(Caret *at)
{
*at = caret;
}
void
vietnamese_state_lifetime(struct ct *t)
{
Str com;
init();
im.l = getlang(LangVI);
im.pre = mkstr("as");
im.raw = mkstr("as");
sclear(&com);
CT_CHECK(t, !keystroke(Kret, 0, &com));
checkstr(t, "committed Telex", "á", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
init();
im.l = getlang(LangVI);
im.pre = mkstr("as");
im.raw = mkstr("as");
sclear(&com);
CT_CHECK(t, !keystroke(Kesc, 0, &com));
checkstr(t, "Escape commits Telex", "á", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
init();
im.l = getlang(LangVI);
im.pre = mkstr("as");
im.raw = mkstr("as");
sclear(&com);
CT_CHECK(t, dotrans('q', &com));
checkstr(t, "completed Telex", "á", &com);
checkstr(t, "next preedit", "q", &im.pre);
checkstr(t, "next raw input", "q", &im.raw);
}
void
engine_clears_candidates(struct ct *t)
{
Str com;
init();
im.l = getlang(LangKO);
im.pre = mkstr("");
im.nkouho = 2;
im.sel = 1;
sclear(&com);
dotrans('k', &com);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
}
void
engine_backspace_clears_candidates(struct ct *t)
{
Lang *lang;
Str com, shown;
lang = getlang(LangJP);
init();
im.l = lang;
sclear(&com);
CT_CHECK(t, keystroke('n', 0, &com));
CT_CHECK(t, keystroke('a', 0, &com));
impre(&im, &shown);
checkstr(t, "before backspace", "", &shown);
im.kouho[0] = mkstr("stale-candidate");
im.nkouho = 1;
im.sel = 0;
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "chosen candidate backspace", "na", &im.raw);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspace commit", "", &com);
checkstr(t, "backspace deletes the kana shown", "", &im.raw);
impre(&im, &shown);
checkstr(t, "shown preedit after backspace", "", &shown);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
}
void
engine_selects_visible_candidate(struct ct *t)
{
static const struct { int n, sel; Rune key; char *want; } cases[] = {
{ Maxdisp, 0, '9', "c9" },
{ Maxkouho, Maxdisp, '1', "c10" },
{ Maxkouho, 2*Maxdisp, '9', "c27" },
};
char name[8];
Str com;
int i, j;
for(i = 0; i < nelem(cases); i++){
init();
for(j = 0; j < cases[i].n; j++){
snprint(name, sizeof name, "c%d", j+1);
im.kouho[j] = mkstr(name);
}
im.nkouho = cases[i].n;
im.sel = cases[i].sel;
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, 0, &com));
checkstr(t, cases[i].want, cases[i].want, &com);
CT_EQ_INT(t, 0, im.nkouho);
}
}
void
engine_candidate_shortcut_modifiers(struct ct *t)
{
static u32int mods[] = { Mshift, Mctrl, Malt, Msuper };
Str com, selected;
int i;
for(i = 0; i < nelem(mods); i++){
init();
im.kouho[9] = mkstr("must-not-select");
im.nkouho = 10;
im.sel = -1;
selected = im.kouho[9];
sclear(&com);
CT_CHECK(t, !keystroke('1', mods[i], &com));
CT_CHECK(t, scmp(&com, &selected) != 0);
}
}
static Keyres
ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
Caret *caret)
{
Channel *reply;
Keyreq req;
Keyres res;
reply = chancreate(sizeof(Keyres), 1);
memset(&req, 0, sizeof req);
req.owner = owner;
req.clientpre = cap;
req.op = op;
req.ks = key;
req.mod = mod;
if(caret != nil)
req.caret = *caret;
req.reply = reply;
imhandlekey(&req);
chanrecv(reply, &res);
chanfree(reply);
return res;
}
static Keyres
ownerrequestat(void *owner, int op, Rune key, u32int mod, Caret *caret)
{
return ownerrequestatcap(owner, 0, op, key, mod, caret);
}
static Keyres
ownerrequest(void *owner, int op, Rune key, u32int mod)
{
return ownerrequestat(owner, op, key, mod, nil);
}
static Keyres
ownerrequestcap(void *owner, int cap, int op, Rune key, u32int mod)
{
return ownerrequestatcap(owner, cap, op, key, mod, nil);
}
void
engine_active_owner_lifecycle(struct ct *t)
{
Keyres res;
Str shown;
char a, b, c;
init();
im.l = getlang(LangJP);
res = ownerrequest(&a, Keypress, 'k', 0);
CT_CHECK(t, res.eaten);
res = ownerrequest(&a, Keypress, 'a', 0);
checkstr(t, "first owner preedit", "", &res.preedit);
CT_EQ_PTR(t, &a, activeowner);
res = ownerrequest(&b, Keypress, 'n', 0);
checkstr(t, "takeover resets old preedit", "", &res.preedit);
CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&a, Keyreset, 0, 0);
checkstr(t, "the taken owner gets its reading back", "", &res.commit);
CT_EQ_INT(t, 0, res.preedit.n);
impre(&im, &shown);
checkstr(t, "stale reset preserves owner", "", &shown);
CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&b, Keypress, 'y', 0);
res = ownerrequest(&b, Keypress, 'a', 0);
checkstr(t, "current owner continues", "にゃ", &res.preedit);
ownerrequest(&a, Keyrelease, 0, 0);
impre(&im, &shown);
checkstr(t, "stale release preserves owner", "にゃ", &shown);
CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&b, Keyrelease, 0, 0);
checkstr(t, "release hands the reading back", "にゃ", &res.commit);
CT_EQ_PTR(t, nil, activeowner);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
ownerrequest(&c, Keypress, 0, 0);
CT_EQ_PTR(t, nil, activeowner);
res = ownerrequest(&c, Keypress, 'k', 0);
CT_EQ_PTR(t, &c, activeowner);
checkstr(t, "next owner first key", "k", &res.preedit);
}
void
engine_active_owner_reset(struct ct *t)
{
Caret at;
Keyres res;
char owner;
memset(&at, 0, sizeof at);
at.valid = 1;
at.x = 10;
at.y = 20;
at.h = 14;
init();
im.l = getlang(LangJP);
ownerrequestat(&owner, Keypress, 'k', 0, &at);
res = ownerrequest(&owner, Keypress, 'a', 0);
checkstr(t, "preedit before reset", "", &res.preedit);
res = ownerrequest(&owner, Keyreset, 0, 0);
CT_EQ_PTR(t, &owner, activeowner);
checkstr(t, "reset hands the reading back", "", &res.commit);
CT_EQ_INT(t, 0, res.preedit.n);
res = ownerrequest(&owner, Keypress, 'n', 0);
checkstr(t, "same owner after reset", "", &res.preedit);
}
void
engine_active_owner_caret(struct ct *t)
{
Caret a, moved;
Drawcmd dc;
char one, two;
memset(&a, 0, sizeof a);
a.valid = 1;
a.x = 10;
a.y = 20;
a.h = 14;
moved = a;
moved.x = 80;
init();
draindraw(nil);
im.l = getlang(LangEN);
ownerrequestat(&one, Keypress, 'x', 0, &a);
CT_CHECK(t, caret.valid);
CT_EQ_INT(t, 10, caret.x);
ownerrequestat(&one, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 80, caret.x);
CT_EQ_INT(t, 0, draindraw(nil));
im.kouho[0] = mkstr("candidate");
im.nkouho = 1;
im.sel = 0;
redraw();
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 80, dc.caret.x);
moved.x = 90;
ownerrequestat(&two, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 80, caret.x);
CT_EQ_INT(t, 0, draindraw(nil));
ownerrequestat(&one, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 90, caret.x);
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 90, dc.caret.x);
ownerrequestat(&one, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 0, draindraw(nil));
ownerrequestat(&one, Keypress, 'x', 0, &moved);
CT_EQ_INT(t, 0, draindraw(nil));
moved.x = 100;
ownerrequestat(&one, Keypress, 'x', 0, &moved);
CT_EQ_INT(t, 100, caret.x);
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 100, dc.caret.x);
ownerrequest(&two, Keypress, 'n', 0);
CT_EQ_PTR(t, &two, activeowner);
CT_CHECK(t, !caret.valid);
}
void
engine_vietnamese_client_preedit(struct ct *t)
{
static const struct {
char *keys;
char *shown;
} cases[] = {
{ "as", "á" },
{ "aa", "â" },
{ "ddoong", "đông" },
{ "hoaf", "hoà" },
{ "quas", "quá" },
};
Drawcmd dc;
Keyres res;
Str com, shown;
char *k, owner;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangVI);
draindraw(nil);
sclear(&com);
for(k = cases[i].keys; *k != '\0'; k++){
res = ownerrequestcap(&owner, 1,
Keypress, *k, 0);
sappend(&com, &res.commit);
if(!res.eaten)
sputr(&com, *k);
}
/* What the client shows, what the popup shows, and what
* Enter commits are the same composed text. */
shown = com;
sappend(&shown, &res.preedit);
checkstr(t, "client preedit", cases[i].shown, &shown);
clientpre = 0;
redraw();
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 0, scmp(&dc.pre, &res.preedit));
CT_CHECK(t, !keystroke(Kret, 0, &com));
checkstr(t, "commit", cases[i].shown, &com);
}
}
void
engine_commit_contract(struct ct *t)
{
static const struct {
Rune key;
char *one;
char *all;
} cases[] = {
{ 'z', "", "ㅋㅋㅋ" },
{ 'r', "", "ㄱㄱㄱ" },
{ 'd', "", "ㅇㅇㅇ" },
{ 'k', "", "ㅏㅏㅏ" },
};
Keyres res;
Str all, com;
char owner;
int i, j;
for(j = 0; j < nelem(cases); j++){
init();
im.l = getlang(LangKO);
sclear(&all);
for(i = 0; i < 3; i++){
res = ownerrequestcap(&owner, 1,
Keypress, cases[j].key, 0);
CT_CHECK(t, res.eaten);
checkstr(t, "repeated jamo commit",
i == 0 ? "" : cases[j].one, &res.commit);
checkstr(t, "repeated jamo preedit", cases[j].one,
&res.preedit);
sappend(&all, &res.commit);
}
sappend(&all, &res.preedit);
checkstr(t, "repeated jamo total", cases[j].all, &all);
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, "ka", &com))
return;
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "return commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, "ka", &com))
return;
im.kouho[0] = mkstr("c1");
im.kouho[1] = mkstr("c2");
im.nkouho = 2;
im.sel = 0;
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "selected candidate", "c2", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, "ka", &com))
return;
im.nkouho = 1;
im.sel = -1;
CT_CHECK(t, !keystroke('!', 0, &com));
checkstr(t, "uneaten-key commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
/* Without candidates the movement keys commit like any other. */
init();
im.l = getlang(LangKO);
sclear(&com);
if(!typekeys(t, "dy", &com))
return;
CT_CHECK(t, !keystroke(Kdown, 0, &com));
checkstr(t, "arrow commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, "ka", &com))
return;
/* A key outside the map commits the reading and passes on. */
CT_CHECK(t, !keystroke(0xf008, 0, &com));
checkstr(t, "non-ASCII key commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
}
void
engine_telex_history_bound(struct ct *t)
{
Str com;
Rune tone;
int i;
init();
im.l = getlang(LangVI);
sclear(&com);
CT_CHECK(t, keystroke('a', 0, &com));
for(i = 0; i < Maxrunes; i++){
tone = i % 2 == 0 ? 's' : 'f';
if(!CT_CHECK(t, keystroke(tone, 0, &com)))
break;
}
checkstr(t, "bounded Telex commit", "à", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
}
void
engine_korean_modifiers_and_backspace(struct ct *t)
{
static const struct {
Rune key;
char *plain;
char *shifted;
} cases[] = {
{ 'r', "", "" }, { 'e', "", "" },
{ 'q', "", "" }, { 't', "", "" },
{ 'w', "", "" }, { 'o', "", "" },
{ 'p', "", "" },
};
Drawcmd dc;
Str com;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, 0, &com));
checkstr(t, "plain Korean key", cases[i].plain, &im.pre);
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, Mshift, &com));
checkstr(t, "shifted Korean key", cases[i].shifted, &im.pre);
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key - 'a' + 'A', 0, &com));
checkstr(t, "Caps Lock Korean key", cases[i].plain, &im.pre);
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, Mshift, &com));
checkstr(t, "Shift+Caps Korean key", cases[i].shifted, &im.pre);
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rkr", &com)){
checkstr(t, "Korean engine preedit", "", &im.pre);
draindraw(nil);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Korean final backspace", "", &im.pre);
if(CT_CHECK(t, draindraw(&dc) == 1))
checkstr(t, "Korean backspace redraw", "", &dc.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Korean vowel backspace", "", &im.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
CT_CHECK(t, !keystroke(Kback, 0, &com));
}
}
void
engine_direct_language_modes(struct ct *t)
{
Drawcmd dc;
Str com;
char owner;
init();
draindraw(nil);
sclear(&com);
CT_EQ_INT(t, LangEN, im.l->lang);
/* A switch shows the mode it switched to until the next key. */
CT_CHECK(t, keystroke('s', Mctrl, &com));
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "mode mark", "", &dc.pre);
CT_CHECK(t, keystroke('t', Mctrl, &com));
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "mode mark", "A", &dc.pre);
CT_CHECK(t, !keystroke('a', 0, &com));
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 0, dc.pre.n);
CT_CHECK(t, !keystroke('a', 0, &com));
CT_EQ_INT(t, 0, draindraw(nil));
CT_EQ_PTR(t, nil, im.l->mapname);
CT_EQ_PTR(t, nil, im.l->map);
CT_EQ_PTR(t, nil, im.l->trans);
CT_CHECK(t, !keystroke('a', 0, &com));
CT_CHECK(t, !keystroke('Z', Mshift, &com));
checkstr(t, "English direct commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, search.lang);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, !search.lang);
CT_CHECK(t, keystroke('s', Mctrl, &com));
CT_EQ_INT(t, LangKO, im.l->lang);
CT_EQ_PTR(t, nil, im.l->mapname);
CT_EQ_PTR(t, nil, im.l->map);
CT_EQ_PTR(t, nil, im.l->dictname);
CT_CHECK(t, strcmp(getlang(LangHANJA)->dictname, "hanja") == 0);
CT_CHECK(t, keystroke('r', 0, &com));
checkstr(t, "Korean without map", "", &im.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
/* Leaving takes the mark away as surely as typing does. */
init();
draindraw(nil);
ownerrequest(&owner, Keypress, 's', Mctrl);
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "mode mark", "", &dc.pre);
ownerrequest(&owner, Keyrelease, 0, 0);
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 0, dc.pre.n);
}
void
engine_language_switch_state(struct ct *t)
{
static const struct {
char *name;
Rune key;
int mod;
int lang;
char *pre;
char *raw;
char *commit;
int stale;
} steps[] = {
{ "select Vietnamese", 'v', Mctrl, LangVI, "", "", "", 0 },
{ "Vietnamese a", 'a', 0, LangVI, "a", "a", "", 0 },
{ "Vietnamese tone", 's', 0, LangVI, "as", "as", "", 0 },
{ "switch to Korean", 's', Mctrl, LangKO, "", "", "á", 1 },
{ "Korean initial", 'r', 0, LangKO, "", "", "", 0 },
{ "Korean syllable", 'k', 0, LangKO, "", "", "", 0 },
{ "switch to Vietnamese", 'v', Mctrl, LangVI, "", "", "", 0 },
{ "Vietnamese a again", 'a', 0, LangVI, "a", "a", "", 0 },
{ "Vietnamese tone again", 's', 0, LangVI, "as", "as", "", 0 },
{ "commit Vietnamese", 't', Mctrl, LangEN, "", "", "á", 0 },
};
char where[80];
Str com;
int eaten, i;
init();
for(i = 0; i < nelem(steps); i++){
if(steps[i].stale){
im.kouho[0] = mkstr("stale");
im.nkouho = 1;
im.sel = -1;
}
sclear(&com);
eaten = keystroke(steps[i].key, steps[i].mod, &com);
if(eaten != 1){
CT_ERRORF(t, "%s: want eaten 1, got %d",
steps[i].name, eaten);
break;
}
if(im.l == nil || im.l->lang != steps[i].lang){
CT_ERRORF(t, "%s: want language %d, got %d",
steps[i].name, steps[i].lang,
im.l == nil ? -1 : im.l->lang);
break;
}
snprint(where, sizeof where, "%s preedit", steps[i].name);
if(!checkstr(t, where, steps[i].pre, &im.pre))
break;
snprint(where, sizeof where, "%s raw", steps[i].name);
if(!checkstr(t, where, steps[i].raw, &im.raw))
break;
snprint(where, sizeof where, "%s commit", steps[i].name);
if(!checkstr(t, where, steps[i].commit, &com))
break;
if(im.nkouho != 0 || im.sel != -1){
CT_ERRORF(t, "%s: want candidates 0 and selection -1, got %d and %d",
steps[i].name, im.nkouho, im.sel);
break;
}
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rk", &com)){
CT_CHECK(t, !keystroke('p', Mctrl, &com));
checkstr(t, "Korean Ctrl passthrough", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
}
init();
im.l = getlang(LangVI);
sclear(&com);
if(typekeys(t, "as", &com)){
CT_CHECK(t, !keystroke('p', Mctrl, &com));
checkstr(t, "Vietnamese Ctrl passthrough", "á", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
}
}
typedef struct Searchfix Searchfix;
struct Searchfix
{
Im im;
Search search;
Lang *dictlang;
Trie *dict;
int clientpre;
Drawcmd lastdraw;
};
static int
draindraw(Drawcmd *last)
{
Drawcmd dc = {0};
int n;
n = 0;
while(channbrecv(drawc, &dc) > 0){
n++;
if(last != nil)
*last = dc;
}
return n;
}
static void
setcandidates(int n)
{
char name[8];
int i;
clearkouho();
for(i = 0; i < n; i++){
snprint(name, sizeof name, "c%d", i+1);
im.kouho[i] = mkstr(name);
}
im.nkouho = n;
}
static void
candidatebegin(int n)
{
init();
im.l = getlang(LangJP);
draindraw(nil);
setcandidates(n);
}
static int
checkcandidatepage(struct ct *t, char *where, int first, int n, int sel)
{
Drawcmd dc;
char want[8];
redraw();
if(!CT_CHECK(t, draindraw(&dc) > 0))
return 0;
if(!CT_EQ_INT(t, n, dc.nkouho) || !CT_EQ_INT(t, sel, dc.sel) ||
!CT_EQ_INT(t, first, dc.first) ||
!CT_EQ_INT(t, im.nkouho, dc.total))
return 0;
if(n == 0)
return 1;
snprint(want, sizeof want, "c%d", first+1);
if(!checkstr(t, where, want, &dc.kouho[0]))
return 0;
snprint(want, sizeof want, "c%d", first+n);
return checkstr(t, where, want, &dc.kouho[n-1]);
}
void
engine_candidate_page_metadata(struct ct *t)
{
static const struct {
int total;
int sel;
int first;
int shown;
int local;
} cases[] = {
{ 1, 0, 0, 1, 0 },
{ 9, 8, 0, 9, 8 },
{ 10, 8, 0, 9, 8 },
{ 10, 9, 9, 1, 0 },
{ 18, 8, 0, 9, 8 },
{ 18, 17, 9, 9, 8 },
{ 19, 0, 0, 9, 0 },
{ 32, 8, 0, 9, 8 },
{ 32, 17, 9, 9, 8 },
{ 32, 26, 18, 9, 8 },
{ 32, 31, 27, 5, 4 },
};
int i;
for(i = 0; i < nelem(cases); i++){
candidatebegin(cases[i].total);
im.sel = cases[i].sel;
checkcandidatepage(t, "candidate page metadata",
cases[i].first, cases[i].shown, cases[i].local);
}
}
void
engine_candidate_page_snapshots(struct ct *t)
{
Drawcmd dc;
/* Clearing a visible popup must publish an empty snapshot. */
candidatebegin(1);
redraw();
draindraw(nil);
setcandidates(0);
redraw();
if(CT_CHECK(t, draindraw(&dc) > 0)){
CT_EQ_INT(t, 0, dc.nkouho);
CT_EQ_INT(t, -1, dc.sel);
CT_EQ_INT(t, 0, dc.first);
CT_EQ_INT(t, 0, dc.total);
}
}
void
engine_candidate_page_movement(struct ct *t)
{
static const struct {
int n;
int sel;
Rune key;
int want;
} cases[] = {
{ 1, 0, Kup, 0 },
{ 9, 8, Kdown, 8 },
{ 10, 8, Kdown, 9 },
{ 10, 9, Kup, 8 },
{ 18, 8, Kpgdown, 17 },
{ 19, 17, Kdown, 18 },
{ 19, 18, Kup, 17 },
{ 32, 26, Kdown, 27 },
{ 32, 27, Kup, 26 },
{ 32, 0, Kpgup, 0 },
{ 32, 9, Kpgup, 0 },
{ 32, 22, Kpgdown, 31 },
{ 32, 31, Kpgup, 22 },
{ 32, 31, Kpgdown, 31 },
};
int first, i, n;
Str com;
for(i = 0; i < nelem(cases); i++){
candidatebegin(cases[i].n);
im.sel = cases[i].sel;
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, 0, &com));
CT_EQ_INT(t, cases[i].want, im.sel);
first = cases[i].want / Maxdisp * Maxdisp;
n = cases[i].n - first;
if(n > Maxdisp)
n = Maxdisp;
checkcandidatepage(t, "moved candidate page", first, n,
cases[i].want-first);
}
}
void
engine_candidate_completion(struct ct *t)
{
Str com;
init();
im.l = getlang(LangJP);
im.pre = mkstr("reading");
setcandidates(1);
sclear(&com);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "unselected candidate commits reading", "reading", &com);
CT_EQ_INT(t, 0, im.nkouho);
init();
im.l = getlang(LangJP);
im.pre = mkstr("reading");
setcandidates(2);
sclear(&com);
CT_CHECK(t, keystroke(Ktab, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Ktab, Mshift, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Tab steps through the candidates", "c2", &com);
candidatebegin(2);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "explicit candidate", "c2", &com);
candidatebegin(2);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "ordinary zero reading", "reading", &com);
candidatebegin(0);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Enter without candidates", "reading", &com);
/* A digit types on until Space engages the list. */
candidatebegin(3);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, !keystroke('2', 0, &com));
checkstr(t, "digit before the list is engaged", "reading", &com);
CT_EQ_INT(t, 0, im.nkouho);
candidatebegin(3);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_CHECK(t, keystroke('2', 0, &com));
checkstr(t, "digit after the list is engaged", "c2", &com);
/* A digit with no such row on the page is an ordinary key: it
* confirms the candidate paged to and passes on. */
candidatebegin(32);
im.pre = mkstr("reading");
im.sel = 27;
sclear(&com);
CT_CHECK(t, !keystroke('9', 0, &com));
checkstr(t, "unavailable short-page digit", "c28", &com);
CT_EQ_INT(t, 0, im.nkouho);
}
static void
setdict(Trie *dict, char *key, char *val)
{
trieput(dict, key, strlen(key), val, strlen(val));
}
static void
searchsave(Searchfix *f, int lang)
{
f->im = im;
f->search = search;
f->dictlang = getlang(lang);
f->dict = f->dictlang->dict;
f->clientpre = clientpre;
f->lastdraw = lastdraw;
draindraw(nil);
}
static void
emojibegin(Searchfix *f, int showpre)
{
searchsave(f, LangEMOJI);
f->dictlang->dict = trienew();
setdict(f->dictlang->dict, "a", "A B");
setdict(f->dictlang->dict, "", "B C");
setdict(f->dictlang->dict, "alpha", "α");
setdict(f->dictlang->dict, "smil", "😀 😄");
setdict(f->dictlang->dict, "smile", "😀 😄");
setdict(f->dictlang->dict, "웃음", "😀 😄 😂");
setdict(f->dictlang->dict, "えがお", "😀 😄 😊");
setdict(f->dictlang->dict, "エガオ", "😀 😄 😊");
setdict(f->dictlang->dict, "heart", "❤️");
setdict(f->dictlang->dict, "coffee", "☕️");
setdict(f->dictlang->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽");
setdict(f->dictlang->dict, "^0", "");
setdict(f->dictlang->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍");
setdict(f->dictlang->dict, "<", "← ≤ ♥ ≠");
setdict(f->dictlang->dict, "many",
"c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12");
init();
im.l = getlang(LangKO);
clientpre = showpre ? 0 : 1;
}
static void
searchend(Searchfix *f)
{
Trie *dict;
dict = f->dictlang->dict;
f->dictlang->dict = f->dict;
trieclose(dict);
draindraw(nil);
im = f->im;
search = f->search;
clientpre = f->clientpre;
lastdraw = f->lastdraw;
}
void
engine_popup_preedit_capability(struct ct *t)
{
Drawcmd dc;
Keyres kres;
Lang *jp;
Trie *saved;
char client, inactive;
jp = getlang(LangJP);
saved = jp->dict;
jp->dict = trienew();
setdict(jp->dict, "", "");
init();
im.l = jp;
draindraw(nil);
ownerrequestcap(&client, 1, Keypress, 'k', 0);
ownerrequestcap(&client, 1, Keypress, 'a', 0);
CT_EQ_INT(t, 1, im.nkouho);
redraw();
if(CT_CHECK(t, draindraw(&dc) > 0)){
CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 1, dc.nkouho);
CT_EQ_INT(t, -1, dc.sel);
checkstr(t, "client-preedit candidate", "", &dc.kouho[0]);
}
kres = ownerrequestcap(&client, 0, Keycap, 0, 0);
CT_CHECK(t, kres.eaten);
checkstr(t, "capability response preedit", "", &kres.preedit);
if(CT_CHECK(t, draindraw(&dc) > 0)){
checkstr(t, "popup preedit", "", &dc.pre);
CT_EQ_INT(t, 1, dc.nkouho);
CT_EQ_INT(t, -1, dc.sel);
checkstr(t, "popup candidate", "", &dc.kouho[0]);
}
CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "retained popup candidate", "", &im.kouho[0]);
kres = ownerrequestcap(&client, 0, Keycap, 0, 0);
CT_CHECK(t, kres.eaten);
CT_EQ_INT(t, 0, draindraw(nil));
kres = ownerrequestcap(&inactive, 1, Keycap, 0, 0);
CT_CHECK(t, !kres.eaten);
CT_EQ_INT(t, 0, kres.preedit.n);
CT_EQ_PTR(t, &client, activeowner);
CT_EQ_INT(t, 0, clientpre);
CT_EQ_INT(t, 0, draindraw(nil));
kres = ownerrequestcap(&client, 1, Keycap, 0, 0);
CT_CHECK(t, kres.eaten);
checkstr(t, "restored client preedit", "", &kres.preedit);
if(CT_CHECK(t, draindraw(&dc) > 0)){
CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 1, dc.nkouho);
CT_EQ_INT(t, -1, dc.sel);
checkstr(t, "restored client candidate", "", &dc.kouho[0]);
}
CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "retained client candidate", "", &im.kouho[0]);
kres = ownerrequestcap(&client, 1, Keycap, 0, 0);
CT_CHECK(t, kres.eaten);
CT_EQ_INT(t, 0, draindraw(nil));
ownerrequest(&client, Keyrelease, 0, 0);
draindraw(nil);
trieclose(jp->dict);
jp->dict = saved;
}
static void
hanjabegin(Searchfix *f, int lang)
{
searchsave(f, LangHANJA);
f->dictlang->dict = trienew();
setdict(f->dictlang->dict, "", "漢 韓");
setdict(f->dictlang->dict, "", "");
init();
im.l = getlang(lang);
}
static int
typekeys(struct ct *t, char *keys, Str *com)
{
for(; *keys != '\0'; keys++)
if(!CT_CHECK(t, keystroke((uchar)*keys, 0, com)))
return 0;
return 1;
}
void
engine_japanese_readings(struct ct *t)
{
static const struct {
char *keys;
char *want;
} cases[] = {
{ "nya", "にゃ" },
{ "n'ya", "んや" },
{ "nnya", "んや" },
{ "nnnya", "んにゃ" },
{ "konnnichiha", "こんにちは" },
{ "wi", "うぃ" },
{ "vu", "" },
{ "xtu", "" },
{ "axtu", "あっ" },
{ "lla", "っぁ" },
{ "matcha", "まっちゃ" },
{ "zi", "" },
{ "[", "" },
{ "kin'youbi", "きんようび" },
};
static const struct {
char *raw;
char *want;
} replay[] = {
{ "kitte", "きって" },
{ "matcha", "まっちゃ" },
{ "baggu", "ばっぐ" },
{ "kanj", "かんj" },
{ "n'ya", "んや" },
};
Str com, raw, shown;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, cases[i].keys, &com))
continue;
checkstr(t, cases[i].keys, "", &com);
impre(&im, &shown);
checkstr(t, cases[i].keys, cases[i].want, &shown);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "committed reading", cases[i].want, &com);
}
for(i = 0; i < nelem(replay); i++){
raw = mkstr(replay[i].raw);
transstr(getlang(LangJP), nil, &raw, &shown);
checkstr(t, replay[i].raw, replay[i].want, &shown);
}
}
void
engine_japanese_candidates(struct ct *t)
{
Trie *saved;
Lang *jp;
Str com, shown;
jp = getlang(LangJP);
saved = jp->dict;
jp->dict = trienew();
setdict(jp->dict, "かんじ", "漢字 幹事");
init();
im.l = jp;
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
impre(&im, &shown);
checkstr(t, "complete Japanese reading", "かんじ", &shown);
if(!CT_EQ_INT(t, 2, im.nkouho))
goto cleanup;
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "candidate one", "漢字", &im.kouho[0]);
checkstr(t, "candidate two", "幹事", &im.kouho[1]);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "selected Kanji", "幹事", &com);
CT_EQ_INT(t, 0, im.nkouho);
/* Space converts: it steps through the candidates and wraps;
* typing on takes the chosen one; Backspace goes back. */
if(!typekeys(t, "kanji", &com))
goto cleanup;
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 2, im.sel);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(' ', Mshift, &com));
CT_EQ_INT(t, 2, im.sel);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, -1, im.sel);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_CHECK(t, keystroke('w', 0, &com));
CT_CHECK(t, keystroke('o', 0, &com));
checkstr(t, "typing on takes the candidate", "漢字", &com);
impre(&im, &shown);
checkstr(t, "reading after the candidate", "", &shown);
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "Space offers Katakana without candidates", "",
&im.kouho[0]);
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Katakana committed", "", &com);
CT_CHECK(t, !keystroke(' ', 0, &com));
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(' ', 0, &com));
if(CT_EQ_INT(t, 3, im.nkouho))
checkstr(t, "Katakana after the candidates", "カンジ",
&im.kouho[2]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* SKK's okuri-ari entries convert verbs: stem plus okurigana. */
setdict(jp->dict, "かk", "書 描");
setdict(jp->dict, "かく", "");
if(!typekeys(t, "kaku", &com))
goto cleanup;
if(CT_EQ_INT(t, 3, im.nkouho)){
checkstr(t, "exact candidate first", "", &im.kouho[0]);
checkstr(t, "okurigana put back", "書く", &im.kouho[1]);
checkstr(t, "second okuri candidate", "描く", &im.kouho[2]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
setdict(jp->dict, "おおk", "");
if(!typekeys(t, "ookii", &com))
goto cleanup;
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "two-kana okurigana", "大きい", &im.kouho[0]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* Shift on the okurigana asks for that split alone, first. */
sclear(&com);
if(!typekeys(t, "ka", &com))
goto cleanup;
CT_CHECK(t, keystroke('K', Mshift, &com));
CT_CHECK(t, keystroke('u', 0, &com));
if(CT_EQ_INT(t, 3, im.nkouho)){
checkstr(t, "marked okurigana first", "書く", &im.kouho[0]);
checkstr(t, "then the second", "描く", &im.kouho[1]);
checkstr(t, "then the reading's own", "", &im.kouho[2]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
setdict(jp->dict, "いt", "言 行");
setdict(jp->dict, "いっt", "言 行");
if(!typekeys(t, "itta", &com))
goto cleanup;
if(CT_EQ_INT(t, 2, im.nkouho)){
checkstr(t, "sokuon written in kana", "言った", &im.kouho[0]);
checkstr(t, "sokuon written in kana", "行った", &im.kouho[1]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* A romaji typo stays in the reading for Backspace to mend. */
if(!typekeys(t, "kanjr", &com))
goto cleanup;
impre(&im, &shown);
checkstr(t, "typo kept", "かんjr", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "typo half mended", "かんj", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
if(!typekeys(t, "ji", &com))
goto cleanup;
impre(&im, &shown);
checkstr(t, "typo mended", "かんじ", &shown);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "0 commits the reading", "かんじ", &com);
sclear(&com);
if(!typekeys(t, "wo", &com))
goto cleanup;
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "0 commits a reading without candidates", "", &com);
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_CHECK(t, keystroke(' ', 0, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, -1, im.sel);
impre(&im, &shown);
checkstr(t, "Escape goes back to the reading", "かんじ", &shown);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_EQ_INT(t, 0, im.nkouho);
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke('s', Mctrl, &com));
checkstr(t, "language switch takes the candidate", "漢字", &com);
CT_EQ_INT(t, LangKO, im.l->lang);
im.l = jp;
/* Backspace looks a complete shorter reading up again. */
setdict(jp->dict, "かん", "");
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_CHECK(t, keystroke(Kback, 0, &com));
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "shorter reading candidate", "", &im.kouho[0]);
cleanup:
trieclose(jp->dict);
jp->dict = saved;
}
void
engine_japanese_backspace_and_boundaries(struct ct *t)
{
Str com, shown;
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "kanji", &com)){
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "backspace deletes じ", "かん", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "backspace deletes ん", "", &shown);
if(typekeys(t, "kyu", &com)){
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "backspace deletes ゅ", "かき", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, !haspre(&im));
}
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "kanji", &com)){
CT_CHECK(t, keystroke('k', Mctrl, &com));
checkstr(t, "Hiragana mode boundary", "かんじ", &com);
CT_EQ_INT(t, LangJPK, im.l->lang);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, keystroke('n', Mctrl, &com));
checkstr(t, "Katakana mode boundary", "", &com);
CT_EQ_INT(t, LangJP, im.l->lang);
}
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "kanji", &com)){
CT_CHECK(t, !keystroke(L'', 0, &com));
checkstr(t, "real input boundary", "かんじ", &com);
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "KA", &com)){
impre(&im, &shown);
checkstr(t, "uppercase romaji", "", &shown);
}
}
void
engine_katakana_sequences(struct ct *t)
{
static const struct {
char *keys;
char *want;
} cases[] = {
{ "kitte", "キッテ" },
{ "katta", "カッタ" },
{ "gakkou", "ガッコウ" },
{ "matcha", "マッチャ" },
{ "baggu", "バッグ" },
{ "xu", "" },
{ "vu", "" },
};
Str com, shown;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangJPK);
sclear(&com);
if(!typekeys(t, cases[i].keys, &com))
continue;
checkstr(t, cases[i].keys, "", &com);
impre(&im, &shown);
checkstr(t, cases[i].keys, cases[i].want, &shown);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "committed Katakana", cases[i].want, &com);
}
CT_EQ_PTR(t, nil, getlang(LangJPK)->dictname);
}
void
engine_emoji_single_candidate(struct ct *t)
{
Drawcmd dc = {0};
Searchfix f;
Str com;
emojibegin(&f, 0);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "empty search shows itself", "", &dc.pre);
if(typekeys(t, "alpha", &com)){
checkstr(t, "no automatic commit", "", &com);
checkstr(t, "raw English query", "alpha", &search.text);
CT_CHECK(t, search.lang);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "single candidate", "α", &im.kouho[0]);
CT_CHECK(t, draindraw(&dc) > 0);
CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 1, dc.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "explicit candidate commit", "α", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
redraw();
CT_CHECK(t, draindraw(&dc) > 0);
CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 0, dc.nkouho);
}
searchend(&f);
}
void
engine_emoji_queries(struct ct *t)
{
Drawcmd dc = {0};
Searchfix f;
Str com;
emojibegin(&f, 1);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke('a', 0, &com));
checkstr(t, "raw query wins", "a", &search.text);
CT_EQ_INT(t, 4, im.nkouho);
checkstr(t, "raw candidate", "A", &im.kouho[0]);
checkstr(t, "deduplicated candidate", "B", &im.kouho[1]);
checkstr(t, "raw prefix candidate", "α", &im.kouho[2]);
checkstr(t, "local candidate", "C", &im.kouho[3]);
CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "prefix popup query", "a", &dc.pre);
CT_EQ_INT(t, 4, dc.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
checkstr(t, "English alias", "smile", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspaced query", "smil", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke('e', 0, &com));
CT_CHECK(t, keystroke('2', 0, &com));
checkstr(t, "selected English alias", "😄", &com);
CT_EQ_INT(t, LangKO, im.l->lang);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "SMILE", &com)){
checkstr(t, "folded English alias", "smile", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "dntdma", &com)){
checkstr(t, "localized Korean query", "웃음", &search.text);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "popup query", "웃음", &dc.pre);
CT_EQ_INT(t, 3, dc.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
checkstr(t, "cancel commit", "", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
searchend(&f);
}
void
engine_emoji_japanese_and_multirune(struct ct *t)
{
Searchfix f;
Str com;
emojibegin(&f, 0);
init();
im.l = getlang(LangJP);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "egao", &com)){
checkstr(t, "localized Hiragana query", "えがお", &search.text);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
init();
im.l = getlang(LangJPK);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "egao", &com)){
checkstr(t, "localized Katakana query", "エガオ", &search.text);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
init();
im.l = getlang(LangEN);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "heart", &com)){
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "variation-selector emoji", "❤️", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "coffee", &com)){
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "multi-codepoint emoji", "☕️", &com);
}
searchend(&f);
}
void
engine_emoji_digit_aliases(struct ct *t)
{
static const struct {
char *name;
Rune prefix;
Rune digit;
char *want;
} cases[] = {
{ "^1", '^', '1', "¹" },
{ "_2", '_', '2', "" },
{ "<3", '<', '3', "" },
};
Searchfix f;
Str com;
int i;
emojibegin(&f, 0);
for(i = 0; i < nelem(cases); i++){
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke(cases[i].prefix, Mshift, &com));
CT_CHECK(t, im.nkouho >= cases[i].digit - '0');
CT_CHECK(t, keystroke(cases[i].digit, 0, &com));
checkstr(t, cases[i].name, cases[i].want, &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke('^', Mshift, &com));
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "zero query", "^0", &search.text);
checkstr(t, "zero does not commit", "", &com);
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "zero alias", "", &com);
sclear(&com);
im.l = getlang(LangEN);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
CT_CHECK(t, keystroke('9', 0, &com));
checkstr(t, "missing number query", "smile9", &search.text);
checkstr(t, "missing number commit", "", &com);
CT_CHECK(t, search.lang);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
searchend(&f);
}
void
engine_emoji_navigation(struct ct *t)
{
Drawcmd dc = {0};
Searchfix f;
Str com;
emojibegin(&f, 0);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "many", &com)){
CT_EQ_INT(t, 12, im.nkouho);
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kpgdown, 0, &com));
CT_EQ_INT(t, Maxdisp, im.sel);
CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "second-page first row", "c10", &dc.kouho[0]);
checkstr(t, "second-page last row", "c12", &dc.kouho[2]);
CT_EQ_INT(t, 3, dc.nkouho);
CT_EQ_INT(t, 0, dc.sel);
CT_CHECK(t, keystroke('1', 0, &com));
checkstr(t, "second-page row one", "c10", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Ktab, Mshift, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Ktab, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
searchend(&f);
}
void
engine_search_candidate_keys(struct ct *t)
{
Searchfix f;
Str com;
emojibegin(&f, 0);
im.l = getlang(LangEN);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "search default Enter", "😀", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "missing", &com)){
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "search query Enter", "missing", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke('^', Mshift, &com));
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "search zero query", "^0", &search.text);
checkstr(t, "search zero commit", "", &com);
CT_CHECK(t, search.lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "many", &com)){
im.sel = Maxdisp;
CT_CHECK(t, keystroke('1', Mshift, &com));
checkstr(t, "modified search digit commit", "", &com);
checkstr(t, "modified search digit query", "many1", &search.text);
CT_CHECK(t, search.lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
searchend(&f);
}
void
engine_emoji_preedit_languages(struct ct *t)
{
Searchfix f;
Str com, shown;
emojibegin(&f, 0);
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, !keystroke(0, 0, &com));
impre(&im, &shown);
checkstr(t, "modifier preserves Japanese", "", &shown);
CT_CHECK(t, keystroke('E', Mctrl, &com));
checkstr(t, "committed Japanese preedit", "", &com);
CT_CHECK(t, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, !keystroke('E', Mctrl|Malt, &com));
CT_EQ_INT(t, 0, search.lang);
CT_CHECK(t, keystroke('E', Mctrl, &com));
CT_CHECK(t, !keystroke('E', Mctrl|Msuper, &com));
CT_EQ_INT(t, 0, search.lang);
CT_CHECK(t, keystroke('E', Mctrl, &com));
CT_CHECK(t, !keystroke('V', Mctrl|Mshift, &com));
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke('E', Mctrl, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke('a', 0, &com));
CT_CHECK(t, !keystroke(0, 0, &com));
CT_CHECK(t, search.lang);
checkstr(t, "modifier preserves search", "a", &search.text);
CT_CHECK(t, !keystroke('P', Mctrl|Mshift, &com));
CT_CHECK(t, !search.lang);
sclear(&com);
if(typekeys(t, "na", &com)){
impre(&im, &shown);
checkstr(t, "Japanese resumed", "", &shown);
}
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rk", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "committed Korean preedit", "", &com);
if(typekeys(t, "smile", &com)){
CT_CHECK(t, keystroke('1', 0, &com));
checkstr(t, "Korean selection", "가😀", &com);
sclear(&com);
if(typekeys(t, "rk", &com))
checkstr(t, "Korean resumed", "", &im.pre);
}
}
init();
im.l = getlang(LangVI);
sclear(&com);
if(typekeys(t, "as", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "committed Telex preedit", "á", &com);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, !search.lang);
CT_EQ_PTR(t, getlang(LangVI), im.l);
sclear(&com);
if(typekeys(t, "as", &com)){
impre(&im, &shown);
checkstr(t, "Telex resumed", "á", &shown);
checkstr(t, "Telex raw resumed", "as", &im.raw);
}
}
searchend(&f);
}
void
engine_emoji_start_and_unknown(struct ct *t)
{
Searchfix f;
Str com;
char want[Maxrunes+1];
int i;
emojibegin(&f, 0);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xyz", &com)){
/* Keys that match nothing show what they type in the language. */
CT_CHECK(t, !keystroke(Kspec|0x51, 0, &com));
checkstr(t, "special commits shown query", "", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
im.l = getlang(LangEN);
/* A language switch commits the query like any other pending text. */
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xy", &com)){
CT_CHECK(t, keystroke('t', Mctrl, &com));
checkstr(t, "language switch commits query", "xy", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangEN, im.l->lang);
}
/* So do the search keys, ending or changing the search. */
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xy", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "search key commits query", "xy", &com);
CT_CHECK(t, !search.lang);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xy", &com)){
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "other search commits query", "xy", &com);
CT_EQ_INT(t, LangHANJA, search.lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
for(i = 0; i < Maxrunes; i++)
CT_CHECK(t, keystroke('q', 0, &com));
CT_EQ_INT(t, Maxrunes, search.raw.n);
CT_CHECK(t, search.lang);
CT_CHECK(t, !keystroke('z', 0, &com));
memset(want, 'q', Maxrunes);
want[Maxrunes] = '\0';
checkstr(t, "bounded query commit", want, &com);
CT_CHECK(t, !search.lang);
searchend(&f);
}
void
engine_hanja_search(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangKO);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
CT_EQ_INT(t, LangHANJA, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
if(!typekeys(t, "gks", &com))
goto cleanup;
checkstr(t, "Hanja reading", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "navigated Hanja", "", &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
cleanup:
searchend(&f);
}
void
engine_hanja_unknown_and_cancel(struct ct *t)
{
static const struct {
char *keys;
char *text;
} empty[] = {
{ "rmf", "" },
{ "r", "" },
{ "gksrmf", "한글" },
};
Searchfix f;
Str com;
int i;
hanjabegin(&f, LangKO);
for(i = 0; i < nelem(empty); i++){
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)) ||
!typekeys(t, empty[i].keys, &com))
goto cleanup;
checkstr(t, "Hanja reading without candidates",
empty[i].text, &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Hanja commit without candidates",
empty[i].text, &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
if(!typekeys(t, "gks", &com))
goto cleanup;
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
checkstr(t, "cancelled Hanja", "", &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_EQ_INT(t, LangHANJA, search.lang);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_EQ_INT(t, 0, search.lang);
cleanup:
searchend(&f);
}
void
engine_hanja_korean_keys(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangKO);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke('r', Mshift, &com));
CT_CHECK(t, keystroke('k', 0, &com));
checkstr(t, "shifted Hanja reading", "", &search.text);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke('R', 0, &com));
CT_CHECK(t, keystroke('k', 0, &com));
checkstr(t, "Caps Lock Hanja reading", "", &search.text);
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
searchend(&f);
}
void
engine_hanja_backspace(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangKO);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
if(!typekeys(t, "gks", &com))
goto cleanup;
checkstr(t, "Hanja before backspace", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Hanja final backspace", "", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Hanja vowel backspace", "", &search.text);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "empty Hanja search", "", &search.text);
CT_EQ_INT(t, LangHANJA, search.lang);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, search.lang);
/* Ctrl+H converts the pending syllable: it seeds the query. */
if(!typekeys(t, "gks", &com))
goto cleanup;
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "nothing committed before Hanja", "", &com);
checkstr(t, "pending syllable is the query", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "seed backspaced", "", &search.text);
CT_EQ_INT(t, LangHANJA, search.lang);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
/* Escape gives the seed back; keys typed compose from it. */
if(!typekeys(t, "gks", &com))
goto cleanup;
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, 0, search.lang);
checkstr(t, "seed pending again", "", &im.pre);
checkstr(t, "nothing committed", "", &com);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "seed 하", "", &search.text);
CT_CHECK(t, keystroke('s', 0, &com));
checkstr(t, "key composes with the seed", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
checkstr(t, "seed alone comes back", "", &im.pre);
CT_CHECK(t, !keystroke(Kesc, 0, &com));
checkstr(t, "Korean Escape commits", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
cleanup:
searchend(&f);
}
void
engine_hanja_input_languages(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangEN);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
if(!typekeys(t, "gks", &com))
goto cleanup;
checkstr(t, "English Hanja input", "gks", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "English Hanja commit", "gks", &com);
CT_EQ_INT(t, LangEN, im.l->lang);
init();
im.l = getlang(LangJP);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
if(!typekeys(t, "ka", &com))
goto cleanup;
checkstr(t, "Japanese Hanja input", "", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Japanese Hanja commit", "", &com);
CT_EQ_INT(t, LangJP, im.l->lang);
/* The keyboard's own keys: 한자 seeds like Ctrl+H, 한/영 toggles. */
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "gks", &com)){
CT_CHECK(t, keystroke(Khanja, 0, &com));
checkstr(t, "Hanja key seed", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Khangul, 0, &com));
checkstr(t, "Hangul key commits the query", "", &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Khangul, 0, &com));
CT_EQ_INT(t, LangKO, im.l->lang);
CT_CHECK(t, keystroke(Kzenkaku, 0, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke(Kkana, 0, &com));
CT_EQ_INT(t, LangJPK, im.l->lang);
CT_CHECK(t, keystroke(Kkana, 0, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke(Kzenkaku, 0, &com));
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Khenkan, 0, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, keystroke(Khenkan, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kmuhenkan, 0, &com));
checkstr(t, "無変換 commits", "", &com);
CT_EQ_INT(t, LangEN, im.l->lang);
}
}
init();
im.l = getlang(LangEN);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke(L'', 0, &com));
checkstr(t, "direct Hanja input", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "direct Hanja selection", "", &com);
CT_EQ_INT(t, LangEN, im.l->lang);
init();
im.l = getlang(LangJP);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
if(!typekeys(t, "ka", &com))
goto cleanup;
CT_CHECK(t, !keystroke('e', Mctrl|Malt, &com));
checkstr(t, "modified search boundary", "", &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang);
sclear(&com);
CT_CHECK(t, !keystroke('h', Mctrl|Msuper, &com));
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang);
cleanup:
searchend(&f);
}
static u32int
stressrand(u32int *state)
{
*state = *state * 1664525U + 1013904223U;
return *state;
}
static int
stressstr(Str *s)
{
return s->n >= 0 && s->n <= Maxrunes;
}
void
engine_randomized_stress(struct ct *t)
{
static Rune text[] = {
'a', 'e', 'g', 'i', 'j', 'k', 'm', 'n', 'o', 'r', 's', 't',
'u', 'w', 'y', '1', '2', '9', '\'', ' ', L'',
};
static Rune modes[] = { 't', 'n', 'k', 's', 'v' };
Searchfix f;
Keyres res;
Caret pos;
u32int rnd, mod;
char contexts[3];
void *owner, *owners[3];
int i, j, op;
emojibegin(&f, 0);
owners[0] = &contexts[0];
owners[1] = &contexts[1];
owners[2] = &contexts[2];
rnd = 0x5eed1234U;
for(i = 0; i < 16000; i++){
rnd = stressrand(&rnd);
owner = owners[(rnd >> 8) % nelem(owners)];
op = rnd % 28;
mod = (rnd >> 16) & Mmask;
memset(&pos, 0, sizeof pos);
pos.valid = (rnd >> 25) & 1;
pos.x = (rnd >> 3) & 0x3ff;
pos.y = (rnd >> 13) & 0x3ff;
pos.h = 1 + ((rnd >> 18) & 0x1f);
if(op < 13)
res = ownerrequestat(owner, Keypress,
text[(rnd >> 5) % nelem(text)], mod, &pos);
else if(op == 13)
res = ownerrequestat(owner, Keypress, Kback, 0, &pos);
else if(op == 14)
res = ownerrequestat(owner, Keypress, Kret, 0, &pos);
else if(op == 15)
res = ownerrequestat(owner, Keypress, Kesc, 0, &pos);
else if(op == 16)
res = ownerrequestat(owner, Keypress, Kdown, 0, &pos);
else if(op == 17)
res = ownerrequestat(owner, Keypress, Kup, 0, &pos);
else if(op == 18)
res = ownerrequestat(owner, Keypress, Ktab,
(rnd & 0x1000) ? Mshift : 0, &pos);
else if(op == 19)
res = ownerrequestat(owner, Keypress,
modes[(rnd >> 12) % nelem(modes)], Mctrl, &pos);
else if(op == 20)
res = ownerrequestat(owner, Keypress, 'e', Mctrl, &pos);
else if(op == 21)
res = ownerrequest(owner, Keyreset, 0, 0);
else if(op == 22)
res = ownerrequest(owner, Keyrelease, 0, 0);
else if(op == 23)
res = ownerrequestat(owner, Keycaret, 0, 0, &pos);
else if(op == 24)
res = ownerrequest(owner, Keypress,
0, 0);
else if(op == 25)
res = ownerrequestat(owner, Keypress, 'p', Mctrl, &pos);
else if(op == 26)
res = ownerrequestat(owner, Keypress, 0xf008, 0, &pos);
else
res = ownerrequestat(owner, Keypress,
'1' + ((rnd >> 11) % 9), 0, &pos);
if(!stressstr(&res.commit) || !stressstr(&res.preedit) ||
!stressstr(&im.pre) || !stressstr(&im.raw) ||
!stressstr(&search.raw) || !stressstr(&search.text) ||
im.l == nil || im.nkouho < 0 || im.nkouho > Maxkouho ||
im.sel < -1 || im.sel >= Maxkouho ||
(im.nkouho > 0 && im.sel >= im.nkouho)){
CT_ERRORF(t, "invalid engine state after stress operation %d", i);
break;
}
for(j = 0; j < im.nkouho; j++)
if(!stressstr(&im.kouho[j])){
CT_ERRORF(t, "invalid candidate after stress operation %d", i);
i = 16000;
break;
}
if(activeowner != nil && activeowner != owners[0] &&
activeowner != owners[1] && activeowner != owners[2]){
CT_ERRORF(t, "unknown active owner after stress operation %d", i);
break;
}
}
searchend(&f);
}
void
engine_full_boundary_passthrough(struct ct *t)
{
Str com, shown;
int i;
init();
im.l = getlang(LangJP);
for(i = 0; i < Maxrunes; i++)
im.pre.r[i] = L'';
im.pre.n = Maxrunes;
sclear(&com);
CT_CHECK(t, !keystroke(L'', 0, &com));
CT_EQ_INT(t, Maxrunes, com.n);
for(i = 0; i < Maxrunes; i++)
CT_EQ_UINT(t, L'', com.r[i]);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
init();
im.l = getlang(LangJP);
for(i = 0; i < Maxrunes-1; i++)
im.pre.r[i] = L'';
im.pre.n = Maxrunes-1;
sclear(&com);
CT_CHECK(t, !keystroke(L'', 0, &com));
CT_EQ_INT(t, Maxrunes-1, com.n);
CT_EQ_INT(t, 0, im.pre.n);
/* Typing on at the limit commits the kana and keeps the syllable. */
init();
im.l = getlang(LangJP);
for(i = 0; i < Maxrunes-1; i++)
im.pre.r[i] = L'';
im.pre.n = Maxrunes-1;
im.raw = mkstr("k");
sclear(&com);
CT_CHECK(t, keystroke('a', 0, &com));
CT_EQ_INT(t, Maxrunes-1, com.n);
impre(&im, &shown);
checkstr(t, "syllable after the flush", "", &shown);
}