From 4eba8c290449af143056742118f1982403de51af Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 16:17:27 +0900 Subject: [PATCH] tests: cover in-process XIM lifecycle --- tests/Makefile | 21 ++- tests/ipc_live_test.c | 3 +- tests/test.h | 3 + tests/unit_test.c | 3 + tests/xim_adapter_test.c | 306 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 330 insertions(+), 6 deletions(-) create mode 100644 tests/xim_adapter_test.c diff --git a/tests/Makefile b/tests/Makefile index a8d23ac..d79bfff 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -2,25 +2,28 @@ CC = 9c LD = 9l HOSTCC = cc CFLAGS = -std=c99 -Wall -Wextra -O2 -g -I.. -I../cutest -TEXT_CFLAGS = $(shell pkg-config --cflags 'pangocairo >= 1.56' 'cairo >= 1.18') -TEXT_LIBS = $(shell pkg-config --libs 'pangocairo >= 1.56' 'cairo >= 1.18') +TEXT_CFLAGS = $(shell pkg-config --cflags pangocairo cairo) +TEXT_LIBS = $(shell pkg-config --libs pangocairo cairo) DBUS_CFLAGS = $(shell pkg-config --cflags dbus-1) DBUS_LIBS = $(shell pkg-config --libs dbus-1) IBUS_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) IBUS_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) -LIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) +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 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 + popup_test.c font_test.c ibus_test.c server_test.c xim_adapter_test.c TESTOBJ = $(TESTSRC:.c=.o) PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \ font.c PARENTOBJ = $(PARENTSRC:%.c=unit_%.o) -OBJS = $(TESTOBJ) $(PARENTOBJ) +XIMOBJ = unit_xim_keymap.o unit_ximtext.o +OBJS = $(TESTOBJ) $(PARENTOBJ) $(XIMOBJ) all: $(PROG) $(LIVE) @@ -56,9 +59,17 @@ $(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h ../cutest/cutest.h engine_test.o: ../strans.c ibus_test.o: CFLAGS += $(IBUS_CFLAGS) ibus_test.o: ../ibus.c +xim_adapter_test.o: CFLAGS += $(XIM_CFLAGS) +xim_adapter_test.o: ../xim/xim.c server_test.o: ../srv.c unit_font.o: CFLAGS += $(TEXT_CFLAGS) +unit_xim_keymap.o: ../xim/keymap.c + $(CC) $(CFLAGS) $(XIM_CFLAGS) -c -o $@ $< + +unit_ximtext.o: ../xim/ximtext.c + $(CC) $(CFLAGS) $(XIM_CFLAGS) -c -o $@ $< + unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h $(CC) $(CFLAGS) -c -o $@ $< diff --git a/tests/ipc_live_test.c b/tests/ipc_live_test.c index de0f013..4b4325e 100644 --- a/tests/ipc_live_test.c +++ b/tests/ipc_live_test.c @@ -236,7 +236,8 @@ startdaemon(Daemon *d, char *program, char *mapdir) } if(setenv("XDG_RUNTIME_DIR", d->runtime, 1) < 0 || setenv("XDG_CONFIG_HOME", d->config, 1) < 0 || - setenv("HOME", d->home, 1) < 0 || unsetenv("DISPLAY") < 0 || + setenv("HOME", d->home, 1) < 0 || + setenv("DISPLAY", "invalid", 1) < 0 || unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || unsetenv("IBUS_ADDRESS") < 0){ dprintf(STDERR_FILENO, "set daemon environment: %s\n", diff --git a/tests/test.h b/tests/test.h index f82d04a..a50ccd9 100644 --- a/tests/test.h +++ b/tests/test.h @@ -75,3 +75,6 @@ void ibus_machine_id_fallback(struct ct*); void ibus_startup_requires_ownership(struct ct*); void ibus_context_lifecycle(struct ct*); void ibus_active_release_lifecycle(struct ct*); +void xim_adapter_key_contract(struct ct*); +void xim_adapter_release_lifecycle(struct ct*); +void xim_adapter_free_waits_for_release(struct ct*); diff --git a/tests/unit_test.c b/tests/unit_test.c index 49f718b..0ca7813 100644 --- a/tests/unit_test.c +++ b/tests/unit_test.c @@ -129,6 +129,9 @@ static const struct ct_test tests[] = { { "ibus/startup-requires-ownership", ibus_startup_requires_ownership }, { "ibus/context-lifecycle", ibus_context_lifecycle }, { "ibus/active-release-lifecycle", ibus_active_release_lifecycle }, + { "xim/adapter-key-contract", xim_adapter_key_contract }, + { "xim/adapter-release-lifecycle", xim_adapter_release_lifecycle }, + { "xim/adapter-free-waits-release", xim_adapter_free_waits_for_release }, }; void diff --git a/tests/xim_adapter_test.c b/tests/xim_adapter_test.c new file mode 100644 index 0000000..7dc8e32 --- /dev/null +++ b/tests/xim_adapter_test.c @@ -0,0 +1,306 @@ +#include "xim/xim.c" +#include "cutest/cutest.h" + +void testengineinit(int); +void testenginehandle(Keyreq*); +void* testengineowner(void); +void testenginepreedit(Str*); + +typedef struct Ximfix Ximfix; +struct Ximfix +{ + Channel *oldreply; + Channel *trace; + Channel *stop; + Channel *done; + Channel *waiting; + Channel *ack; + int holdrelease; + int pumpactive; +}; + +typedef struct Freejob Freejob; +struct Freejob +{ + Ic *state; + Channel *done; +}; + +static void +enginepump(void *arg) +{ + Ximfix *f; + Keyreq req; + uchar token; + Alt alts[] = { + {keyc, &req, CHANRCV, nil}, + {nil, &token, CHANRCV, nil}, + {nil, nil, CHANEND, nil}, + }; + + f = arg; + alts[1].c = f->stop; + for(;;) + switch(alt(alts)){ + case 0: + chansend(f->trace, &req); + if(f->holdrelease && req.op == Keyrelease){ + token = 0; + chansend(f->waiting, &token); + chanrecv(f->ack, &token); + } + testenginehandle(&req); + break; + case 1: + chansend(f->done, &token); + return; + } +} + +static int +ximbegin(struct ct *t, Ximfix *f, int lang) +{ + Drawcmd dc; + + memset(f, 0, sizeof *f); + while(channbrecv(drawc, &dc) > 0) + ; + testengineinit(lang); + f->oldreply = replyc; + replyc = chancreate(sizeof(Keyres), 0); + f->trace = chancreate(sizeof(Keyreq), 16); + f->stop = chancreate(sizeof(uchar), 0); + f->done = chancreate(sizeof(uchar), 0); + f->waiting = chancreate(sizeof(uchar), 0); + f->ack = chancreate(sizeof(uchar), 0); + f->pumpactive = threadcreate(enginepump, f, 8192) >= 0; + return CT_CHECK(t, replyc != nil && f->trace != nil && + f->stop != nil && f->done != nil && f->waiting != nil && + f->ack != nil && f->pumpactive); +} + +static void +ximend(Ximfix *f) +{ + Drawcmd dc; + uchar token; + + if(f->pumpactive){ + token = 0; + chansend(f->stop, &token); + chanrecv(f->done, &token); + } + testengineinit(LangEN); + while(channbrecv(drawc, &dc) > 0) + ; + chanfree(replyc); + replyc = f->oldreply; + chanfree(f->trace); + chanfree(f->stop); + chanfree(f->done); + chanfree(f->waiting); + chanfree(f->ack); +} + +static Keyreq +nexttrace(struct ct *t, Ximfix *f, int op, Ic *state) +{ + Keyreq req; + + memset(&req, 0, sizeof req); + if(!CT_CHECK(t, channbrecv(f->trace, &req) > 0)) + return req; + CT_EQ_INT(t, op, req.op); + CT_EQ_PTR(t, state, req.owner); + CT_EQ_PTR(t, replyc, req.reply); + return req; +} + +static void +notrace(struct ct *t, Ximfix *f) +{ + Keyreq req; + + CT_CHECK(t, channbrecv(f->trace, &req) <= 0); +} + +static void +checkstr(struct ct *t, char *want, Str *got) +{ + char buf[Maxutf]; + + stoutf(got, buf, sizeof buf); + CT_EQ_STR(t, want, buf); +} + +static void +checkenginepreedit(struct ct *t, char *want) +{ + Str preedit; + + testenginepreedit(&preedit); + checkstr(t, want, &preedit); +} + +void +xim_adapter_key_contract(struct ct *t) +{ + Ximfix f; + Ic state; + Keyreq req; + Keyres res; + u32int allmod; + + memset(&state, 0, sizeof state); + if(!ximbegin(t, &f, LangJP)) + goto cleanup; + allmod = ~0; + keypress(&state, 'x', allmod, &res); + req = nexttrace(t, &f, Keypress, &state); + CT_EQ_UINT(t, Mmask, req.mod); + CT_CHECK(t, !res.eaten); + keypress(&state, 'k', 0, &res); + req = nexttrace(t, &f, Keypress, &state); + CT_EQ_UINT(t, 0, req.mod); + CT_CHECK(t, res.eaten); + keypress(&state, 'a', 0, &res); + nexttrace(t, &f, Keypress, &state); + CT_CHECK(t, res.eaten); + checkstr(t, "か", &res.preedit); + + keypress(&state, Kspec|0x55, 0, &res); + nexttrace(t, &f, Keypress, &state); + CT_CHECK(t, !res.eaten); + checkstr(t, "か", &res.commit); + checkstr(t, "", &res.preedit); +cleanup: + ximend(&f); +} + +void +xim_adapter_release_lifecycle(struct ct *t) +{ + Ximfix f; + Ic a, b, *dead; + Keyres res; + + memset(&a, 0, sizeof a); + memset(&b, 0, sizeof b); + dead = nil; + if(!ximbegin(t, &f, LangJP)) + goto cleanup; + keypress(&a, 'k', 0, &res); + nexttrace(t, &f, Keypress, &a); + keypress(&a, 'a', 0, &res); + nexttrace(t, &f, Keypress, &a); + keypress(&b, 'n', 0, &res); + nexttrace(t, &f, Keypress, &b); + checkstr(t, "ん", &res.preedit); + CT_EQ_PTR(t, &b, testengineowner()); + + /* ResetIC and focus loss use the same idempotent release path. */ + release(&a); + nexttrace(t, &f, Keyrelease, &a); + checkenginepreedit(t, "ん"); + CT_EQ_PTR(t, &b, testengineowner()); + release(&a); + notrace(t, &f); + + release(&b); + nexttrace(t, &f, Keyrelease, &b); + CT_EQ_PTR(t, nil, testengineowner()); + keypress(&b, 'k', 0, &res); + nexttrace(t, &f, Keypress, &b); + release(&b); + nexttrace(t, &f, Keyrelease, &b); + release(&b); + notrace(t, &f); + + dead = calloc(1, sizeof *dead); + if(!CT_CHECK(t, dead != nil)) + goto cleanup; + dead->next = ics; + ics = dead; + keypress(dead, 'n', 0, &res); + nexttrace(t, &f, Keypress, dead); + CT_EQ_PTR(t, dead, testengineowner()); + icfree(dead); + nexttrace(t, &f, Keyrelease, dead); + dead = nil; + CT_EQ_PTR(t, nil, ics); + CT_EQ_PTR(t, nil, testengineowner()); +cleanup: + if(dead != nil){ + icunlink(dead); + free(dead); + } + ximend(&f); +} + +static void +freeworker(void *arg) +{ + Freejob *job; + uchar token; + + job = arg; + icfree(job->state); + token = 0; + chansend(job->done, &token); +} + +void +xim_adapter_free_waits_for_release(struct ct *t) +{ + Ximfix f; + Freejob job; + Ic *state; + Channel *freed; + Keyres res; + uchar token; + int workeractive; + + state = nil; + freed = nil; + workeractive = 0; + if(!ximbegin(t, &f, LangJP)) + goto cleanup; + state = calloc(1, sizeof *state); + freed = chancreate(sizeof(uchar), 0); + if(!CT_CHECK(t, state != nil && freed != nil)) + goto cleanup; + state->next = ics; + ics = state; + keypress(state, 'n', 0, &res); + nexttrace(t, &f, Keypress, state); + f.holdrelease = 1; + job.state = state; + job.done = freed; + workeractive = threadcreate(freeworker, &job, 8192) >= 0; + if(!CT_CHECK(t, workeractive)) + goto cleanup; + chanrecv(f.waiting, &token); + nexttrace(t, &f, Keyrelease, state); + CT_CHECK(t, channbrecv(freed, &token) <= 0); + CT_EQ_PTR(t, state, ics); + CT_EQ_PTR(t, state, testengineowner()); + chansend(f.ack, &token); + chanrecv(freed, &token); + workeractive = 0; + state = nil; + CT_EQ_PTR(t, nil, ics); + CT_EQ_PTR(t, nil, testengineowner()); +cleanup: + if(workeractive){ + chansend(f.ack, &token); + chanrecv(freed, &token); + state = nil; + } + if(state != nil){ + icunlink(state); + free(state); + } + if(freed != nil) + chanfree(freed); + ximend(&f); +}