Files
strans/tests/xim_live_test.c
Hojun-Cho 7220286ab5 test: share one daemon and D-Bus harness across live tests
Six live tests carried private copies of the same daemon harness: the
private XDG_RUNTIME_DIR, the fork/exec with captured stderr, the
readiness waits, the SIGTERM and SIGKILL paths, the IBus address file
reader, the socket connect, and the IPC probe. Three of them also
carried the same libdbus helpers.

Extract one implementation into tests/live.c and, so that the binaries
that do not link dbus-1 keep not linking it, the D-Bus half into
tests/livebus.c. Both are compiled into each binary the way ../ipc.c
already is. gtk_live_test keeps its own fake-server harness and only
takes fail, nowms and leftms; xim_live_test keeps its own Xvfb and
process-group daemon spawn, which must inherit DISPLAY, and takes the
temporary directory, timing and cleanup halves.

The copies had drifted; the harness keeps the stricter behaviour.

  - nowms reports a broken clock (-1) instead of pretending it read
    zero, and leftms turns that into an expired deadline, so a loop
    ends in a timeout failure rather than spinning. livesetup checks
    the clock once up front, as daemon_restart_test did.
  - Timeouts compare with <= 0, not == 0.
  - readuntil keeps the three-way result (complete, peer closed, error)
    from ipc_live_test and daemon_restart_test rather than folding
    peer closure into ECONNRESET.
  - The daemon's stdout and stderr are both captured, and a failing
    setenv is reported, for every daemon; daemon_failure_test captured
    stderr only and said nothing about setenv.
  - The child keeps daemon_restart_test's careful redirect that also
    works when the pipe lands on fd 1 or 2.
  - parseaddress requires a positive declared PID.
  - killdaemon reports ECHILD after SIGKILL as a failure; one copy
    accepted it. It now reaps with a blocking waitpid, which SIGKILL
    guarantees will return, instead of daemon_restart_test's polled
    wait with its own timeout diagnostic.
  - stopdaemon tolerates ESRCH on SIGTERM, a benign race two copies
    reported as an error.
  - liveclean removes the socket and address files and then rmdirs
    each directory, reporting leftovers, rather than deleting the
    temporary root recursively.
  - ibus_live_test now waits for the IPC socket and the address file
    by polling, dropping its inotify variant; it asserted only the
    address file before.
2026-08-16 16:44:05 +09:00

787 lines
18 KiB
C

#define _GNU_SOURCE
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <errno.h>
#include <fcntl.h>
#include <locale.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "ipc.h"
#include "live.h"
enum
{
Eventtimeout = 4000,
};
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
{
Live l;
Child xvfb;
Child daemon;
char display[32];
};
struct Prelog
{
int start;
int nonempty;
int empty;
int done;
int doneafterempty;
int badorder;
};
static int xerror;
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
spawndaemon(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->l.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;
if(!livesetup(&r->l, "xim"))
return 0;
if(snprintf(r->xvfb.log, sizeof r->xvfb.log, "%s/xvfb.log", r->l.root)
>= (int)sizeof r->xvfb.log ||
snprintf(r->daemon.log, sizeof r->daemon.log, "%s/daemon.log", r->l.root)
>= (int)sizeof r->daemon.log)
return fail("temporary path is too long");
if(!spawnxvfb(r, xvfb))
return 0;
if(setenv("DISPLAY", r->display, 1) < 0 ||
setenv("XMODIFIERS", "@im=strans", 1) < 0 ||
setenv("XDG_RUNTIME_DIR", r->l.runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", r->l.config, 1) < 0 ||
setenv("HOME", r->l.home, 1) < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS") < 0)
return fail("set private environment: %s", strerror(errno));
if(!spawndaemon(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
cleanup(Run *r, int noisy)
{
stopchild(&r->daemon);
stopchild(&r->xvfb);
if(noisy){
fprintf(stderr, "%s: daemon log:\n", testname);
showlog(r->daemon.log);
fprintf(stderr, "%s: Xvfb log:\n", testname);
showlog(r->xvfb.log);
}
if(r->daemon.log[0] != '\0')
unlink(r->daemon.log);
if(r->xvfb.log[0] != '\0')
unlink(r->xvfb.log);
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 || 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
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;
}
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;
testname = "xim_live_test";
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;
}