#include #include static void wirecommit(xcb_im_t*, xcb_im_input_context_t*, uint32_t, const char*, uint32_t, uint32_t); static void wirestart(xcb_im_t*, xcb_im_input_context_t*); static void wiredraw(xcb_im_t*, xcb_im_input_context_t*, xcb_im_preedit_draw_fr_t*); static void wiredone(xcb_im_t*, xcb_im_input_context_t*); static void* wiregetdata(xcb_im_input_context_t*); static int wireflush(xcb_connection_t*); #define xcb_im_commit_string wirecommit #define xcb_im_preedit_start_callback wirestart #define xcb_im_preedit_draw_callback wiredraw #define xcb_im_preedit_done_callback wiredone #define xcb_im_input_context_get_data wiregetdata #define xcb_flush wireflush #include "xim/xim.c" #undef xcb_im_commit_string #undef xcb_im_preedit_start_callback #undef xcb_im_preedit_draw_callback #undef xcb_im_preedit_done_callback #undef xcb_im_input_context_get_data #undef xcb_flush #include "cutest/cutest.h" enum { Wstart, Wdraw, Wdone, Wcommit, Maxwire = 64, }; typedef struct Wirecall Wirecall; struct Wirecall { int op; xcb_im_input_context_t *ic; u32int caret; u32int first; u32int changed; u32int status; u32int flag; u32int keysym; int nbyte; int nfeedback; uchar text[Maxutf]; u32int feedback[Maxrunes]; }; typedef struct Icbinding Icbinding; struct Icbinding { xcb_im_input_context_t *ic; void *data; }; static Wirecall wirecalls[Maxwire]; static Icbinding icbindings[8]; static int nwirecalls; static int nflush; static Wirecall* newwire(int op, xcb_im_input_context_t *ic) { Wirecall *call; if(nwirecalls >= nelem(wirecalls)) return nil; call = &wirecalls[nwirecalls++]; memset(call, 0, sizeof *call); call->op = op; call->ic = ic; return call; } static void wirecommit(xcb_im_t *im, xcb_im_input_context_t *ic, uint32_t flag, const char *text, uint32_t nbyte, uint32_t keysym) { Wirecall *call; USED(im); call = newwire(Wcommit, ic); if(call == nil) return; call->flag = flag; call->keysym = keysym; call->nbyte = min(nbyte, sizeof call->text); if(call->nbyte > 0) memmove(call->text, text, call->nbyte); } static void wirestart(xcb_im_t *im, xcb_im_input_context_t *ic) { USED(im); newwire(Wstart, ic); } static void wiredraw(xcb_im_t *im, xcb_im_input_context_t *ic, xcb_im_preedit_draw_fr_t *frame) { Wirecall *call; USED(im); call = newwire(Wdraw, ic); if(call == nil) return; call->caret = frame->caret; call->first = frame->chg_first; call->changed = frame->chg_length; call->status = frame->status; call->nbyte = min(frame->length_of_preedit_string, sizeof call->text); if(call->nbyte > 0) memmove(call->text, frame->preedit_string, call->nbyte); call->nfeedback = min(frame->feedback_array.size, nelem(call->feedback)); if(call->nfeedback > 0) memmove(call->feedback, frame->feedback_array.items, call->nfeedback * sizeof call->feedback[0]); } static void wiredone(xcb_im_t *im, xcb_im_input_context_t *ic) { USED(im); newwire(Wdone, ic); } static void* wiregetdata(xcb_im_input_context_t *ic) { int i; for(i = 0; i < nelem(icbindings); i++) if(icbindings[i].ic == ic) return icbindings[i].data; return nil; } static int wireflush(xcb_connection_t *c) { USED(c); nflush++; return 1; } static void wireclear(void) { memset(wirecalls, 0, sizeof wirecalls); memset(icbindings, 0, sizeof icbindings); nwirecalls = 0; nflush = 0; preowner = nil; } static void wirebind(int n, xcb_im_input_context_t *ic, Ic *state) { icbindings[n].ic = ic; icbindings[n].data = state; } void testengineinit(int); void testenginehandle(Keyreq*); void* testengineowner(void); void testenginepreedit(Str*); typedef struct Ximfix Ximfix; struct Ximfix { Channel *oldreply; Channel *trace; Channel *stop; Channel *done; Channel *waiting; Channel *ack; int holdrelease; int pumpactive; }; typedef struct Freejob Freejob; struct Freejob { Ic *state; Channel *done; }; static void enginepump(void *arg) { Ximfix *f; Keyreq req; uchar token; Alt alts[] = { {keyc, &req, CHANRCV, nil}, {nil, &token, CHANRCV, nil}, {nil, nil, CHANEND, nil}, }; f = arg; alts[1].c = f->stop; for(;;) switch(alt(alts)){ case 0: chansend(f->trace, &req); if(f->holdrelease && req.op == Keyrelease){ token = 0; chansend(f->waiting, &token); chanrecv(f->ack, &token); } testenginehandle(&req); break; case 1: chansend(f->done, &token); return; } } static int ximbegin(struct ct *t, Ximfix *f, int lang) { Drawcmd dc; memset(f, 0, sizeof *f); while(channbrecv(drawc, &dc) > 0) ; testengineinit(lang); f->oldreply = replyc; replyc = chancreate(sizeof(Keyres), 0); f->trace = chancreate(sizeof(Keyreq), 16); f->stop = chancreate(sizeof(uchar), 0); f->done = chancreate(sizeof(uchar), 0); f->waiting = chancreate(sizeof(uchar), 0); f->ack = chancreate(sizeof(uchar), 0); f->pumpactive = threadcreate(enginepump, f, 8192) >= 0; return CT_CHECK(t, replyc != nil && f->trace != nil && f->stop != nil && f->done != nil && f->waiting != nil && f->ack != nil && f->pumpactive); } static void ximend(Ximfix *f) { Drawcmd dc; uchar token; if(f->pumpactive){ token = 0; chansend(f->stop, &token); chanrecv(f->done, &token); } testengineinit(LangEN); while(channbrecv(drawc, &dc) > 0) ; chanfree(replyc); replyc = f->oldreply; chanfree(f->trace); chanfree(f->stop); chanfree(f->done); chanfree(f->waiting); chanfree(f->ack); } static Keyreq nexttrace(struct ct *t, Ximfix *f, int op, Ic *state) { Keyreq req; memset(&req, 0, sizeof req); if(!CT_CHECK(t, channbrecv(f->trace, &req) > 0)) return req; CT_EQ_INT(t, op, req.op); CT_EQ_PTR(t, state, req.owner); CT_EQ_INT(t, state->cap, req.cap); CT_EQ_PTR(t, replyc, req.reply); return req; } static void notrace(struct ct *t, Ximfix *f) { Keyreq req; CT_CHECK(t, channbrecv(f->trace, &req) <= 0); } static void checkstr(struct ct *t, char *want, Str *got) { char buf[Maxutf]; stoutf(got, buf, sizeof buf); CT_EQ_STR(t, want, buf); } static void checkenginepreedit(struct ct *t, char *want) { Str preedit; testenginepreedit(&preedit); checkstr(t, want, &preedit); } void xim_adapter_key_contract(struct ct *t) { Ximfix f; Ic state; Keyreq req; Keyres res; u32int allmod; memset(&state, 0, sizeof state); if(!ximbegin(t, &f, LangJP)) goto cleanup; allmod = ~0; keypress(&state, 'x', allmod, &res); req = nexttrace(t, &f, Keypress, &state); CT_EQ_UINT(t, Mmask, req.mod); CT_CHECK(t, !res.eaten); keypress(&state, 'k', 0, &res); req = nexttrace(t, &f, Keypress, &state); CT_EQ_UINT(t, 0, req.mod); CT_CHECK(t, res.eaten); keypress(&state, 'a', 0, &res); nexttrace(t, &f, Keypress, &state); CT_CHECK(t, res.eaten); checkstr(t, "か", &res.preedit); keypress(&state, Kspec|0x55, 0, &res); nexttrace(t, &f, Keypress, &state); CT_CHECK(t, !res.eaten); checkstr(t, "か", &res.commit); checkstr(t, "", &res.preedit); cleanup: ximend(&f); } void xim_adapter_release_lifecycle(struct ct *t) { Ximfix f; Ic a, b, *dead; Keyres res; memset(&a, 0, sizeof a); memset(&b, 0, sizeof b); dead = nil; if(!ximbegin(t, &f, LangJP)) goto cleanup; keypress(&a, 'k', 0, &res); nexttrace(t, &f, Keypress, &a); keypress(&a, 'a', 0, &res); nexttrace(t, &f, Keypress, &a); keypress(&b, 'n', 0, &res); nexttrace(t, &f, Keypress, &b); checkstr(t, "ん", &res.preedit); CT_EQ_PTR(t, &b, testengineowner()); /* ResetIC and focus loss use the same idempotent release path. */ release(&a); nexttrace(t, &f, Keyrelease, &a); checkenginepreedit(t, "ん"); CT_EQ_PTR(t, &b, testengineowner()); release(&a); notrace(t, &f); release(&b); nexttrace(t, &f, Keyrelease, &b); CT_EQ_PTR(t, nil, testengineowner()); keypress(&b, 'k', 0, &res); nexttrace(t, &f, Keypress, &b); release(&b); nexttrace(t, &f, Keyrelease, &b); release(&b); notrace(t, &f); dead = calloc(1, sizeof *dead); if(!CT_CHECK(t, dead != nil)) goto cleanup; dead->next = ics; ics = dead; keypress(dead, 'n', 0, &res); nexttrace(t, &f, Keypress, dead); CT_EQ_PTR(t, dead, testengineowner()); icfree(dead); nexttrace(t, &f, Keyrelease, dead); dead = nil; CT_EQ_PTR(t, nil, ics); CT_EQ_PTR(t, nil, testengineowner()); cleanup: if(dead != nil){ icunlink(dead); free(dead); } ximend(&f); } static void freeworker(void *arg) { Freejob *job; uchar token; job = arg; icfree(job->state); token = 0; chansend(job->done, &token); } void xim_adapter_free_waits_for_release(struct ct *t) { Ximfix f; Freejob job; Ic *state; Channel *freed; Keyres res; uchar token; int workeractive; state = nil; freed = nil; workeractive = 0; if(!ximbegin(t, &f, LangJP)) goto cleanup; state = calloc(1, sizeof *state); freed = chancreate(sizeof(uchar), 0); if(!CT_CHECK(t, state != nil && freed != nil)) goto cleanup; state->next = ics; ics = state; keypress(state, 'n', 0, &res); nexttrace(t, &f, Keypress, state); f.holdrelease = 1; job.state = state; job.done = freed; workeractive = threadcreate(freeworker, &job, 8192) >= 0; if(!CT_CHECK(t, workeractive)) goto cleanup; chanrecv(f.waiting, &token); nexttrace(t, &f, Keyrelease, state); CT_CHECK(t, channbrecv(freed, &token) <= 0); CT_EQ_PTR(t, state, ics); CT_EQ_PTR(t, state, testengineowner()); chansend(f.ack, &token); chanrecv(freed, &token); workeractive = 0; state = nil; CT_EQ_PTR(t, nil, ics); CT_EQ_PTR(t, nil, testengineowner()); cleanup: if(workeractive){ chansend(f.ack, &token); chanrecv(freed, &token); state = nil; } if(state != nil){ icunlink(state); free(state); } if(freed != nil) chanfree(freed); ximend(&f); } void xim_adapter_styles(struct ct *t) { static u32int want[] = { XCB_IM_PreeditPosition | XCB_IM_StatusNothing, XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing, XCB_IM_PreeditNothing | XCB_IM_StatusNothing, }; Ic state; int cap, i; CT_EQ_SIZE(t, nelem(want), nelem(styles)); for(i = 0; i < nelem(want); i++){ CT_EQ_UINT(t, want[i], styles[i]); CT_CHECK(t, stylecap(want[i], &cap)); CT_EQ_INT(t, i == 1 ? Cclientpreedit : 0, cap); CT_CHECK(t, !(styles[i] & XCB_IM_PreeditNone)); } CT_CHECK(t, !stylecap(XCB_IM_PreeditNone | XCB_IM_StatusNothing, &cap)); CT_CHECK(t, !stylecap(XCB_IM_PreeditCallbacks | XCB_IM_StatusCallbacks, &cap)); CT_EQ_INT(t, 0, cap); memset(&state, 0, sizeof state); setstyle(&state, XCB_IM_PreeditNone | XCB_IM_StatusNothing); CT_EQ_UINT(t, XCB_IM_PreeditNothing | XCB_IM_StatusNothing, state.style); CT_EQ_INT(t, 0, state.cap); setstyle(&state, XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing); CT_EQ_INT(t, Cclientpreedit, state.cap); } void xim_adapter_encoding_negotiation(struct ct *t) { xcb_im_packet_header_fr_t hdr; xcb_im_client_t *a, *b; u16int selected; a = (xcb_im_client_t*)(uintptr)1; b = (xcb_im_client_t*)(uintptr)2; memset(clientenc, 0, sizeof clientenc); CT_EQ_SIZE(t, 2, nelem(encs)); CT_EQ_STR(t, "COMPOUND_TEXT", encs[0]); CT_EQ_STR(t, "UTF8_STRING", encs[1]); memset(&hdr, 0, sizeof hdr); hdr.major_opcode = XCB_XIM_ENCODING_NEGOTIATION; selected = 1; callback(nil, a, nil, &hdr, nil, &selected, nil); selected = 0; callback(nil, b, nil, &hdr, nil, &selected, nil); CT_EQ_INT(t, Eutf8, getencoding(a)); CT_EQ_INT(t, Ecompound, getencoding(b)); hdr.major_opcode = XCB_XIM_DISCONNECT; callback(nil, a, nil, &hdr, nil, nil, nil); CT_EQ_INT(t, Ecompound, getencoding(a)); CT_EQ_INT(t, Ecompound, getencoding(b)); hdr.major_opcode = XCB_XIM_CLOSE; callback(nil, b, nil, &hdr, nil, nil, nil); CT_EQ_PTR(t, nil, clientenc[0].client); CT_EQ_PTR(t, nil, clientenc[1].client); } void xim_adapter_callback_replacement(struct ct *t) { Ic state; Str pre; xcb_im_input_context_t *ic; int i; ic = (xcb_im_input_context_t*)(uintptr)3; memset(&state, 0, sizeof state); state.xic = ic; state.style = XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing; state.encoding = Eutf8; wireclear(); sinit(&pre, "k", strlen("k")); updatepreedit(&state, &pre); sinit(&pre, "한😀", strlen("한😀")); updatepreedit(&state, &pre); sclear(&pre); updatepreedit(&state, &pre); CT_EQ_INT(t, 5, nwirecalls); CT_EQ_INT(t, Wstart, wirecalls[0].op); CT_EQ_INT(t, Wdraw, wirecalls[1].op); CT_EQ_UINT(t, 0, wirecalls[1].first); CT_EQ_UINT(t, 0, wirecalls[1].changed); CT_EQ_UINT(t, 1, wirecalls[1].caret); CT_EQ_INT(t, 1, wirecalls[1].nfeedback); CT_EQ_INT(t, Wdraw, wirecalls[2].op); CT_EQ_UINT(t, 0, wirecalls[2].first); CT_EQ_UINT(t, 1, wirecalls[2].changed); CT_EQ_UINT(t, 2, wirecalls[2].caret); CT_EQ_INT(t, strlen("한😀"), wirecalls[2].nbyte); CT_EQ_MEM(t, "한😀", wirecalls[2].text, wirecalls[2].nbyte); CT_EQ_INT(t, 2, wirecalls[2].nfeedback); for(i = 0; i < wirecalls[2].nfeedback; i++) CT_EQ_UINT(t, XCB_XIM_UNDERLINE, wirecalls[2].feedback[i]); CT_EQ_INT(t, Wdraw, wirecalls[3].op); CT_EQ_UINT(t, 2, wirecalls[3].changed); CT_EQ_UINT(t, 1, wirecalls[3].status); CT_EQ_INT(t, 0, wirecalls[3].nbyte); CT_EQ_INT(t, Wdone, wirecalls[4].op); CT_CHECK(t, !state.prestarted); CT_EQ_INT(t, 0, state.nprerune); CT_EQ_PTR(t, nil, preowner); CT_EQ_INT(t, 1, nflush); } void xim_adapter_callback_unicode(struct ct *t) { static char *text[] = { "한글", "かなカナ", "漢字", "𠀋", "😀", "❤️", "👩‍💻", }; Ic state; Str pre; Wirecall *call; char *want; size_t nwant; int i; xcb_compound_text_init(); for(i = 0; i < nelem(text); i++){ memset(&state, 0, sizeof state); state.xic = (xcb_im_input_context_t*)(uintptr)(10+i); state.style = XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing; state.encoding = Eutf8; sinit(&pre, text[i], strlen(text[i])); wireclear(); updatepreedit(&state, &pre); CT_EQ_INT(t, 2, nwirecalls); call = &wirecalls[1]; CT_EQ_INT(t, Wdraw, call->op); CT_EQ_UINT(t, pre.n, call->caret); CT_EQ_INT(t, pre.n, call->nfeedback); CT_EQ_INT(t, strlen(text[i]), call->nbyte); CT_EQ_MEM(t, text[i], call->text, call->nbyte); memset(&state, 0, sizeof state); state.xic = (xcb_im_input_context_t*)(uintptr)(20+i); state.style = XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing; state.encoding = Ecompound; wireclear(); updatepreedit(&state, &pre); want = ximcompound(text[i], strlen(text[i]), &nwant); if(!CT_CHECK(t, want != nil)) continue; call = &wirecalls[1]; CT_EQ_SIZE(t, nwant, call->nbyte); CT_EQ_MEM(t, want, call->text, nwant); CT_EQ_UINT(t, pre.n, call->caret); CT_EQ_INT(t, pre.n, call->nfeedback); free(want); } } static void startstate(Ic *state, xcb_im_input_context_t *ic) { Str pre; memset(state, 0, sizeof *state); state->xic = ic; state->style = XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing; state->encoding = Eutf8; sinit(&pre, "あ", strlen("あ")); updatepreedit(state, &pre); } void xim_adapter_callback_cleanup(struct ct *t) { static int opcode[] = { XCB_XIM_RESET_IC, XCB_XIM_UNSET_IC_FOCUS, XCB_XIM_DESTROY_IC, }; xcb_im_packet_header_fr_t hdr; xcb_im_client_t *client; xcb_im_input_context_t *ic; Ic state, *dead; int i; for(i = 0; i < nelem(opcode); i++){ ic = (xcb_im_input_context_t*)(uintptr)(30+i); wireclear(); wirebind(0, ic, &state); startstate(&state, ic); memset(&hdr, 0, sizeof hdr); hdr.major_opcode = opcode[i]; callback(nil, nil, ic, &hdr, nil, nil, nil); CT_EQ_INT(t, 4, nwirecalls); CT_EQ_INT(t, Wdraw, wirecalls[2].op); CT_EQ_UINT(t, 1, wirecalls[2].changed); CT_EQ_INT(t, Wdone, wirecalls[3].op); CT_CHECK(t, !state.prestarted); CT_EQ_PTR(t, nil, preowner); } client = (xcb_im_client_t*)(uintptr)40; ic = (xcb_im_input_context_t*)(uintptr)40; wireclear(); startstate(&state, ic); state.client = client; state.next = ics; ics = &state; hdr.major_opcode = XCB_XIM_DISCONNECT; callback(nil, client, nil, &hdr, nil, nil, nil); ics = state.next; CT_EQ_INT(t, Wdraw, wirecalls[2].op); CT_EQ_INT(t, Wdone, wirecalls[3].op); CT_CHECK(t, !state.prestarted); ic = (xcb_im_input_context_t*)(uintptr)41; wireclear(); dead = calloc(1, sizeof *dead); if(!CT_CHECK(t, dead != nil)) return; dead->next = ics; ics = dead; startstate(dead, ic); icfree(dead); CT_EQ_INT(t, 4, nwirecalls); CT_EQ_INT(t, Wdraw, wirecalls[2].op); CT_EQ_INT(t, Wdone, wirecalls[3].op); CT_EQ_PTR(t, nil, preowner); } void xim_adapter_callback_transfer(struct ct *t) { Ximfix f; Ic a, b; xcb_im_input_context_t *aic; Keyres res; Str pre; aic = (xcb_im_input_context_t*)(uintptr)50; memset(&a, 0, sizeof a); memset(&b, 0, sizeof b); if(!ximbegin(t, &f, LangJP)) goto cleanup; wireclear(); a.xic = aic; a.style = XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing; a.encoding = Eutf8; sinit(&pre, "あ", strlen("あ")); updatepreedit(&a, &pre); keypress(&b, 'x', 0, &res); nexttrace(t, &f, Keypress, &b); CT_EQ_INT(t, 4, nwirecalls); CT_EQ_INT(t, Wdraw, wirecalls[2].op); CT_EQ_INT(t, Wdone, wirecalls[3].op); CT_CHECK(t, !a.prestarted); CT_EQ_PTR(t, nil, preowner); release(&b); nexttrace(t, &f, Keyrelease, &b); cleanup: preowner = nil; ximend(&f); } void xim_adapter_commit_encoding(struct ct *t) { static char text[] = "한𠀋❤️👩‍💻"; Ic state; xcb_im_input_context_t *ic; char *want; size_t nwant; ic = (xcb_im_input_context_t*)(uintptr)60; memset(&state, 0, sizeof state); state.xic = ic; state.encoding = Eutf8; wireclear(); commit(&state, text, strlen(text)); CT_EQ_INT(t, 1, nwirecalls); CT_EQ_INT(t, Wcommit, wirecalls[0].op); CT_EQ_UINT(t, XCB_XIM_LOOKUP_CHARS, wirecalls[0].flag); CT_EQ_UINT(t, 0, wirecalls[0].keysym); CT_EQ_INT(t, strlen(text), wirecalls[0].nbyte); CT_EQ_MEM(t, text, wirecalls[0].text, wirecalls[0].nbyte); xcb_compound_text_init(); state.encoding = Ecompound; wireclear(); commit(&state, text, strlen(text)); want = ximcompound(text, strlen(text), &nwant); if(!CT_CHECK(t, want != nil)) return; CT_EQ_INT(t, 1, nwirecalls); CT_EQ_INT(t, Wcommit, wirecalls[0].op); CT_EQ_SIZE(t, nwant, wirecalls[0].nbyte); CT_EQ_MEM(t, want, wirecalls[0].text, nwant); free(want); }