From f399eb0af6f73c26d3bde9f48041890807214450 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 13 Aug 2026 23:55:27 +0900 Subject: [PATCH] tests: cover live ibus transport lifecycle --- .gitignore | 1 + Dockerfile | 1 + Makefile | 2 +- tests/Makefile | 12 +- tests/ibus_live_test.c | 1014 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1026 insertions(+), 4 deletions(-) create mode 100644 tests/ibus_live_test.c diff --git a/.gitignore b/.gitignore index 2e632d5..016337e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /strans /tests/*.o /tests/unit_test +/tests/ibus_live_test /xim/*.o /xim/strans-xim /xim/xim_test diff --git a/Dockerfile b/Dockerfile index 778566f..9862144 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,7 @@ RUN pacman -Syu --noconfirm --needed \ && pacman -Scc --noconfirm RUN printf "egrep='grep -E'\n" > /usr/lib/plan9/config +RUN dbus-uuidgen --ensure=/etc/machine-id ENV PLAN9=/usr/lib/plan9 ENV PATH=$PATH:/usr/lib/plan9/bin diff --git a/Makefile b/Makefile index 9e08978..ef85571 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ docker: docker-image check: verify-map test $(MAKE) -C xim check -test: +test: $(PROG) $(MAKE) -C tests check TESTARGS="$(TESTARGS)" verify-map: diff --git a/tests/Makefile b/tests/Makefile index 50e4525..f742d83 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,5 +1,6 @@ CC = 9c LD = 9l +HOSTCC = cc CFLAGS = -std=c99 -Wall -Wextra -O2 -g -I.. -I../cutest FT_CFLAGS = $(shell pkg-config --cflags freetype2) FT_LIBS = $(shell pkg-config --libs freetype2) @@ -8,6 +9,7 @@ IBUS_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) LIBS = -lthread -lbio $(FT_LIBS) $(IBUS_LIBS) PROG = unit_test +LIVE = ibus_live_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 @@ -17,14 +19,18 @@ PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \ PARENTOBJ = $(PARENTSRC:%.c=unit_%.o) OBJS = $(TESTOBJ) $(PARENTOBJ) -all: $(PROG) +all: $(PROG) $(LIVE) -check test: $(PROG) +check test: $(PROG) $(LIVE) ./$(PROG) $(TESTARGS) + ./ibus_live_test ../strans ../map $(PROG): $(OBJS) $(LD) -o $@ $(OBJS) $(LIBS) +ibus_live_test: ibus_live_test.c + $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g $(IBUS_CFLAGS) -o $@ $< $(IBUS_LIBS) + $(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h ../cutest/cutest.h engine_test.o: ../strans.c ibus_test.o: CFLAGS += $(IBUS_CFLAGS) @@ -39,6 +45,6 @@ unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h $(CC) $(CFLAGS) -c -o $@ $< clean: - rm -f $(OBJS) $(PROG) + rm -f $(OBJS) $(PROG) $(LIVE) .PHONY: all check test clean diff --git a/tests/ibus_live_test.c b/tests/ibus_live_test.c new file mode 100644 index 0000000..90b1196 --- /dev/null +++ b/tests/ibus_live_test.c @@ -0,0 +1,1014 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum +{ + Calltimeout = 4000, + Starttimeout = 8000, + Stoptimeout = 3000, + Relmask = 1<<30, + Ctrlmask = 1<<2, +}; + +typedef struct Daemon Daemon; +typedef struct Siglog Siglog; + +struct Daemon +{ + pid_t pid; + int errfd; + char root[256]; + char runtime[320]; + char config[320]; + char ibus[384]; + char bus[448]; + char home[320]; + char socket[384]; + char addrfile[512]; + char address[512]; + char err[4096]; + size_t nerr; +}; + +struct Siglog +{ + int invalid; + int npreedit; + int ncommit; + char path[96]; + char preedit[256]; + char commit[256]; + dbus_uint32_t cursor; + dbus_bool_t visible; +}; + +static int +fail(char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "ibus_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 int +makedir(char *path) +{ + if(mkdir(path, 0700) == 0) + return 1; + return fail("mkdir %s: %s", path, strerror(errno)); +} + +static void +readerrors(Daemon *d) +{ + ssize_t n; + + if(d->errfd < 0) + return; + while(d->nerr + 1 < sizeof d->err){ + n = read(d->errfd, d->err + d->nerr, + sizeof d->err - d->nerr - 1); + if(n > 0){ + d->nerr += n; + continue; + } + if(n < 0 && errno == EINTR) + continue; + break; + } + d->err[d->nerr] = '\0'; +} + +static void +showerrors(Daemon *d) +{ + readerrors(d); + if(d->nerr != 0) + fprintf(stderr, "ibus_live_test: daemon stderr:\n%s", d->err); +} + +static int +readaddress(Daemon *d) +{ + FILE *fp; + char line[768], *p; + long pid; + int haveaddr, havepid; + + fp = fopen(d->addrfile, "r"); + if(fp == NULL) + return fail("open %s: %s", d->addrfile, strerror(errno)); + haveaddr = havepid = 0; + pid = -1; + while(fgets(line, sizeof line, fp) != NULL){ + p = strchr(line, '\n'); + if(p != NULL) + *p = '\0'; + if(strncmp(line, "IBUS_ADDRESS=", 13) == 0){ + if(snprintf(d->address, sizeof d->address, "%s", line + 13) + >= (int)sizeof d->address) + break; + haveaddr = d->address[0] != '\0'; + }else if(strncmp(line, "IBUS_DAEMON_PID=", 16) == 0){ + errno = 0; + pid = strtol(line + 16, &p, 10); + havepid = errno == 0 && *p == '\0'; + } + } + if(fclose(fp) != 0) + return fail("close %s: %s", d->addrfile, strerror(errno)); + if(!haveaddr || !havepid) + return fail("invalid IBus address file %s", d->addrfile); + if(pid != d->pid) + return fail("address file PID %ld, expected %ld", pid, (long)d->pid); + return 1; +} + +static int +waitaddress(Daemon *d, int notifyfd) +{ + struct pollfd pfd[2]; + char buf[4096]; + struct inotify_event *ev; + ssize_t n; + int i, timeout, status; + int64_t deadline; + + deadline = nowms() + Starttimeout; + for(;;){ + if(waitpid(d->pid, &status, WNOHANG) == d->pid){ + d->pid = -1; + readerrors(d); + return fail("daemon exited before publishing IBus address"); + } + timeout = leftms(deadline); + if(timeout == 0) + return fail("timed out waiting for IBus address file"); + pfd[0].fd = notifyfd; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + pfd[1].fd = d->errfd; + pfd[1].events = POLLIN; + pfd[1].revents = 0; + i = poll(pfd, 2, timeout); + if(i < 0 && errno == EINTR) + continue; + if(i < 0) + return fail("poll for IBus address: %s", strerror(errno)); + if(i == 0) + continue; + if(pfd[1].revents != 0) + readerrors(d); + if(!(pfd[0].revents & POLLIN)) + continue; + n = read(notifyfd, buf, sizeof buf); + if(n < 0 && (errno == EINTR || errno == EAGAIN)) + continue; + if(n < 0) + return fail("read IBus directory watch: %s", strerror(errno)); + for(i = 0; i < n; i += sizeof *ev + ev->len){ + ev = (struct inotify_event*)(buf + i); + if(!(ev->mask & IN_MOVED_TO) || ev->len == 0) + continue; + if(snprintf(d->addrfile, sizeof d->addrfile, "%s/%s", + d->bus, ev->name) >= (int)sizeof d->addrfile) + return fail("IBus address path is too long"); + return readaddress(d); + } + } +} + +static int +startdaemon(Daemon *d, char *program, char *mapdir) +{ + char missing[384]; + int notifyfd, watch, errpipe[2]; + pid_t pid; + + memset(d, 0, sizeof *d); + d->pid = -1; + d->errfd = -1; + snprintf(d->root, sizeof d->root, "/tmp/strans-ibus.XXXXXX"); + if(mkdtemp(d->root) == NULL) + return fail("mkdtemp: %s", strerror(errno)); + if(snprintf(d->runtime, sizeof d->runtime, "%s/runtime", d->root) + >= (int)sizeof d->runtime || + snprintf(d->config, sizeof d->config, "%s/config", d->root) + >= (int)sizeof d->config || + snprintf(d->ibus, sizeof d->ibus, "%s/ibus", d->config) + >= (int)sizeof d->ibus || + snprintf(d->bus, sizeof d->bus, "%s/bus", d->ibus) + >= (int)sizeof d->bus || + snprintf(d->home, sizeof d->home, "%s/home", d->root) + >= (int)sizeof d->home || + snprintf(d->socket, sizeof d->socket, "%s/strans.sock", d->runtime) + >= (int)sizeof d->socket || + snprintf(missing, sizeof missing, "%s/missing-font.ttf", d->root) + >= (int)sizeof missing) + return fail("temporary path is too long"); + if(!makedir(d->runtime) || !makedir(d->config) || !makedir(d->ibus) || + !makedir(d->bus) || !makedir(d->home)) + return 0; + notifyfd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK); + if(notifyfd < 0) + return fail("inotify_init1: %s", strerror(errno)); + watch = inotify_add_watch(notifyfd, d->bus, IN_MOVED_TO); + if(watch < 0){ + close(notifyfd); + return fail("inotify_add_watch: %s", strerror(errno)); + } + if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0){ + close(notifyfd); + return fail("pipe2: %s", strerror(errno)); + } + pid = fork(); + if(pid < 0){ + close(errpipe[0]); + close(errpipe[1]); + close(notifyfd); + return fail("fork: %s", strerror(errno)); + } + if(pid == 0){ + close(errpipe[0]); + close(notifyfd); + if(dup2(errpipe[1], STDOUT_FILENO) < 0 || + dup2(errpipe[1], STDERR_FILENO) < 0) + _exit(126); + close(errpipe[1]); + setenv("XDG_RUNTIME_DIR", d->runtime, 1); + setenv("XDG_CONFIG_HOME", d->config, 1); + setenv("HOME", d->home, 1); + unsetenv("DISPLAY"); + unsetenv("WAYLAND_DISPLAY"); + unsetenv("DBUS_SESSION_BUS_ADDRESS"); + unsetenv("IBUS_ADDRESS"); + execl(program, program, mapdir, missing, (char*)0); + dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno)); + _exit(127); + } + close(errpipe[1]); + d->pid = pid; + d->errfd = errpipe[0]; + if(!waitaddress(d, notifyfd)){ + inotify_rm_watch(notifyfd, watch); + close(notifyfd); + return 0; + } + inotify_rm_watch(notifyfd, watch); + close(notifyfd); + return 1; +} + +static int +clearbus(Daemon *d) +{ + DIR *dir; + struct dirent *de; + char path[576]; + int ok; + + if(d->bus[0] == '\0') + return 1; + dir = opendir(d->bus); + if(dir == NULL) + return errno == ENOENT || fail("open cleanup directory %s: %s", + d->bus, strerror(errno)); + ok = 1; + errno = 0; + while((de = readdir(dir)) != NULL){ + if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + if(snprintf(path, sizeof path, "%s/%s", d->bus, de->d_name) + >= (int)sizeof path){ + fail("cleanup path is too long: %s", de->d_name); + ok = 0; + continue; + } + if(unlink(path) < 0 && errno != ENOENT){ + fail("remove IBus address file %s: %s", de->d_name, + strerror(errno)); + ok = 0; + } + errno = 0; + } + if(errno != 0){ + fail("read cleanup directory %s: %s", d->bus, strerror(errno)); + ok = 0; + } + closedir(dir); + return ok; +} + +static int +rmdirknown(char *path) +{ + if(path[0] == '\0' || rmdir(path) == 0 || errno == ENOENT) + return 1; + return fail("rmdir %s: %s", path, strerror(errno)); +} + +static int +stopdaemon(Daemon *d) +{ + struct pollfd pfd; + int ok, status, n; + int64_t deadline; + + ok = 1; + if(d->pid > 0){ + if(kill(d->pid, SIGTERM) < 0 && errno != ESRCH){ + fail("kill %ld: %s", (long)d->pid, strerror(errno)); + ok = 0; + } + deadline = nowms() + Stoptimeout; + for(;;){ + n = waitpid(d->pid, &status, WNOHANG); + if(n == d->pid) + break; + if(n < 0 && errno != EINTR){ + fail("waitpid %ld: %s", (long)d->pid, strerror(errno)); + ok = 0; + break; + } + n = leftms(deadline); + if(n == 0){ + fail("daemon %ld did not stop after SIGTERM", (long)d->pid); + ok = 0; + kill(d->pid, SIGKILL); + while(waitpid(d->pid, &status, 0) < 0 && errno == EINTR) + ; + break; + } + pfd.fd = d->errfd; + pfd.events = POLLIN|POLLHUP; + pfd.revents = 0; + if(poll(&pfd, 1, n) < 0 && errno != EINTR){ + fail("poll daemon %ld: %s", (long)d->pid, strerror(errno)); + ok = 0; + kill(d->pid, SIGKILL); + while(waitpid(d->pid, &status, 0) < 0 && errno == EINTR) + ; + break; + } + readerrors(d); + } + d->pid = -1; + } + readerrors(d); + if(d->errfd >= 0){ + close(d->errfd); + d->errfd = -1; + } + if(d->socket[0] != '\0' && unlink(d->socket) < 0 && errno != ENOENT){ + fail("remove IPC socket %s: %s", d->socket, strerror(errno)); + ok = 0; + } + if(!clearbus(d)) ok = 0; + if(!rmdirknown(d->bus)) ok = 0; + if(!rmdirknown(d->ibus)) ok = 0; + if(!rmdirknown(d->config)) ok = 0; + if(!rmdirknown(d->runtime)) ok = 0; + if(!rmdirknown(d->home)) ok = 0; + if(!rmdirknown(d->root)) ok = 0; + return ok; +} + +static int +emptyarray(DBusMessageIter *it, int element) +{ + DBusMessageIter sub; + + if(dbus_message_iter_get_arg_type(it) != DBUS_TYPE_ARRAY || + dbus_message_iter_get_element_type(it) != element) + return 0; + dbus_message_iter_recurse(it, &sub); + return dbus_message_iter_get_arg_type(&sub) == DBUS_TYPE_INVALID; +} + +static int +ibusattrs(DBusMessageIter *it) +{ + DBusMessageIter v, st; + const char *name; + + if(dbus_message_iter_get_arg_type(it) != DBUS_TYPE_VARIANT) + return 0; + dbus_message_iter_recurse(it, &v); + if(dbus_message_iter_get_arg_type(&v) != DBUS_TYPE_STRUCT) + return 0; + dbus_message_iter_recurse(&v, &st); + if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_STRING) + return 0; + dbus_message_iter_get_basic(&st, &name); + if(strcmp(name, "IBusAttrList") != 0 || !dbus_message_iter_next(&st) || + !emptyarray(&st, DBUS_TYPE_DICT_ENTRY) || + !dbus_message_iter_next(&st) || !emptyarray(&st, DBUS_TYPE_VARIANT) || + dbus_message_iter_next(&st)) + return 0; + return !dbus_message_iter_next(&v); +} + +static int +ibustext(DBusMessageIter *it, char *text, size_t ntext) +{ + DBusMessageIter v, st; + const char *name, *s; + + if(dbus_message_iter_get_arg_type(it) != DBUS_TYPE_VARIANT) + return 0; + dbus_message_iter_recurse(it, &v); + if(dbus_message_iter_get_arg_type(&v) != DBUS_TYPE_STRUCT) + return 0; + dbus_message_iter_recurse(&v, &st); + if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_STRING) + return 0; + dbus_message_iter_get_basic(&st, &name); + if(strcmp(name, "IBusText") != 0 || !dbus_message_iter_next(&st) || + !emptyarray(&st, DBUS_TYPE_DICT_ENTRY) || !dbus_message_iter_next(&st) || + dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_STRING) + return 0; + dbus_message_iter_get_basic(&st, &s); + if(snprintf(text, ntext, "%s", s) >= (int)ntext || + !dbus_message_iter_next(&st) || !ibusattrs(&st) || + dbus_message_iter_next(&st)) + return 0; + return !dbus_message_iter_next(&v); +} + +static DBusHandlerResult +onsignal(DBusConnection *conn, DBusMessage *m, void *arg) +{ + Siglog *log; + DBusMessageIter it; + const char *path; + + (void)conn; + log = arg; + if(dbus_message_get_type(m) != DBUS_MESSAGE_TYPE_SIGNAL || + !dbus_message_has_interface(m, "org.freedesktop.IBus.InputContext")) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + path = dbus_message_get_path(m); + if(path == NULL || snprintf(log->path, sizeof log->path, "%s", path) + >= (int)sizeof log->path){ + log->invalid = 1; + return DBUS_HANDLER_RESULT_HANDLED; + } + if(dbus_message_has_member(m, "UpdatePreeditText")){ + log->npreedit++; + if(!dbus_message_has_signature(m, "vub") || + !dbus_message_iter_init(m, &it) || + !ibustext(&it, log->preedit, sizeof log->preedit) || + !dbus_message_iter_next(&it) || + dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_UINT32){ + log->invalid = 1; + return DBUS_HANDLER_RESULT_HANDLED; + } + dbus_message_iter_get_basic(&it, &log->cursor); + if(!dbus_message_iter_next(&it) || + dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_BOOLEAN){ + log->invalid = 1; + return DBUS_HANDLER_RESULT_HANDLED; + } + dbus_message_iter_get_basic(&it, &log->visible); + if(dbus_message_iter_next(&it)) + log->invalid = 1; + return DBUS_HANDLER_RESULT_HANDLED; + } + if(dbus_message_has_member(m, "CommitText")){ + log->ncommit++; + if(!dbus_message_has_signature(m, "v") || + !dbus_message_iter_init(m, &it) || + !ibustext(&it, log->commit, sizeof log->commit) || + dbus_message_iter_next(&it)) + log->invalid = 1; + return DBUS_HANDLER_RESULT_HANDLED; + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static void +resetsig(Siglog *log) +{ + memset(log, 0, sizeof *log); +} + +static DBusConnection* +openbus(char *address, Siglog *log) +{ + DBusConnection *conn; + DBusError err; + + dbus_error_init(&err); + conn = dbus_connection_open_private(address, &err); + if(conn == NULL){ + fail("connect to private IBus address: %s", err.message); + dbus_error_free(&err); + return NULL; + } + dbus_connection_set_exit_on_disconnect(conn, FALSE); + if(!dbus_connection_add_filter(conn, onsignal, log, NULL)){ + fail("add D-Bus signal filter: out of memory"); + dbus_connection_close(conn); + dbus_connection_unref(conn); + return NULL; + } + return conn; +} + +static void +closebus(DBusConnection **conn, Siglog *log) +{ + if(*conn == NULL) + return; + dbus_connection_remove_filter(*conn, onsignal, log); + dbus_connection_close(*conn); + dbus_connection_unref(*conn); + *conn = NULL; +} + +static DBusMessage* +method(char *path, char *iface, char *member) +{ + DBusMessage *m; + + m = dbus_message_new_method_call("org.freedesktop.IBus", path, + iface, member); + if(m == NULL) + fail("allocate %s call: out of memory", member); + return m; +} + +static DBusMessage* +sendcall(DBusConnection *conn, DBusMessage *m) +{ + DBusMessage *reply; + DBusError err; + char member[64], path[128]; + const char *s; + + if(m == NULL) + return NULL; + s = dbus_message_get_member(m); + snprintf(member, sizeof member, "%s", s != NULL ? s : "method"); + s = dbus_message_get_path(m); + snprintf(path, sizeof path, "%s", s != NULL ? s : "path"); + dbus_error_init(&err); + reply = dbus_connection_send_with_reply_and_block(conn, m, + Calltimeout, &err); + dbus_message_unref(m); + if(reply == NULL){ + fail("%s on %s: %s: %s", member, path, + err.name != NULL ? err.name : "D-Bus error", + err.message != NULL ? err.message : "no reply"); + dbus_error_free(&err); + return NULL; + } + dbus_error_free(&err); + while(dbus_connection_get_dispatch_status(conn) == DBUS_DISPATCH_DATA_REMAINS) + dbus_connection_dispatch(conn); + return reply; +} + +static int +hello(DBusConnection *conn, char *name, size_t nname) +{ + DBusMessage *m, *reply; + DBusError err; + const char *s; + + m = dbus_message_new_method_call("org.freedesktop.DBus", + "/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello"); + reply = sendcall(conn, m); + if(reply == NULL) + return 0; + dbus_error_init(&err); + if(!dbus_message_has_signature(reply, "s") || + !dbus_message_get_args(reply, &err, DBUS_TYPE_STRING, &s, + DBUS_TYPE_INVALID)){ + dbus_message_unref(reply); + dbus_error_free(&err); + return fail("Hello returned an invalid reply"); + } + if(snprintf(name, nname, "%s", s) >= (int)nname){ + dbus_message_unref(reply); + return fail("Hello name is too long"); + } + dbus_message_unref(reply); + return name[0] == ':' || fail("Hello returned invalid name %s", name); +} + +static int +createcontext(DBusConnection *conn, char *path, size_t npath) +{ + DBusMessage *m, *reply; + DBusError err; + const char *client, *p; + + client = "ibus-live-test"; + m = method("/org/freedesktop/IBus", "org.freedesktop.IBus", + "CreateInputContext"); + if(m == NULL || !dbus_message_append_args(m, DBUS_TYPE_STRING, &client, + DBUS_TYPE_INVALID)){ + if(m != NULL) dbus_message_unref(m); + return fail("build CreateInputContext call"); + } + reply = sendcall(conn, m); + if(reply == NULL) + return 0; + dbus_error_init(&err); + if(!dbus_message_has_signature(reply, "o") || + !dbus_message_get_args(reply, &err, DBUS_TYPE_OBJECT_PATH, &p, + DBUS_TYPE_INVALID)){ + dbus_message_unref(reply); + dbus_error_free(&err); + return fail("CreateInputContext returned an invalid reply"); + } + if(snprintf(path, npath, "%s", p) >= (int)npath){ + dbus_message_unref(reply); + return fail("input context path is too long"); + } + dbus_message_unref(reply); + return path[0] == '/' || fail("invalid input context path %s", path); +} + +static int +emptycall(DBusConnection *conn, Siglog *log, char *path, char *member) +{ + DBusMessage *reply; + + resetsig(log); + reply = sendcall(conn, method(path, + "org.freedesktop.IBus.InputContext", member)); + if(reply == NULL) + return 0; + if(!dbus_message_has_signature(reply, "")){ + dbus_message_unref(reply); + return fail("%s returned a nonempty reply", member); + } + dbus_message_unref(reply); + return 1; +} + +static int +keycall(DBusConnection *conn, Siglog *log, char *path, dbus_uint32_t sym, + dbus_uint32_t state, int *eaten) +{ + DBusMessage *m, *reply; + DBusError err; + dbus_uint32_t code; + dbus_bool_t b; + + resetsig(log); + code = 0; + m = method(path, "org.freedesktop.IBus.InputContext", "ProcessKeyEvent"); + if(m == NULL || !dbus_message_append_args(m, + DBUS_TYPE_UINT32, &sym, DBUS_TYPE_UINT32, &code, + DBUS_TYPE_UINT32, &state, DBUS_TYPE_INVALID)){ + if(m != NULL) dbus_message_unref(m); + return fail("build ProcessKeyEvent call"); + } + reply = sendcall(conn, m); + if(reply == NULL) + return 0; + dbus_error_init(&err); + if(!dbus_message_has_signature(reply, "b") || + !dbus_message_get_args(reply, &err, DBUS_TYPE_BOOLEAN, &b, + DBUS_TYPE_INVALID)){ + dbus_message_unref(reply); + dbus_error_free(&err); + return fail("ProcessKeyEvent returned an invalid reply"); + } + *eaten = b != FALSE; + dbus_message_unref(reply); + return 1; +} + +static int +cursorcall(DBusConnection *conn, Siglog *log, char *path, + int x, int y, int w, int h) +{ + DBusMessage *m, *reply; + dbus_int32_t dx, dy, dw, dh; + + resetsig(log); + dx = x; + dy = y; + dw = w; + dh = h; + m = method(path, "org.freedesktop.IBus.InputContext", + "SetCursorLocation"); + if(m == NULL || !dbus_message_append_args(m, + DBUS_TYPE_INT32, &dx, DBUS_TYPE_INT32, &dy, + DBUS_TYPE_INT32, &dw, DBUS_TYPE_INT32, &dh, + DBUS_TYPE_INVALID)){ + if(m != NULL) dbus_message_unref(m); + return fail("build SetCursorLocation call"); + } + reply = sendcall(conn, m); + if(reply == NULL) + return 0; + if(!dbus_message_has_signature(reply, "")){ + dbus_message_unref(reply); + return fail("SetCursorLocation returned a nonempty reply"); + } + dbus_message_unref(reply); + return 1; +} + +static int +unknowncall(DBusConnection *conn, char *path, char *member, char *error) +{ + DBusMessage *m, *reply; + DBusError err; + + m = method(path, "org.freedesktop.IBus.InputContext", member); + if(m == NULL) + return 0; + dbus_error_init(&err); + reply = dbus_connection_send_with_reply_and_block(conn, m, + Calltimeout, &err); + dbus_message_unref(m); + if(reply != NULL){ + dbus_message_unref(reply); + dbus_error_free(&err); + return fail("%s on %s unexpectedly succeeded", member, path); + } + if(!dbus_error_has_name(&err, error)){ + fail("%s on %s returned %s, expected %s", member, path, + err.name != NULL ? err.name : "no error", error); + dbus_error_free(&err); + return 0; + } + dbus_error_free(&err); + return 1; +} + +static int +limitscall(DBusConnection *conn) +{ + DBusMessage *m, *reply; + DBusError err; + const char *client; + + client = "ibus-live-test-overflow"; + m = method("/org/freedesktop/IBus", "org.freedesktop.IBus", + "CreateInputContext"); + if(m == NULL || !dbus_message_append_args(m, DBUS_TYPE_STRING, &client, + DBUS_TYPE_INVALID)){ + if(m != NULL) dbus_message_unref(m); + return fail("build overflow CreateInputContext call"); + } + dbus_error_init(&err); + reply = dbus_connection_send_with_reply_and_block(conn, m, + Calltimeout, &err); + dbus_message_unref(m); + if(reply != NULL){ + dbus_message_unref(reply); + dbus_error_free(&err); + return fail("65th input context unexpectedly succeeded"); + } + if(!dbus_error_has_name(&err, DBUS_ERROR_LIMITS_EXCEEDED)){ + fail("65th input context returned %s, expected %s", + err.name != NULL ? err.name : "no error", + DBUS_ERROR_LIMITS_EXCEEDED); + dbus_error_free(&err); + return 0; + } + dbus_error_free(&err); + return 1; +} + +static int +nosignal(Siglog *log, char *where) +{ + if(log->invalid || log->npreedit != 0 || log->ncommit != 0) + return fail("%s emitted invalid or unexpected signals", where); + return 1; +} + +static int +preedit(Siglog *log, char *path, char *text, unsigned int cursor, int visible, + char *where) +{ + if(log->invalid || log->npreedit != 1 || log->ncommit != 0 || + strcmp(log->path, path) != 0 || strcmp(log->preedit, text) != 0 || + log->cursor != cursor || (log->visible != FALSE) != (visible != 0)) + return fail("%s preedit: count=%d commit=%d path=%s text=%s cursor=%u visible=%d", + where, log->npreedit, log->ncommit, log->path, log->preedit, + (unsigned)log->cursor, log->visible != FALSE); + return 1; +} + +static int +committed(Siglog *log, char *path, char *text, char *where) +{ + if(log->invalid || log->npreedit != 1 || log->ncommit != 1 || + strcmp(log->path, path) != 0 || strcmp(log->commit, text) != 0 || + strcmp(log->preedit, "") != 0 || log->cursor != 0 || + log->visible != FALSE) + return fail("%s signals: preedit=%d commit=%d text=%s/%s", + where, log->npreedit, log->ncommit, log->commit, log->preedit); + return 1; +} + +static int +expectkey(DBusConnection *conn, Siglog *log, char *path, + dbus_uint32_t sym, dbus_uint32_t state, int eaten, char *text, char *where) +{ + int got; + + if(!keycall(conn, log, path, sym, state, &got)) + return 0; + if(got != eaten) + return fail("%s eaten=%d, expected %d", where, got, eaten); + if(text == NULL) + return nosignal(log, where); + return preedit(log, path, text, (unsigned)strlen(text) == 0 ? 0 : 1, + text[0] != '\0', where); +} + +static int +runlifecycle(Daemon *d) +{ + DBusConnection *c1, *c2, *c3; + Siglog s1, s2, s3; + char n1[32], n2[32], n3[32]; + char a[96], a2[96], b[96], active[96], path[96], first[96]; + int eaten, i, ok; + + c1 = c2 = c3 = NULL; + memset(&s1, 0, sizeof s1); + memset(&s2, 0, sizeof s2); + memset(&s3, 0, sizeof s3); + ok = 0; + c1 = openbus(d->address, &s1); + c2 = openbus(d->address, &s2); + if(c1 == NULL || c2 == NULL) + goto out; + if(!hello(c1, n1, sizeof n1) || !hello(c2, n2, sizeof n2) || + strcmp(n1, n2) == 0) + goto out; + if(!createcontext(c1, a, sizeof a) || !createcontext(c2, b, sizeof b)) + goto out; + if(!unknowncall(c1, "/org/freedesktop/IBus/InputContext_missing", + "FocusIn", DBUS_ERROR_UNKNOWN_OBJECT)) + goto out; + if(!expectkey(c1, &s1, a, 'x', 0, 0, NULL, "unfocused key")) + goto out; + if(!cursorcall(c1, &s1, a, 10, 20, 0, 14) || + !nosignal(&s1, "pre-focus cursor") || + !emptycall(c1, &s1, a, "FocusIn") || !nosignal(&s1, "FocusIn") || + !expectkey(c1, &s1, a, 'x', Relmask, 0, NULL, "physical release")) + goto out; + if(!expectkey(c1, &s1, a, 'n', Ctrlmask, 1, "", "select Japanese") || + !expectkey(c1, &s1, a, 'k', 0, 1, "k", "A key k") || + !expectkey(c1, &s1, a, 'a', 0, 1, "か", "A key a")) + goto out; + if(!emptycall(c2, &s2, b, "FocusIn") || !nosignal(&s2, "B FocusIn") || + !expectkey(c2, &s2, b, 'n', 0, 1, "ん", "B takeover") || + !expectkey(c1, &s1, a, 0xffe1, 0, 0, "", "stale A state") || + !expectkey(c2, &s2, b, 0xffe1, 0, 0, "ん", "active B state")) + goto out; + if(!emptycall(c1, &s1, a, "Reset") || + !preedit(&s1, a, "", 0, 0, "stale Reset reply") || + !expectkey(c2, &s2, b, 0xffe1, 0, 0, "ん", "B after stale Reset") || + !emptycall(c1, &s1, a, "FocusOut") || + !preedit(&s1, a, "", 0, 0, "stale FocusOut reply") || + !expectkey(c2, &s2, b, 0xffe1, 0, 0, "ん", "B after stale FocusOut")) + goto out; + if(!emptycall(c1, &s1, a, "FocusIn") || !nosignal(&s1, "stale FocusIn") || + !cursorcall(c1, &s1, a, 90, 100, 0, 20) || + !nosignal(&s1, "stale cursor") || + !expectkey(c2, &s2, b, 0xffe1, 0, 0, "ん", "B after stale cursor") || + !emptycall(c1, &s1, a, "Destroy") || !nosignal(&s1, "stale Destroy") || + !expectkey(c2, &s2, b, 0xffe1, 0, 0, "ん", "B after stale Destroy") || + !unknowncall(c1, a, "FocusIn", DBUS_ERROR_UNKNOWN_OBJECT)) + goto out; + if(!createcontext(c1, a2, sizeof a2) || + !emptycall(c1, &s1, a2, "FocusIn") || !nosignal(&s1, "A2 FocusIn") || + !cursorcall(c1, &s1, a2, 120, 130, 0, 18) || + !nosignal(&s1, "A2 stale cursor")) + goto out; + closebus(&c1, &s1); + if(!expectkey(c2, &s2, b, 0xffe1, 0, 0, "ん", "B during stale close") || + !expectkey(c2, &s2, b, 0xffe1, 0, 0, "ん", "B after stale close")) + goto out; + if(!emptycall(c2, &s2, b, "Reset") || + !preedit(&s2, b, "", 0, 0, "active Reset") || + !expectkey(c2, &s2, b, 'k', 0, 1, "k", "reset owner key k") || + !expectkey(c2, &s2, b, 'a', 0, 1, "か", "reset owner key a")) + goto out; + if(!keycall(c2, &s2, b, 0xff0d, 0, &eaten) || !eaten || + !committed(&s2, b, "か", "active commit")) + goto out; + if(!expectkey(c2, &s2, b, 'k', 0, 1, "k", "pre-FocusOut key") || + !cursorcall(c2, &s2, b, 50, 60, 0, 12) || + !nosignal(&s2, "active cursor") || + !emptycall(c2, &s2, b, "FocusOut") || + !preedit(&s2, b, "", 0, 0, "active FocusOut") || + !emptycall(c2, &s2, b, "FocusOut") || + !preedit(&s2, b, "", 0, 0, "repeated FocusOut") || + !emptycall(c2, &s2, b, "FocusIn") || !nosignal(&s2, "B refocus") || + !expectkey(c2, &s2, b, 0xffe1, 0, 0, "", "B state after FocusOut") || + !expectkey(c2, &s2, b, 'k', 0, 1, "k", "B after refocus") || + !emptycall(c2, &s2, b, "Destroy") || !nosignal(&s2, "active Destroy") || + !unknowncall(c2, b, "Reset", DBUS_ERROR_UNKNOWN_OBJECT)) + goto out; + if(!createcontext(c2, active, sizeof active)) + goto out; + for(i = 1; i < 64; i++) + if(!createcontext(c2, path, sizeof path)) + goto out; + if(!emptycall(c2, &s2, active, "FocusIn") || + !nosignal(&s2, "capacity owner FocusIn") || + !expectkey(c2, &s2, active, 'k', 0, 1, "k", "capacity owner key")) + goto out; + closebus(&c2, &s2); + c3 = openbus(d->address, &s3); + if(c3 == NULL || !hello(c3, n3, sizeof n3)) + goto out; + for(i = 0; i < 64; i++){ + if(!createcontext(c3, path, sizeof path)) + goto out; + if(i == 0) + snprintf(first, sizeof first, "%s", path); + } + if(!limitscall(c3) || + !emptycall(c3, &s3, first, "FocusIn") || + !nosignal(&s3, "reused context FocusIn") || + !expectkey(c3, &s3, first, 'k', 0, 1, "k", "reused context key")) + goto out; + ok = 1; +out: + closebus(&c1, &s1); + closebus(&c2, &s2); + closebus(&c3, &s3); + return ok; +} + +int +main(int argc, char **argv) +{ + Daemon daemon; + int ok; + + if(argc != 3){ + fprintf(stderr, "usage: ibus_live_test strans mapdir\n"); + return 2; + } + ok = startdaemon(&daemon, argv[1], argv[2]); + if(ok) + ok = runlifecycle(&daemon); + if(!ok) + showerrors(&daemon); + if(!stopdaemon(&daemon)) + ok = 0; + if(!ok) + return 1; + printf("ibus live lifecycle: ok\n"); + return 0; +}