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.
939 lines
23 KiB
C
939 lines
23 KiB
C
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <gtk/gtk.h>
|
|
#include <poll.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/un.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include "ipc.h"
|
|
#include "live.h"
|
|
|
|
enum
|
|
{
|
|
Maxevent = 256,
|
|
Timeout = 4000,
|
|
};
|
|
|
|
enum
|
|
{
|
|
Ecap,
|
|
Ecaret,
|
|
Ekey,
|
|
Ereset,
|
|
};
|
|
|
|
enum
|
|
{
|
|
Rnormal,
|
|
Rcommitpre,
|
|
Rbadcommit,
|
|
Rbadpreedit,
|
|
};
|
|
|
|
typedef struct Event Event;
|
|
typedef struct Server Server;
|
|
typedef struct Siglog Siglog;
|
|
|
|
struct Event
|
|
{
|
|
int type;
|
|
int want;
|
|
int valid;
|
|
uint32_t key;
|
|
int32_t x;
|
|
int32_t y;
|
|
int32_t h;
|
|
};
|
|
|
|
struct Server
|
|
{
|
|
pthread_t thread;
|
|
pthread_mutex_t lock;
|
|
pthread_cond_t changed;
|
|
int listenfd;
|
|
int wake[2];
|
|
int stop;
|
|
int old;
|
|
int eaten;
|
|
int stall;
|
|
int response;
|
|
int nclose;
|
|
char pre[Ipcfieldmax+1];
|
|
Event ev[Maxevent];
|
|
int nev;
|
|
};
|
|
|
|
struct Siglog
|
|
{
|
|
char event[128];
|
|
int n;
|
|
char pre[Ipcfieldmax+1];
|
|
char shown[Ipcfieldmax+1];
|
|
char commit[Ipcfieldmax+1];
|
|
int prechanged;
|
|
};
|
|
|
|
static void
|
|
record(Server *s, Event *e)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
if(s->nev < Maxevent)
|
|
s->ev[s->nev++] = *e;
|
|
pthread_cond_broadcast(&s->changed);
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static void
|
|
closed(Server *s)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
s->nclose++;
|
|
pthread_cond_broadcast(&s->changed);
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static int
|
|
sendresponse(Server *s, int fd, int eaten, int want, int key)
|
|
{
|
|
unsigned char buf[Ipcmaxresp];
|
|
const char *commit;
|
|
char pre[Ipcfieldmax+1];
|
|
int n, response, stall;
|
|
size_t ncommit, npreedit;
|
|
|
|
pthread_mutex_lock(&s->lock);
|
|
memcpy(pre, s->pre, sizeof pre);
|
|
response = s->response;
|
|
stall = s->stall;
|
|
pthread_mutex_unlock(&s->lock);
|
|
if(stall)
|
|
return 1;
|
|
commit = "";
|
|
ncommit = 0;
|
|
npreedit = strlen(pre);
|
|
if(key && response == Rcommitpre){
|
|
commit = pre;
|
|
ncommit = npreedit;
|
|
}else if(key && response == Rbadcommit){
|
|
commit = "\xc3(";
|
|
ncommit = 2;
|
|
}else if(key && response == Rbadpreedit){
|
|
pre[0] = 'x';
|
|
pre[1] = 0;
|
|
pre[2] = 'y';
|
|
npreedit = 3;
|
|
}
|
|
n = ipcpackresp(buf, sizeof buf, eaten, commit, ncommit,
|
|
pre, npreedit, want);
|
|
return n >= 0 && ipcsend(fd, buf, n) == 0;
|
|
}
|
|
|
|
static int
|
|
request(Server *s, int fd)
|
|
{
|
|
unsigned char buf[Ipccaretsz];
|
|
uint32_t mod, key;
|
|
Event e;
|
|
int old, type, want;
|
|
|
|
if(ipcreadn(fd, buf, Ipcreqsz) < 0)
|
|
return 0;
|
|
type = ipcreqtype(buf);
|
|
memset(&e, 0, sizeof e);
|
|
switch(type){
|
|
case Ipccap:
|
|
e.type = Ecap;
|
|
ipcunpackreq(buf, &e.want, &mod, &key);
|
|
record(s, &e);
|
|
pthread_mutex_lock(&s->lock);
|
|
old = s->old;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return sendresponse(s, fd, !old, e.want, 0);
|
|
case Ipccaret:
|
|
if(ipcreadn(fd, buf + Ipcreqsz, Ipccaretsz - Ipcreqsz) < 0 ||
|
|
ipcunpackcaret(buf, &e.valid, &e.x, &e.y, &e.h) < 0)
|
|
return 0;
|
|
e.type = Ecaret;
|
|
record(s, &e);
|
|
return 1;
|
|
case Ipckey:
|
|
ipcunpackreq(buf, &want, &mod, &key);
|
|
e.want = want;
|
|
e.key = key;
|
|
e.type = ipcreqreset(buf) ? Ereset : Ekey;
|
|
record(s, &e);
|
|
pthread_mutex_lock(&s->lock);
|
|
old = s->eaten;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return sendresponse(s, fd, e.type == Ekey && old, want,
|
|
e.type == Ekey);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void*
|
|
serverthread(void *v)
|
|
{
|
|
Server *s;
|
|
struct pollfd pfd[3];
|
|
int fd, n;
|
|
char c;
|
|
|
|
s = v;
|
|
fd = -1;
|
|
for(;;){
|
|
pfd[0].fd = s->wake[0];
|
|
pfd[0].events = POLLIN;
|
|
pfd[1].fd = s->listenfd;
|
|
pfd[1].events = fd < 0 ? POLLIN : 0;
|
|
pfd[2].fd = fd;
|
|
pfd[2].events = fd >= 0 ? POLLIN : 0;
|
|
n = poll(pfd, 3, -1);
|
|
if(n < 0 && errno == EINTR)
|
|
continue;
|
|
if(n < 0 || (pfd[0].revents & POLLIN))
|
|
break;
|
|
if(pfd[1].revents & POLLIN){
|
|
fd = accept(s->listenfd, NULL, NULL);
|
|
continue;
|
|
}
|
|
if(fd >= 0 && pfd[2].revents != 0){
|
|
if((pfd[2].revents & POLLIN) && request(s, fd))
|
|
continue;
|
|
close(fd);
|
|
fd = -1;
|
|
closed(s);
|
|
}
|
|
}
|
|
if(fd >= 0){
|
|
close(fd);
|
|
closed(s);
|
|
}
|
|
while(read(s->wake[0], &c, 1) < 0 && errno == EINTR)
|
|
;
|
|
return NULL;
|
|
}
|
|
|
|
static int
|
|
startserver(Server *s, char *path)
|
|
{
|
|
struct sockaddr_un addr;
|
|
int cond;
|
|
|
|
memset(s, 0, sizeof *s);
|
|
s->eaten = 1;
|
|
s->listenfd = -1;
|
|
s->wake[0] = s->wake[1] = -1;
|
|
if(pthread_mutex_init(&s->lock, NULL) != 0)
|
|
return fail("initialize server lock");
|
|
cond = pthread_cond_init(&s->changed, NULL) == 0;
|
|
if(!cond){
|
|
fail("initialize server condition");
|
|
goto bad;
|
|
}
|
|
if(pipe(s->wake) < 0){
|
|
fail("pipe: %s", strerror(errno));
|
|
goto bad;
|
|
}
|
|
s->listenfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if(s->listenfd < 0){
|
|
fail("socket: %s", strerror(errno));
|
|
goto bad;
|
|
}
|
|
memset(&addr, 0, sizeof addr);
|
|
addr.sun_family = AF_UNIX;
|
|
if(strlen(path) >= sizeof addr.sun_path){
|
|
fail("socket path is too long");
|
|
goto bad;
|
|
}
|
|
strcpy(addr.sun_path, path);
|
|
if(bind(s->listenfd, (struct sockaddr*)&addr, sizeof addr) < 0 ||
|
|
listen(s->listenfd, 4) < 0){
|
|
fail("listen %s: %s", path, strerror(errno));
|
|
goto bad;
|
|
}
|
|
if(pthread_create(&s->thread, NULL, serverthread, s) != 0){
|
|
fail("create server thread");
|
|
goto bad;
|
|
}
|
|
return 1;
|
|
bad:
|
|
if(s->listenfd >= 0)
|
|
close(s->listenfd);
|
|
if(s->wake[0] >= 0)
|
|
close(s->wake[0]);
|
|
if(s->wake[1] >= 0)
|
|
close(s->wake[1]);
|
|
if(cond)
|
|
pthread_cond_destroy(&s->changed);
|
|
pthread_mutex_destroy(&s->lock);
|
|
s->listenfd = s->wake[0] = s->wake[1] = -1;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
stopserver(Server *s)
|
|
{
|
|
char c;
|
|
|
|
if(s->wake[1] >= 0){
|
|
c = 0;
|
|
while(write(s->wake[1], &c, 1) < 0 && errno == EINTR)
|
|
;
|
|
pthread_join(s->thread, NULL);
|
|
}
|
|
if(s->listenfd >= 0)
|
|
close(s->listenfd);
|
|
if(s->wake[0] >= 0)
|
|
close(s->wake[0]);
|
|
if(s->wake[1] >= 0)
|
|
close(s->wake[1]);
|
|
pthread_cond_destroy(&s->changed);
|
|
pthread_mutex_destroy(&s->lock);
|
|
s->listenfd = s->wake[0] = s->wake[1] = -1;
|
|
}
|
|
|
|
static void
|
|
setpre(Server *s, char *pre)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
snprintf(s->pre, sizeof s->pre, "%s", pre);
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static void
|
|
setold(Server *s, int old)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
s->old = old;
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static void
|
|
setreply(Server *s, int eaten, int response, int stall)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
s->eaten = eaten;
|
|
s->response = response;
|
|
s->stall = stall;
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static int
|
|
eventcount(Server *s)
|
|
{
|
|
int n;
|
|
|
|
pthread_mutex_lock(&s->lock);
|
|
n = s->nev;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return n;
|
|
}
|
|
|
|
static int
|
|
closecount(Server *s)
|
|
{
|
|
int n;
|
|
|
|
pthread_mutex_lock(&s->lock);
|
|
n = s->nclose;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return n;
|
|
}
|
|
|
|
static int
|
|
waitcount(Server *s, int n)
|
|
{
|
|
struct timespec ts;
|
|
int r;
|
|
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
ts.tv_sec += Timeout / 1000;
|
|
ts.tv_nsec += (Timeout % 1000) * 1000000;
|
|
if(ts.tv_nsec >= 1000000000){
|
|
ts.tv_sec++;
|
|
ts.tv_nsec -= 1000000000;
|
|
}
|
|
pthread_mutex_lock(&s->lock);
|
|
while(s->nev < n){
|
|
r = pthread_cond_timedwait(&s->changed, &s->lock, &ts);
|
|
if(r == ETIMEDOUT)
|
|
break;
|
|
}
|
|
r = s->nev >= n;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return r;
|
|
}
|
|
|
|
static int
|
|
waitclose(Server *s, int n)
|
|
{
|
|
struct timespec ts;
|
|
int r;
|
|
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
ts.tv_sec += Timeout / 1000;
|
|
pthread_mutex_lock(&s->lock);
|
|
while(s->nclose < n){
|
|
r = pthread_cond_timedwait(&s->changed, &s->lock, &ts);
|
|
if(r == ETIMEDOUT)
|
|
break;
|
|
}
|
|
r = s->nclose >= n;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return r;
|
|
}
|
|
|
|
static Event
|
|
getevent(Server *s, int n)
|
|
{
|
|
Event e;
|
|
|
|
memset(&e, 0, sizeof e);
|
|
pthread_mutex_lock(&s->lock);
|
|
if(n >= 0 && n < s->nev)
|
|
e = s->ev[n];
|
|
pthread_mutex_unlock(&s->lock);
|
|
return e;
|
|
}
|
|
|
|
static int
|
|
runquery(char *module, char *cache)
|
|
{
|
|
int fd, status;
|
|
pid_t pid;
|
|
|
|
fd = open(cache, O_WRONLY|O_CREAT|O_TRUNC, 0600);
|
|
if(fd < 0)
|
|
return fail("open module cache: %s", strerror(errno));
|
|
pid = fork();
|
|
if(pid < 0){
|
|
close(fd);
|
|
return fail("fork gtk-query-immodules: %s", strerror(errno));
|
|
}
|
|
if(pid == 0){
|
|
dup2(fd, STDOUT_FILENO);
|
|
close(fd);
|
|
execlp("gtk-query-immodules-3.0", "gtk-query-immodules-3.0",
|
|
module, (char*)NULL);
|
|
_exit(127);
|
|
}
|
|
close(fd);
|
|
if(waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ||
|
|
WEXITSTATUS(status) != 0)
|
|
return fail("gtk-query-immodules-3.0 failed");
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
startxvfb(pid_t *pidp, char *display, size_t ndisplay)
|
|
{
|
|
struct pollfd pfd;
|
|
char fdarg[32], line[32];
|
|
int p[2], n, status;
|
|
pid_t pid;
|
|
|
|
if(pipe(p) < 0)
|
|
return fail("Xvfb display pipe: %s", strerror(errno));
|
|
pid = fork();
|
|
if(pid < 0){
|
|
close(p[0]);
|
|
close(p[1]);
|
|
return fail("fork Xvfb: %s", strerror(errno));
|
|
}
|
|
if(pid == 0){
|
|
close(p[0]);
|
|
snprintf(fdarg, sizeof fdarg, "%d", p[1]);
|
|
execlp("Xvfb", "Xvfb", "-displayfd", fdarg, "-screen", "0",
|
|
"1024x768x24", "-nolisten", "tcp", "-noreset", (char*)NULL);
|
|
_exit(127);
|
|
}
|
|
close(p[1]);
|
|
pfd.fd = p[0];
|
|
pfd.events = POLLIN;
|
|
if(poll(&pfd, 1, Timeout) <= 0){
|
|
kill(pid, SIGTERM);
|
|
waitpid(pid, NULL, 0);
|
|
close(p[0]);
|
|
return fail("Xvfb did not publish a display");
|
|
}
|
|
n = read(p[0], line, sizeof line - 1);
|
|
close(p[0]);
|
|
if(n <= 0){
|
|
waitpid(pid, &status, 0);
|
|
return fail("read Xvfb display number");
|
|
}
|
|
line[n] = '\0';
|
|
line[strcspn(line, "\r\n")] = '\0';
|
|
if(snprintf(display, ndisplay, ":%s", line) >= (int)ndisplay){
|
|
kill(pid, SIGTERM);
|
|
waitpid(pid, NULL, 0);
|
|
return fail("Xvfb display number is too long");
|
|
}
|
|
*pidp = pid;
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
stopxvfb(pid_t *pidp)
|
|
{
|
|
int status;
|
|
int64_t deadline;
|
|
|
|
if(*pidp <= 0)
|
|
return;
|
|
kill(*pidp, SIGTERM);
|
|
deadline = nowms() + Timeout;
|
|
while(waitpid(*pidp, &status, WNOHANG) == 0 && leftms(deadline) > 0)
|
|
g_usleep(10000);
|
|
if(waitpid(*pidp, &status, WNOHANG) == 0){
|
|
kill(*pidp, SIGKILL);
|
|
waitpid(*pidp, &status, 0);
|
|
}
|
|
*pidp = -1;
|
|
}
|
|
|
|
static void
|
|
pump(void)
|
|
{
|
|
while(gtk_events_pending())
|
|
gtk_main_iteration();
|
|
}
|
|
|
|
static void
|
|
logsig(Siglog *l, char c)
|
|
{
|
|
if(l->n + 1 < (int)sizeof l->event){
|
|
l->event[l->n++] = c;
|
|
l->event[l->n] = '\0';
|
|
}
|
|
}
|
|
|
|
static void
|
|
prestart(GtkIMContext *ctx, Siglog *l)
|
|
{
|
|
(void)ctx;
|
|
logsig(l, 'S');
|
|
}
|
|
|
|
static void
|
|
prechange(GtkIMContext *ctx, Siglog *l)
|
|
{
|
|
gchar *s;
|
|
|
|
gtk_im_context_get_preedit_string(ctx, &s, NULL, NULL);
|
|
if(strcmp(l->pre, s) != 0)
|
|
l->prechanged = 1;
|
|
snprintf(l->pre, sizeof l->pre, "%s", s);
|
|
g_free(s);
|
|
logsig(l, 'C');
|
|
}
|
|
|
|
static void
|
|
preend(GtkIMContext *ctx, Siglog *l)
|
|
{
|
|
(void)ctx;
|
|
logsig(l, 'E');
|
|
}
|
|
|
|
static void
|
|
commit(GtkIMContext *ctx, gchar *s, Siglog *l)
|
|
{
|
|
(void)ctx;
|
|
snprintf(l->commit, sizeof l->commit, "%s", s);
|
|
logsig(l, 'K');
|
|
}
|
|
|
|
static void
|
|
clearevent(Siglog *l)
|
|
{
|
|
l->event[0] = '\0';
|
|
l->n = 0;
|
|
l->commit[0] = '\0';
|
|
l->prechanged = 0;
|
|
}
|
|
|
|
static void
|
|
applyclient(Siglog *l)
|
|
{
|
|
if(l->commit[0] != '\0')
|
|
l->shown[0] = '\0';
|
|
if(l->prechanged)
|
|
snprintf(l->shown, sizeof l->shown, "%s", l->pre);
|
|
}
|
|
|
|
static void
|
|
clearlog(Siglog *l)
|
|
{
|
|
memset(l, 0, sizeof *l);
|
|
}
|
|
|
|
static GtkIMContext*
|
|
newcontext(Siglog *l)
|
|
{
|
|
GtkIMContext *ctx;
|
|
|
|
ctx = gtk_im_multicontext_new();
|
|
gtk_im_multicontext_set_context_id(GTK_IM_MULTICONTEXT(ctx), "strans");
|
|
g_signal_connect(ctx, "preedit-start", G_CALLBACK(prestart), l);
|
|
g_signal_connect(ctx, "preedit-changed", G_CALLBACK(prechange), l);
|
|
g_signal_connect(ctx, "preedit-end", G_CALLBACK(preend), l);
|
|
g_signal_connect(ctx, "commit", G_CALLBACK(commit), l);
|
|
return ctx;
|
|
}
|
|
|
|
static int
|
|
keyevent(GtkIMContext *ctx, GdkWindow *win, GdkEventType type,
|
|
guint keyval, guint state)
|
|
{
|
|
GdkEvent *ev;
|
|
int r;
|
|
|
|
ev = gdk_event_new(type);
|
|
ev->key.window = g_object_ref(win);
|
|
ev->key.send_event = TRUE;
|
|
ev->key.time = GDK_CURRENT_TIME;
|
|
ev->key.keyval = keyval;
|
|
ev->key.state = state;
|
|
r = gtk_im_context_filter_keypress(ctx, &ev->key);
|
|
gdk_event_free(ev);
|
|
return r;
|
|
}
|
|
|
|
static int
|
|
keystate(GtkIMContext *ctx, GdkWindow *win, guint keyval, guint state)
|
|
{
|
|
return keyevent(ctx, win, GDK_KEY_PRESS, keyval, state);
|
|
}
|
|
|
|
static int
|
|
key(GtkIMContext *ctx, GdkWindow *win, guint keyval)
|
|
{
|
|
return keystate(ctx, win, keyval, 0);
|
|
}
|
|
|
|
static GtkWidget*
|
|
newwindow(int x, int y)
|
|
{
|
|
GtkWidget *w;
|
|
|
|
w = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
|
gtk_window_set_default_size(GTK_WINDOW(w), 240, 120);
|
|
gtk_window_move(GTK_WINDOW(w), x, y);
|
|
gtk_widget_show(w);
|
|
pump();
|
|
return w;
|
|
}
|
|
|
|
#define Check(c, ...) do{ if(!(c)){ fail(__VA_ARGS__); goto out; } }while(0)
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
Server srv;
|
|
Siglog log;
|
|
GtkIMContext *ctx, *oldctx, *badctx;
|
|
GtkWidget *top;
|
|
GdkWindow *win;
|
|
GdkRectangle rect;
|
|
Event e;
|
|
static const GtkInputPurpose purposes[] = {
|
|
GTK_INPUT_PURPOSE_PASSWORD,
|
|
GTK_INPUT_PURPOSE_PIN,
|
|
};
|
|
static const int badresponses[] = { Rbadcommit, Rbadpreedit };
|
|
char root[] = "/tmp/strans-gtk.XXXXXX";
|
|
char runtime[320] = "", socket[384] = "", cache[384] = "";
|
|
char display[32];
|
|
char *module;
|
|
pid_t xvfb;
|
|
int first, i, ox, oy, scale, closes, ok, rootmade, serverup;
|
|
int64_t stalledms, started;
|
|
|
|
testname = "gtk_live_test";
|
|
ctx = oldctx = badctx = NULL;
|
|
top = NULL;
|
|
xvfb = -1;
|
|
rootmade = 0;
|
|
serverup = 0;
|
|
ok = 0;
|
|
if(argc != 2){
|
|
fail("usage: gtk_live_test /path/to/im-strans.so");
|
|
return 1;
|
|
}
|
|
module = realpath(argv[1], NULL);
|
|
if(module == NULL){
|
|
fail("realpath %s: %s", argv[1], strerror(errno));
|
|
return 1;
|
|
}
|
|
Check(mkdtemp(root) != NULL, "mkdtemp: %s", strerror(errno));
|
|
rootmade = 1;
|
|
Check(snprintf(runtime, sizeof runtime, "%s/runtime", root) <
|
|
(int)sizeof runtime, "runtime path is too long");
|
|
Check(snprintf(socket, sizeof socket, "%s/strans.sock", runtime) <
|
|
(int)sizeof socket, "socket path is too long");
|
|
Check(snprintf(cache, sizeof cache, "%s/immodules.cache", root) <
|
|
(int)sizeof cache, "cache path is too long");
|
|
Check(mkdir(runtime, 0700) == 0, "mkdir runtime: %s", strerror(errno));
|
|
Check(startxvfb(&xvfb, display, sizeof display), "start private Xvfb");
|
|
Check(setenv("DISPLAY", display, 1) == 0 &&
|
|
setenv("GDK_BACKEND", "x11", 1) == 0 &&
|
|
setenv("GDK_SCALE", "2", 1) == 0 &&
|
|
setenv("XDG_RUNTIME_DIR", runtime, 1) == 0,
|
|
"set private environment: %s", strerror(errno));
|
|
Check(runquery(module, cache), "generate GTK module cache");
|
|
Check(setenv("GTK_IM_MODULE_FILE", cache, 1) == 0 &&
|
|
setenv("GTK_IM_MODULE", "strans", 1) == 0,
|
|
"set GTK module environment: %s", strerror(errno));
|
|
Check(startserver(&srv, socket), "start fake daemon");
|
|
serverup = 1;
|
|
gtk_init(&argc, &argv);
|
|
|
|
clearlog(&log);
|
|
ctx = newcontext(&log);
|
|
top = newwindow(73, 41);
|
|
win = gtk_widget_get_window(top);
|
|
gtk_im_context_set_client_window(ctx, win);
|
|
rect.x = 11;
|
|
rect.y = 17;
|
|
rect.width = 3;
|
|
rect.height = 19;
|
|
gtk_im_context_set_cursor_location(ctx, &rect);
|
|
Check(eventcount(&srv) == 0, "cursor reporting opened the socket");
|
|
setpre(&srv, "");
|
|
Check(key(ctx, win, GDK_KEY_a), "initial key was not eaten");
|
|
Check(waitcount(&srv, 3), "initial protocol frames timed out");
|
|
e = getevent(&srv, 0);
|
|
Check(e.type == Ecap && e.want == 1, "default preedit capability missing");
|
|
e = getevent(&srv, 1);
|
|
Check(e.type == Ecaret && e.valid, "cached caret was not negotiated");
|
|
gdk_window_get_origin(win, &ox, &oy);
|
|
Check(ox != 0 && oy != 0, "test window did not get a nonzero root origin");
|
|
scale = gdk_window_get_scale_factor(win);
|
|
Check(scale == 2, "private Xvfb did not honor GDK_SCALE=2");
|
|
Check(e.x == (ox + rect.x) * scale &&
|
|
e.y == (oy + rect.y) * scale && e.h == rect.height * scale,
|
|
"caret conversion got %d,%d,%d want %d,%d,%d",
|
|
e.x, e.y, e.h, (ox + rect.x) * scale,
|
|
(oy + rect.y) * scale, rect.height * scale);
|
|
e = getevent(&srv, 2);
|
|
Check(e.type == Ekey && e.want == 1, "initial key framing changed");
|
|
Check(log.n == 0, "initial empty preedit emitted %s", log.event);
|
|
setpre(&srv, "ㅋ");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_z), "first preedit key was not eaten");
|
|
applyclient(&log);
|
|
Check(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "ㅋ") == 0 &&
|
|
strcmp(log.shown, "ㅋ") == 0,
|
|
"first preedit signals were %s (preedit %s, shown %s)",
|
|
log.event, log.pre, log.shown);
|
|
clearevent(&log);
|
|
setreply(&srv, 1, Rcommitpre, 0);
|
|
Check(key(ctx, win, GDK_KEY_z), "repeated preedit key was not eaten");
|
|
applyclient(&log);
|
|
Check(strcmp(log.event, "CEKSC") == 0 &&
|
|
strcmp(log.commit, "ㅋ") == 0 && strcmp(log.pre, "ㅋ") == 0 &&
|
|
strcmp(log.shown, "ㅋ") == 0,
|
|
"repeated preedit signals were %s (commit %s, preedit %s, shown %s)",
|
|
log.event, log.commit, log.pre, log.shown);
|
|
Check(g_utf8_strlen(log.commit, -1) + g_utf8_strlen(log.shown, -1) == 2,
|
|
"two physical keys produced commit %s and shown preedit %s",
|
|
log.commit, log.shown);
|
|
|
|
/* Unconsumed input uses GtkIMContextSimple, including its stateful forms. */
|
|
setpre(&srv, "");
|
|
setreply(&srv, 0, Rnormal, 0);
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_Multi_key) &&
|
|
key(ctx, win, GDK_KEY_apostrophe) && key(ctx, win, GDK_KEY_e),
|
|
"Compose sequence was not consumed");
|
|
Check(strcmp(log.commit, "é") == 0, "Compose committed %s", log.commit);
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e),
|
|
"dead-key sequence was not consumed");
|
|
Check(strcmp(log.commit, "é") == 0, "dead key committed %s", log.commit);
|
|
clearlog(&log);
|
|
Check(keystate(ctx, win, GDK_KEY_U, GDK_CONTROL_MASK|GDK_SHIFT_MASK),
|
|
"Unicode prefix was not consumed");
|
|
(void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_U,
|
|
GDK_CONTROL_MASK|GDK_SHIFT_MASK);
|
|
(void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_Shift_L,
|
|
GDK_CONTROL_MASK);
|
|
(void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_Control_L, 0);
|
|
Check(key(ctx, win, GDK_KEY_0), "Unicode digit 1 was not consumed");
|
|
Check(key(ctx, win, GDK_KEY_0), "Unicode digit 2 was not consumed");
|
|
Check(key(ctx, win, GDK_KEY_e), "Unicode digit 3 was not consumed");
|
|
Check(key(ctx, win, GDK_KEY_9), "Unicode digit 4 was not consumed");
|
|
(void)key(ctx, win, GDK_KEY_Return);
|
|
Check(strcmp(log.commit, "é") == 0, "Unicode entry committed %s", log.commit);
|
|
setreply(&srv, 1, Rnormal, 0);
|
|
|
|
/* PASSWORD and PIN transitions reset an existing composition. */
|
|
rect.height = 12;
|
|
gtk_im_context_set_cursor_location(ctx, &rect);
|
|
for(i = 0; i < (int)(sizeof purposes / sizeof purposes[0]); i++){
|
|
g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL);
|
|
setpre(&srv, i == 0 ? "password" : "pin");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_c + i), "private setup key was not eaten");
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
g_object_set(ctx, "input-purpose", purposes[i], NULL);
|
|
Check(waitcount(&srv, first + 1), "private reset timed out");
|
|
Check(getevent(&srv, first).type == Ereset &&
|
|
strcmp(log.event, "CE") == 0,
|
|
"private transition did not reset and clear");
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
Check(key(ctx, win, GDK_KEY_x), "private fallback did not commit");
|
|
Check(eventcount(&srv) == first && strcmp(log.commit, "x") == 0,
|
|
"private key reached daemon or did not commit");
|
|
}
|
|
|
|
g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL);
|
|
setpre(&srv, "reset");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_e), "reset setup key was not eaten");
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
gtk_im_context_reset(ctx);
|
|
Check(waitcount(&srv, first + 1), "explicit reset timed out");
|
|
Check(getevent(&srv, first).type == Ereset && strcmp(log.event, "CE") == 0,
|
|
"explicit reset did not clear preedit");
|
|
setpre(&srv, "focus");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_f), "focus setup key was not eaten");
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
closes = closecount(&srv);
|
|
gtk_im_context_focus_out(ctx);
|
|
Check(waitclose(&srv, closes + 1), "focus-out did not close connection");
|
|
Check(eventcount(&srv) == first, "focus-out sent a request before closing");
|
|
Check(strcmp(log.event, "CE") == 0, "focus-out clear order was %s", log.event);
|
|
|
|
/* Dispose is repeatable, closes the connection, and emits no preedit. */
|
|
gtk_im_context_set_client_window(ctx, win);
|
|
setpre(&srv, "final");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_g), "dispose setup key was not eaten");
|
|
clearlog(&log);
|
|
closes = closecount(&srv);
|
|
g_object_run_dispose(G_OBJECT(ctx));
|
|
g_object_run_dispose(G_OBJECT(ctx));
|
|
g_object_unref(ctx);
|
|
ctx = NULL;
|
|
Check(waitclose(&srv, closes + 1), "dispose did not close connection");
|
|
Check(log.n == 0, "dispose emitted %s", log.event);
|
|
|
|
/* Malformed commit and preedit fields close the protocol connection. */
|
|
for(i = 0; i < (int)(sizeof badresponses / sizeof badresponses[0]); i++){
|
|
setreply(&srv, 1, badresponses[i], 0);
|
|
clearlog(&log);
|
|
badctx = newcontext(&log);
|
|
gtk_im_context_set_client_window(badctx, win);
|
|
closes = closecount(&srv);
|
|
Check(key(badctx, win, GDK_KEY_m + i),
|
|
"malformed response did not fall back");
|
|
Check(waitclose(&srv, closes + 1),
|
|
"malformed response did not close connection");
|
|
Check(log.commit[0] == 'm' + i && log.commit[1] == '\0',
|
|
"malformed response fallback committed %s", log.commit);
|
|
g_object_unref(badctx);
|
|
badctx = NULL;
|
|
}
|
|
|
|
/* A peer which accepts a connection but never replies cannot freeze GTK. */
|
|
setreply(&srv, 1, Rnormal, 1);
|
|
clearlog(&log);
|
|
badctx = newcontext(&log);
|
|
closes = closecount(&srv);
|
|
started = nowms();
|
|
Check(key(badctx, win, GDK_KEY_t), "stalled peer did not fall back");
|
|
stalledms = nowms() - started;
|
|
Check(stalledms < 4*Ipcwaitms, "stalled peer blocked GTK");
|
|
Check(waitclose(&srv, closes + 1), "stalled connection was not closed");
|
|
Check(strcmp(log.commit, "t") == 0, "stalled fallback committed %s",
|
|
log.commit);
|
|
g_object_unref(badctx);
|
|
badctx = NULL;
|
|
setreply(&srv, 1, Rnormal, 0);
|
|
|
|
/* An old daemon still honors set_use_preedit(FALSE). */
|
|
setold(&srv, 1);
|
|
clearlog(&log);
|
|
oldctx = newcontext(&log);
|
|
gtk_im_context_set_client_window(oldctx, win);
|
|
rect.x = 2;
|
|
rect.y = 3;
|
|
rect.height = 10;
|
|
gtk_im_context_set_cursor_location(oldctx, &rect);
|
|
gtk_im_context_set_use_preedit(oldctx, FALSE);
|
|
setpre(&srv, "legacy");
|
|
first = eventcount(&srv);
|
|
Check(key(oldctx, win, GDK_KEY_h), "old-daemon key was not eaten");
|
|
Check(waitcount(&srv, first + 2), "old-daemon exchange timed out");
|
|
e = getevent(&srv, first);
|
|
Check(e.type == Ecap && e.want == 0, "old-daemon probe framing is wrong");
|
|
e = getevent(&srv, first + 1);
|
|
Check(e.type == Ekey && e.want == 0, "old daemon forced inline preedit");
|
|
for(i = first; i < eventcount(&srv); i++)
|
|
Check(getevent(&srv, i).type != Ecaret,
|
|
"caret extension sent to old daemon");
|
|
Check(log.n == 0 && log.pre[0] == '\0',
|
|
"old daemon emitted inline preedit %s (%s)", log.event, log.pre);
|
|
closes = closecount(&srv);
|
|
g_object_unref(oldctx);
|
|
oldctx = NULL;
|
|
Check(waitclose(&srv, closes + 1), "old-daemon context did not close");
|
|
|
|
/* A failed first connection uses GtkIMContextSimple. */
|
|
stopserver(&srv);
|
|
serverup = 0;
|
|
unlink(socket);
|
|
clearlog(&log);
|
|
badctx = newcontext(&log);
|
|
Check(key(badctx, win, GDK_KEY_z), "connection failure did not fall back");
|
|
Check(strcmp(log.commit, "z") == 0 && log.pre[0] == '\0',
|
|
"connection failure left stale preedit");
|
|
|
|
ok = 1;
|
|
out:
|
|
if(badctx != NULL)
|
|
g_object_unref(badctx);
|
|
if(oldctx != NULL)
|
|
g_object_unref(oldctx);
|
|
if(ctx != NULL)
|
|
g_object_unref(ctx);
|
|
if(top != NULL)
|
|
gtk_widget_destroy(top);
|
|
pump();
|
|
if(serverup)
|
|
stopserver(&srv);
|
|
if(socket[0] != '\0')
|
|
unlink(socket);
|
|
if(cache[0] != '\0')
|
|
unlink(cache);
|
|
if(runtime[0] != '\0')
|
|
rmdir(runtime);
|
|
if(rootmade)
|
|
rmdir(root);
|
|
stopxvfb(&xvfb);
|
|
free(module);
|
|
if(ok)
|
|
printf("gtk_live_test: ok (scale %d, stalled peer %lld ms)\n",
|
|
scale, (long long)stalledms);
|
|
return !ok;
|
|
}
|