Files
strans/tests/engine_test.c
Hojun-Cho 47f9801ad8 engine: a search just begun shows itself
Ctrl+E and Ctrl+H gave no sign until a key produced candidates; with
nothing pending the popup even went away.  An empty search now shows
☺ or 漢 in the popup, so the user knows the next keys are a query.
2026-08-17 01:45:27 +09:00

2177 lines
55 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 = &testvi;
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 = &testvi;
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 = &testvi;
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, "pending romaji after backspace", "n", &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.cap = 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);
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, Kmodfirst, 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 = &testvi;
draindraw(nil);
sclear(&com);
for(k = cases[i].keys; *k != '\0'; k++){
res = ownerrequestcap(&owner, Cclientpreedit,
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);
activecap = 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, Cclientpreedit,
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 = &testvi;
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)
{
Str com;
init();
sclear(&com);
CT_EQ_INT(t, LangEN, 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->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);
}
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];
Lang *vi;
Trie *saved;
Str com;
int eaten, i;
vi = getlang(LangVI);
if(!CT_CHECK(t, vi != nil))
return;
saved = vi->map;
vi->map = testvi.map;
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 = &testvi;
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);
}
vi->map = saved;
}
typedef struct Searchfix Searchfix;
struct Searchfix
{
Im im;
Search search;
Lang *dictlang;
Trie *dict;
int activecap;
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)
{
static Rune keys[] = { Kret, Ktab };
Str com;
int i;
for(i = 0; i < nelem(keys); i++){
init();
im.l = getlang(LangJP);
im.pre = mkstr("reading");
setcandidates(1);
sclear(&com);
CT_CHECK(t, keystroke(keys[i], 0, &com));
checkstr(t, "unselected candidate commits reading",
"reading", &com);
CT_EQ_INT(t, 0, im.nkouho);
}
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 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->activecap = activecap;
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);
activecap = showpre ? 0 : Cclientpreedit;
}
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;
activecap = f->activecap;
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, Cclientpreedit, Keypress, 'k', 0);
ownerrequestcap(&client, Cclientpreedit, 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, Cclientpreedit, 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, activecap & Cclientpreedit);
CT_EQ_INT(t, 0, draindraw(nil));
kres = ownerrequestcap(&client, Cclientpreedit, 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, Cclientpreedit, 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));
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));
CT_EQ_INT(t, 0, im.nkouho);
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 pending i", "かんj", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "backspace pending j", "かん", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "backspace accumulated kana", "", &shown);
}
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(Kmodfirst+2, 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(Kmodfirst+2, 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 = &testvi;
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, &testvi, 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;
Lang *vi;
Trie *vimap;
Keyres res;
Caret pos;
u32int rnd, mod;
char contexts[3];
void *owner, *owners[3];
int i, j, op;
vi = getlang(LangVI);
if(!CT_CHECK(t, vi != nil))
return;
vimap = vi->map;
vi->map = testvi.map;
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,
Kmodfirst + ((rnd >> 20) & 7), 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);
vi->map = vimap;
}
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);
}