#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ipc.h" #include "live.h" enum { Maxevent = 256, Timeout = 4000, }; enum { Ecap, Ecaret, Ekey, Ereset, Esurround, }; enum { Rnormal, Rcommitpre, Rbadcommit, Rbadpreedit, }; typedef struct Event Event; typedef struct Server Server; typedef struct Siglog Siglog; struct Event { int type; int want; int valid; uint32_t key; int32_t x; int32_t y; int32_t h; char text[Ipcfieldmax+1]; }; struct Server { pthread_t thread; pthread_mutex_t lock; pthread_cond_t changed; int listenfd; int wake[2]; int stop; int old; int eaten; int stall; int response; int del; int nclose; char pre[Ipcfieldmax+1]; Event ev[Maxevent]; int nev; }; struct Siglog { char event[128]; int n; char pre[Ipcfieldmax+1]; char shown[Ipcfieldmax+1]; char commit[Ipcfieldmax+1]; int prechanged; char around[64]; /* what the widget answers retrieve-surrounding */ int aroundcursor; int deloffset; int deln; }; static void record(Server *s, Event *e) { pthread_mutex_lock(&s->lock); if(s->nev < Maxevent) s->ev[s->nev++] = *e; pthread_cond_broadcast(&s->changed); pthread_mutex_unlock(&s->lock); } static void closed(Server *s) { pthread_mutex_lock(&s->lock); s->nclose++; pthread_cond_broadcast(&s->changed); pthread_mutex_unlock(&s->lock); } static int sendresponse(Server *s, int fd, int eaten, int want, int key) { unsigned char buf[Ipcmaxresp]; const char *commit; char pre[Ipcfieldmax+1]; int del, n, response, stall; size_t ncommit, npreedit; pthread_mutex_lock(&s->lock); memcpy(pre, s->pre, sizeof pre); response = s->response; stall = s->stall; del = key ? s->del : 0; pthread_mutex_unlock(&s->lock); if(stall) return 1; commit = ""; ncommit = 0; npreedit = strlen(pre); if(key && response == Rcommitpre){ commit = pre; ncommit = npreedit; }else if(key && response == Rbadcommit){ commit = "\xc3("; ncommit = 2; }else if(key && response == Rbadpreedit){ pre[0] = 'x'; pre[1] = 0; pre[2] = 'y'; npreedit = 3; } n = ipcpackresp(buf, sizeof buf, eaten, del, commit, ncommit, pre, npreedit, want); return n >= 0 && ipcsend(fd, buf, n) == 0; } static int request(Server *s, int fd) { unsigned char buf[Ipccaretsz]; uint32_t mod, key; Event e; size_t n; int old, type, want; if(ipcreadn(fd, buf, Ipcreqsz) < 0) return 0; type = ipcreqtype(buf); memset(&e, 0, sizeof e); switch(type){ case Ipccap: e.type = Ecap; ipcunpackreq(buf, &e.want, &mod, &key); record(s, &e); pthread_mutex_lock(&s->lock); old = s->old; pthread_mutex_unlock(&s->lock); return sendresponse(s, fd, !old, e.want, 0); case Ipccaret: if(ipcreadn(fd, buf + Ipcreqsz, Ipccaretsz - Ipcreqsz) < 0 || ipcunpackcaret(buf, &e.valid, &e.x, &e.y, &e.h) < 0) return 0; e.type = Ecaret; record(s, &e); return 1; case Ipcsurround: n = ipcsurroundlen(buf); if(n > 0 && ipcreadn(fd, e.text, n) < 0) return 0; e.text[n] = '\0'; e.type = Esurround; record(s, &e); return 1; case Ipckey: ipcunpackreq(buf, &want, &mod, &key); e.want = want; e.key = key; e.type = ipcreqreset(buf) ? Ereset : Ekey; record(s, &e); pthread_mutex_lock(&s->lock); old = s->eaten; pthread_mutex_unlock(&s->lock); return sendresponse(s, fd, e.type == Ekey && old, want, e.type == Ekey); } return 0; } static void* serverthread(void *v) { Server *s; struct pollfd pfd[3]; int fd, n; char c; s = v; fd = -1; for(;;){ pfd[0].fd = s->wake[0]; pfd[0].events = POLLIN; pfd[1].fd = s->listenfd; pfd[1].events = fd < 0 ? POLLIN : 0; pfd[2].fd = fd; pfd[2].events = fd >= 0 ? POLLIN : 0; n = poll(pfd, 3, -1); if(n < 0 && errno == EINTR) continue; if(n < 0 || (pfd[0].revents & POLLIN)) break; if(pfd[1].revents & POLLIN){ fd = accept(s->listenfd, NULL, NULL); continue; } if(fd >= 0 && pfd[2].revents != 0){ if((pfd[2].revents & POLLIN) && request(s, fd)) continue; close(fd); fd = -1; closed(s); } } if(fd >= 0){ close(fd); closed(s); } while(read(s->wake[0], &c, 1) < 0 && errno == EINTR) ; return NULL; } static int startserver(Server *s, char *path) { struct sockaddr_un addr; int cond; memset(s, 0, sizeof *s); s->eaten = 1; s->listenfd = -1; s->wake[0] = s->wake[1] = -1; if(pthread_mutex_init(&s->lock, NULL) != 0) return fail("initialize server lock"); cond = pthread_cond_init(&s->changed, NULL) == 0; if(!cond){ fail("initialize server condition"); goto bad; } if(pipe(s->wake) < 0){ fail("pipe: %s", strerror(errno)); goto bad; } s->listenfd = socket(AF_UNIX, SOCK_STREAM, 0); if(s->listenfd < 0){ fail("socket: %s", strerror(errno)); goto bad; } memset(&addr, 0, sizeof addr); addr.sun_family = AF_UNIX; if(strlen(path) >= sizeof addr.sun_path){ fail("socket path is too long"); goto bad; } strcpy(addr.sun_path, path); if(bind(s->listenfd, (struct sockaddr*)&addr, sizeof addr) < 0 || listen(s->listenfd, 4) < 0){ fail("listen %s: %s", path, strerror(errno)); goto bad; } if(pthread_create(&s->thread, NULL, serverthread, s) != 0){ fail("create server thread"); goto bad; } return 1; bad: if(s->listenfd >= 0) close(s->listenfd); if(s->wake[0] >= 0) close(s->wake[0]); if(s->wake[1] >= 0) close(s->wake[1]); if(cond) pthread_cond_destroy(&s->changed); pthread_mutex_destroy(&s->lock); s->listenfd = s->wake[0] = s->wake[1] = -1; return 0; } static void stopserver(Server *s) { char c; if(s->wake[1] >= 0){ c = 0; while(write(s->wake[1], &c, 1) < 0 && errno == EINTR) ; pthread_join(s->thread, NULL); } if(s->listenfd >= 0) close(s->listenfd); if(s->wake[0] >= 0) close(s->wake[0]); if(s->wake[1] >= 0) close(s->wake[1]); pthread_cond_destroy(&s->changed); pthread_mutex_destroy(&s->lock); s->listenfd = s->wake[0] = s->wake[1] = -1; } static void setpre(Server *s, char *pre) { pthread_mutex_lock(&s->lock); snprintf(s->pre, sizeof s->pre, "%s", pre); pthread_mutex_unlock(&s->lock); } static void setold(Server *s, int old) { pthread_mutex_lock(&s->lock); s->old = old; pthread_mutex_unlock(&s->lock); } static void setreply(Server *s, int eaten, int response, int stall) { pthread_mutex_lock(&s->lock); s->eaten = eaten; s->response = response; s->stall = stall; pthread_mutex_unlock(&s->lock); } static int eventcount(Server *s) { int n; pthread_mutex_lock(&s->lock); n = s->nev; pthread_mutex_unlock(&s->lock); return n; } static int closecount(Server *s) { int n; pthread_mutex_lock(&s->lock); n = s->nclose; pthread_mutex_unlock(&s->lock); return n; } static int waitcount(Server *s, int n) { struct timespec ts; int r; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += Timeout / 1000; ts.tv_nsec += (Timeout % 1000) * 1000000; if(ts.tv_nsec >= 1000000000){ ts.tv_sec++; ts.tv_nsec -= 1000000000; } pthread_mutex_lock(&s->lock); while(s->nev < n){ r = pthread_cond_timedwait(&s->changed, &s->lock, &ts); if(r == ETIMEDOUT) break; } r = s->nev >= n; pthread_mutex_unlock(&s->lock); return r; } static int waitclose(Server *s, int n) { struct timespec ts; int r; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += Timeout / 1000; pthread_mutex_lock(&s->lock); while(s->nclose < n){ r = pthread_cond_timedwait(&s->changed, &s->lock, &ts); if(r == ETIMEDOUT) break; } r = s->nclose >= n; pthread_mutex_unlock(&s->lock); return r; } static Event getevent(Server *s, int n) { Event e; memset(&e, 0, sizeof e); pthread_mutex_lock(&s->lock); if(n >= 0 && n < s->nev) e = s->ev[n]; pthread_mutex_unlock(&s->lock); return e; } static int runquery(char *module, char *cache) { int fd, status; pid_t pid; fd = open(cache, O_WRONLY|O_CREAT|O_TRUNC, 0600); if(fd < 0) return fail("open module cache: %s", strerror(errno)); pid = fork(); if(pid < 0){ close(fd); return fail("fork gtk-query-immodules: %s", strerror(errno)); } if(pid == 0){ dup2(fd, STDOUT_FILENO); close(fd); execlp("gtk-query-immodules-3.0", "gtk-query-immodules-3.0", module, (char*)NULL); _exit(127); } close(fd); if(waitpid(pid, &status, 0) != pid || !WIFEXITED(status) || WEXITSTATUS(status) != 0) return fail("gtk-query-immodules-3.0 failed"); return 1; } static void pump(void) { while(gtk_events_pending()) gtk_main_iteration(); } static void logsig(Siglog *l, char c) { if(l->n + 1 < (int)sizeof l->event){ l->event[l->n++] = c; l->event[l->n] = '\0'; } } static void prestart(GtkIMContext *ctx, Siglog *l) { (void)ctx; logsig(l, 'S'); } static void prechange(GtkIMContext *ctx, Siglog *l) { gchar *s; gtk_im_context_get_preedit_string(ctx, &s, NULL, NULL); if(strcmp(l->pre, s) != 0) l->prechanged = 1; snprintf(l->pre, sizeof l->pre, "%s", s); g_free(s); logsig(l, 'C'); } static void preend(GtkIMContext *ctx, Siglog *l) { (void)ctx; logsig(l, 'E'); } static void commit(GtkIMContext *ctx, gchar *s, Siglog *l) { (void)ctx; snprintf(l->commit, sizeof l->commit, "%s", s); logsig(l, 'K'); } static void clearevent(Siglog *l) { l->event[0] = '\0'; l->n = 0; l->commit[0] = '\0'; l->prechanged = 0; } static void applyclient(Siglog *l) { if(l->commit[0] != '\0') l->shown[0] = '\0'; if(l->prechanged) snprintf(l->shown, sizeof l->shown, "%s", l->pre); } static void clearlog(Siglog *l) { memset(l, 0, sizeof *l); } /* The widget's half of the surrounding-text contract. */ static gboolean retrievearound(GtkIMContext *ctx, Siglog *l) { if(l->around[0] == '\0') return FALSE; gtk_im_context_set_surrounding(ctx, l->around, -1, l->aroundcursor); return TRUE; } static gboolean deletearound(GtkIMContext *ctx, gint offset, gint n, Siglog *l) { (void)ctx; l->deloffset = offset; l->deln = n; return TRUE; } static GtkIMContext* newcontext(Siglog *l) { GtkIMContext *ctx; ctx = gtk_im_multicontext_new(); gtk_im_multicontext_set_context_id(GTK_IM_MULTICONTEXT(ctx), "strans"); g_signal_connect(ctx, "retrieve-surrounding", G_CALLBACK(retrievearound), l); g_signal_connect(ctx, "delete-surrounding", G_CALLBACK(deletearound), l); g_signal_connect(ctx, "preedit-start", G_CALLBACK(prestart), l); g_signal_connect(ctx, "preedit-changed", G_CALLBACK(prechange), l); g_signal_connect(ctx, "preedit-end", G_CALLBACK(preend), l); g_signal_connect(ctx, "commit", G_CALLBACK(commit), l); return ctx; } static int keyevent(GtkIMContext *ctx, GdkWindow *win, GdkEventType type, guint keyval, guint state) { GdkEvent *ev; int r; ev = gdk_event_new(type); ev->key.window = g_object_ref(win); ev->key.send_event = TRUE; ev->key.time = GDK_CURRENT_TIME; ev->key.keyval = keyval; ev->key.state = state; r = gtk_im_context_filter_keypress(ctx, &ev->key); gdk_event_free(ev); return r; } static int keystate(GtkIMContext *ctx, GdkWindow *win, guint keyval, guint state) { return keyevent(ctx, win, GDK_KEY_PRESS, keyval, state); } static int key(GtkIMContext *ctx, GdkWindow *win, guint keyval) { return keystate(ctx, win, keyval, 0); } static GtkWidget* newwindow(int x, int y) { GtkWidget *w; w = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_default_size(GTK_WINDOW(w), 240, 120); gtk_window_move(GTK_WINDOW(w), x, y); gtk_widget_show(w); pump(); return w; } /* A GtkEntry gives its context a window of its own, under the toplevel's. */ static GdkWindow* entrywindow(GtkWidget *entry) { GdkWindow *w; GList *kids, *l; gpointer u; w = NULL; kids = gdk_window_get_children(gtk_widget_get_window(entry)); for(l = kids; l != NULL; l = l->next){ gdk_window_get_user_data(l->data, &u); if(u == entry) w = l->data; } g_list_free(kids); return w; } #define Check(c, ...) do{ if(!(c)){ fail(__VA_ARGS__); goto out; } }while(0) int main(int argc, char **argv) { Server srv; Siglog log; GtkIMContext *ctx, *oldctx, *badctx; GtkWidget *top, *entry; GdkWindow *win, *entrywin; GdkRectangle rect; Event e; static const GtkInputPurpose purposes[] = { GTK_INPUT_PURPOSE_PASSWORD, GTK_INPUT_PURPOSE_PIN, }; static const int badresponses[] = { Rbadcommit, Rbadpreedit }; char root[] = "/tmp/strans-gtk.XXXXXX"; char runtime[320] = "", socket[384] = "", cache[384] = ""; char *module; Daemon xvfb; Live l; int first, i, ox, oy, scale, closes, ok, rootmade, serverup; int64_t stalledms, started; testname = "gtk_live_test"; ctx = oldctx = badctx = NULL; top = NULL; memset(&l, 0, sizeof l); daemoninit(&xvfb, "Xvfb"); rootmade = 0; serverup = 0; ok = 0; if(argc != 2){ fail("usage: gtk_live_test /path/to/im-strans.so"); return 1; } module = realpath(argv[1], NULL); if(module == NULL){ fail("realpath %s: %s", argv[1], strerror(errno)); return 1; } Check(mkdtemp(root) != NULL, "mkdtemp: %s", strerror(errno)); rootmade = 1; Check(snprintf(runtime, sizeof runtime, "%s/runtime", root) < (int)sizeof runtime, "runtime path is too long"); Check(snprintf(socket, sizeof socket, "%s/strans.sock", runtime) < (int)sizeof socket, "socket path is too long"); Check(snprintf(cache, sizeof cache, "%s/immodules.cache", root) < (int)sizeof cache, "cache path is too long"); Check(mkdir(runtime, 0700) == 0, "mkdir runtime: %s", strerror(errno)); Check(startxvfb(&l, &xvfb, "Xvfb"), "start private Xvfb"); Check(setenv("DISPLAY", l.display, 1) == 0 && setenv("GDK_BACKEND", "x11", 1) == 0 && setenv("GDK_SCALE", "2", 1) == 0 && setenv("XDG_RUNTIME_DIR", runtime, 1) == 0, "set private environment: %s", strerror(errno)); Check(runquery(module, cache), "generate GTK module cache"); Check(setenv("GTK_IM_MODULE_FILE", cache, 1) == 0 && setenv("GTK_IM_MODULE", "strans", 1) == 0, "set GTK module environment: %s", strerror(errno)); Check(startserver(&srv, socket), "start fake daemon"); serverup = 1; gtk_init(&argc, &argv); clearlog(&log); ctx = newcontext(&log); top = newwindow(73, 41); win = gtk_widget_get_window(top); gtk_im_context_set_client_window(ctx, win); rect.x = 11; rect.y = 17; rect.width = 3; rect.height = 19; gtk_im_context_set_cursor_location(ctx, &rect); Check(eventcount(&srv) == 0, "cursor reporting opened the socket"); setpre(&srv, ""); Check(key(ctx, win, GDK_KEY_a), "initial key was not eaten"); Check(waitcount(&srv, 4), "initial protocol frames timed out"); e = getevent(&srv, 0); Check(e.type == Ecap && e.want == 1, "default preedit capability missing"); e = getevent(&srv, 1); Check(e.type == Ecaret && e.valid, "cached caret was not negotiated"); gdk_window_get_origin(win, &ox, &oy); Check(ox != 0 && oy != 0, "test window did not get a nonzero root origin"); scale = gdk_window_get_scale_factor(win); Check(scale == 2, "private Xvfb did not honor GDK_SCALE=2"); Check(e.x == (ox + rect.x) * scale && e.y == (oy + rect.y) * scale && e.h == rect.height * scale, "caret conversion got %d,%d,%d want %d,%d,%d", e.x, e.y, e.h, (ox + rect.x) * scale, (oy + rect.y) * scale, rect.height * scale); e = getevent(&srv, 2); Check(e.type == Esurround && e.text[0] == '\0', "a widget that keeps no text sent some"); e = getevent(&srv, 3); Check(e.type == Ekey && e.want == 1, "initial key framing changed"); Check(log.n == 0, "initial empty preedit emitted %s", log.event); setpre(&srv, "ㅋ"); clearlog(&log); Check(key(ctx, win, GDK_KEY_z), "first preedit key was not eaten"); applyclient(&log); Check(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "ㅋ") == 0 && strcmp(log.shown, "ㅋ") == 0, "first preedit signals were %s (preedit %s, shown %s)", log.event, log.pre, log.shown); clearevent(&log); setreply(&srv, 1, Rcommitpre, 0); Check(key(ctx, win, GDK_KEY_z), "repeated preedit key was not eaten"); applyclient(&log); Check(strcmp(log.event, "CEKSC") == 0 && strcmp(log.commit, "ㅋ") == 0 && strcmp(log.pre, "ㅋ") == 0 && strcmp(log.shown, "ㅋ") == 0, "repeated preedit signals were %s (commit %s, preedit %s, shown %s)", log.event, log.commit, log.pre, log.shown); Check(g_utf8_strlen(log.commit, -1) + g_utf8_strlen(log.shown, -1) == 2, "two physical keys produced commit %s and shown preedit %s", log.commit, log.shown); /* Unconsumed input uses GtkIMContextSimple, including its stateful forms. */ setpre(&srv, ""); setreply(&srv, 0, Rnormal, 0); clearlog(&log); Check(key(ctx, win, GDK_KEY_Multi_key) && key(ctx, win, GDK_KEY_apostrophe) && key(ctx, win, GDK_KEY_e), "Compose sequence was not consumed"); Check(strcmp(log.commit, "é") == 0, "Compose committed %s", log.commit); clearlog(&log); Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e), "dead-key sequence was not consumed"); Check(strcmp(log.commit, "é") == 0, "dead key committed %s", log.commit); /* A dead key over a preedit hands the pending text back first. */ setreply(&srv, 1, Rnormal, 0); setpre(&srv, "하"); clearlog(&log); Check(key(ctx, win, GDK_KEY_h), "dead-key setup key was not eaten"); clearevent(&log); first = eventcount(&srv); Check(!key(ctx, win, GDK_KEY_Shift_L), "a modifier press was eaten"); Check(eventcount(&srv) == first && strcmp(log.pre, "하") == 0, "a modifier press disturbed the preedit"); setreply(&srv, 0, Rnormal, 0); setpre(&srv, ""); Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e), "dead key over a preedit was not consumed"); Check(getevent(&srv, first).type == Ereset, "dead key did not hand the pending text back"); Check(strcmp(log.commit, "é") == 0 && log.pre[0] == '\0', "dead key over a preedit committed %s (preedit %s)", log.commit, log.pre); clearlog(&log); Check(keystate(ctx, win, GDK_KEY_U, GDK_CONTROL_MASK|GDK_SHIFT_MASK), "Unicode prefix was not consumed"); (void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_U, GDK_CONTROL_MASK|GDK_SHIFT_MASK); (void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_Shift_L, GDK_CONTROL_MASK); (void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_Control_L, 0); Check(key(ctx, win, GDK_KEY_0), "Unicode digit 1 was not consumed"); Check(key(ctx, win, GDK_KEY_0), "Unicode digit 2 was not consumed"); Check(key(ctx, win, GDK_KEY_e), "Unicode digit 3 was not consumed"); Check(key(ctx, win, GDK_KEY_9), "Unicode digit 4 was not consumed"); (void)key(ctx, win, GDK_KEY_Return); Check(strcmp(log.commit, "é") == 0, "Unicode entry committed %s", log.commit); setreply(&srv, 1, Rnormal, 0); /* PASSWORD and PIN transitions reset an existing composition. */ rect.height = 12; gtk_im_context_set_cursor_location(ctx, &rect); for(i = 0; i < (int)(sizeof purposes / sizeof purposes[0]); i++){ g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL); setpre(&srv, i == 0 ? "password" : "pin"); clearlog(&log); Check(key(ctx, win, GDK_KEY_c + i), "private setup key was not eaten"); clearlog(&log); first = eventcount(&srv); g_object_set(ctx, "input-purpose", purposes[i], NULL); Check(waitcount(&srv, first + 1), "private reset timed out"); Check(getevent(&srv, first).type == Ereset && strcmp(log.event, "CE") == 0, "private transition did not reset and clear"); clearlog(&log); first = eventcount(&srv); Check(key(ctx, win, GDK_KEY_x), "private fallback did not commit"); Check(eventcount(&srv) == first && strcmp(log.commit, "x") == 0, "private key reached daemon or did not commit"); } g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL); /* An entry hidden the older way is private too, purpose or no purpose. */ entry = gtk_entry_new(); gtk_container_add(GTK_CONTAINER(top), entry); gtk_widget_show(entry); pump(); entrywin = entrywindow(entry); Check(entrywin != NULL, "the test GtkEntry has no window of its own"); first = eventcount(&srv); gtk_im_context_set_client_window(ctx, entrywin); /* The caret the new window invalidates must land before keys are counted. */ Check(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ecaret, "the entry window did not renegotiate the caret"); setpre(&srv, ""); gtk_entry_set_visibility(GTK_ENTRY(entry), FALSE); clearlog(&log); first = eventcount(&srv); Check(key(ctx, entrywin, GDK_KEY_y), "hidden-entry fallback did not commit"); Check(eventcount(&srv) == first && strcmp(log.commit, "y") == 0, "hidden-entry key reached daemon or did not commit"); gtk_entry_set_visibility(GTK_ENTRY(entry), TRUE); clearlog(&log); first = eventcount(&srv); Check(key(ctx, entrywin, GDK_KEY_y), "visible-entry key was not eaten"); Check(waitcount(&srv, first + 1) && log.commit[0] == '\0', "visible-entry key did not reach the daemon"); gtk_im_context_set_client_window(ctx, win); /* The widget's own text goes over, and a take-back comes back. */ setpre(&srv, ""); clearlog(&log); snprintf(log.around, sizeof log.around, "%s", "가나한"); log.aroundcursor = 9; first = eventcount(&srv); Check(key(ctx, win, GDK_KEY_j), "surrounding-text key was not eaten"); Check(waitcount(&srv, first + 2), "surrounding text did not reach the daemon"); e = getevent(&srv, first); Check(e.type == Esurround && strcmp(e.text, "가나한") == 0, "the daemon was sent %s, not the text before the cursor", e.text); Check(getevent(&srv, first + 1).type == Ekey, "the text did not ride ahead of the key"); /* Sent again only when it changes. */ first = eventcount(&srv); Check(key(ctx, win, GDK_KEY_j), "second surrounding-text key was not eaten"); Check(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ekey, "unchanged surrounding text was sent again"); setreply(&srv, 1, Rnormal, 0); pthread_mutex_lock(&srv.lock); srv.del = 2; pthread_mutex_unlock(&srv.lock); log.deln = 0; Check(key(ctx, win, GDK_KEY_j), "take-back key was not eaten"); Check(log.deloffset == -2 && log.deln == 2, "the widget was told to take back %d runes at %d", log.deln, log.deloffset); pthread_mutex_lock(&srv.lock); srv.del = 0; pthread_mutex_unlock(&srv.lock); log.around[0] = '\0'; setpre(&srv, "reset"); clearlog(&log); Check(key(ctx, win, GDK_KEY_e), "reset setup key was not eaten"); clearlog(&log); first = eventcount(&srv); gtk_im_context_reset(ctx); Check(waitcount(&srv, first + 1), "explicit reset timed out"); Check(getevent(&srv, first).type == Ereset && strcmp(log.event, "CE") == 0, "explicit reset did not clear preedit"); setpre(&srv, "focus"); clearlog(&log); Check(key(ctx, win, GDK_KEY_f), "focus setup key was not eaten"); clearlog(&log); first = eventcount(&srv); closes = closecount(&srv); gtk_im_context_focus_out(ctx); Check(waitclose(&srv, closes + 1), "focus-out did not close connection"); Check(eventcount(&srv) == first + 1 && getevent(&srv, first).type == Ereset, "focus-out did not hand the pending text back before closing"); Check(strcmp(log.event, "CE") == 0, "focus-out clear order was %s", log.event); /* Dispose is repeatable, closes the connection, and emits no preedit. */ gtk_im_context_set_client_window(ctx, win); setpre(&srv, "final"); clearlog(&log); Check(key(ctx, win, GDK_KEY_g), "dispose setup key was not eaten"); clearlog(&log); closes = closecount(&srv); g_object_run_dispose(G_OBJECT(ctx)); g_object_run_dispose(G_OBJECT(ctx)); g_object_unref(ctx); ctx = NULL; Check(waitclose(&srv, closes + 1), "dispose did not close connection"); Check(log.n == 0, "dispose emitted %s", log.event); /* Malformed commit and preedit fields close the protocol connection. */ for(i = 0; i < (int)(sizeof badresponses / sizeof badresponses[0]); i++){ setreply(&srv, 1, badresponses[i], 0); clearlog(&log); badctx = newcontext(&log); gtk_im_context_set_client_window(badctx, win); closes = closecount(&srv); Check(key(badctx, win, GDK_KEY_m + i), "malformed response did not fall back"); Check(waitclose(&srv, closes + 1), "malformed response did not close connection"); Check(log.commit[0] == 'm' + i && log.commit[1] == '\0', "malformed response fallback committed %s", log.commit); g_object_unref(badctx); badctx = NULL; } /* A peer which accepts a connection but never replies cannot freeze GTK. */ setreply(&srv, 1, Rnormal, 1); clearlog(&log); badctx = newcontext(&log); closes = closecount(&srv); started = nowms(); Check(key(badctx, win, GDK_KEY_t), "stalled peer did not fall back"); stalledms = nowms() - started; Check(stalledms < 4*Ipcwaitms, "stalled peer blocked GTK"); Check(waitclose(&srv, closes + 1), "stalled connection was not closed"); Check(strcmp(log.commit, "t") == 0, "stalled fallback committed %s", log.commit); g_object_unref(badctx); badctx = NULL; setreply(&srv, 1, Rnormal, 0); /* An old daemon still honors set_use_preedit(FALSE). */ setold(&srv, 1); clearlog(&log); oldctx = newcontext(&log); gtk_im_context_set_client_window(oldctx, win); rect.x = 2; rect.y = 3; rect.height = 10; gtk_im_context_set_cursor_location(oldctx, &rect); gtk_im_context_set_use_preedit(oldctx, FALSE); setpre(&srv, "legacy"); first = eventcount(&srv); Check(key(oldctx, win, GDK_KEY_h), "old-daemon key was not eaten"); Check(waitcount(&srv, first + 2), "old-daemon exchange timed out"); e = getevent(&srv, first); Check(e.type == Ecap && e.want == 0, "old-daemon probe framing is wrong"); e = getevent(&srv, first + 1); Check(e.type == Ekey && e.want == 0, "old daemon forced inline preedit"); for(i = first; i < eventcount(&srv); i++) Check(getevent(&srv, i).type != Ecaret, "caret extension sent to old daemon"); Check(log.n == 0 && log.pre[0] == '\0', "old daemon emitted inline preedit %s (%s)", log.event, log.pre); closes = closecount(&srv); g_object_unref(oldctx); oldctx = NULL; Check(waitclose(&srv, closes + 1), "old-daemon context did not close"); /* A failed first connection uses GtkIMContextSimple. */ stopserver(&srv); serverup = 0; unlink(socket); clearlog(&log); badctx = newcontext(&log); Check(key(badctx, win, GDK_KEY_z), "connection failure did not fall back"); Check(strcmp(log.commit, "z") == 0 && log.pre[0] == '\0', "connection failure left stale preedit"); ok = 1; out: if(badctx != NULL) g_object_unref(badctx); if(oldctx != NULL) g_object_unref(oldctx); if(ctx != NULL) g_object_unref(ctx); if(top != NULL) gtk_widget_destroy(top); pump(); if(serverup) stopserver(&srv); if(socket[0] != '\0') unlink(socket); if(cache[0] != '\0') unlink(cache); if(runtime[0] != '\0') rmdir(runtime); if(rootmade) rmdir(root); stopdaemon(&xvfb); closeerrors(&xvfb); free(module); if(ok) printf("gtk_live_test: ok (scale %d, stalled peer %lld ms)\n", scale, (long long)stalledms); return !ok; }