test: separate quick, live, and stress checks

This commit is contained in:
2026-08-14 23:13:32 +09:00
parent bfe83d0119
commit 516ec98a70
9 changed files with 469 additions and 2091 deletions

View File

@@ -11,7 +11,6 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
@@ -28,10 +27,6 @@ enum
Starttimeout = 8000,
Stoptimeout = 3000,
Ctrlmask = 1<<2,
Phaseignore,
Phasequiet,
Phaserecover,
};
typedef struct Test Test;
@@ -45,19 +40,9 @@ struct Test
int firsterrfd;
int seconderrfd;
int helpererrfd;
int notifyfd;
int runtimewd;
int buswd;
int ipcfirst;
int ipcnew;
int busfirstfd;
int phase;
int socketdeleted;
int socketcreated;
int bustempcreated;
int bustempmoved;
int buspublished;
uint32_t buscookie;
DBusConnection *busfirst;
DBusConnection *busnew;
char root[256];
@@ -68,18 +53,8 @@ struct Test
char home[320];
char socket[384];
char addrfile[512];
char addrbase[256];
char firstaddress[512];
char secondaddress[512];
char firstcontents[2048];
size_t nfirstcontents;
char secondcontents[2048];
size_t nsecondcontents;
char bustemp[256];
struct stat firstsocketst;
struct stat firstaddrst;
struct stat secondsocketst;
struct stat secondaddrst;
char firsterr[8192];
size_t nfirsterr;
char seconderr[8192];
@@ -88,9 +63,6 @@ struct Test
size_t nhelpererr;
};
static uint32_t watchmask = IN_CREATE|IN_DELETE|IN_ATTRIB|IN_MOVED_FROM|
IN_MOVED_TO|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF;
static int
fail(char *fmt, ...)
{
@@ -191,13 +163,9 @@ setup(Test *t)
t->firsterrfd = -1;
t->seconderrfd = -1;
t->helpererrfd = -1;
t->notifyfd = -1;
t->runtimewd = -1;
t->buswd = -1;
t->ipcfirst = -1;
t->ipcnew = -1;
t->busfirstfd = -1;
t->phase = Phaseignore;
memset(&sa, 0, sizeof sa);
sa.sa_handler = SIG_DFL;
if(sigemptyset(&sa.sa_mask) < 0 || sigaction(SIGCHLD, &sa, NULL) < 0)
@@ -225,15 +193,6 @@ setup(Test *t)
if(!makedir(t->runtime) || !makedir(t->config) || !makedir(t->ibus) ||
!makedir(t->bus) || !makedir(t->home))
return 0;
t->notifyfd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK);
if(t->notifyfd < 0)
return fail("inotify_init1: %s", strerror(errno));
t->runtimewd = inotify_add_watch(t->notifyfd, t->runtime, watchmask);
if(t->runtimewd < 0)
return fail("watch runtime directory: %s", strerror(errno));
t->buswd = inotify_add_watch(t->notifyfd, t->bus, watchmask);
if(t->buswd < 0)
return fail("watch IBus directory: %s", strerror(errno));
return 1;
}
@@ -284,7 +243,8 @@ startchild(Test *t, char *program, char *mapdir, pid_t *child, int *errfd)
setenv("XDG_CONFIG_HOME", t->config, 1) < 0 ||
setenv("HOME", t->home, 1) < 0 || unsetenv("DISPLAY") < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS") < 0){
unsetenv("IBUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS_FILE") < 0){
dprintf(STDERR_FILENO, "set daemon environment: %s\n",
strerror(errno));
_exit(126);
@@ -299,150 +259,6 @@ startchild(Test *t, char *program, char *mapdir, pid_t *child, int *errfd)
return 1;
}
static int
runtimeevent(Test *t, struct inotify_event *ev)
{
uint32_t mask;
if(ev->len == 0 || strcmp(ev->name, "strans.sock") != 0)
return fail("unexpected runtime entry event %#x for %s", ev->mask,
ev->len != 0 ? ev->name : "directory");
mask = ev->mask & watchmask;
if(mask & IN_DELETE){
if(t->socketdeleted || t->socketcreated)
return fail("IPC socket deletion occurred out of order");
t->socketdeleted = 1;
mask &= ~IN_DELETE;
}
if(mask & IN_CREATE){
if(!t->socketdeleted || t->socketcreated)
return fail("IPC socket creation occurred before stale deletion");
t->socketcreated = 1;
mask &= ~IN_CREATE;
}
if(mask & IN_ATTRIB){
if(!t->socketcreated)
return fail("IPC socket protection occurred before recreation");
mask &= ~IN_ATTRIB;
}
if(mask != 0)
return fail("unexpected IPC recovery event %#x", ev->mask);
return 1;
}
static int
tempname(Test *t, char *name)
{
size_t n, nn;
n = strlen(t->addrbase);
nn = strlen(name);
if(nn <= n + 5)
return 0;
return strncmp(name, t->addrbase, n) == 0 &&
strncmp(name + n, ".tmp.", 5) == 0;
}
static int
busevent(Test *t, struct inotify_event *ev)
{
uint32_t mask;
if(ev->len == 0)
return fail("unexpected unnamed IBus directory event %#x", ev->mask);
mask = ev->mask & watchmask;
if(tempname(t, ev->name)){
if(mask & IN_CREATE){
if(t->bustempcreated || snprintf(t->bustemp, sizeof t->bustemp,
"%s", ev->name) >= (int)sizeof t->bustemp)
return fail("invalid replacement IBus temporary file");
t->bustempcreated = 1;
mask &= ~IN_CREATE;
}
if(!t->bustempcreated || strcmp(ev->name, t->bustemp) != 0)
return fail("unowned IBus temporary file event for %s", ev->name);
if(mask & IN_ATTRIB)
mask &= ~IN_ATTRIB;
if(mask & IN_CLOSE_WRITE)
mask &= ~IN_CLOSE_WRITE;
if(mask & IN_MOVED_FROM){
if(t->bustempmoved || ev->cookie == 0)
return fail("invalid IBus temporary-file rename");
t->bustempmoved = 1;
t->buscookie = ev->cookie;
mask &= ~IN_MOVED_FROM;
}
if(mask != 0)
return fail("unexpected IBus temporary-file event %#x", ev->mask);
return 1;
}
if(strcmp(ev->name, t->addrbase) != 0)
return fail("unexpected IBus recovery entry %s", ev->name);
if(mask == IN_MOVED_TO && t->bustempmoved && ev->cookie == t->buscookie){
t->buspublished = 1;
return 1;
}
return fail("IBus address replacement was not one atomic rename: %#x",
ev->mask);
}
static int
drainnotify(Test *t)
{
char buf[4096];
struct inotify_event *ev;
ssize_t n;
size_t off;
int ok;
ok = 1;
for(;;){
n = read(t->notifyfd, buf, sizeof buf);
if(n < 0 && errno == EINTR)
continue;
if(n < 0 && errno == EAGAIN)
return ok;
if(n < 0)
return fail("read endpoint watches: %s", strerror(errno));
if(n == 0)
return ok;
for(off = 0; off + sizeof *ev <= (size_t)n;
off += sizeof *ev + ev->len){
ev = (struct inotify_event*)(buf + off);
if(off + sizeof *ev + ev->len > (size_t)n)
return fail("truncated inotify event");
if(ev->mask & IN_Q_OVERFLOW){
fail("endpoint watch queue overflowed");
ok = 0;
continue;
}
if(ev->mask & (IN_IGNORED|IN_UNMOUNT)){
fail("endpoint watch became invalid: %#x", ev->mask);
ok = 0;
continue;
}
if(ev->wd != t->runtimewd && ev->wd != t->buswd)
continue;
if(t->phase == Phaseignore)
continue;
if(t->phase == Phasequiet){
fail("hard crash changed %s endpoint %s with event %#x",
ev->wd == t->runtimewd ? "IPC" : "IBus",
ev->len != 0 ? ev->name : "directory", ev->mask);
ok = 0;
continue;
}
if(ev->wd == t->runtimewd){
if(!runtimeevent(t, ev))
ok = 0;
}else if(!busevent(t, ev))
ok = 0;
}
if(off != (size_t)n)
return fail("truncated inotify event buffer");
}
}
static int
findaddress(Test *t)
{
@@ -600,9 +416,9 @@ childalive(pid_t *pid, char *which)
static int
waitready(Test *t, pid_t *child, int errfd, char *which)
{
struct pollfd pfd[2];
struct pollfd pfd;
struct stat st;
char contents[2048], address[512];
char contents[2048], *address;
size_t ncontents;
pid_t declared;
int count, n, socketready, timeout;
@@ -613,6 +429,7 @@ waitready(Test *t, pid_t *child, int errfd, char *which)
return fail("read monotonic clock before %s startup: %s", which,
strerror(errno));
deadline += Starttimeout;
address = child == &t->first ? t->firstaddress : t->secondaddress;
for(;;){
socketready = lstat(t->socket, &st) == 0 && S_ISSOCK(st.st_mode) &&
(st.st_mode & 0777) == 0600;
@@ -621,7 +438,7 @@ waitready(Test *t, pid_t *child, int errfd, char *which)
return 0;
if(socketready && count == 1 &&
readfile(t->addrfile, contents, sizeof contents, &ncontents) &&
parseaddress(contents, ncontents, address, sizeof address,
parseaddress(contents, ncontents, address, 512,
&declared) && declared == *child && privateaddress(address, declared))
return 1;
if(!childalive(child, which))
@@ -632,27 +449,19 @@ waitready(Test *t, pid_t *child, int errfd, char *which)
strerror(errno));
if(timeout == 0)
return fail("timed out waiting for both %s daemon endpoints", which);
pfd[0].fd = t->notifyfd;
pfd[0].events = POLLIN;
pfd[0].revents = 0;
pfd[1].fd = errfd;
pfd[1].events = POLLIN|POLLHUP;
pfd[1].revents = 0;
n = poll(pfd, 2, timeout);
pfd.fd = errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
n = poll(&pfd, 1, timeout > 20 ? 20 : timeout);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("poll %s daemon readiness: %s", which,
strerror(errno));
if(pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL))
return fail("endpoint watch became unusable: %#x", pfd[0].revents);
if(pfd[1].revents & (POLLERR|POLLNVAL))
if(pfd.revents & (POLLERR|POLLNVAL))
return fail("%s stderr pipe became unusable: %#x", which,
pfd[1].revents);
if(pfd[0].revents & POLLIN)
if(!drainnotify(t))
return 0;
if(pfd[1].revents & (POLLIN|POLLHUP)){
pfd.revents);
if(pfd.revents & (POLLIN|POLLHUP)){
if(child == &t->first)
readpipe(errfd, t->firsterr, sizeof t->firsterr,
&t->nfirsterr);
@@ -663,39 +472,6 @@ waitready(Test *t, pid_t *child, int errfd, char *which)
}
}
static int
snapshotfirst(Test *t)
{
char *base;
pid_t declared;
if(lstat(t->socket, &t->firstsocketst) < 0)
return fail("stat first IPC socket: %s", strerror(errno));
if(!S_ISSOCK(t->firstsocketst.st_mode) ||
(t->firstsocketst.st_mode & 0777) != 0600)
return fail("first IPC endpoint is not a protected socket");
if(lstat(t->addrfile, &t->firstaddrst) < 0)
return fail("stat first IBus address file: %s", strerror(errno));
if(!S_ISREG(t->firstaddrst.st_mode) ||
(t->firstaddrst.st_mode & 0777) != 0600)
return fail("first IBus address is not a protected regular file");
if(!readfile(t->addrfile, t->firstcontents, sizeof t->firstcontents,
&t->nfirstcontents) ||
!parseaddress(t->firstcontents, t->nfirstcontents, t->firstaddress,
sizeof t->firstaddress, &declared))
return 0;
if(declared != t->firstpid)
return fail("first IBus address PID %ld, expected %ld", (long)declared,
(long)t->firstpid);
if(!privateaddress(t->firstaddress, declared))
return 0;
base = strrchr(t->addrfile, '/');
if(base == NULL || snprintf(t->addrbase, sizeof t->addrbase, "%s", base+1)
>= (int)sizeof t->addrbase)
return fail("first IBus address filename is invalid");
return 1;
}
static int
connectsocket(char *path, int64_t deadline)
{
@@ -1200,47 +976,6 @@ waitbusclosed(Test *t)
}
}
static int
onlyaddress(Test *t, char *where)
{
DIR *dir;
struct dirent *de;
int count, ok;
dir = opendir(t->bus);
if(dir == NULL)
return fail("open IBus directory %s %s: %s", t->bus, where,
strerror(errno));
count = 0;
ok = 1;
errno = 0;
while((de = readdir(dir)) != NULL){
if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
continue;
count++;
if(strcmp(de->d_name, t->addrbase) != 0){
fail("unexpected IBus entry %s %s", de->d_name, where);
ok = 0;
}
errno = 0;
}
if(errno != 0){
fail("read IBus directory %s %s: %s", t->bus, where,
strerror(errno));
ok = 0;
}
if(closedir(dir) < 0){
fail("close IBus directory %s %s: %s", t->bus, where,
strerror(errno));
ok = 0;
}
if(count != 1){
fail("IBus directory has %d entries %s", count, where);
ok = 0;
}
return ok;
}
static int
checkstale(Test *t)
{
@@ -1251,24 +986,16 @@ checkstale(Test *t)
if(lstat(t->socket, &st) < 0)
return fail("stale IPC socket disappeared: %s", strerror(errno));
if(!S_ISSOCK(st.st_mode) || st.st_mode != t->firstsocketst.st_mode ||
st.st_dev != t->firstsocketst.st_dev ||
st.st_ino != t->firstsocketst.st_ino)
return fail("stale IPC socket identity or protection changed");
if(!S_ISSOCK(st.st_mode))
return fail("stale IPC endpoint is not a socket");
if(lstat(t->addrfile, &st) < 0)
return fail("stale IBus address file disappeared: %s", strerror(errno));
if(st.st_mode != t->firstaddrst.st_mode ||
st.st_dev != t->firstaddrst.st_dev || st.st_ino != t->firstaddrst.st_ino)
return fail("stale IBus address identity or protection changed");
if(!readfile(t->addrfile, contents, sizeof contents, &ncontents) ||
!parseaddress(contents, ncontents, address, sizeof address, &declared))
return 0;
if(ncontents != t->nfirstcontents ||
memcmp(contents, t->firstcontents, ncontents) != 0)
return fail("stale IBus address contents changed after hard crash");
if(declared != t->firstpid || strcmp(address, t->firstaddress) != 0)
return fail("stale IBus address no longer names the dead first daemon");
return onlyaddress(t, "after hard crash");
return 1;
}
static int
@@ -1479,52 +1206,6 @@ rejectold(Test *t, char *self, char *where)
return 1;
}
static int
recoveryevents(Test *t)
{
if(!t->socketdeleted || !t->socketcreated)
return fail("replacement did not delete and recreate the stale IPC path");
if(!t->bustempcreated || !t->bustempmoved || !t->buspublished)
return fail("replacement IBus address was not atomically published");
return 1;
}
static int
snapshotsecond(Test *t)
{
pid_t declared;
if(lstat(t->socket, &t->secondsocketst) < 0)
return fail("stat replacement IPC socket: %s", strerror(errno));
if(!S_ISSOCK(t->secondsocketst.st_mode) ||
(t->secondsocketst.st_mode & 0777) != 0600)
return fail("replacement IPC endpoint is not a protected socket");
if(lstat(t->addrfile, &t->secondaddrst) < 0)
return fail("stat replacement IBus address file: %s", strerror(errno));
if(!S_ISREG(t->secondaddrst.st_mode) ||
(t->secondaddrst.st_mode & 0777) != 0600)
return fail("replacement IBus address is not a protected regular file");
if(t->secondaddrst.st_dev == t->firstaddrst.st_dev &&
t->secondaddrst.st_ino == t->firstaddrst.st_ino)
return fail("replacement IBus address retained the stale inode");
if(!readfile(t->addrfile, t->secondcontents, sizeof t->secondcontents,
&t->nsecondcontents) ||
!parseaddress(t->secondcontents, t->nsecondcontents, t->secondaddress,
sizeof t->secondaddress, &declared))
return 0;
if(declared != t->second)
return fail("replacement IBus address PID %ld, expected %ld",
(long)declared, (long)t->second);
if(!privateaddress(t->secondaddress, declared))
return 0;
if(t->nsecondcontents == t->nfirstcontents &&
memcmp(t->secondcontents, t->firstcontents, t->nfirstcontents) == 0)
return fail("replacement IBus address contents did not change");
if(strcmp(t->secondaddress, t->firstaddress) == 0)
return fail("replacement reused the dead private IBus address");
return onlyaddress(t, "after replacement");
}
static int
openreplacement(Test *t)
{
@@ -1747,7 +1428,6 @@ cleanup(Test *t)
ok = 0;
if(t->first > 0 && !killreap(&t->first, t->firsterrfd, "first daemon"))
ok = 0;
t->phase = Phaseignore;
if(!stopsecond(t))
ok = 0;
readpipe(t->firsterrfd, t->firsterr, sizeof t->firsterr, &t->nfirsterr);
@@ -1776,23 +1456,6 @@ cleanup(Test *t)
}
t->helpererrfd = -1;
}
if(t->notifyfd >= 0){
if(t->runtimewd >= 0 &&
inotify_rm_watch(t->notifyfd, t->runtimewd) < 0 && errno != EINVAL){
fail("remove runtime watch: %s", strerror(errno));
ok = 0;
}
if(t->buswd >= 0 &&
inotify_rm_watch(t->notifyfd, t->buswd) < 0 && errno != EINVAL){
fail("remove IBus watch: %s", strerror(errno));
ok = 0;
}
if(close(t->notifyfd) < 0){
fail("close endpoint watches: %s", strerror(errno));
ok = 0;
}
t->notifyfd = -1;
}
if(!removeroot(t))
ok = 0;
return ok;
@@ -1805,23 +1468,19 @@ runrestart(Test *t, char *self, char *program, char *mapdir)
return 0;
t->firstpid = t->first;
if(!waitready(t, &t->first, t->firsterrfd, "first") ||
!snapshotfirst(t) || !openpersistent(t) || !drainnotify(t))
!openpersistent(t))
return 0;
t->phase = Phasequiet;
if(!hardcrash(t) || !waitipcclosed(t) || !waitbusclosed(t) ||
!drainnotify(t) || !checkstale(t) ||
!rejectold(t, self, "after hard crash") || !drainnotify(t))
!checkstale(t) || !rejectold(t, self, "after hard crash"))
return 0;
closebus(&t->busfirst);
if(close(t->ipcfirst) < 0)
return fail("close disconnected persistent IPC client: %s",
strerror(errno));
t->ipcfirst = -1;
t->phase = Phaserecover;
if(!startchild(t, program, mapdir, &t->second, &t->seconderrfd) ||
!waitready(t, &t->second, t->seconderrfd, "second") ||
!drainnotify(t) || !recoveryevents(t) || !snapshotsecond(t) ||
!rejectold(t, self, "after replacement") || !openreplacement(t))
!openreplacement(t))
return 0;
return childalive(&t->second, "second");
}