From 3576c4ea68c91645afc398f0ed0e8f6611ef8d8d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 20:09:49 +0900 Subject: [PATCH] tests: cover frontend input-panel protocols --- .gitignore | 1 + Dockerfile | 1 + tests/Makefile | 11 +- tests/xim_live_test.c | 946 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 957 insertions(+), 2 deletions(-) create mode 100644 tests/xim_live_test.c diff --git a/.gitignore b/.gitignore index 967fe90..e65a41b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /tests/ibus_live_test /tests/ibus_client_smoke /tests/gtk_live_test +/tests/xim_live_test /tests/ipc_live_test /tests/daemon_collision_test /tests/daemon_failure_test diff --git a/Dockerfile b/Dockerfile index 42b9e66..3287506 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN pacman -Syu --noconfirm --needed \ gtk3 \ ibus \ harfbuzz \ + libx11 \ libxcb \ libxkbcommon \ make \ diff --git a/tests/Makefile b/tests/Makefile index a1be7ba..ddfcc96 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -12,13 +12,15 @@ IBUS_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) IBUS_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) IBUS_CLIENT_CFLAGS = $(shell pkg-config --cflags ibus-1.0) IBUS_CLIENT_LIBS = $(shell pkg-config --libs ibus-1.0) +X11_CFLAGS = $(shell pkg-config --cflags x11) +X11_LIBS = $(shell pkg-config --libs x11) XIM_CFLAGS = $(shell pkg-config --cflags xcb-imdkit xkbcommon) XIM_LIBS = $(shell pkg-config --libs xcb-imdkit xkbcommon) LIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS) PROG = unit_test -LIVE = ibus_live_test ibus_client_smoke gtk_live_test ipc_live_test \ - daemon_collision_test daemon_failure_test daemon_restart_test +LIVE = ibus_live_test ibus_client_smoke gtk_live_test xim_live_test \ + ipc_live_test daemon_collision_test daemon_failure_test daemon_restart_test TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \ popup_test.c font_test.c ibus_test.c server_test.c xim_adapter_test.c @@ -35,6 +37,7 @@ check test: $(PROG) $(LIVE) ./$(PROG) $(TESTARGS) ./ibus_live_test ../strans ../map ./ibus_client_smoke ./gtk_live_test ../gtk/im-strans.so + ./xim_live_test ../strans ../map ./ipc_live_test ../strans ../map ./daemon_collision_test ../strans ../map ./daemon_failure_test ../strans @@ -54,6 +57,10 @@ gtk_live_test: gtk_live_test.c ../ipc.c ../ipc.h $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. $(GTK_CFLAGS) -o $@ \ gtk_live_test.c ../ipc.c $(GTK_LIBS) -pthread +xim_live_test: xim_live_test.c ../ipc.c ../ipc.h + $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. $(X11_CFLAGS) -o $@ \ + xim_live_test.c ../ipc.c $(X11_LIBS) + ipc_live_test: ipc_live_test.c ../ipc.c ../ipc.h $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. -o $@ ipc_live_test.c ../ipc.c diff --git a/tests/xim_live_test.c b/tests/xim_live_test.c new file mode 100644 index 0000000..dce3b46 --- /dev/null +++ b/tests/xim_live_test.c @@ -0,0 +1,946 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ipc.h" + +enum +{ + Starttimeout = 8000, + Eventtimeout = 4000, + Stoptimeout = 3000, +}; + +typedef struct Child Child; +typedef struct Run Run; +typedef struct Prelog Prelog; + +struct Child +{ + pid_t pid; + pid_t pgid; + char log[320]; +}; + +struct Run +{ + Child xvfb; + Child daemon; + char root[256]; + char runtime[320]; + char config[320]; + char ibus[352]; + char bus[384]; + char home[320]; + char socket[384]; + char display[32]; +}; + +struct Prelog +{ + int start; + int nonempty; + int empty; + int done; + int doneafterempty; + int badorder; +}; + +static int xerror; + +static int +fail(char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "xim_live_test: "); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + return 0; +} + +static int64_t +nowms(void) +{ + struct timespec ts; + + if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0) + return 0; + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int +leftms(int64_t deadline) +{ + int64_t n; + + n = deadline - nowms(); + if(n <= 0) + return 0; + if(n > 0x7fffffff) + return 0x7fffffff; + return n; +} + +static void +pausems(int n) +{ + struct timespec ts; + + ts.tv_sec = n / 1000; + ts.tv_nsec = (n % 1000) * 1000000L; + while(nanosleep(&ts, &ts) < 0 && errno == EINTR) + ; +} + +static int +makedir(char *path) +{ + if(mkdir(path, 0700) == 0) + return 1; + return fail("mkdir %s: %s", path, strerror(errno)); +} + +static int +childlog(char *path) +{ + int fd; + + fd = open(path, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0600); + return fd; +} + +static int +spawnxvfb(Run *r, char *program) +{ + char fdstr[16], line[32]; + int displayfd[2], errfd, flags, n, status; + int64_t deadline; + pid_t pid; + struct pollfd pfd; + ssize_t nr; + + if(pipe2(displayfd, O_CLOEXEC) < 0) + return fail("create Xvfb display pipe: %s", strerror(errno)); + errfd = childlog(r->xvfb.log); + if(errfd < 0){ + close(displayfd[0]); + close(displayfd[1]); + return fail("open Xvfb log: %s", strerror(errno)); + } + pid = fork(); + if(pid < 0){ + close(displayfd[0]); + close(displayfd[1]); + close(errfd); + return fail("fork Xvfb: %s", strerror(errno)); + } + if(pid == 0){ + setpgid(0, 0); + close(displayfd[0]); + flags = fcntl(displayfd[1], F_GETFD); + if(flags < 0 || fcntl(displayfd[1], F_SETFD, + flags & ~FD_CLOEXEC) < 0) + _exit(126); + if(dup2(errfd, STDOUT_FILENO) < 0 || + dup2(errfd, STDERR_FILENO) < 0) + _exit(126); + close(errfd); + snprintf(fdstr, sizeof fdstr, "%d", displayfd[1]); + execlp(program, program, "-displayfd", fdstr, + "-screen", "0", "800x600x24", "-nolisten", "tcp", + "-noreset", (char*)0); + _exit(127); + } + close(displayfd[1]); + close(errfd); + if((setpgid(pid, pid) < 0 && errno != EACCES) || getpgid(pid) != pid){ + kill(pid, SIGKILL); + while(waitpid(pid, &status, 0) < 0 && errno == EINTR) + ; + close(displayfd[0]); + return fail("isolate Xvfb process group: %s", strerror(errno)); + } + r->xvfb.pid = pid; + r->xvfb.pgid = pid; + pfd.fd = displayfd[0]; + pfd.events = POLLIN|POLLHUP; + deadline = nowms() + Starttimeout; + n = 0; + while(n + 1 < (int)sizeof line){ + pfd.revents = 0; + status = poll(&pfd, 1, leftms(deadline)); + if(status < 0 && errno == EINTR) + continue; + if(status <= 0) + break; + nr = read(displayfd[0], line + n, sizeof line - n - 1); + if(nr > 0){ + n += nr; + line[n] = '\0'; + if(strchr(line, '\n') != NULL) + break; + continue; + } + if(nr < 0 && errno == EINTR) + continue; + break; + } + close(displayfd[0]); + if(n == 0 || strchr(line, '\n') == NULL) + return fail("Xvfb did not report a private display"); + line[strcspn(line, "\r\n")] = '\0'; + for(n = 0; line[n] != '\0'; n++) + if(line[n] < '0' || line[n] > '9') + return fail("invalid Xvfb display number %s", line); + if(snprintf(r->display, sizeof r->display, ":%s", line) + >= (int)sizeof r->display) + return fail("Xvfb display number is too long"); + return 1; +} + +static int +startdaemon(Run *r, char *program, char *mapdir) +{ + int errfd; + pid_t pid; + + errfd = childlog(r->daemon.log); + if(errfd < 0) + return fail("open daemon log: %s", strerror(errno)); + pid = fork(); + if(pid < 0){ + close(errfd); + return fail("fork daemon: %s", strerror(errno)); + } + if(pid == 0){ + setpgid(0, 0); + if(dup2(errfd, STDOUT_FILENO) < 0 || + dup2(errfd, STDERR_FILENO) < 0) + _exit(126); + close(errfd); + execl(program, program, mapdir, (char*)0); + _exit(127); + } + close(errfd); + if((setpgid(pid, pid) < 0 && errno != EACCES) || getpgid(pid) != pid){ + kill(pid, SIGKILL); + while(waitpid(pid, NULL, 0) < 0 && errno == EINTR) + ; + return fail("isolate daemon process group: %s", strerror(errno)); + } + r->daemon.pid = pid; + r->daemon.pgid = pid; + return 1; +} + +static int +waitsocket(Run *r) +{ + struct stat st; + int status; + int64_t deadline; + + deadline = nowms() + Starttimeout; + while(leftms(deadline) > 0){ + if(lstat(r->socket, &st) == 0 && S_ISSOCK(st.st_mode) && + (st.st_mode & 0777) == 0600) + return 1; + if(waitpid(r->daemon.pid, &status, WNOHANG) == r->daemon.pid){ + r->daemon.pid = -1; + 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); + r->xvfb.pid = r->daemon.pid = -1; + r->xvfb.pgid = r->daemon.pgid = -1; + snprintf(r->root, sizeof r->root, "/tmp/strans-xim.XXXXXX"); + if(mkdtemp(r->root) == NULL) + return fail("mkdtemp: %s", strerror(errno)); + if(snprintf(r->runtime, sizeof r->runtime, "%s/runtime", r->root) + >= (int)sizeof r->runtime || + snprintf(r->config, sizeof r->config, "%s/config", r->root) + >= (int)sizeof r->config || + snprintf(r->ibus, sizeof r->ibus, "%s/ibus", r->config) + >= (int)sizeof r->ibus || + snprintf(r->bus, sizeof r->bus, "%s/bus", r->ibus) + >= (int)sizeof r->bus || + snprintf(r->home, sizeof r->home, "%s/home", r->root) + >= (int)sizeof r->home || + snprintf(r->socket, sizeof r->socket, "%s/strans.sock", r->runtime) + >= (int)sizeof r->socket || + snprintf(r->xvfb.log, sizeof r->xvfb.log, "%s/xvfb.log", r->root) + >= (int)sizeof r->xvfb.log || + snprintf(r->daemon.log, sizeof r->daemon.log, "%s/daemon.log", r->root) + >= (int)sizeof r->daemon.log) + return fail("temporary path is too long"); + if(!makedir(r->runtime) || !makedir(r->config) || + !makedir(r->ibus) || !makedir(r->bus) || !makedir(r->home)) + return 0; + if(!spawnxvfb(r, xvfb)) + return 0; + if(setenv("DISPLAY", r->display, 1) < 0 || + setenv("XMODIFIERS", "@im=strans", 1) < 0 || + setenv("XDG_RUNTIME_DIR", r->runtime, 1) < 0 || + setenv("XDG_CONFIG_HOME", r->config, 1) < 0 || + setenv("HOME", r->home, 1) < 0 || + unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || + unsetenv("IBUS_ADDRESS") < 0) + return fail("set private environment: %s", strerror(errno)); + if(!startdaemon(r, daemon, mapdir)) + return 0; + return waitsocket(r); +} + +static void +showlog(char *name) +{ + char buf[4096]; + ssize_t n; + int fd; + + fd = open(name, O_RDONLY|O_CLOEXEC); + if(fd < 0) + return; + while((n = read(fd, buf, sizeof buf)) > 0) + fwrite(buf, 1, n, stderr); + close(fd); +} + +static void +stopchild(Child *c) +{ + int i, status; + pid_t n; + + if(c->pgid > 0 && kill(-c->pgid, SIGTERM) < 0 && errno != ESRCH) + fail("terminate process group %ld: %s", (long)c->pgid, + strerror(errno)); + if(c->pid > 0) + for(i = 0; i < Stoptimeout / 20; i++){ + n = waitpid(c->pid, &status, WNOHANG); + if(n == c->pid || (n < 0 && errno == ECHILD)){ + c->pid = -1; + break; + } + if(n < 0 && errno != EINTR) + break; + pausems(20); + } + if(c->pgid > 0) + kill(-c->pgid, SIGKILL); + if(c->pid > 0){ + while(waitpid(c->pid, &status, 0) < 0 && errno == EINTR) + ; + c->pid = -1; + } + c->pgid = -1; +} + +static void +cleardir(char *path) +{ + char name[512]; + struct dirent *de; + DIR *dir; + + dir = opendir(path); + if(dir == NULL) + return; + while((de = readdir(dir)) != NULL){ + if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + if(snprintf(name, sizeof name, "%s/%s", path, de->d_name) + < (int)sizeof name) + unlink(name); + } + closedir(dir); +} + +static void +cleanup(Run *r, int noisy) +{ + stopchild(&r->daemon); + stopchild(&r->xvfb); + if(noisy){ + fprintf(stderr, "xim_live_test: daemon log:\n"); + showlog(r->daemon.log); + fprintf(stderr, "xim_live_test: Xvfb log:\n"); + showlog(r->xvfb.log); + } + if(r->socket[0] != '\0') + unlink(r->socket); + cleardir(r->bus); + if(r->daemon.log[0] != '\0') + unlink(r->daemon.log); + if(r->xvfb.log[0] != '\0') + unlink(r->xvfb.log); + if(r->bus[0] != '\0') rmdir(r->bus); + if(r->ibus[0] != '\0') rmdir(r->ibus); + if(r->config[0] != '\0') rmdir(r->config); + if(r->runtime[0] != '\0') rmdir(r->runtime); + if(r->home[0] != '\0') rmdir(r->home); + if(r->root[0] != '\0') rmdir(r->root); +} + +static int +xerr(Display *dpy, XErrorEvent *e) +{ + char msg[128]; + + xerror++; + XGetErrorText(dpy, e->error_code, msg, sizeof msg); + fprintf(stderr, "xim_live_test: X error: %s (request %d.%d)\n", + 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 || status == XLookupBoth) && + ((size_t)n >= cap || commit == NULL))) + return 0; + if((status == XLookupChars || status == XLookupBoth) && 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 || status == XLookupBoth) && 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 +rawtakeover(void) +{ + unsigned char req[Ipcreqsz]; + char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1]; + Ipcresp resp; + struct timeval tv; + int fd; + + fd = ipcconnect(); + if(fd < 0) + return fail("connect raw IPC owner: %s", strerror(errno)); + tv.tv_sec = Eventtimeout / 1000; + tv.tv_usec = (Eventtimeout % 1000) * 1000; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); + ipcpackreq(req, 0, 0, 'z'); + if(ipcsend(fd, req, sizeof req) < 0 || + ipcreadresp(fd, 0, commit, sizeof commit, + preedit, sizeof preedit, &resp) < 0){ + close(fd); + return fail("raw IPC ownership transfer: %s", strerror(errno)); + } + return fd; +} + +static int +waitclear(Display *dpy, Prelog *p, int empty, int done) +{ + int64_t deadline; + + deadline = nowms() + Eventtimeout; + while(leftms(deadline) > 0){ + pump(dpy, leftms(deadline)); + if(p->empty >= empty && p->done >= done) + return 1; + } + return fail("timed out waiting for empty DRAW and DONE"); +} + +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; + int fd, empty, done; + + 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); + } + empty = log.empty; + done = log.done; + fd = rawtakeover(); + if(fd < 0){ + XDestroyIC(ic); + return 0; + } + if(!waitclear(dpy, &log, empty + 1, done + 1)){ + close(fd); + XDestroyIC(ic); + return 0; + } + /* Leave the live IC time to expose a delayed duplicate transfer clear. */ + pump(dpy, 100); + if(log.empty != empty + 1 || log.done != done + 1 || + log.doneafterempty != 1 || log.badorder != 0){ + int gotempty, gotdone, ordered; + + gotempty = log.empty - empty; + gotdone = log.done - done; + ordered = log.doneafterempty; + close(fd); + XDestroyIC(ic); + return fail("owner loss callback counts: empty=%d done=%d ordered=%d", + gotempty, gotdone, ordered); + } + reset = Xutf8ResetIC(ic); + if(reset != NULL) + XFree(reset); + XUnsetICFocus(ic); + XDestroyIC(ic); + pump(dpy, 100); + close(fd); + pump(dpy, 100); + if(log.empty != empty + 1 || log.done != done + 1) + return fail("late lifecycle produced duplicate clear callbacks"); + return 1; +} + +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) || + !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; + + if(argc != 3 && argc != 4){ + fprintf(stderr, "usage: xim_live_test daemon mapdir [Xvfb]\n"); + return 2; + } + memset(&run, 0, sizeof run); + run.xvfb.pid = run.daemon.pid = -1; + run.xvfb.pgid = run.daemon.pgid = -1; + 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.display); + if(dpy == NULL){ + fail("open private display %s", run.display); + cleanup(&run, 1); + return 1; + } + im = openim(dpy); + if(im == NULL){ + fail("open strans XIM on private display %s", run.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; +}