Files
strans/tests/engine_test.c

2036 lines
49 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 "cutest/cutest.h"
extern Lang testvi;
Str mkstr(char*);
int checkstr(struct ct*, char*, char*, Str*);
Str shownpre(Im*);
static int typekeys(struct ct*, char*, Str*);
static int draindraw(Drawcmd*);
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(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));
CT_EQ_INT(t, 0, com.n);
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));
shown = shownpre(&im);
checkstr(t, "before backspace", "", &shown);
im.kouho[0] = mkstr("stale-candidate");
im.nkouho = 1;
im.sel = 0;
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspace commit", "", &com);
checkstr(t, "pending romaji after backspace", "n", &im.raw);
shown = shownpre(&im);
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 = 9;
selected = im.kouho[9];
sclear(&com);
CT_CHECK(t, !keystroke('1', mods[i], &com));
CT_CHECK(t, scmp(&com, &selected) != 0);
}
}
void
engine_dictionary_queue_latest_wins(struct ct *t)
{
Channel *saved;
Dictreq old, got;
Str key;
saved = dictreqc;
dictreqc = chancreate(sizeof(Dictreq), 1);
memset(&old, 0, sizeof old);
old.key = mkstr("old-key");
old.pre = mkstr("old-preedit");
old.lang = LangJP;
old.seq = 0;
CT_CHECK(t, channbsend(dictreqc, &old) > 0);
init();
im.l = getlang(LangJP);
im.pre = mkstr("new-preedit");
key = mkstr("new-key");
dictsend(&im, &key);
if(CT_CHECK(t, channbrecv(dictreqc, &got) > 0)){
checkstr(t, "newest dictionary key", "new-key", &got.key);
checkstr(t, "newest dictionary preedit", "new-preedit", &got.pre);
CT_EQ_INT(t, LangJP, got.lang);
CT_EQ_UINT(t, dictseq, got.seq);
}
CT_CHECK(t, channbrecv(dictreqc, &got) <= 0);
chanfree(dictreqc);
dictreqc = saved;
}
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);
shown = shownpre(&im);
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);
shown = shownpre(&im);
checkstr(t, "stale release preserves owner", "にゃ", &shown);
CT_EQ_PTR(t, &b, activeowner);
ownerrequest(&b, Keyrelease, 0, 0);
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);
CT_EQ_INT(t, 0, res.preedit.n);
CT_CHECK(t, !caret.valid);
res = ownerrequest(&owner, Keypress, 'n', 0);
checkstr(t, "same owner after reset", "", &res.preedit);
}
void
engine_rejects_stale_dictionary_results(struct ct *t)
{
Dictreq a, b;
Dictres res;
Hmap *saved;
Lang *jp;
char one, two;
int n;
jp = getlang(LangJP);
saved = jp->dict;
jp->dict = hmapalloc(1);
init();
im.l = jp;
while(channbrecv(dictreqc, &a) > 0)
;
ownerrequest(&one, Keypress, 'k', 0);
ownerrequest(&one, Keypress, 'a', 0);
n = 0;
while(channbrecv(dictreqc, &a) > 0)
n++;
if(!CT_CHECK(t, n > 0))
goto cleanup;
ownerrequest(&one, Keyreset, 0, 0);
ownerrequest(&two, Keypress, 'k', 0);
ownerrequest(&two, Keypress, 'a', 0);
n = 0;
while(channbrecv(dictreqc, &b) > 0)
n++;
if(!CT_CHECK(t, n > 0))
goto cleanup;
CT_EQ_INT(t, 0, scmp(&a.key, &b.key));
CT_EQ_INT(t, 0, scmp(&a.pre, &b.pre));
CT_EQ_INT(t, a.lang, b.lang);
CT_CHECK(t, a.seq != b.seq);
memset(&res, 0, sizeof res);
res.key = a.pre;
res.lang = a.lang;
res.seq = a.seq;
res.kouho[0] = mkstr("stale");
res.nkouho = 1;
dictresult(&res);
CT_EQ_INT(t, 0, im.nkouho);
res.key = b.pre;
res.lang = b.lang;
res.seq = b.seq;
res.kouho[0] = mkstr("current");
dictresult(&res);
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "current dictionary result", "current", &im.kouho[0]);
cleanup:
while(channbrecv(dictreqc, &a) > 0)
;
hmapfree(jp->dict);
jp->dict = saved;
}
void
engine_active_owner_caret(struct ct *t)
{
Caret a, moved;
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();
im.l = getlang(LangJP);
ownerrequestat(&one, Keypress, 'k', 0, &a);
CT_CHECK(t, caret.valid);
CT_EQ_INT(t, 10, caret.x);
ownerrequestat(&two, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 10, caret.x);
ownerrequestat(&one, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 80, caret.x);
ownerrequest(&two, Keypress, 'n', 0);
CT_EQ_PTR(t, &two, activeowner);
CT_CHECK(t, !caret.valid);
}
void
engine_commit_contract(struct ct *t)
{
Str com;
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('q', 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);
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, "ka", &com))
return;
CT_CHECK(t, keystroke(0xf008, 0, &com));
CT_EQ_INT(t, 2, com.n);
CT_EQ_UINT(t, 0x304b, com.r[0]);
CT_EQ_UINT(t, 0xf008, com.r[1]);
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_EQ_PTR(t, nil, im.l->dictq);
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", Kret, 0, LangVI, "", "", "á", 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 = 0;
}
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;
Hmap *dict;
int activecap;
int visible;
int candidatechosen;
};
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)
{
Dictres res;
char name[8];
int i;
memset(&res, 0, sizeof res);
for(i = 0; i < n; i++){
snprint(name, sizeof name, "c%d", i+1);
res.kouho[i] = mkstr(name);
}
res.nkouho = n;
setkouho(&res);
}
static void
candidatebegin(int n)
{
init();
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[] = {
{ 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 },
{ 32, 8, 0, 9, 8 },
{ 32, 17, 9, 9, 8 },
{ 32, 26, 18, 9, 8 },
{ 32, 31, 27, 5, 4 },
};
Drawcmd dc;
int i;
/* A clearing draw is the observable zero-candidate 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);
}
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)
{
static int totals[] = { 0, 1, 9, 10, 18, 19, 32 };
int i, n;
for(i = 0; i < nelem(totals); i++){
candidatebegin(totals[i]);
CT_EQ_INT(t, totals[i] == 0 ? -1 : 0, im.sel);
if(totals[i] == 0){
redraw();
CT_EQ_INT(t, 0, draindraw(nil));
continue;
}
n = totals[i] < Maxdisp ? totals[i] : Maxdisp;
checkcandidatepage(t, "default candidate page", 0, n, 0);
}
}
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 };
Dictres res;
Str com;
int arrived, i;
for(i = 0; i < nelem(keys); i++)
for(arrived = 0; arrived < 2; arrived++){
init();
im.l = getlang(LangJP);
im.pre = mkstr("reading");
memset(&res, 0, sizeof res);
res.key = im.pre;
res.lang = LangJP;
res.seq = dictseq;
res.kouho[0] = mkstr("candidate");
res.nkouho = 1;
if(arrived)
dictresult(&res);
sclear(&com);
CT_CHECK(t, keystroke(keys[i], 0, &com));
if(!arrived)
dictresult(&res);
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(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);
candidatebegin(32);
im.sel = 27;
sclear(&com);
CT_CHECK(t, !keystroke('9', 0, &com));
checkstr(t, "unavailable short-page digit", "", &com);
CT_EQ_INT(t, 32, im.nkouho);
CT_EQ_INT(t, 27, im.sel);
}
static void
setdict(Hmap **dict, char *key, char *val)
{
Str s;
s = mkstr(key);
hmapset(dict, &s, 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->visible = visible;
f->candidatechosen = candidatechosen;
draindraw(nil);
}
static void
emojibegin(Searchfix *f, int showpre)
{
searchsave(f, LangEMOJI);
f->dictlang->dict = hmapalloc(32);
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)
{
Hmap *dict;
dict = f->dictlang->dict;
f->dictlang->dict = f->dict;
hmapfree(dict);
draindraw(nil);
im = f->im;
search = f->search;
activecap = f->activecap;
visible = f->visible;
candidatechosen = f->candidatechosen;
}
void
engine_popup_preedit_capability(struct ct *t)
{
Dictres res;
Drawcmd dc;
Keyres kres;
char client, inactive;
init();
im.l = getlang(LangJP);
draindraw(nil);
ownerrequestcap(&client, Cclientpreedit, Keypress, 'k', 0);
ownerrequestcap(&client, Cclientpreedit, Keypress, 'a', 0);
memset(&res, 0, sizeof res);
res.lang = LangJP;
res.key = mkstr("");
res.seq = dictseq;
res.kouho[0] = mkstr("");
res.nkouho = 1;
dictresult(&res);
im.sel = 0;
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, 0, 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, 0, dc.sel);
checkstr(t, "popup candidate", "", &dc.kouho[0]);
}
CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, 0, 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, 0, dc.sel);
checkstr(t, "restored client candidate", "", &dc.kouho[0]);
}
CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, 0, 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);
}
static void
hanjabegin(Searchfix *f, int lang)
{
searchsave(f, LangHANJA);
f->dictlang->dict = hmapalloc(8);
setdict(&f->dictlang->dict, "", "漢 韓");
setdict(&f->dictlang->dict, "", "");
setdict(&f->dictlang->dict, "", "");
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", "んにゃ" },
{ "kin'youbi", "きんようび" },
};
static const struct {
char *raw;
char *want;
} replay[] = {
{ "kitte", "きって" },
{ "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);
shown = shownpre(&im);
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), &raw, &shown);
checkstr(t, replay[i].raw, replay[i].want, &shown);
}
}
static int
newestrequest(Dictreq *req)
{
int n;
n = 0;
while(channbrecv(dictreqc, req) > 0)
n++;
return n;
}
void
engine_japanese_candidates(struct ct *t)
{
Dictreq req;
Dictres res;
Hmap *saved;
Lang *jp;
Str com, key, shown;
jp = getlang(LangJP);
saved = jp->dict;
jp->dict = hmapalloc(8);
key = mkstr("かんじ");
hmapset(&jp->dict, &key, "漢字 幹事", strlen("漢字 幹事"));
init();
im.l = jp;
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
shown = shownpre(&im);
checkstr(t, "complete Japanese reading", "かんじ", &shown);
if(!CT_CHECK(t, newestrequest(&req) > 0))
goto cleanup;
checkstr(t, "dictionary lookup key", "かんじ", &req.key);
checkstr(t, "dictionary request identity", "かんじ", &req.pre);
dictlookup(&req, &res);
dictresult(&res);
if(!CT_EQ_INT(t, 2, im.nkouho))
goto cleanup;
CT_EQ_INT(t, 0, 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, 1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "selected Kanji", "幹事", &com);
cleanup:
newestrequest(&req);
hmapfree(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));
shown = shownpre(&im);
checkstr(t, "backspace pending i", "かんj", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
shown = shownpre(&im);
checkstr(t, "backspace pending j", "かん", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
shown = shownpre(&im);
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(' ', 0, &com));
checkstr(t, "real input boundary", "かんじ ", &com);
}
}
void
engine_katakana_sequences(struct ct *t)
{
static const struct {
char *keys;
char *want;
} cases[] = {
{ "kitte", "キッテ" },
{ "katta", "カッタ" },
{ "gakkou", "ガッコウ" },
{ "matcha", "マッチャ" },
{ "xu", "" },
{ "vu", "" },
};
Dictreq req;
Str com, shown;
int i;
newestrequest(&req);
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);
shown = shownpre(&im);
checkstr(t, cases[i].keys, cases[i].want, &shown);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "committed Katakana", cases[i].want, &com);
}
CT_EQ_INT(t, 0, newestrequest(&req));
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(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, 3, im.nkouho);
checkstr(t, "raw candidate", "A", &im.kouho[0]);
checkstr(t, "deduplicated candidate", "B", &im.kouho[1]);
checkstr(t, "local candidate", "C", &im.kouho[2]);
CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "prefix popup query", "a", &dc.pre);
CT_EQ_INT(t, 3, 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);
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(Kret, 0, &com));
checkstr(t, "missing number text", "smile9", &com);
}
searchend(&f);
}
void
engine_emoji_navigation(struct ct *t)
{
Drawcmd dc = {0};
Searchfix f;
Str com;
int i;
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);
for(i = 0; i < Maxdisp; i++)
CT_CHECK(t, keystroke(Kdown, 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, "many", &com)){
for(i = 0; i < 11; i++)
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 11, im.sel);
CT_CHECK(t, keystroke('3', 0, &com));
checkstr(t, "short-page row three", "c12", &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(Kdown, 0, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Kup, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "navigated candidate", "😀", &com);
}
searchend(&f);
}
void
engine_search_candidate_keys(struct ct *t)
{
Searchfix f;
Str com;
emojibegin(&f, 0);
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));
shown = shownpre(&im);
checkstr(t, "modifier preserves Japanese", "", &shown);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &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|Mshift, &com));
CT_CHECK(t, !keystroke('E', Mctrl|Msuper, &com));
CT_EQ_INT(t, 0, search.lang);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &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)){
shown = shownpre(&im);
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)){
shown = shownpre(&im);
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);
if(typekeys(t, "dkssud", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "committed Korean preedit", "안녕", &com);
CT_CHECK(t, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
if(typekeys(t, "xyz", &com)){
checkstr(t, "unknown displayed query", "xyz", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "unknown query commit", "안녕xyz", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xyz", &com)){
CT_CHECK(t, !keystroke(Kspec|0x51, 0, &com));
checkstr(t, "special commits shown query", "xyz", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
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_emoji_dictionary_identity(struct ct *t)
{
Dictres res;
Searchfix f;
emojibegin(&f, 0);
im.pre = mkstr("same");
im.kouho[0] = mkstr("old");
im.nkouho = 1;
im.sel = 0;
memset(&res, 0, sizeof res);
res.key = im.pre;
res.seq = dictseq;
res.kouho[0] = mkstr("wrong");
res.nkouho = 1;
res.lang = LangJP;
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "wrong language ignored", "old", &im.kouho[0]);
res.lang = LangKO;
res.key = mkstr("other");
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "wrong preedit ignored", "old", &im.kouho[0]);
res.key = im.pre;
search.lang = LangEMOJI;
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "search response ignored", "old", &im.kouho[0]);
search.lang = 0;
res.kouho[0] = mkstr("right");
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, 0, im.sel);
checkstr(t, "matching response accepted", "right", &im.kouho[0]);
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)
{
Searchfix f;
Str com;
hanjabegin(&f, LangKO);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
if(!typekeys(t, "rmf", &com))
goto cleanup;
checkstr(t, "unknown Hanja reading", "", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "unknown Hanja commit", "", &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, "r", &com))
goto cleanup;
checkstr(t, "incomplete Hanja reading", "", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "incomplete Hanja commit", "", &com);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
if(!typekeys(t, "gksrmf", &com))
goto cleanup;
checkstr(t, "multiple Hanja syllables", "한글", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "multiple Hanja syllable commit", "한글", &com);
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);
if(!typekeys(t, "gks", &com))
goto cleanup;
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "preedit committed before Hanja", "", &com);
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);
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);
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;
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(' ', 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(' ', 0, &com));
CT_EQ_INT(t, Maxrunes, com.n);
CT_EQ_UINT(t, ' ', com.r[Maxrunes-1]);
}