#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ipc.h" #include "live.h" enum { Eventtimeout = 4000, }; typedef struct Run Run; typedef struct Prelog Prelog; struct Run { Live l; Daemon xvfb; Daemon daemon; }; struct Prelog { int start; int nonempty; int empty; int done; int doneafterempty; int badorder; }; static int xerror; static int waitsocket(Run *r) { struct stat st; int64_t deadline; deadline = nowms() + Starttimeout; while(leftms(deadline) > 0){ if(lstat(r->l.socket, &st) == 0 && S_ISSOCK(st.st_mode) && (st.st_mode & 0777) == 0600) return 1; if(!daemonalive(&r->daemon)) return fail("daemon exited before creating its IPC socket"); pausems(20); } return fail("timed out waiting for daemon IPC socket"); } static int start(Run *r, char *daemon, char *mapdir, char *xvfb) { memset(r, 0, sizeof *r); daemoninit(&r->xvfb, "Xvfb"); daemoninit(&r->daemon, "daemon"); if(!livesetup(&r->l, "xim") || !startxvfb(&r->l, &r->xvfb, xvfb)) return 0; if(setenv("DISPLAY", r->l.display, 1) < 0 || setenv("XMODIFIERS", "@im=strans", 1) < 0) return fail("set private environment: %s", strerror(errno)); if(!startdaemon(&r->l, &r->daemon, daemon, mapdir)) return 0; return waitsocket(r); } static void cleanup(Run *r, int noisy) { stopdaemon(&r->daemon); stopdaemon(&r->xvfb); if(noisy){ showerrors(&r->daemon); showerrors(&r->xvfb); } closeerrors(&r->daemon); closeerrors(&r->xvfb); liveclean(&r->l); } static int xerr(Display *dpy, XErrorEvent *e) { char msg[128]; xerror++; XGetErrorText(dpy, e->error_code, msg, sizeof msg); fail("X error: %s (request %d.%d)", msg, e->request_code, e->minor_code); return 0; } static int pumpinput(Display *dpy, XIC ic, char *commit, size_t cap, int timeout) { struct pollfd pfd; XEvent ev; KeySym sym; Status status; char buf[Ipcfieldmax+1]; int n; XFlush(dpy); if(XPending(dpy) == 0){ pfd.fd = ConnectionNumber(dpy); pfd.events = POLLIN; pfd.revents = 0; while(poll(&pfd, 1, timeout) < 0 && errno == EINTR) ; } while(XPending(dpy) != 0){ XNextEvent(dpy, &ev); if(XFilterEvent(&ev, None) || ic == NULL || ev.type != KeyPress) continue; n = Xutf8LookupString(ic, &ev.xkey, buf, sizeof buf - 1, &sym, &status); if(n < 0 || (status == XLookupChars && ((size_t)n >= cap || commit == NULL))) return 0; if(status == XLookupChars && n != 0){ memcpy(commit, buf, n); commit[n] = '\0'; } } return 1; } static void pump(Display *dpy, int timeout) { pumpinput(dpy, NULL, NULL, 0, timeout); } static int prestart(XIC ic, XPointer data, XPointer call) { Prelog *p; (void)ic; (void)call; p = (Prelog*)data; if(p->start != p->done) p->badorder++; p->start++; return Ipcfieldmax; } static void predraw(XIM im, XPointer data, XPointer call) { Prelog *p; XIMPreeditDrawCallbackStruct *draw; (void)im; p = (Prelog*)data; draw = (XIMPreeditDrawCallbackStruct*)call; if(draw != NULL && draw->text != NULL && draw->text->length != 0){ if(p->start != p->done + 1) p->badorder++; p->nonempty++; }else{ if(p->start != p->done + 1) p->badorder++; p->empty++; } } static void predone(XIM im, XPointer data, XPointer call) { Prelog *p; (void)im; (void)call; p = (Prelog*)data; if(p->empty > p->done) p->doneafterempty++; else p->badorder++; p->done++; } static void precaret(XIM im, XPointer data, XPointer call) { (void)im; (void)data; (void)call; } static XIC callbackic(XIM im, Window win, Prelog *log) { union { int (*start)(XIC, XPointer, XPointer); XIMProc xim; } fn; XIMCallback start, draw, done, caret; XVaNestedList pre; XIC ic; start.client_data = (XPointer)log; fn.start = prestart; start.callback = fn.xim; draw.client_data = (XPointer)log; draw.callback = predraw; done.client_data = (XPointer)log; done.callback = predone; caret.client_data = (XPointer)log; caret.callback = precaret; pre = XVaCreateNestedList(0, XNPreeditStartCallback, &start, XNPreeditDrawCallback, &draw, XNPreeditDoneCallback, &done, XNPreeditCaretCallback, &caret, NULL); if(pre == NULL) return NULL; ic = XCreateIC(im, XNInputStyle, XIMPreeditCallbacks|XIMStatusNothing, XNClientWindow, win, XNFocusWindow, win, XNPreeditAttributes, pre, NULL); XFree(pre); return ic; } static int sendkey(Display *dpy, XIC ic, Window win, KeySym sym, unsigned int state, char *commit, size_t cap) { XKeyPressedEvent key; XEvent ev; KeySym got; Status status; char buf[Ipcfieldmax+1]; int n; if(cap != 0) commit[0] = '\0'; memset(&key, 0, sizeof key); key.type = KeyPress; key.display = dpy; key.window = win; key.root = DefaultRootWindow(dpy); key.time = CurrentTime; key.x = key.y = key.x_root = key.y_root = 1; key.same_screen = True; key.keycode = XKeysymToKeycode(dpy, sym); key.state = state; memset(&ev, 0, sizeof ev); ev.xkey = key; if(XFilterEvent(&ev, None)){ return pumpinput(dpy, ic, commit, cap, 20); } n = Xutf8LookupString(ic, &key, buf, sizeof buf - 1, &got, &status); if(n < 0) return fail("Xutf8LookupString failed for keysym %#lx", (unsigned long)sym); if(status == XLookupChars && n != 0){ if((size_t)n >= cap) return fail("commit buffer overflow"); memcpy(commit, buf, n); commit[n] = '\0'; } return pumpinput(dpy, ic, commit, cap, 20); } static int waitcommit(Display *dpy, XIC ic, char *commit, size_t cap) { int64_t deadline; deadline = nowms() + Eventtimeout; while(leftms(deadline) > 0){ if(!pumpinput(dpy, ic, commit, cap, leftms(deadline))) return fail("read committed XIM text"); if(commit[0] != '\0') return 1; } return 0; } static int waitpreedit(Display *dpy, Prelog *p) { int64_t deadline; deadline = nowms() + Eventtimeout; while(leftms(deadline) > 0){ pump(dpy, leftms(deadline)); if(p->empty != 0 || p->done != 0 || p->badorder != 0) return -1; if(p->start != 0 && p->nonempty != 0) return 1; } return 0; } static XIM openim(Display *dpy) { XIM im; int64_t deadline; deadline = nowms() + Starttimeout; do{ im = XOpenIM(dpy, NULL, "strans", "Strans"); if(im != NULL) return im; pump(dpy, 20); }while(leftms(deadline) > 0); return NULL; } static int testcallbacks(Display *dpy, XIM im, Window win) { char commit[Ipcfieldmax+1], *reset; Prelog log; XIC ic; memset(&log, 0, sizeof log); ic = callbackic(im, win, &log); if(ic == NULL) return fail("create PreeditCallbacks input context"); XSetICFocus(ic); if(!sendkey(dpy, ic, win, XK_s, ControlMask, commit, sizeof commit) || !sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit)){ XDestroyIC(ic); return 0; } if(waitpreedit(dpy, &log) != 1){ int start, draw, empty, done; start = log.start; draw = log.nonempty; empty = log.empty; done = log.done; XDestroyIC(ic); return fail("invalid callback preedit: start=%d draw=%d empty=%d done=%d", start, draw, empty, done); } pump(dpy, 50); if(log.start != 1 || log.nonempty < 1 || log.done != 0 || log.empty != 0 || log.badorder != 0){ int start, draw, empty, done, ordered, bad; start = log.start; draw = log.nonempty; empty = log.empty; done = log.done; ordered = log.doneafterempty; bad = log.badorder; XDestroyIC(ic); return fail("bad initial callback sequence: start=%d draw=%d empty=%d done=%d ordered=%d bad=%d", start, draw, empty, done, ordered, bad); } reset = Xutf8ResetIC(ic); if(reset == NULL || strcmp(reset, "ㄱ") != 0){ if(reset != NULL) XFree(reset); XDestroyIC(ic); return fail("ResetIC did not return pending preedit"); } XFree(reset); if(!sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit) || !sendkey(dpy, ic, win, XK_k, 0, commit, sizeof commit) || !sendkey(dpy, ic, win, XK_Return, 0, commit, sizeof commit)){ XDestroyIC(ic); return 0; } if(commit[0] == '\0' && !waitcommit(dpy, ic, commit, sizeof commit)){ XDestroyIC(ic); return fail("input context did not compose after ResetIC"); } if(strcmp(commit, "가") != 0){ XDestroyIC(ic); return fail("post-reset composition did not contain 가"); } XUnsetICFocus(ic); XDestroyIC(ic); pump(dpy, 100); if(log.badorder != 0) return fail("bad callback order after ResetIC"); return 1; } /* * The popup is the one viewable override-redirect window on the private * display; created before the client window, it must still stack above it. */ static int popupabove(Display *dpy, Window client) { XWindowAttributes wa; Window root, parent, *kids; unsigned i, n; int64_t deadline; int above; deadline = nowms() + Eventtimeout; for(;;){ above = 0; root = DefaultRootWindow(dpy); if(!XQueryTree(dpy, root, &root, &parent, &kids, &n)) return fail("query the window tree"); for(i = 0; i < n; i++){ if(kids[i] == client) above = 1; else if(XGetWindowAttributes(dpy, kids[i], &wa) && wa.override_redirect && wa.map_state == IsViewable) break; } XFree(kids); if(i < n) return above || fail("popup is stacked below the client"); if(leftms(deadline) == 0) return fail("popup did not appear"); pump(dpy, 20); } } static int testnothing(Display *dpy, XIM im, Window win) { char commit[Ipcfieldmax+1]; XIC ic; ic = XCreateIC(im, XNInputStyle, XIMPreeditNothing|XIMStatusNothing, XNClientWindow, win, XNFocusWindow, win, NULL); if(ic == NULL) return fail("create PreeditNothing input context"); XSetICFocus(ic); if(!sendkey(dpy, ic, win, XK_s, ControlMask, commit, sizeof commit)){ XDestroyIC(ic); return 0; } pump(dpy, 100); if(!sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit) || !popupabove(dpy, win) || !sendkey(dpy, ic, win, XK_k, 0, commit, sizeof commit) || !sendkey(dpy, ic, win, XK_Return, 0, commit, sizeof commit)){ XDestroyIC(ic); return 0; } if(commit[0] == '\0' && !waitcommit(dpy, ic, commit, sizeof commit)){ XDestroyIC(ic); return fail("PreeditNothing composition produced no commit"); } if(strcmp(commit, "\352\260\200") != 0){ XDestroyIC(ic); return fail("PreeditNothing commit did not contain 가"); } XUnsetICFocus(ic); XDestroyIC(ic); return 1; } static int testposition(Display *dpy, XIM im, Window client, Window focus) { char commit[Ipcfieldmax+1]; char **missing, *def; int nmissing; XPoint spot; XVaNestedList pre; XFontSet fontset; XIC ic; missing = NULL; nmissing = 0; def = NULL; fontset = XCreateFontSet(dpy, "fixed", &missing, &nmissing, &def); if(missing != NULL) XFreeStringList(missing); if(fontset == NULL) return fail("create XIM position font set"); spot.x = 7; spot.y = 11; pre = XVaCreateNestedList(0, XNSpotLocation, &spot, XNFontSet, fontset, NULL); if(pre == NULL){ XFreeFontSet(dpy, fontset); return fail("create XNSpotLocation list"); } ic = XCreateIC(im, XNInputStyle, XIMPreeditPosition|XIMStatusNothing, XNClientWindow, client, XNFocusWindow, focus, XNPreeditAttributes, pre, NULL); XFree(pre); if(ic == NULL){ XFreeFontSet(dpy, fontset); return fail("create nested PreeditPosition input context"); } XSetICFocus(ic); if(!sendkey(dpy, ic, focus, XK_s, ControlMask, commit, sizeof commit) || !sendkey(dpy, ic, focus, XK_r, 0, commit, sizeof commit)){ XDestroyIC(ic); XFreeFontSet(dpy, fontset); return 0; } spot.x = 13; spot.y = 17; pre = XVaCreateNestedList(0, XNSpotLocation, &spot, NULL); if(pre == NULL || XSetICValues(ic, XNPreeditAttributes, pre, NULL) != NULL){ if(pre != NULL) XFree(pre); XDestroyIC(ic); XFreeFontSet(dpy, fontset); return fail("update nested XNSpotLocation"); } XFree(pre); if(!sendkey(dpy, ic, focus, XK_Escape, 0, commit, sizeof commit)){ XDestroyIC(ic); XFreeFontSet(dpy, fontset); return 0; } XUnsetICFocus(ic); XDestroyIC(ic); XFreeFontSet(dpy, fontset); return 1; } int main(int argc, char **argv) { Display *dpy; Window client, focus, root; XIM im; Run run; int ok; char *xvfb; testname = "xim_live_test"; if(argc != 3 && argc != 4){ fprintf(stderr, "usage: xim_live_test daemon mapdir [Xvfb]\n"); return 2; } xvfb = argc == 4 ? argv[3] : "Xvfb"; ok = start(&run, argv[1], argv[2], xvfb); if(!ok){ cleanup(&run, 1); return 1; } if(setlocale(LC_CTYPE, "C.UTF-8") == NULL || !XSupportsLocale() || XSetLocaleModifiers("@im=strans") == NULL){ fail("initialize UTF-8 X locale for strans"); cleanup(&run, 1); return 1; } XSetErrorHandler(xerr); dpy = XOpenDisplay(run.l.display); if(dpy == NULL){ fail("open private display %s", run.l.display); cleanup(&run, 1); return 1; } im = openim(dpy); if(im == NULL){ fail("open strans XIM on private display %s", run.l.display); XCloseDisplay(dpy); cleanup(&run, 1); return 1; } root = DefaultRootWindow(dpy); client = XCreateSimpleWindow(dpy, root, 41, 53, 240, 100, 0, 0, 0); focus = XCreateSimpleWindow(dpy, client, 17, 19, 160, 50, 0, 0, 0); XSelectInput(dpy, client, KeyPressMask|StructureNotifyMask); XSelectInput(dpy, focus, KeyPressMask|StructureNotifyMask); XMapWindow(dpy, client); XMapWindow(dpy, focus); XSync(dpy, False); ok = testcallbacks(dpy, im, client) && testnothing(dpy, im, client) && testposition(dpy, im, client, focus); XDestroyWindow(dpy, focus); XDestroyWindow(dpy, client); XCloseIM(im); XSync(dpy, False); XCloseDisplay(dpy); if(xerror != 0){ fail("observed %d X protocol errors", xerror); ok = 0; } cleanup(&run, !ok); if(!ok) return 1; printf("xim_live_test: ok\n"); return 0; }