The XIM test carried its own Xvfb and daemon spawner, its own child struct, log files and process-group teardown, and the GTK test a second Xvfb launcher, because live.c's startdaemon always unset DISPLAY. Live now carries the display its own startxvfb reports, startdaemon passes it on, and both tests use the shared pair: 300 lines fewer, one place that knows how a child is started, watched, and stopped.
614 lines
14 KiB
C
614 lines
14 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 Run Run;
|
|
typedef struct Prelog Prelog;
|
|
|
|
struct Run
|
|
{
|
|
Live l;
|
|
Daemon xvfb;
|
|
Daemon daemon;
|
|
};
|
|
|
|
struct Prelog
|
|
{
|
|
int start;
|
|
int nonempty;
|
|
int empty;
|
|
int done;
|
|
int doneafterempty;
|
|
int badorder;
|
|
};
|
|
|
|
static int xerror;
|
|
|
|
static int
|
|
waitsocket(Run *r)
|
|
{
|
|
struct stat st;
|
|
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(!daemonalive(&r->daemon))
|
|
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);
|
|
daemoninit(&r->xvfb, "Xvfb");
|
|
daemoninit(&r->daemon, "daemon");
|
|
if(!livesetup(&r->l, "xim") || !startxvfb(&r->l, &r->xvfb, xvfb))
|
|
return 0;
|
|
if(setenv("DISPLAY", r->l.display, 1) < 0 ||
|
|
setenv("XMODIFIERS", "@im=strans", 1) < 0)
|
|
return fail("set private environment: %s", strerror(errno));
|
|
if(!startdaemon(&r->l, &r->daemon, daemon, mapdir))
|
|
return 0;
|
|
return waitsocket(r);
|
|
}
|
|
|
|
static void
|
|
cleanup(Run *r, int noisy)
|
|
{
|
|
stopdaemon(&r->daemon);
|
|
stopdaemon(&r->xvfb);
|
|
if(noisy){
|
|
showerrors(&r->daemon);
|
|
showerrors(&r->xvfb);
|
|
}
|
|
closeerrors(&r->daemon);
|
|
closeerrors(&r->xvfb);
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* The popup is the one viewable override-redirect window on the private
|
|
* display; created before the client window, it must still stack above it.
|
|
*/
|
|
static int
|
|
popupabove(Display *dpy, Window client)
|
|
{
|
|
XWindowAttributes wa;
|
|
Window root, parent, *kids;
|
|
unsigned i, n;
|
|
int64_t deadline;
|
|
int above;
|
|
|
|
deadline = nowms() + Eventtimeout;
|
|
for(;;){
|
|
above = 0;
|
|
root = DefaultRootWindow(dpy);
|
|
if(!XQueryTree(dpy, root, &root, &parent, &kids, &n))
|
|
return fail("query the window tree");
|
|
for(i = 0; i < n; i++){
|
|
if(kids[i] == client)
|
|
above = 1;
|
|
else if(XGetWindowAttributes(dpy, kids[i], &wa) &&
|
|
wa.override_redirect && wa.map_state == IsViewable)
|
|
break;
|
|
}
|
|
XFree(kids);
|
|
if(i < n)
|
|
return above || fail("popup is stacked below the client");
|
|
if(leftms(deadline) == 0)
|
|
return fail("popup did not appear");
|
|
pump(dpy, 20);
|
|
}
|
|
}
|
|
|
|
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) ||
|
|
!popupabove(dpy, win) ||
|
|
!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;
|
|
}
|
|
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.l.display);
|
|
if(dpy == NULL){
|
|
fail("open private display %s", run.l.display);
|
|
cleanup(&run, 1);
|
|
return 1;
|
|
}
|
|
im = openim(dpy);
|
|
if(im == NULL){
|
|
fail("open strans XIM on private display %s", run.l.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;
|
|
}
|